feat(coffee): Wave A override contract — 49 builtin overrides, 7 page templates, persona blocks, seed
WO-TF-013. Bring the coffee theme up to the frozen OVERRIDE-CONTRACT: - 49 mandatory builtin overrides in templates/overrides/coffee/ (chrome, marketing, primitives, commerce/local, blog family, auth surface, page-suggestions), each re-skinned in cafe/roastery chrome over the builtin data model. Legacy button/heading/image/text field reads updated to the new builtin contract (link/label/style, media/text). - Signature moves: chalkboard menu panel (menu-list), hand-drawn doodle dividers, torn-paper edges, kraft-tag stamped buttons, circular roast stamps, hours "today" highlight. Semantic tokens only; CSS-first motion with prefers-reduced-motion fallbacks. - 7 page templates: default, landing, article, full-width, blog-index, contact, auth. - Blocks rework: deleted the five builtin-duplicating theme blocks (menu_board, hours_strip, location_card, footer, doodle_divider) — now overrides. Kept/added 4 persona blocks: featured_pour, roast_profile, loyalty_card, brew_guide. - 5 dual-mode presets (mode "both"), all 19 tokens, light and dark tuned. - Bundled Fraunces / Work Sans / Caveat woff2 (OFL, latin subset, license recorded); fonts.json populated; all font-family via var(--font-*). Updated RECOMMENDED_FONTS.md, added RECOMMENDED_ICONS.md; required_icon_packs declared. - Themed email-safe wrapper retained (tables, inline styles). - Master pages rebuilt on builtin keys. Demo seed (demo:true): home, about, blog + 3 posts, contact (form + contact_submissions data table + row_inserted email-admins workflow), login. Cafe voice. make (codeless build + verify) and check-safety both exit 0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
9680039b39
commit
023bd23d96
@ -1,49 +1,50 @@
|
|||||||
# Coffee — Recommended fonts
|
# Coffee — Recommended fonts
|
||||||
|
|
||||||
The Coffee theme ships `fonts.json = []` per the wave-1
|
The Coffee theme **bundles** its type as woff2 in `assets/fonts/` (latin
|
||||||
[FONTS.md](../docs/FONTS.md) policy. No woff2s are bundled in this pass.
|
subsets, SIL Open Font License 1.1 — see `assets/fonts/OFL.txt`). The three
|
||||||
Templates resolve font families through the BlockNinja CSS variables
|
families become `source=template` rows in the fonts table and are selectable
|
||||||
`--font-heading`, `--font-body`, and `--font-mono` with the fallback stacks
|
in the admin typography picker. Templates always resolve font families through
|
||||||
defined in `assets/style.css`. The site admin assigns fonts in the
|
the BlockNinja CSS variables `--font-heading`, `--font-body`, `--font-mono`
|
||||||
typography panel; the picks below are the spec-aligned defaults that match
|
with the fallback stacks defined in the theme CSS — no family is hardcoded.
|
||||||
the visual identity.
|
|
||||||
|
|
||||||
## How to apply
|
## Bundled families
|
||||||
|
|
||||||
1. Open the admin → Theme → Typography panel.
|
| Slot | Family | Weights | Role |
|
||||||
2. Switch to the **Google Fonts** tab.
|
|-----------|------------|-----------|------|
|
||||||
3. Search and pick each family below.
|
| Heading | Fraunces | 400, 600 | Ligature-rich display serif for headings and prices. Warm, hand-cut character. |
|
||||||
4. Assign to the matching slot (Heading / Body / Mono).
|
| Body | Work Sans | 400, 600 | Relaxed grotesque for long copy at 17px+. Reads well on cream and espresso backgrounds. |
|
||||||
5. Save. The picks are persisted as `google:<Family>` and rendered as
|
| Accent | Caveat | 400, 700 | Hand-drawn script for badges, stamps, and doodle captions. |
|
||||||
`@import` URLs by `theme.GenerateCSS()`.
|
|
||||||
|
|
||||||
## 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 |
|
## Default behaviour (no admin picks)
|
||||||
|----------|------------------|------------------|-----|
|
|
||||||
| 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`. |
|
|
||||||
|
|
||||||
All three are already in the curated Google Fonts list in the picker and
|
On a fresh install the admin has not assigned any slot, so `--font-heading` /
|
||||||
have permissive SIL OFL 1.1 licences.
|
`--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-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`
|
- `--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.
|
|
||||||
|
|||||||
26
RECOMMENDED_ICONS.md
Normal file
26
RECOMMENDED_ICONS.md
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Coffee — Recommended icon packs
|
||||||
|
|
||||||
|
The Coffee theme renders icons through the CMS sprite system only
|
||||||
|
(`<svg><use href="/icons/<pack>.svg#<name>"/></svg>`). 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.
|
||||||
111
assets/fonts/OFL.txt
Normal file
111
assets/fonts/OFL.txt
Normal file
@ -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.
|
||||||
BIN
assets/fonts/caveat-latin-400.woff2
Normal file
BIN
assets/fonts/caveat-latin-400.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/caveat-latin-700.woff2
Normal file
BIN
assets/fonts/caveat-latin-700.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/fraunces-latin-400.woff2
Normal file
BIN
assets/fonts/fraunces-latin-400.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/fraunces-latin-600.woff2
Normal file
BIN
assets/fonts/fraunces-latin-600.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/worksans-latin-400.woff2
Normal file
BIN
assets/fonts/worksans-latin-400.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/worksans-latin-600.woff2
Normal file
BIN
assets/fonts/worksans-latin-600.woff2
Normal file
Binary file not shown.
183
assets/style.css
183
assets/style.css
@ -1,51 +1,49 @@
|
|||||||
/* Coffee theme styles
|
/* Coffee theme styles
|
||||||
*
|
*
|
||||||
* Uses the 19 shadcn-style HSL token CSS variables (--background,
|
* Dual-mode via the 19 shadcn-style HSL token CSS variables (--background,
|
||||||
* --foreground, --primary, --accent, --border, --muted, --card, ...) via
|
* --foreground, --primary, --accent, --border, --muted, --card, ...) consumed
|
||||||
* `hsl(var(--token))`. Font families are resolved through the BlockNinja font
|
* as hsl(var(--token)). No literal colors. Font families resolve ONLY through
|
||||||
* variables (--font-heading, --font-body, --font-mono) with fallback stacks
|
* the BlockNinja font variables (--font-heading, --font-body, --font-mono) with
|
||||||
* derived from the spec §3 typography list.
|
* 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 {
|
:root {
|
||||||
--coffee-heading-fallback: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif;
|
--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;
|
--coffee-mono-fallback: "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Paper grain background --------------------------------------------
|
/* --- 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.
|
|
||||||
*/
|
|
||||||
body.coffee-paper {
|
body.coffee-paper {
|
||||||
background-color: hsl(var(--background));
|
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-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;
|
background-repeat: repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Headings ----------------------------------------------------------- */
|
/* --- Type roles --------------------------------------------------------- */
|
||||||
.coffee-display {
|
.coffee-display {
|
||||||
font-family: var(--font-heading, var(--coffee-heading-fallback));
|
font-family: var(--font-heading, var(--coffee-heading-fallback));
|
||||||
font-feature-settings: "liga" 1, "dlig" 1;
|
font-feature-settings: "liga" 1, "dlig" 1;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coffee-body {
|
.coffee-body {
|
||||||
font-family: var(--font-body, var(--coffee-body-fallback));
|
font-family: var(--font-body, var(--coffee-body-fallback));
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coffee-mono {
|
.coffee-mono {
|
||||||
font-family: var(--font-mono, var(--coffee-mono-fallback));
|
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 {
|
.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-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;
|
background-repeat: no-repeat;
|
||||||
@ -54,7 +52,7 @@ body.coffee-paper {
|
|||||||
padding-bottom: 0.18em;
|
padding-bottom: 0.18em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Drop-cap (text override / article body) ---------------------------- */
|
/* --- Drop-cap ----------------------------------------------------------- */
|
||||||
.coffee-dropcap > p:first-of-type::first-letter {
|
.coffee-dropcap > p:first-of-type::first-letter {
|
||||||
font-family: var(--font-heading, var(--coffee-heading-fallback));
|
font-family: var(--font-heading, var(--coffee-heading-fallback));
|
||||||
font-size: 4em;
|
font-size: 4em;
|
||||||
@ -64,7 +62,7 @@ body.coffee-paper {
|
|||||||
color: hsl(var(--primary));
|
color: hsl(var(--primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Kraft-tag button --------------------------------------------------- */
|
/* --- Kraft-tag button (stamp press on hover) ---------------------------- */
|
||||||
.kraft-tag {
|
.kraft-tag {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -78,22 +76,29 @@ body.coffee-paper {
|
|||||||
font-family: var(--font-body, var(--coffee-body-fallback));
|
font-family: var(--font-body, var(--coffee-body-fallback));
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0.02em;
|
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;
|
transition: transform 120ms ease, box-shadow 120ms ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kraft-tag:hover {
|
.kraft-tag:hover {
|
||||||
transform: rotate(-1.2deg);
|
transform: translateY(1px) rotate(-1.2deg);
|
||||||
box-shadow: 0 2px 0 hsl(var(--border));
|
box-shadow: 0 1px 0 hsl(var(--border));
|
||||||
}
|
}
|
||||||
|
|
||||||
.kraft-tag:focus-visible {
|
.kraft-tag:focus-visible {
|
||||||
outline: 2px solid hsl(var(--ring));
|
outline: 2px solid hsl(var(--ring));
|
||||||
outline-offset: 3px;
|
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 {
|
.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");
|
-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");
|
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%;
|
-webkit-mask-size: 100% 100%;
|
||||||
mask-size: 100% 100%;
|
mask-size: 100% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coffee-torn-bottom {
|
.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");
|
-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");
|
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%;
|
mask-size: 100% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Coffee surfaces ---------------------------------------------------- */
|
/* --- Surfaces ----------------------------------------------------------- */
|
||||||
.coffee-card {
|
.coffee-card {
|
||||||
background-color: hsl(var(--card));
|
background-color: hsl(var(--card));
|
||||||
color: hsl(var(--card-foreground));
|
color: hsl(var(--card-foreground));
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coffee-frame {
|
.coffee-frame {
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
background-color: hsl(var(--card));
|
background-color: hsl(var(--card));
|
||||||
}
|
}
|
||||||
|
|
||||||
.coffee-pencil-rule {
|
.coffee-pencil-rule {
|
||||||
border-color: hsl(var(--border));
|
border-color: hsl(var(--border));
|
||||||
border-style: solid;
|
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 {
|
.coffee-hours-today {
|
||||||
background-color: hsl(var(--accent) / 0.15);
|
background-color: hsl(var(--accent) / 0.15);
|
||||||
color: hsl(var(--accent-foreground));
|
color: hsl(var(--accent-foreground));
|
||||||
@ -138,14 +157,108 @@ body.coffee-paper {
|
|||||||
padding-left: 0.75rem;
|
padding-left: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Price typography --------------------------------------------------- */
|
/* --- Price + spec chips ------------------------------------------------- */
|
||||||
.coffee-price {
|
.coffee-price {
|
||||||
font-family: var(--font-mono, var(--coffee-mono-fallback));
|
font-family: var(--font-mono, var(--coffee-mono-fallback));
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
color: hsl(var(--accent));
|
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 {
|
.coffee-pin {
|
||||||
color: hsl(var(--accent));
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -1,42 +1,32 @@
|
|||||||
# Coffee theme block definitions (codeless .bnp, WO-WZ-021).
|
# Coffee theme block definitions (codeless .bnp).
|
||||||
# Keys are fully qualified (coffee:<key>) because the codeless loader registers
|
# Only true persona furniture stays a theme block. Everything that duplicated a
|
||||||
# definitions verbatim — there is no PluginBlockRegistry prefixing in the
|
# builtin (menu, hours, location, footer, divider) is now a template override of
|
||||||
# codeless path (unlike the wasm/templ path). The qualified keys match the
|
# the builtin key under templates/overrides/coffee/ — not a block here.
|
||||||
# hardcoded data-block attributes and the master_pages.json references.
|
#
|
||||||
|
# Keys are fully qualified (coffee:<key>): the codeless loader registers
|
||||||
|
# definitions verbatim (no PluginBlockRegistry prefixing in the codeless path).
|
||||||
blocks:
|
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
|
- key: coffee:featured_pour
|
||||||
title: 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
|
category: content
|
||||||
schema: featured_pour.schema.json
|
schema: featured_pour.schema.json
|
||||||
template: featured_pour.ninjatpl
|
template: featured_pour.ninjatpl
|
||||||
- key: coffee:footer
|
- key: coffee:roast_profile
|
||||||
title: Footer
|
title: Roast Profile
|
||||||
description: Torn-edge footer with optional location summary and newsletter caption.
|
description: Roastery spec card — origin, process, roast level and tasting notes with a circular roast stamp.
|
||||||
category: layout
|
category: content
|
||||||
schema: footer.schema.json
|
schema: roast_profile.schema.json
|
||||||
template: footer.ninjatpl
|
template: roast_profile.ninjatpl
|
||||||
- key: coffee:doodle_divider
|
- key: coffee:loyalty_card
|
||||||
title: Doodle Divider
|
title: Loyalty Card
|
||||||
description: Hand-drawn divider in one of four motifs (beans, croissant, cup, leaf).
|
description: Ten-stamp coffee punch card with a reward on the tenth stamp.
|
||||||
category: layout
|
category: content
|
||||||
schema: doodle_divider.schema.json
|
schema: loyalty_card.schema.json
|
||||||
template: doodle_divider.ninjatpl
|
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
|
||||||
|
|||||||
29
blocks/brew_guide.ninjatpl
Normal file
29
blocks/brew_guide.ninjatpl
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<section data-block="coffee:brew_guide" class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-3xl px-4">
|
||||||
|
<div class="coffee-card p-6 md:p-8">
|
||||||
|
{% if kicker %}<p class="coffee-hand text-2xl text-accent">{{ kicker }}</p>{% endif %}
|
||||||
|
{% if method %}<h3 class="coffee-display text-3xl text-primary"><span class="coffee-doodle-underline">{{ method }}</span></h3>{% endif %}
|
||||||
|
{% if intro %}<p class="coffee-body mt-3 text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||||
|
<div class="mt-6 flex flex-wrap gap-2">
|
||||||
|
{% if ratio %}<span class="coffee-chip"><span class="coffee-hand text-base text-accent">Ratio</span> <span class="coffee-mono">{{ ratio }}</span></span>{% endif %}
|
||||||
|
{% if grind %}<span class="coffee-chip"><span class="coffee-hand text-base text-accent">Grind</span> {{ grind }}</span>{% endif %}
|
||||||
|
{% if water %}<span class="coffee-chip"><span class="coffee-hand text-base text-accent">Water</span> <span class="coffee-mono">{{ water }}</span></span>{% endif %}
|
||||||
|
{% if temperature %}<span class="coffee-chip"><span class="coffee-hand text-base text-accent">Temp</span> <span class="coffee-mono">{{ temperature }}</span></span>{% endif %}
|
||||||
|
{% if time %}<span class="coffee-chip"><span class="coffee-hand text-base text-accent">Time</span> <span class="coffee-mono">{{ time }}</span></span>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if steps %}
|
||||||
|
<ol class="mt-8 space-y-5">
|
||||||
|
{% for step in steps %}
|
||||||
|
<li class="flex gap-4">
|
||||||
|
<span class="coffee-stamp coffee-stamp-sm shrink-0">{{ forloop.Counter }}</span>
|
||||||
|
<div class="pt-1">
|
||||||
|
{% if step.title %}<p class="coffee-display text-lg text-primary">{{ step.title }}</p>{% endif %}
|
||||||
|
{% if step.text %}<p class="coffee-body text-foreground">{{ step.text }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
67
blocks/brew_guide.schema.json
Normal file
67
blocks/brew_guide.schema.json
Normal file
@ -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" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,39 +0,0 @@
|
|||||||
<div data-block="coffee:doodle_divider" data-motif="{{ motif|default:"beans" }}" class="my-8 flex items-center justify-center text-accent">
|
|
||||||
{% if motif == "croissant" %}
|
|
||||||
<svg viewBox="0 0 200 32" class="w-48 h-8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<path d="M4 18 L70 18"></path>
|
|
||||||
<path d="M130 18 L196 18"></path>
|
|
||||||
<path d="M85 22 Q 90 8 100 8 Q 110 8 115 22"></path>
|
|
||||||
<path d="M88 20 L92 14"></path>
|
|
||||||
<path d="M96 18 L100 10"></path>
|
|
||||||
<path d="M104 18 L108 10"></path>
|
|
||||||
<path d="M112 20 L108 14"></path>
|
|
||||||
</svg>
|
|
||||||
{% elif motif == "cup" %}
|
|
||||||
<svg viewBox="0 0 200 32" class="w-48 h-8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<path d="M4 18 L75 18"></path>
|
|
||||||
<path d="M125 18 L196 18"></path>
|
|
||||||
<path d="M88 8 L112 8 L110 24 L90 24 Z"></path>
|
|
||||||
<path d="M112 12 Q 120 12 120 16 Q 120 20 112 20"></path>
|
|
||||||
<path d="M93 4 Q 95 1 97 4"></path>
|
|
||||||
<path d="M99 4 Q 101 1 103 4"></path>
|
|
||||||
<path d="M105 4 Q 107 1 109 4"></path>
|
|
||||||
</svg>
|
|
||||||
{% elif motif == "leaf" %}
|
|
||||||
<svg viewBox="0 0 200 32" class="w-48 h-8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<path d="M4 18 L80 18"></path>
|
|
||||||
<path d="M120 18 L196 18"></path>
|
|
||||||
<path d="M90 20 Q 100 4 110 20 Q 100 24 90 20 Z"></path>
|
|
||||||
<path d="M93 19 L107 19"></path>
|
|
||||||
</svg>
|
|
||||||
{% else %}
|
|
||||||
<svg viewBox="0 0 200 32" class="w-48 h-8" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<path d="M4 18 L80 18"></path>
|
|
||||||
<path d="M120 18 L196 18"></path>
|
|
||||||
<ellipse cx="95" cy="18" rx="6" ry="9"></ellipse>
|
|
||||||
<path d="M95 9 L95 27"></path>
|
|
||||||
<ellipse cx="108" cy="18" rx="6" ry="9"></ellipse>
|
|
||||||
<path d="M108 9 L108 27"></path>
|
|
||||||
</svg>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
@ -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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,15 +1,14 @@
|
|||||||
<section data-block="coffee:featured_pour" class="my-10">
|
<section data-block="coffee:featured_pour" class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
<div class="coffee-card max-w-4xl mx-auto p-6 grid gap-6 md:grid-cols-[2fr_3fr] items-center relative">
|
<div class="coffee-card coffee-torn-bottom relative mx-auto grid max-w-4xl gap-6 p-6 md:grid-cols-[2fr_3fr] md:items-center">
|
||||||
<span class="absolute -top-3 left-6 inline-flex items-center gap-1 px-3 py-1 text-xs uppercase tracking-wider rounded-sm bg-accent text-accent-foreground coffee-body">
|
<span class="coffee-hand absolute -top-4 left-6 inline-flex rotate-[-3deg] items-center gap-1 rounded-full bg-accent px-4 py-1 text-lg text-accent-foreground shadow-sm">{{ label|default:"Featured pour" }}</span>
|
||||||
Featured
|
|
||||||
</span>
|
|
||||||
<div class="aspect-square overflow-hidden rounded-sm bg-secondary">
|
<div class="aspect-square overflow-hidden rounded-sm bg-secondary">
|
||||||
{% if image %}<img src="{{ image }}" alt="{{ name }}" class="w-full h-full object-cover" loading="lazy">{% else %}<div class="w-full h-full flex items-center justify-center coffee-body text-sm text-muted-foreground italic">Add a hero image</div>{% endif %}
|
{% if image %}{% img image alt=name|default:"Featured pour" class="h-full w-full object-cover" %}{% else %}<div class="flex h-full w-full items-center justify-center coffee-hand text-2xl text-muted-foreground">Add a hero image</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-3">
|
<div class="flex flex-col gap-3">
|
||||||
{% if name %}<h3 class="coffee-display text-3xl text-primary">{{ name }}</h3>{% else %}<h3 class="coffee-display text-3xl text-muted-foreground italic">Featured pour</h3>{% endif %}
|
{% if origin %}<p class="coffee-hand text-xl text-accent">{{ origin }}</p>{% endif %}
|
||||||
{% if tasting %}<div class="coffee-body text-foreground prose-sm max-w-none">{{ tasting|safe }}</div>{% endif %}
|
{% if name %}<h3 class="coffee-display text-3xl text-primary"><span class="coffee-doodle-underline">{{ name }}</span></h3>{% else %}<h3 class="coffee-display text-3xl italic text-muted-foreground">Featured pour</h3>{% endif %}
|
||||||
{% if price %}<div class="coffee-price price text-xl">{{ price }}</div>{% endif %}
|
{% if tasting %}<div class="coffee-body max-w-none text-foreground [&_strong]:text-primary">{{ tasting|safe }}</div>{% endif %}
|
||||||
|
{% if price %}<div class="coffee-price mt-1 text-2xl">{{ currency|default:"$" }}{{ price }}</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@ -4,6 +4,19 @@
|
|||||||
"description": "Hero card for a featured coffee, tea, or pastry with image, tasting notes and price",
|
"description": "Hero card for a featured coffee, tea, or pastry with image, tasting notes and price",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"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": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "Name",
|
"title": "Name",
|
||||||
@ -22,6 +35,12 @@
|
|||||||
"description": "Hero image",
|
"description": "Hero image",
|
||||||
"x-editor": "media"
|
"x-editor": "media"
|
||||||
},
|
},
|
||||||
|
"currency": {
|
||||||
|
"type": "string",
|
||||||
|
"title": "Currency Symbol",
|
||||||
|
"default": "$",
|
||||||
|
"x-editor": "text"
|
||||||
|
},
|
||||||
"price": {
|
"price": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"title": "Price",
|
"title": "Price",
|
||||||
|
|||||||
@ -1,28 +0,0 @@
|
|||||||
<div data-block="coffee:footer" class="coffee-torn-top bg-card text-card-foreground pt-12 pb-8 mt-12">
|
|
||||||
<div class="max-w-5xl mx-auto px-6 grid gap-8 md:grid-cols-3 coffee-body">
|
|
||||||
<div>
|
|
||||||
<h4 class="coffee-display text-xl text-primary mb-2">Hello there</h4>
|
|
||||||
<p class="text-sm text-muted-foreground">Pull up a seat. Pastries from 7, coffee until late.</p>
|
|
||||||
</div>
|
|
||||||
{% if showLocation != "false" and showLocation != false %}
|
|
||||||
<div>
|
|
||||||
<h4 class="coffee-display text-xl text-primary mb-2">Visit</h4>
|
|
||||||
<address class="not-italic text-sm text-foreground whitespace-pre-line">42 Roastery Lane
|
|
||||||
City, State 9000</address>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div>
|
|
||||||
<h4 class="coffee-display text-xl text-primary mb-2">Stay in touch</h4>
|
|
||||||
{% if newsletterText %}<p class="text-sm text-muted-foreground mb-3">{{ newsletterText }}</p>{% else %}<p class="text-sm text-muted-foreground mb-3">Slow notes from the bar. Brew tips, seasonal pours, no spam.</p>{% endif %}
|
|
||||||
<form class="flex gap-2" action="#" method="post" onsubmit="event.preventDefault();">
|
|
||||||
<label class="sr-only" for="coffee-newsletter-email">Email</label>
|
|
||||||
<input id="coffee-newsletter-email" name="email" type="email" required placeholder="you@cafe.com" class="flex-1 px-3 py-2 text-sm coffee-body bg-input text-foreground border border-border rounded-sm focus:outline-none focus:ring-2 focus:ring-ring">
|
|
||||||
<button type="submit" class="kraft-tag">Subscribe</button>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="max-w-5xl mx-auto px-6 mt-10 pt-6 border-t coffee-pencil-rule text-xs coffee-body text-muted-foreground flex flex-wrap gap-2 justify-between">
|
|
||||||
<span>© Coffee theme — kraft paper edition.</span>
|
|
||||||
<span>Hand-drawn with care.</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,13 +0,0 @@
|
|||||||
{% if context.now %}{% set today = context.now|date:"D"|lower %}{% endif %}
|
|
||||||
<aside data-block="coffee:hours_strip" class="my-4">
|
|
||||||
<ul class="coffee-card grid grid-cols-1 sm:grid-cols-2 md:grid-cols-7 gap-1 p-2 max-w-5xl mx-auto">
|
|
||||||
{% if not hours %}<li class="coffee-body text-sm text-muted-foreground italic px-3 py-2 col-span-full text-center">Add weekday hours to display the strip.</li>{% endif %}
|
|
||||||
{% for row in hours %}
|
|
||||||
<li class="coffee-body px-2 py-1 flex items-baseline gap-2 text-sm rounded-sm {% if row.day and row.day|lower|slice:":3" == today %}coffee-hours-today is-today today{% endif %}">
|
|
||||||
<span class="coffee-display font-semibold text-foreground w-12 shrink-0">{{ row.day }}</span>
|
|
||||||
{% if row.day and row.day|lower|slice:":3" == today %}<span class="coffee-body text-[10px] uppercase tracking-wider text-accent">{{ todayLabel|default:"Today" }}</span>{% endif %}
|
|
||||||
<span class="coffee-mono text-foreground">{% if row.open or row.close %}{{ row.open }} – {{ row.close }}{% else %}Closed{% endif %}</span>
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</aside>
|
|
||||||
@ -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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
<section data-block="coffee:location_card" class="my-8">
|
|
||||||
<div class="coffee-card max-w-3xl mx-auto p-6 grid gap-6 md:grid-cols-2">
|
|
||||||
<div class="relative aspect-[4/3] overflow-hidden rounded-sm bg-secondary">
|
|
||||||
{% if mapImage %}<img src="{{ mapImage }}" alt="Map of our location" class="absolute inset-0 w-full h-full object-cover" loading="lazy">{% else %}<div class="absolute inset-0 flex items-center justify-center coffee-body text-sm text-muted-foreground italic">Map preview</div>{% endif %}
|
|
||||||
<svg class="coffee-pin absolute top-1/3 left-1/2 -translate-x-1/2 w-10 h-10" viewBox="0 0 40 56" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
|
|
||||||
<path d="M20 4 Q 6 4 6 22 Q 6 36 20 52 Q 34 36 34 22 Q 34 4 20 4 Z"></path>
|
|
||||||
<circle cx="20" cy="22" r="6"></circle>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<div class="flex flex-col gap-3 justify-center">
|
|
||||||
<h3 class="coffee-display text-2xl text-primary">
|
|
||||||
<span class="coffee-doodle-underline">Find us</span>
|
|
||||||
</h3>
|
|
||||||
{% if address %}<address class="coffee-body not-italic text-foreground whitespace-pre-line">{{ address }}</address>{% else %}<p class="coffee-body text-sm text-muted-foreground italic">Add an address to render the location card.</p>{% endif %}
|
|
||||||
{% if directionsUrl %}<a href="{{ directionsUrl }}" class="kraft-tag mt-2 self-start" target="_blank" rel="noopener noreferrer">Directions</a>{% endif %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
24
blocks/loyalty_card.ninjatpl
Normal file
24
blocks/loyalty_card.ninjatpl
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{% with filled=filled|default:0 %}
|
||||||
|
<section data-block="coffee:loyalty_card" class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-md px-4">
|
||||||
|
<div class="coffee-card coffee-torn-top coffee-torn-bottom relative p-6 text-center">
|
||||||
|
{% if title %}<h3 class="coffee-display text-2xl text-primary">{{ title }}</h3>{% endif %}
|
||||||
|
{% if subtitle %}<p class="coffee-hand mt-1 text-xl text-accent">{{ subtitle }}</p>{% endif %}
|
||||||
|
<div class="mt-6 grid grid-cols-5 gap-3">
|
||||||
|
<span class="coffee-punch{% if filled >= 1 %} coffee-punch-on{% endif %}">{% if filled >= 1 %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
||||||
|
<span class="coffee-punch{% if filled >= 2 %} coffee-punch-on{% endif %}">{% if filled >= 2 %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
||||||
|
<span class="coffee-punch{% if filled >= 3 %} coffee-punch-on{% endif %}">{% if filled >= 3 %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
||||||
|
<span class="coffee-punch{% if filled >= 4 %} coffee-punch-on{% endif %}">{% if filled >= 4 %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
||||||
|
<span class="coffee-punch{% if filled >= 5 %} coffee-punch-on{% endif %}">{% if filled >= 5 %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
||||||
|
<span class="coffee-punch{% if filled >= 6 %} coffee-punch-on{% endif %}">{% if filled >= 6 %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
||||||
|
<span class="coffee-punch{% if filled >= 7 %} coffee-punch-on{% endif %}">{% if filled >= 7 %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
||||||
|
<span class="coffee-punch{% if filled >= 8 %} coffee-punch-on{% endif %}">{% if filled >= 8 %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
||||||
|
<span class="coffee-punch{% if filled >= 9 %} coffee-punch-on{% endif %}">{% if filled >= 9 %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}</span>
|
||||||
|
<span class="coffee-punch{% if filled >= 10 %} coffee-punch-on coffee-punch-reward{% endif %}"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#gift"></use></svg></span>
|
||||||
|
</div>
|
||||||
|
{% if reward %}<p class="coffee-body mt-6 text-sm text-muted-foreground">{{ reward }}</p>{% endif %}
|
||||||
|
{% if footnote %}<p class="coffee-hand mt-2 text-lg text-accent">{{ footnote }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endwith %}
|
||||||
38
blocks/loyalty_card.schema.json
Normal file
38
blocks/loyalty_card.schema.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<section data-block="coffee:menu_board" class="my-10">
|
|
||||||
<div class="coffee-card max-w-3xl mx-auto px-6 py-8">
|
|
||||||
<h2 class="coffee-display text-3xl mb-6 text-center">
|
|
||||||
<span class="coffee-doodle-underline">{{ title|default:"Menu" }}</span>
|
|
||||||
</h2>
|
|
||||||
{% if not sections %}<p class="coffee-body text-sm text-muted-foreground text-center italic">Add a section to get started.</p>{% endif %}
|
|
||||||
{% for section in sections %}
|
|
||||||
<div class="{% if forloop.First %}mb-6{% else %}mt-8 mb-6{% endif %}">
|
|
||||||
{% if section.name %}<h3 class="coffee-display text-xl mb-3 text-primary">{{ section.name }}</h3>{% endif %}
|
|
||||||
{% if not section.items %}<p class="coffee-body text-sm text-muted-foreground italic">No items yet.</p>{% endif %}
|
|
||||||
<ul class="space-y-2">
|
|
||||||
{% for item in section.items %}
|
|
||||||
<li class="flex items-baseline gap-3 py-1 border-b coffee-pencil-rule border-dashed">
|
|
||||||
<div class="flex-1 min-w-0">
|
|
||||||
<div class="coffee-body font-medium text-foreground">{{ item.name }}</div>
|
|
||||||
{% if item.note %}<div class="coffee-body text-sm text-muted-foreground">{{ item.note }}</div>{% endif %}
|
|
||||||
{% if item.allergens %}<div class="coffee-body text-xs text-muted-foreground italic">Allergens: {{ item.allergens }}</div>{% endif %}
|
|
||||||
</div>
|
|
||||||
{% if item.price %}<div class="coffee-price price text-base whitespace-nowrap">{{ item.price }}</div>{% endif %}
|
|
||||||
</li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
{% endfor %}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
@ -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"]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
26
blocks/roast_profile.ninjatpl
Normal file
26
blocks/roast_profile.ninjatpl
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<section data-block="coffee:roast_profile" class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-4xl px-4">
|
||||||
|
<div class="coffee-card grid gap-8 p-6 md:grid-cols-[3fr_2fr] md:p-8">
|
||||||
|
<div class="flex flex-col gap-4">
|
||||||
|
<div class="flex items-start justify-between gap-4">
|
||||||
|
<div>
|
||||||
|
{% if origin %}<p class="coffee-hand text-2xl text-accent">{{ origin }}</p>{% endif %}
|
||||||
|
{% if name %}<h3 class="coffee-display text-3xl text-primary">{{ name }}</h3>{% endif %}
|
||||||
|
{% if producer %}<p class="coffee-body mt-1 text-sm text-muted-foreground">{{ producer }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if roastLevel %}<span class="coffee-stamp shrink-0">{{ roastLevel }}</span>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if notes %}<div class="coffee-body text-foreground">{{ notes|safe }}</div>{% endif %}
|
||||||
|
<dl class="mt-2 grid grid-cols-2 gap-x-6 gap-y-3 border-t border-dashed border-border pt-4 text-sm">
|
||||||
|
{% if process %}<div><dt class="coffee-hand text-base text-accent">Process</dt><dd class="coffee-body font-medium">{{ process }}</dd></div>{% endif %}
|
||||||
|
{% if varietal %}<div><dt class="coffee-hand text-base text-accent">Varietal</dt><dd class="coffee-body font-medium">{{ varietal }}</dd></div>{% endif %}
|
||||||
|
{% if altitude %}<div><dt class="coffee-hand text-base text-accent">Altitude</dt><dd class="coffee-body font-medium">{{ altitude }}</dd></div>{% endif %}
|
||||||
|
{% if harvest %}<div><dt class="coffee-hand text-base text-accent">Harvest</dt><dd class="coffee-body font-medium">{{ harvest }}</dd></div>{% endif %}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
<div class="overflow-hidden rounded-sm bg-secondary coffee-torn-top">
|
||||||
|
{% if image %}{% img image alt=name|default:"Roast profile" class="h-full w-full object-cover" %}{% else %}<div class="flex h-full min-h-[12rem] items-center justify-center coffee-hand text-2xl text-muted-foreground">Bean photo</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
64
blocks/roast_profile.schema.json
Normal file
64
blocks/roast_profile.schema.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
27
fonts.json
27
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" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|||||||
271
manifest.yaml
271
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).
|
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
|
||||||
theme_presets: presets.json
|
theme_presets: presets.json
|
||||||
bundled_fonts: fonts.json
|
bundled_fonts: fonts.json
|
||||||
master_pages: master_pages.json
|
master_pages: master_pages.json
|
||||||
|
|
||||||
|
required_icon_packs:
|
||||||
|
- lucide
|
||||||
|
- simple-icons
|
||||||
|
|
||||||
system_templates:
|
system_templates:
|
||||||
- key: coffee
|
- key: coffee
|
||||||
title: 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:
|
page_templates:
|
||||||
- system: coffee
|
- system: coffee
|
||||||
key: default
|
key: default
|
||||||
title: Default
|
title: Default
|
||||||
description: "Header + main + footer with warm paper background"
|
description: "Header + main + footer over warm paper grain."
|
||||||
slots: [header, main, footer]
|
slots: [header, main, footer]
|
||||||
- system: coffee
|
- system: coffee
|
||||||
key: landing
|
key: landing
|
||||||
title: Landing
|
title: Landing
|
||||||
description: "Hero pour + featured menu, big imagery"
|
description: "Hero pour, featured menu, story and invitation."
|
||||||
slots: [hero, menu, story, cta, footer]
|
slots: [hero, menu, story, cta, footer]
|
||||||
- system: coffee
|
- system: coffee
|
||||||
key: article
|
key: article
|
||||||
title: 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]
|
slots: [header, main, footer]
|
||||||
- system: coffee
|
- system: coffee
|
||||||
key: full-width
|
key: full-width
|
||||||
title: Full Width
|
title: Full Width
|
||||||
description: "Edge-to-edge gallery / interior shots"
|
description: "Edge-to-edge gallery and interior shots."
|
||||||
slots: [header, main, footer]
|
slots: [header, main, footer]
|
||||||
|
- system: coffee
|
||||||
template_overrides:
|
key: blog-index
|
||||||
- { template: coffee, block: heading }
|
title: Blog Index
|
||||||
- { template: coffee, block: text }
|
description: "Blog hero, featured posts and a post grid."
|
||||||
- { template: coffee, block: button }
|
slots: [header, featured, main, footer]
|
||||||
- { template: coffee, block: image }
|
- 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:
|
email_wrappers:
|
||||||
- coffee
|
- 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:
|
css:
|
||||||
input_css_append: |
|
input_css_append: |
|
||||||
/* Coffee theme styles
|
/* Coffee theme styles
|
||||||
*
|
*
|
||||||
* Uses the 19 shadcn-style HSL token CSS variables (--background,
|
* Dual-mode via the 19 shadcn-style HSL token CSS variables (--background,
|
||||||
* --foreground, --primary, --accent, --border, --muted, --card, ...) via
|
* --foreground, --primary, --accent, --border, --muted, --card, ...) consumed
|
||||||
* `hsl(var(--token))`. Font families are resolved through the BlockNinja font
|
* as hsl(var(--token)). No literal colors. Font families resolve ONLY through
|
||||||
* variables (--font-heading, --font-body, --font-mono) with fallback stacks
|
* the BlockNinja font variables (--font-heading, --font-body, --font-mono) with
|
||||||
* derived from the spec §3 typography list.
|
* 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 {
|
:root {
|
||||||
--coffee-heading-fallback: "Fraunces", "Playfair Display", Georgia, "Times New Roman", serif;
|
--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;
|
--coffee-mono-fallback: "JetBrains Mono", "Fira Code", Menlo, Consolas, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Paper grain background --------------------------------------------
|
/* --- 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.
|
|
||||||
*/
|
|
||||||
body.coffee-paper {
|
body.coffee-paper {
|
||||||
background-color: hsl(var(--background));
|
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-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;
|
background-repeat: repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Headings ----------------------------------------------------------- */
|
/* --- Type roles --------------------------------------------------------- */
|
||||||
.coffee-display {
|
.coffee-display {
|
||||||
font-family: var(--font-heading, var(--coffee-heading-fallback));
|
font-family: var(--font-heading, var(--coffee-heading-fallback));
|
||||||
font-feature-settings: "liga" 1, "dlig" 1;
|
font-feature-settings: "liga" 1, "dlig" 1;
|
||||||
letter-spacing: -0.01em;
|
letter-spacing: -0.01em;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coffee-body {
|
.coffee-body {
|
||||||
font-family: var(--font-body, var(--coffee-body-fallback));
|
font-family: var(--font-body, var(--coffee-body-fallback));
|
||||||
line-height: 1.65;
|
line-height: 1.65;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coffee-mono {
|
.coffee-mono {
|
||||||
font-family: var(--font-mono, var(--coffee-mono-fallback));
|
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 {
|
.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-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;
|
background-repeat: no-repeat;
|
||||||
@ -98,7 +160,7 @@ css:
|
|||||||
padding-bottom: 0.18em;
|
padding-bottom: 0.18em;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Drop-cap (text override / article body) ---------------------------- */
|
/* --- Drop-cap ----------------------------------------------------------- */
|
||||||
.coffee-dropcap > p:first-of-type::first-letter {
|
.coffee-dropcap > p:first-of-type::first-letter {
|
||||||
font-family: var(--font-heading, var(--coffee-heading-fallback));
|
font-family: var(--font-heading, var(--coffee-heading-fallback));
|
||||||
font-size: 4em;
|
font-size: 4em;
|
||||||
@ -108,7 +170,7 @@ css:
|
|||||||
color: hsl(var(--primary));
|
color: hsl(var(--primary));
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Kraft-tag button --------------------------------------------------- */
|
/* --- Kraft-tag button (stamp press on hover) ---------------------------- */
|
||||||
.kraft-tag {
|
.kraft-tag {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
@ -122,22 +184,29 @@ css:
|
|||||||
font-family: var(--font-body, var(--coffee-body-fallback));
|
font-family: var(--font-body, var(--coffee-body-fallback));
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
letter-spacing: 0.02em;
|
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;
|
transition: transform 120ms ease, box-shadow 120ms ease;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
.kraft-tag:hover {
|
.kraft-tag:hover {
|
||||||
transform: rotate(-1.2deg);
|
transform: translateY(1px) rotate(-1.2deg);
|
||||||
box-shadow: 0 2px 0 hsl(var(--border));
|
box-shadow: 0 1px 0 hsl(var(--border));
|
||||||
}
|
}
|
||||||
|
|
||||||
.kraft-tag:focus-visible {
|
.kraft-tag:focus-visible {
|
||||||
outline: 2px solid hsl(var(--ring));
|
outline: 2px solid hsl(var(--ring));
|
||||||
outline-offset: 3px;
|
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 {
|
.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");
|
-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");
|
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%;
|
-webkit-mask-size: 100% 100%;
|
||||||
mask-size: 100% 100%;
|
mask-size: 100% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coffee-torn-bottom {
|
.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");
|
-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");
|
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%;
|
mask-size: 100% 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Coffee surfaces ---------------------------------------------------- */
|
/* --- Surfaces ----------------------------------------------------------- */
|
||||||
.coffee-card {
|
.coffee-card {
|
||||||
background-color: hsl(var(--card));
|
background-color: hsl(var(--card));
|
||||||
color: hsl(var(--card-foreground));
|
color: hsl(var(--card-foreground));
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.coffee-frame {
|
.coffee-frame {
|
||||||
border: 1px solid hsl(var(--border));
|
border: 1px solid hsl(var(--border));
|
||||||
background-color: hsl(var(--card));
|
background-color: hsl(var(--card));
|
||||||
}
|
}
|
||||||
|
|
||||||
.coffee-pencil-rule {
|
.coffee-pencil-rule {
|
||||||
border-color: hsl(var(--border));
|
border-color: hsl(var(--border));
|
||||||
border-style: solid;
|
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 {
|
.coffee-hours-today {
|
||||||
background-color: hsl(var(--accent) / 0.15);
|
background-color: hsl(var(--accent) / 0.15);
|
||||||
color: hsl(var(--accent-foreground));
|
color: hsl(var(--accent-foreground));
|
||||||
@ -182,14 +265,108 @@ css:
|
|||||||
padding-left: 0.75rem;
|
padding-left: 0.75rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* --- Price typography --------------------------------------------------- */
|
/* --- Price + spec chips ------------------------------------------------- */
|
||||||
.coffee-price {
|
.coffee-price {
|
||||||
font-family: var(--font-mono, var(--coffee-mono-fallback));
|
font-family: var(--font-mono, var(--coffee-mono-fallback));
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
color: hsl(var(--accent));
|
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 {
|
.coffee-pin {
|
||||||
color: hsl(var(--accent));
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -2,12 +2,11 @@
|
|||||||
{
|
{
|
||||||
"key": "coffee:default-master",
|
"key": "coffee:default-master",
|
||||||
"title": "Coffee Default Master",
|
"title": "Coffee Default Master",
|
||||||
"page_templates": ["default", "article"],
|
"page_templates": ["default", "article", "full-width", "contact"],
|
||||||
"blocks": [
|
"blocks": [
|
||||||
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main" }, "slot": "header", "sort_order": 0 },
|
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main", "companyName": "Ritual Coffee" }, "slot": "header", "sort_order": 0 },
|
||||||
{ "block_key": "coffee:hours_strip", "title": "Hours Strip", "content": { "todayLabel": "Today" }, "slot": "header", "sort_order": 1 },
|
|
||||||
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Pour something in here" }, "slot": "main", "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",
|
"title": "Coffee Landing Master",
|
||||||
"page_templates": ["landing"],
|
"page_templates": ["landing"],
|
||||||
"blocks": [
|
"blocks": [
|
||||||
{ "block_key": "navbar", "title": "Main Navigation", "content": { "menuName": "main" }, "slot": "header", "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 Slot", "content": { "slotName": "hero", "placeholder": "Drop a hero block" }, "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": "coffee:featured_pour", "title": "Featured Pour", "content": {}, "slot": "menu", "sort_order": 0 },
|
{ "block_key": "slot", "title": "Menu", "content": { "slotName": "menu", "placeholder": "Feature the menu" }, "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": "slot", "title": "Story", "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": "slot", "title": "Call To Action", "content": { "slotName": "cta", "placeholder": "Invite them in" }, "slot": "cta", "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 }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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 }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@ -7,3 +7,4 @@ description = "Warm, hand-crafted theme for cafes, bakeries, and slow-craft make
|
|||||||
kind = "theme"
|
kind = "theme"
|
||||||
categories = ["templates"]
|
categories = ["templates"]
|
||||||
tags = ["warm", "hospitality", "cafe", "bakery", "food", "handcraft", "organic", "artisan", "menu"]
|
tags = ["warm", "hospitality", "cafe", "bakery", "food", "handcraft", "organic", "artisan", "menu"]
|
||||||
|
required_icon_packs = ["lucide", "simple-icons"]
|
||||||
|
|||||||
112
presets.json
112
presets.json
@ -2,9 +2,9 @@
|
|||||||
{
|
{
|
||||||
"id": "morning-pour",
|
"id": "morning-pour",
|
||||||
"name": "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": {
|
"theme": {
|
||||||
"mode": "light",
|
"mode": "both",
|
||||||
"lightColors": {
|
"lightColors": {
|
||||||
"background": "36 30% 96%",
|
"background": "36 30% 96%",
|
||||||
"foreground": "25 35% 18%",
|
"foreground": "25 35% 18%",
|
||||||
@ -52,9 +52,9 @@
|
|||||||
{
|
{
|
||||||
"id": "dark-roast",
|
"id": "dark-roast",
|
||||||
"name": "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": {
|
"theme": {
|
||||||
"mode": "dark",
|
"mode": "both",
|
||||||
"lightColors": {
|
"lightColors": {
|
||||||
"background": "36 30% 96%",
|
"background": "36 30% 96%",
|
||||||
"foreground": "25 35% 18%",
|
"foreground": "25 35% 18%",
|
||||||
@ -102,9 +102,9 @@
|
|||||||
{
|
{
|
||||||
"id": "kraft-cream",
|
"id": "kraft-cream",
|
||||||
"name": "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": {
|
"theme": {
|
||||||
"mode": "light",
|
"mode": "both",
|
||||||
"lightColors": {
|
"lightColors": {
|
||||||
"background": "32 35% 92%",
|
"background": "32 35% 92%",
|
||||||
"foreground": "22 40% 16%",
|
"foreground": "22 40% 16%",
|
||||||
@ -148,5 +148,105 @@
|
|||||||
"ring": "16 50% 55%"
|
"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%"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
177
seed/seed.json
Normal file
177
seed/seed.json
Normal file
@ -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": "<p>Bright and floral. Notes of peach, jasmine and brown sugar.</p>", "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": "<p>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.</p><p>Everything on the board is made in-house. The croissants are laminated overnight. The jam is cooked in small pots on the back bench.</p>", "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": "<p>Our new single origin comes from the Guji zone in southern Ethiopia. It is washed, which keeps it clean and bright.</p><p>We taste peach and jasmine up front, then a soft brown sugar finish. It works as espresso and shines as filter.</p><p>Come in this week and try it on the bar.</p>" } },
|
||||||
|
{ "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": "<p>Peach, jasmine, brown sugar.</p>", "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": "<p>We roast on Tuesdays and Fridays. Each batch is small, and we rest it before it goes on the bar.</p><p>Resting lets the coffee settle after roasting. Pulled too soon, it tastes sharp and gassy. Given a few days, it opens up.</p><p>That is why the date on the bag matters more than the date on the door.</p>" } },
|
||||||
|
{ "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": "<p>Filter is the clearest way to taste a coffee. We keep the recipe simple so you can repeat it at home.</p><p>Weigh the coffee and the water. Keep the grind even. Pour in slow circles and give it room to draw down.</p>" } },
|
||||||
|
{ "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": "<p>Members get first pick of new roasts.</p>" } }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
25
seed/workflows.json
Normal file
25
seed/workflows.json
Normal file
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
{{ head_html|safe }}
|
{{ head_html|safe }}
|
||||||
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||||
{{ admin_banner_html|safe }}
|
{{ admin_banner_html|safe }}
|
||||||
|
|||||||
19
templates/coffee/auth.ninjatpl
Normal file
19
templates/coffee/auth.ninjatpl
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
<main class="flex flex-grow w-full flex-col items-center justify-center px-4 py-12">
|
||||||
|
<div class="w-full max-w-md">
|
||||||
|
<a href="/" class="mb-6 flex items-center justify-center" aria-label="Home">
|
||||||
|
{% if site_settings.Logo %}<img src="{{ site_settings.Logo }}" alt="{{ site_settings.LogoAlt|default:site_settings.Title }}" class="h-10 w-auto">{% else %}<span class="coffee-display text-3xl text-primary">{{ site_settings.Title|default:"Coffee" }}</span>{% endif %}
|
||||||
|
</a>
|
||||||
|
<div>
|
||||||
|
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="coffee-card p-6 text-center coffee-body text-muted-foreground"><p>No form assigned to this page.</p></div>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if slots.footer %}<div class="coffee-body mt-6 text-center text-sm text-muted-foreground">{{ slots.footer|safe }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
15
templates/coffee/blog-index.ninjatpl
Normal file
15
templates/coffee/blog-index.ninjatpl
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
<header class="w-full">{{ slots.header|safe }}</header>
|
||||||
|
<main class="flex-grow w-full">
|
||||||
|
{% if title %}<div class="mx-auto max-w-6xl px-4 pt-12"><h1 class="coffee-display text-4xl text-primary md:text-5xl"><span class="coffee-doodle-underline">{{ title }}</span></h1>{% if page_meta.MetaDescription %}<p class="coffee-body mt-3 max-w-2xl text-lg text-muted-foreground">{{ page_meta.MetaDescription }}</p>{% endif %}</div>{% endif %}
|
||||||
|
{% if slots.featured %}<section class="w-full">{{ slots.featured|safe }}</section>{% endif %}
|
||||||
|
{% if slots.main %}<section class="w-full">{{ slots.main|safe }}</section>{% else %}<div class="py-20 text-center coffee-hand text-2xl text-muted-foreground"><p>No stories brewed yet.</p></div>{% endif %}
|
||||||
|
</main>
|
||||||
|
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
13
templates/coffee/contact.ninjatpl
Normal file
13
templates/coffee/contact.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
|
{{ head_html|safe }}
|
||||||
|
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||||
|
{{ admin_banner_html|safe }}
|
||||||
|
<header class="w-full">{{ slots.header|safe }}</header>
|
||||||
|
<main class="flex-grow w-full">
|
||||||
|
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center coffee-hand text-2xl text-muted-foreground"><p>Add a contact form and your hours.</p></div>{% endif %}
|
||||||
|
</main>
|
||||||
|
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||||
|
{{ body_end_html|safe }}
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@ -1,11 +1,11 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
{{ head_html|safe }}
|
{{ head_html|safe }}
|
||||||
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||||
{{ admin_banner_html|safe }}
|
{{ admin_banner_html|safe }}
|
||||||
<header class="w-full">{{ slots.header|safe }}</header>
|
<header class="w-full">{{ slots.header|safe }}</header>
|
||||||
<main class="flex-grow max-w-5xl mx-auto w-full px-4 py-8">
|
<main class="flex-grow w-full">
|
||||||
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center coffee-body text-muted-foreground italic"><p>Pour something in here.</p></div>{% endif %}
|
{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center coffee-hand text-2xl text-muted-foreground"><p>Pour something in here.</p></div>{% endif %}
|
||||||
</main>
|
</main>
|
||||||
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>
|
||||||
{{ body_end_html|safe }}
|
{{ body_end_html|safe }}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
{{ head_html|safe }}
|
{{ head_html|safe }}
|
||||||
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||||
{{ admin_banner_html|safe }}
|
{{ admin_banner_html|safe }}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||||
{{ head_html|safe }}
|
{{ head_html|safe }}
|
||||||
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
<body class="coffee-paper coffee-body bg-background text-foreground antialiased min-h-screen flex flex-col">
|
||||||
{{ admin_banner_html|safe }}
|
{{ admin_banner_html|safe }}
|
||||||
|
|||||||
21
templates/overrides/coffee/announcement-bar.ninjatpl
Normal file
21
templates/overrides/coffee/announcement-bar.ninjatpl
Normal file
@ -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 %}
|
||||||
|
<div data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full {{ anncls }} {{ class }}">
|
||||||
|
<div class="relative mx-auto flex max-w-7xl items-center justify-center gap-2 px-10 py-2 text-center text-sm">
|
||||||
|
<p class="coffee-body font-medium">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline decoration-dashed underline-offset-2 hover:opacity-80">{{ linkText }}</a>{% endif %}</p>
|
||||||
|
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="absolute right-3 top-1/2 inline-flex -translate-y-1/2 items-center justify-center rounded p-1 transition-colors hover:bg-black/10"><svg class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></button>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if dismissable %}<script>
|
||||||
|
(function(){
|
||||||
|
var bars=document.querySelectorAll('[data-bn-announcement]:not([data-bn-ann-wired])');
|
||||||
|
bars.forEach(function(bar){
|
||||||
|
bar.setAttribute('data-bn-ann-wired','1');
|
||||||
|
var key='bnann:'+bar.getAttribute('data-bn-annkey');
|
||||||
|
try{if(localStorage.getItem(key)==='1'){bar.style.display='none';return;}}catch(e){}
|
||||||
|
var btn=bar.querySelector('[data-bn-ann-dismiss]');
|
||||||
|
if(btn)btn.addEventListener('click',function(){bar.style.display='none';try{localStorage.setItem(key,'1');}catch(e){}});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>{% endif %}
|
||||||
15
templates/overrides/coffee/audio-embed.ninjatpl
Normal file
15
templates/overrides/coffee/audio-embed.ninjatpl
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<figure class="bn-audio coffee-card my-8 overflow-hidden{% if class %} {{ class }}{% endif %}" data-bn-audio data-audio-url="{{ url|default:"" }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}"><div class="flex items-center gap-4 p-4">{% if artwork %}<div class="h-16 w-16 shrink-0 overflow-hidden rounded-sm bg-secondary">{% img artwork alt=title|default:"" class="h-full w-full object-cover" %}</div>{% endif %}<div class="min-w-0 flex-1">{% if title %}<p class="coffee-display truncate text-lg text-primary">{{ title }}</p>{% endif %}{% if artist %}<p class="coffee-hand truncate text-lg text-accent">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="kraft-tag text-xs" aria-label="Play{% if title %}: {{ title }}{% endif %}"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg> Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="coffee-body inline-flex items-center gap-1 text-sm text-muted-foreground hover:text-foreground"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#download"></use></svg> Download</a>{% endif %}</div></div></div></figure><script>
|
||||||
|
(function(){
|
||||||
|
if(window.__bnAudioFacade)return;window.__bnAudioFacade=true;
|
||||||
|
document.addEventListener('click',function(e){
|
||||||
|
var btn=e.target.closest('[data-bn-audio-play]');if(!btn)return;
|
||||||
|
var fig=btn.closest('[data-bn-audio]');if(!fig)return;
|
||||||
|
e.preventDefault();
|
||||||
|
var src=fig.getAttribute('data-audio-media')||fig.getAttribute('data-audio-url')||'';
|
||||||
|
if(!src)return;
|
||||||
|
var slot=btn.closest('[data-bn-audio-slot]');
|
||||||
|
var a=document.createElement('audio');a.src=src;a.controls=true;a.autoplay=true;a.preload='none';a.style.cssText='width:100%;min-width:12rem;height:2.25rem;';
|
||||||
|
btn.remove();slot.insertBefore(a,slot.firstChild);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
36
templates/overrides/coffee/auth-form.ninjatpl
Normal file
36
templates/overrides/coffee/auth-form.ninjatpl
Normal file
@ -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 %}
|
||||||
|
<div class="auth-form-logged-in coffee-card mx-auto max-w-md p-6 text-center">
|
||||||
|
<p class="coffee-body text-muted-foreground">You are logged in as <strong class="text-primary">{{ auth.display_name }}</strong></p>
|
||||||
|
<a href="/account" class="kraft-tag mt-4">My account</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% set defaultTab = default_tab|default:"login" %}
|
||||||
|
<div class="auth-form coffee-card mx-auto max-w-md p-6">
|
||||||
|
<div id="auth-message"></div>
|
||||||
|
<div class="mb-5 flex border-b border-dashed border-border">
|
||||||
|
<button type="button" onclick="document.getElementById('login-panel').classList.remove('hidden');document.getElementById('register-panel').classList.add('hidden');this.className='coffee-display flex-1 py-2 text-center border-b-2 border-primary text-primary';this.nextElementSibling.className='coffee-display flex-1 py-2 text-center border-b-2 border-transparent text-muted-foreground'" class="coffee-display flex-1 py-2 text-center border-b-2 {% if defaultTab == "register" %}border-transparent text-muted-foreground{% else %}border-primary text-primary{% endif %}">Login</button>
|
||||||
|
<button type="button" onclick="document.getElementById('register-panel').classList.remove('hidden');document.getElementById('login-panel').classList.add('hidden');this.className='coffee-display flex-1 py-2 text-center border-b-2 border-primary text-primary';this.previousElementSibling.className='coffee-display flex-1 py-2 text-center border-b-2 border-transparent text-muted-foreground'" class="coffee-display flex-1 py-2 text-center border-b-2 {% if defaultTab == "register" %}border-primary text-primary{% else %}border-transparent text-muted-foreground{% endif %}">Register</button>
|
||||||
|
</div>
|
||||||
|
<div id="login-panel" class="{% if defaultTab == "register" %}hidden{% endif %}">
|
||||||
|
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
|
||||||
|
<div class="mb-3"><label class="coffee-hand mb-1 block text-lg text-accent">Email</label><input type="email" name="email" required class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
||||||
|
<div class="mb-4"><label class="coffee-hand mb-1 block text-lg text-accent">Password</label><input type="password" name="password" required class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
||||||
|
<button type="submit" class="kraft-tag w-full justify-center" hx-disabled-elt="this">Login</button>
|
||||||
|
<p class="coffee-body mt-3 text-center text-sm"><a href="/forgot-password" class="text-accent underline decoration-dashed">Forgot password?</a></p>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div id="register-panel" class="{% if defaultTab == "register" %}{% else %}hidden{% endif %}">
|
||||||
|
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
|
||||||
|
<div class="mb-3"><label class="coffee-hand mb-1 block text-lg text-accent">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
||||||
|
<div class="mb-3"><label class="coffee-hand mb-1 block text-lg text-accent">Email</label><input type="email" name="email" required class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
||||||
|
<div class="mb-4"><label class="coffee-hand mb-1 block text-lg text-accent">Password</label><input type="password" name="password" required minlength="8" class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
||||||
|
<button type="submit" class="kraft-tag w-full justify-center" hx-disabled-elt="this">Create account</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
27
templates/overrides/coffee/auth-status.ninjatpl
Normal file
27
templates/overrides/coffee/auth-status.ninjatpl
Normal file
@ -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 %}
|
||||||
|
<div class="auth-status flex items-center gap-2">
|
||||||
|
<a href="/login" class="coffee-body rounded-lg px-3 py-1.5 text-sm font-medium opacity-80 transition-colors hover:bg-muted hover:opacity-100">Login</a>
|
||||||
|
<a href="/login?tab=register" class="kraft-tag text-sm">Sign up</a>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% if not auth.email_verified %}<div class="fixed inset-x-0 top-16 z-40 bg-accent px-4 py-2 text-center text-sm text-accent-foreground">Please verify your email address. <button hx-post="/api/auth/resend-verification" hx-swap="outerHTML" class="font-medium underline">Resend verification email</button></div>{% endif %}
|
||||||
|
<div class="auth-status relative">
|
||||||
|
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="coffee-body flex items-center gap-2 rounded-lg px-3 py-1.5 text-sm font-medium opacity-90 transition-colors hover:bg-muted hover:opacity-100">
|
||||||
|
<span class="coffee-hand flex h-7 w-7 items-center justify-center rounded-full bg-accent text-sm text-accent-foreground">{{ auth.display_name|first|upper }}</span>
|
||||||
|
<span>{{ auth.display_name }}</span>
|
||||||
|
<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
|
||||||
|
</button>
|
||||||
|
<div id="auth-dropdown-menu" class="coffee-card absolute right-0 top-full z-50 mt-1 hidden w-48 py-1 shadow-lg">
|
||||||
|
<a href="/account" class="coffee-body block px-4 py-2 text-sm text-foreground hover:bg-muted">My profile</a>
|
||||||
|
<a href="/account/reviews" class="coffee-body block px-4 py-2 text-sm text-foreground hover:bg-muted">My reviews</a>
|
||||||
|
<hr class="my-1 border-dashed border-border">
|
||||||
|
<button hx-post="/api/auth/logout" hx-swap="none" class="coffee-body block w-full px-4 py-2 text-left text-sm text-destructive hover:bg-muted">Logout</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
(function(){document.addEventListener('click',function(e){var menu=document.getElementById('auth-dropdown-menu');var btn=document.querySelector('.auth-status > button');if(!menu||!btn)return;if(!btn.contains(e.target)&&!menu.contains(e.target)){menu.classList.add('hidden');}});})();
|
||||||
|
</script>
|
||||||
|
{% endif %}
|
||||||
20
templates/overrides/coffee/author-bio-hero.ninjatpl
Normal file
20
templates/overrides/coffee/author-bio-hero.ninjatpl
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{% if author %}
|
||||||
|
<section class="border-b border-dashed border-border bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto flex max-w-3xl flex-col items-center px-4 text-center">
|
||||||
|
{% if showAvatar and author.avatar_url %}<div class="mb-5 h-24 w-24 overflow-hidden rounded-full bg-secondary ring-2 ring-border">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
|
||||||
|
<h1 class="coffee-display text-3xl text-primary md:text-4xl">{{ author.display_name }}</h1>
|
||||||
|
{% if author.job_title or author.company %}<p class="coffee-hand mt-1 text-xl text-accent">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
|
||||||
|
{% if author.bio %}<p class="coffee-body mt-4 max-w-2xl leading-relaxed text-muted-foreground">{{ author.bio }}</p>{% endif %}
|
||||||
|
<div class="coffee-body mt-5 flex flex-wrap items-center justify-center gap-4 text-sm text-muted-foreground">
|
||||||
|
{% if showPostCount %}<span>{{ author.post_count|default:0 }} post{% if author.post_count != 1 %}s{% endif %}</span>{% endif %}
|
||||||
|
{% if author.location %}<span>{{ author.location }}</span>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if showSocial %}<div class="mt-5 flex items-center justify-center gap-3">
|
||||||
|
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener" class="text-muted-foreground transition-colors hover:text-accent" title="Website"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/lucide.svg#globe"></use></svg></a>{% endif %}
|
||||||
|
{% if author.twitter_handle %}<a href="https://twitter.com/{{ author.twitter_handle|cut:"@" }}" target="_blank" rel="noopener" class="text-muted-foreground transition-colors hover:text-accent" title="Twitter"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#x"></use></svg></a>{% endif %}
|
||||||
|
{% if author.linkedin_url %}<a href="{{ author.linkedin_url }}" target="_blank" rel="noopener" class="text-muted-foreground transition-colors hover:text-accent" title="LinkedIn"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"></use></svg></a>{% endif %}
|
||||||
|
{% if author.github_url %}<a href="{{ author.github_url }}" target="_blank" rel="noopener" class="text-muted-foreground transition-colors hover:text-accent" title="GitHub"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/simple-icons.svg#github"></use></svg></a>{% endif %}
|
||||||
|
</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
12
templates/overrides/coffee/author-bio.ninjatpl
Normal file
12
templates/overrides/coffee/author-bio.ninjatpl
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% if post.author_name %}
|
||||||
|
<aside class="coffee-card p-6{% if class %} {{ class }}{% endif %}">
|
||||||
|
{% if heading %}<p class="coffee-hand mb-4 text-xl text-accent">{{ heading }}</p>{% endif %}
|
||||||
|
<div class="flex items-start gap-4">
|
||||||
|
{% if showAvatar and post.author_avatar %}<div class="h-14 w-14 flex-shrink-0 overflow-hidden rounded-full bg-secondary ring-2 ring-border">{% img post.author_avatar alt=post.author_name class="h-full w-full object-cover" %}</div>{% endif %}
|
||||||
|
<div class="min-w-0">
|
||||||
|
<h3 class="coffee-display text-base text-primary">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-accent">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
|
||||||
|
{% if post.author_bio %}<p class="coffee-body mt-1 text-sm text-muted-foreground">{{ post.author_bio }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</aside>
|
||||||
|
{% endif %}
|
||||||
22
templates/overrides/coffee/blog-hero.ninjatpl
Normal file
22
templates/overrides/coffee/blog-hero.ninjatpl
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<section class="bg-background py-12 text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-6xl px-4">
|
||||||
|
<div class="max-w-2xl">
|
||||||
|
{% if title %}<h1 class="coffee-display text-4xl text-primary md:text-5xl"><span class="coffee-doodle-underline">{{ title }}</span></h1>{% endif %}
|
||||||
|
{% if subtitle %}<p class="coffee-body mt-3 text-lg text-muted-foreground">{{ subtitle }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if posts %}
|
||||||
|
<div class="mt-10 grid grid-cols-1 gap-6 md:grid-cols-3">
|
||||||
|
{% for post in posts %}
|
||||||
|
<article class="group flex flex-col{% if forloop.First %} md:col-span-3 md:flex-row md:items-stretch md:gap-6{% endif %}">
|
||||||
|
{% if post.featured_image_url %}<a href="{{ post.url }}" class="coffee-frame mb-3 block overflow-hidden rounded-sm p-1.5{% if forloop.First %} md:mb-0 md:w-1/2{% endif %}">{% img post.featured_image_url alt=post.title class="aspect-video h-full w-full rounded-sm object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
|
||||||
|
<div class="flex flex-col justify-center{% if forloop.First %} md:w-1/2{% endif %}">
|
||||||
|
<h2 class="coffee-display {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} text-primary"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h2>
|
||||||
|
{% if showExcerpt and post.excerpt %}<p class="coffee-body mt-2 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
|
||||||
|
<div class="coffee-hand mt-3 flex flex-wrap items-center gap-x-3 text-lg text-accent">{% if post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span class="text-muted-foreground">{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
12
templates/overrides/coffee/breadcrumbs.ninjatpl
Normal file
12
templates/overrides/coffee/breadcrumbs.ninjatpl
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% set arrows = template == "arrows" %}
|
||||||
|
<nav aria-label="Breadcrumb" class="py-3{% if class %} {{ class }}{% endif %}">
|
||||||
|
<ol class="coffee-body flex flex-wrap items-center gap-1 text-sm">
|
||||||
|
{% if showHome %}<li class="flex items-center"><a href="/" class="text-muted-foreground transition-colors hover:text-accent">{{ homeLabel|default:"Home" }}</a></li>{% endif %}
|
||||||
|
{% for item in breadcrumbs %}
|
||||||
|
<li class="flex items-center">
|
||||||
|
{% if showHome or not forloop.First %}{% if arrows %}<svg class="mx-2 h-4 w-4 text-accent" aria-hidden="true"><use href="/icons/lucide.svg#chevron-right"></use></svg>{% else %}<span class="mx-2 text-accent">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
|
||||||
|
{% if item.is_current %}{% if showCurrent %}<span class="coffee-display text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="text-muted-foreground transition-colors hover:text-accent">{{ item.label }}</a>{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
@ -1 +1,5 @@
|
|||||||
{% if url %}<a href="{{ url }}" class="kraft-tag {% if variant == "secondary" %}bg-secondary text-secondary-foreground{% elif variant == "destructive" %}bg-destructive text-destructive-foreground{% endif %}">{{ text }}</a>{% else %}<button type="button" class="kraft-tag {% if variant == "secondary" %}bg-secondary text-secondary-foreground{% elif variant == "destructive" %}bg-destructive text-destructive-foreground{% endif %}">{{ text }}</button>{% endif %}
|
{% with align=align|default:"left" style=style|default:"primary" %}
|
||||||
|
<div class="bn-button flex {% if align == "center" %}justify-center{% elif align == "right" %}justify-end{% else %}justify-start{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||||
|
{% if link %}<a href="{{ link }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="kraft-tag {% if style == "secondary" %}bg-secondary text-secondary-foreground{% elif style == "outline" or style == "ghost" %}kraft-tag--ghost{% elif style == "destructive" %}bg-destructive text-destructive-foreground{% endif %} {% if size == "lg" %}text-base px-6 py-3{% elif size == "sm" %}text-xs px-3 py-1.5{% endif %}">{% if icon and iconPosition != "right" %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}</a>{% else %}<button type="button" class="kraft-tag {% if style == "secondary" %}bg-secondary text-secondary-foreground{% elif style == "outline" or style == "ghost" %}kraft-tag--ghost{% elif style == "destructive" %}bg-destructive text-destructive-foreground{% endif %} {% if size == "lg" %}text-base px-6 py-3{% elif size == "sm" %}text-xs px-3 py-1.5{% endif %}">{% if icon and iconPosition != "right" %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"></use></svg>{% endif %}</button>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
|
|||||||
12
templates/overrides/coffee/card.ninjatpl
Normal file
12
templates/overrides/coffee/card.ninjatpl
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% with layout=layout|default:"vertical" %}
|
||||||
|
<article class="bn-card coffee-card overflow-hidden transition-shadow hover:shadow-md {% if layout == "horizontal" %}flex flex-col sm:flex-row{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||||
|
{% if link %}<a href="{{ link }}" class="contents">{% endif %}
|
||||||
|
{% if media %}<div class="overflow-hidden bg-secondary {% if layout == "horizontal" %}sm:w-2/5 sm:shrink-0{% endif %}">{% if layout == "horizontal" %}{% img media alt=title|default:"" class="h-full w-full object-cover" %}{% else %}{% img media alt=title|default:"" class="aspect-video h-full w-full object-cover" %}{% endif %}</div>{% endif %}
|
||||||
|
<div class="flex flex-col gap-2 p-5 md:p-6 {% if layout == "horizontal" %}justify-center{% endif %}">
|
||||||
|
{% if title %}<h3 class="coffee-display text-xl text-primary">{{ title }}</h3>{% endif %}
|
||||||
|
{% if text %}<div class="coffee-body text-sm leading-relaxed text-muted-foreground">{{ text|safe }}</div>{% endif %}
|
||||||
|
{% if linkLabel and link %}<span class="coffee-hand mt-2 inline-flex items-center gap-1 text-lg text-accent">{{ linkLabel }} <svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></span>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if link %}</a>{% endif %}
|
||||||
|
</article>
|
||||||
|
{% endwith %}
|
||||||
17
templates/overrides/coffee/category-list.ninjatpl
Normal file
17
templates/overrides/coffee/category-list.ninjatpl
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% set pills = style == "pills" %}
|
||||||
|
<section class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
{% if heading %}<h2 class="coffee-hand mb-4 text-2xl text-accent">{{ heading }}</h2>{% endif %}
|
||||||
|
{% if pills %}
|
||||||
|
<div class="flex flex-wrap gap-2">
|
||||||
|
{% for cat in categories %}
|
||||||
|
<a href="{{ cat.url }}" class="coffee-body inline-flex items-center gap-1.5 rounded-full border border-dashed border-border bg-card px-3 py-1 text-sm text-card-foreground transition-colors hover:border-accent hover:text-accent">{{ cat.name }}{% if showCount %}<span class="text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<ul class="space-y-1">
|
||||||
|
{% for cat in categories %}
|
||||||
|
<li><a href="{{ cat.url }}" class="coffee-body flex items-center justify-between rounded-md px-3 py-2 text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground"><span>{{ cat.name }}</span>{% if showCount %}<span class="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</section>
|
||||||
17
templates/overrides/coffee/contact-card.ninjatpl
Normal file
17
templates/overrides/coffee/contact-card.ninjatpl
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-4xl px-4">
|
||||||
|
{% if heading %}<h2 class="coffee-display text-3xl text-primary md:text-4xl"><span class="coffee-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||||
|
{% if description %}<p class="coffee-body mt-3 max-w-2xl text-lg text-muted-foreground">{{ description }}</p>{% endif %}
|
||||||
|
<div class="mt-10 grid grid-cols-1 gap-4 {% if columns == 3 %}sm:grid-cols-2 lg:grid-cols-3{% elif columns == 1 %}{% else %}sm:grid-cols-2{% endif %}">
|
||||||
|
{% for channel in channels %}
|
||||||
|
<div class="coffee-card flex items-start gap-4 p-5">
|
||||||
|
{% if channel.icon %}<span class="inline-flex h-10 w-10 shrink-0 items-center justify-center rounded-full bg-accent/15 text-accent"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ channel.icon|split:":"|first }}.svg#{{ channel.icon|split:":"|last }}"></use></svg></span>{% endif %}
|
||||||
|
<div class="min-w-0">
|
||||||
|
{% if channel.label %}<div class="coffee-hand text-lg text-accent">{{ channel.label }}</div>{% endif %}
|
||||||
|
{% if channel.value %}<div class="coffee-body mt-0.5 break-words font-medium">{% if channel.link %}<a href="{{ channel.link }}" class="transition-colors hover:text-accent">{{ channel.value }}</a>{% else %}{{ channel.value }}{% endif %}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
39
templates/overrides/coffee/contact-form.ninjatpl
Normal file
39
templates/overrides/coffee/contact-form.ninjatpl
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
{% with bid=_block_id|default:context.blockId %}
|
||||||
|
<section id="contact-form-{{ bid }}" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-2xl px-4">
|
||||||
|
{% if form.submitted %}
|
||||||
|
<div class="coffee-card coffee-torn-top coffee-torn-bottom p-8 text-center">
|
||||||
|
<div class="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full bg-accent/15 text-accent"><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg></div>
|
||||||
|
<p class="coffee-display text-xl text-primary">{{ form.message|default:successMessage|default:"Thanks. Your message is on its way." }}</p>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
{% if heading %}<h2 class="coffee-display text-3xl text-primary md:text-4xl"><span class="coffee-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||||
|
{% if description %}<p class="coffee-body mt-3 text-lg text-muted-foreground">{{ description }}</p>{% endif %}
|
||||||
|
{% if form.errors._form %}<div class="coffee-body mt-6 rounded-md border border-destructive/50 bg-destructive/10 px-4 py-3 text-sm text-destructive">{{ form.errors._form }}</div>{% 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 %}
|
||||||
|
<div class="{% if field.width == "half" %}sm:col-span-1{% else %}sm:col-span-2{% endif %}">
|
||||||
|
{% if field.label %}<label for="cf-{{ bid }}-{{ field.name }}" class="coffee-hand mb-1.5 block text-lg text-accent">{{ field.label }}{% if field.required %} <span class="text-destructive">*</span>{% endif %}</label>{% endif %}
|
||||||
|
{% if field.type == "textarea" %}
|
||||||
|
<textarea id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" rows="5"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring"></textarea>
|
||||||
|
{% elif field.type == "select" %}
|
||||||
|
<select id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}"{% if field.required %} required{% endif %} class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring">
|
||||||
|
{% if field.placeholder %}<option value="">{{ field.placeholder }}</option>{% endif %}
|
||||||
|
{% for opt in field.options %}<option value="{{ opt.value }}">{{ opt.label|default:opt.value }}</option>{% endfor %}
|
||||||
|
</select>
|
||||||
|
{% else %}
|
||||||
|
<input id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}" type="{{ field.type|default:"text" }}"{% if field.required %} required{% endif %}{% if field.placeholder %} placeholder="{{ field.placeholder }}"{% endif %} class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm placeholder:text-muted-foreground focus:outline-none focus:ring-2 focus:ring-ring">
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
<div class="hidden" aria-hidden="true"><label>Leave this field empty<input type="text" name="website" tabindex="-1" autocomplete="off"></label></div>
|
||||||
|
<div class="sm:col-span-2">
|
||||||
|
<button type="submit" class="kraft-tag">{{ submitLabel|default:"Send message" }}</button>
|
||||||
|
</div>
|
||||||
|
{% endform %}
|
||||||
|
{% endwith %}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endwith %}
|
||||||
42
templates/overrides/coffee/countdown.ninjatpl
Normal file
42
templates/overrides/coffee/countdown.ninjatpl
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-4xl px-4 text-center">
|
||||||
|
{% if heading %}<h2 class="coffee-display text-3xl text-primary md:text-4xl">{{ heading }}</h2>{% endif %}
|
||||||
|
{% if intro %}<p class="coffee-body mx-auto mt-4 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||||
|
<div class="mt-10" data-countdown>
|
||||||
|
<div class="flex items-start justify-center gap-3 sm:gap-6" data-countdown-timer data-target="{{ target }}" role="timer" aria-label="Time remaining">
|
||||||
|
<div class="flex flex-col items-center {% if variant == "boxes" %}coffee-card min-w-[4.5rem] px-3 py-4{% endif %}"><span class="coffee-mono text-4xl font-bold tabular-nums text-accent sm:text-5xl" data-unit="days">--</span><span class="coffee-hand mt-1 text-lg text-muted-foreground">{{ labelDays|default:"Days" }}</span></div>
|
||||||
|
<div class="flex flex-col items-center {% if variant == "boxes" %}coffee-card min-w-[4.5rem] px-3 py-4{% endif %}"><span class="coffee-mono text-4xl font-bold tabular-nums text-accent sm:text-5xl" data-unit="hours">--</span><span class="coffee-hand mt-1 text-lg text-muted-foreground">{{ labelHours|default:"Hours" }}</span></div>
|
||||||
|
<div class="flex flex-col items-center {% if variant == "boxes" %}coffee-card min-w-[4.5rem] px-3 py-4{% endif %}"><span class="coffee-mono text-4xl font-bold tabular-nums text-accent sm:text-5xl" data-unit="minutes">--</span><span class="coffee-hand mt-1 text-lg text-muted-foreground">{{ labelMinutes|default:"Minutes" }}</span></div>
|
||||||
|
<div class="flex flex-col items-center {% if variant == "boxes" %}coffee-card min-w-[4.5rem] px-3 py-4{% endif %}"><span class="coffee-mono text-4xl font-bold tabular-nums text-accent sm:text-5xl" data-unit="seconds">--</span><span class="coffee-hand mt-1 text-lg text-muted-foreground">{{ labelSeconds|default:"Seconds" }}</span></div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-8 hidden" data-countdown-expired>
|
||||||
|
<p class="coffee-display text-xl text-primary">{{ expiredMessage|default:"This has started." }}</p>
|
||||||
|
{% if expiredUrl and expiredLabel %}<a href="{{ expiredUrl }}" class="kraft-tag mt-4 text-base px-5 py-3">{{ expiredLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<noscript><p class="coffee-body mt-6 text-muted-foreground">Counting down to <time datetime="{{ target }}">{{ target }}</time>.</p></noscript>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
document.querySelectorAll('[data-countdown-timer]').forEach(function(el){
|
||||||
|
var target = new Date(el.getAttribute('data-target')).getTime();
|
||||||
|
if (isNaN(target)) return;
|
||||||
|
var wrap = el.closest('[data-countdown]');
|
||||||
|
var expired = wrap ? wrap.querySelector('[data-countdown-expired]') : null;
|
||||||
|
function pad(n){ return (n < 10 ? '0' : '') + n; }
|
||||||
|
function set(unit, value){ var node = el.querySelector('[data-unit="' + unit + '"]'); if (node) node.textContent = value; }
|
||||||
|
var interval;
|
||||||
|
function tick(){
|
||||||
|
var diff = target - Date.now();
|
||||||
|
if (diff <= 0){ el.style.display = 'none'; if (expired) expired.classList.remove('hidden'); if (interval) clearInterval(interval); return; }
|
||||||
|
set('days', Math.floor(diff / 86400000));
|
||||||
|
set('hours', pad(Math.floor(diff % 86400000 / 3600000)));
|
||||||
|
set('minutes', pad(Math.floor(diff % 3600000 / 60000)));
|
||||||
|
set('seconds', pad(Math.floor(diff % 60000 / 1000)));
|
||||||
|
}
|
||||||
|
tick();
|
||||||
|
interval = setInterval(tick, 1000);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
14
templates/overrides/coffee/cta.ninjatpl
Normal file
14
templates/overrides/coffee/cta.ninjatpl
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-6xl px-4">
|
||||||
|
<div class="{% if background == "band" %}coffee-torn-top coffee-torn-bottom rounded-2xl bg-primary px-6 py-14 text-center text-primary-foreground md:px-12{% elif background == "card" %}coffee-card px-6 py-14 text-center md:px-12{% else %}flex flex-col items-center gap-8 rounded-2xl bg-muted px-6 py-12 text-foreground md:flex-row md:justify-between md:px-12 md:text-left{% endif %}">
|
||||||
|
<div class="{% if background != "split" %}mx-auto max-w-2xl{% endif %}">
|
||||||
|
{% if heading %}<h2 class="coffee-display text-3xl md:text-4xl {% if background == "band" %}text-primary-foreground{% else %}text-primary{% endif %}">{{ heading }}</h2>{% endif %}
|
||||||
|
{% if text %}<p class="coffee-body mt-3 text-lg {% if background == "band" %}text-primary-foreground/80{% else %}text-muted-foreground{% endif %}">{{ text }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-wrap items-center justify-center gap-3 {% if background != "split" %}mt-8{% else %}shrink-0{% endif %}">
|
||||||
|
{% if primaryUrl and primaryLabel %}<a href="{{ primaryUrl }}" class="kraft-tag text-base px-6 py-3 {% if background == "band" %}bg-background text-foreground{% endif %}">{{ primaryLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
||||||
|
{% if secondaryUrl and secondaryLabel %}<a href="{{ secondaryUrl }}" class="kraft-tag kraft-tag--ghost text-base px-6 py-3 {% if background == "band" %}border-primary-foreground/40 text-primary-foreground{% endif %}">{{ secondaryLabel }}</a>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
17
templates/overrides/coffee/divider.ninjatpl
Normal file
17
templates/overrides/coffee/divider.ninjatpl
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{% with variant=variant|default:"line" width=width|default:"full" %}
|
||||||
|
<div class="bn-divider coffee-doodle-draw mx-auto my-8 flex items-center justify-center {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator">
|
||||||
|
{% if variant == "ornament" or variant == "spacer-rule" %}
|
||||||
|
<span class="h-px flex-1 bg-border"></span>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="72" height="26" viewBox="0 0 72 26" fill="none" stroke="hsl(var(--accent))" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="mx-4 shrink-0" aria-hidden="true">
|
||||||
|
<ellipse cx="30" cy="13" rx="6" ry="9"></ellipse><path d="M30 4 L30 22"></path>
|
||||||
|
<ellipse cx="42" cy="13" rx="6" ry="9"></ellipse><path d="M42 4 L42 22"></path>
|
||||||
|
<path d="M4 13 L18 13"></path><path d="M54 13 L68 13"></path>
|
||||||
|
</svg>
|
||||||
|
<span class="h-px flex-1 bg-border"></span>
|
||||||
|
{% else %}
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 300 12" preserveAspectRatio="none" class="h-3 w-full" fill="none" stroke="hsl(var(--border))" stroke-width="2" stroke-linecap="round" aria-hidden="true">
|
||||||
|
<path d="M2 7 Q 40 2 80 7 T 160 6 T 240 7 T 298 6"></path>
|
||||||
|
</svg>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
40
templates/overrides/coffee/event-list.ninjatpl
Normal file
40
templates/overrides/coffee/event-list.ninjatpl
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-6xl px-4">
|
||||||
|
{% if heading %}<h2 class="coffee-display text-3xl text-primary md:text-4xl"><span class="coffee-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||||
|
{% if intro %}<p class="coffee-body mt-3 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||||
|
{% if layout == "cards" %}
|
||||||
|
<div class="mt-10 grid grid-cols-1 gap-6 sm:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{% for event in events %}{% if not (hidePast and event.past) %}
|
||||||
|
<article class="coffee-card flex flex-col overflow-hidden{% if event.past %} opacity-60{% endif %}">
|
||||||
|
{% if event.media %}<div class="aspect-video overflow-hidden bg-secondary">{% img event.media alt=event.title class="h-full w-full object-cover" %}</div>{% endif %}
|
||||||
|
<div class="flex flex-1 flex-col gap-2 p-5">
|
||||||
|
<div class="coffee-hand text-lg text-accent"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}{% if event.end %} – {{ event.end }}{% endif %}</time>{% if event.timezone %} <span class="text-muted-foreground">{{ event.timezone }}</span>{% endif %}</div>
|
||||||
|
<h3 class="coffee-display text-lg text-primary">{{ event.title }}</h3>
|
||||||
|
{% if event.venue %}<p class="coffee-body text-sm text-muted-foreground">{{ event.venue }}</p>{% endif %}
|
||||||
|
{% if event.description %}<p class="coffee-body text-sm text-muted-foreground">{{ event.description }}</p>{% endif %}
|
||||||
|
{% if event.link %}<a href="{{ event.link }}" class="coffee-hand mt-auto inline-flex items-center gap-1 pt-2 text-lg text-accent hover:underline">{{ event.linkLabel|default:"Details" }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endif %}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<ul class="mt-10 divide-y divide-dashed divide-border border-y border-dashed border-border">
|
||||||
|
{% for event in events %}{% if not (hidePast and event.past) %}
|
||||||
|
<li class="flex flex-col gap-3 py-5 sm:flex-row sm:items-center sm:gap-6{% if event.past %} opacity-60{% endif %}">
|
||||||
|
<div class="shrink-0 sm:w-52">
|
||||||
|
<div class="coffee-display text-sm text-primary"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}</time></div>
|
||||||
|
{% if event.end %}<div class="coffee-body text-xs text-muted-foreground">to {{ event.end }}</div>{% endif %}
|
||||||
|
{% if event.timezone %}<div class="coffee-body text-xs text-muted-foreground">{{ event.timezone }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<h3 class="coffee-display text-base text-primary">{{ event.title }}</h3>
|
||||||
|
{% if event.venue %}<p class="coffee-body text-sm text-muted-foreground">{{ event.venue }}</p>{% endif %}
|
||||||
|
{% if event.description %}<p class="coffee-body mt-1 text-sm text-muted-foreground">{{ event.description }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if event.link %}<a href="{{ event.link }}" class="kraft-tag kraft-tag--ghost shrink-0 text-xs">{{ event.linkLabel|default:"Details" }}</a>{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endif %}{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
17
templates/overrides/coffee/faq.ninjatpl
Normal file
17
templates/overrides/coffee/faq.ninjatpl
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-3xl px-4">
|
||||||
|
{% if heading %}<h2 class="coffee-display text-center text-3xl text-primary md:text-4xl">{{ heading }}</h2>{% endif %}
|
||||||
|
{% if intro %}<p class="coffee-body mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||||
|
<div class="mt-10 divide-y divide-dashed divide-border border-y border-dashed border-border">
|
||||||
|
{% for item in items %}
|
||||||
|
<details class="group py-2"{% if variant == "open-list" %} open{% endif %}>
|
||||||
|
<summary class="coffee-display flex cursor-pointer list-none items-center justify-between gap-4 py-3 text-lg text-primary marker:content-none [&::-webkit-details-marker]:hidden">
|
||||||
|
<span>{{ item.question }}</span>
|
||||||
|
<svg class="h-5 w-5 shrink-0 text-accent transition-transform duration-200 group-open:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
|
||||||
|
</summary>
|
||||||
|
<div class="coffee-body pb-4 text-muted-foreground [&_a]:text-accent [&_a]:underline [&_a]:decoration-dashed">{{ item.answer|safe }}</div>
|
||||||
|
</details>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
17
templates/overrides/coffee/feature-grid.ninjatpl
Normal file
17
templates/overrides/coffee/feature-grid.ninjatpl
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-6xl px-4">
|
||||||
|
{% if eyebrow %}<p class="coffee-hand text-center text-2xl text-accent">{{ eyebrow }}</p>{% endif %}
|
||||||
|
{% if heading %}<h2 class="coffee-display mt-1 text-center text-3xl text-primary md:text-4xl"><span class="coffee-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||||
|
{% if intro %}<p class="coffee-body mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||||
|
<div class="mt-12 grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}">
|
||||||
|
{% for item in items %}
|
||||||
|
<div class="{% if layout == "card" %}coffee-card p-6 transition-shadow hover:shadow-md{% elif layout == "centered" %}text-center{% endif %}">
|
||||||
|
{% if item.icon %}<div class="mb-4 inline-flex h-11 w-11 items-center justify-center rounded-full bg-accent/15 text-accent{% if layout == "centered" %} mx-auto{% endif %}"><svg class="h-6 w-6" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"></use></svg></div>{% endif %}
|
||||||
|
<h3 class="coffee-display text-xl text-primary">{% if item.linkUrl %}<a href="{{ item.linkUrl }}" class="transition-colors hover:text-accent">{{ item.title }}</a>{% else %}{{ item.title }}{% endif %}</h3>
|
||||||
|
{% if item.text %}<p class="coffee-body mt-2 text-muted-foreground">{{ item.text }}</p>{% endif %}
|
||||||
|
{% if item.linkUrl and item.linkLabel %}<a href="{{ item.linkUrl }}" class="coffee-hand mt-3 inline-flex items-center gap-1 text-lg text-accent hover:underline">{{ item.linkLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
21
templates/overrides/coffee/featured-posts.ninjatpl
Normal file
21
templates/overrides/coffee/featured-posts.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<section class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-6xl px-4">
|
||||||
|
{% if heading %}<h2 class="coffee-display mb-8 text-2xl text-primary md:text-3xl"><span class="coffee-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||||
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}">
|
||||||
|
{% for post in posts %}
|
||||||
|
<article class="coffee-card group flex flex-col overflow-hidden transition-shadow hover:shadow-md">
|
||||||
|
{% if showFeaturedImage and post.featured_image_url %}<a href="{{ post.url }}" class="block aspect-video overflow-hidden bg-secondary">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
|
||||||
|
<div class="flex flex-1 flex-col p-5">
|
||||||
|
<h3 class="coffee-display text-lg text-primary"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
|
||||||
|
{% if showExcerpt and post.excerpt %}<p class="coffee-body mt-2 flex-1 text-sm text-muted-foreground">{{ post.excerpt }}</p>{% endif %}
|
||||||
|
<div class="coffee-hand mt-4 flex flex-wrap items-center gap-x-3 gap-y-1 text-base text-accent">
|
||||||
|
{% if showAuthor and post.author_name %}<span>{{ post.author_name }}</span>{% endif %}
|
||||||
|
{% if showDate and post.published_at %}<span class="text-muted-foreground">{{ post.published_at|date:"M j, Y" }}</span>{% endif %}
|
||||||
|
{% if post.reading_time %}<span class="text-muted-foreground">{{ post.reading_time }} min</span>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
38
templates/overrides/coffee/footer.ninjatpl
Normal file
38
templates/overrides/coffee/footer.ninjatpl
Normal file
@ -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 %}<div class="flex items-center gap-3 {{ cls }}">{% for s in social_items %}<a href="{{ s.url }}" target="_blank" rel="noopener" title="{{ s.label }}" class="text-muted-foreground transition-colors hover:text-accent"><span class="sr-only">{{ s.label }}</span>{% if s.icon %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ s.icon|split:":"|first }}.svg#{{ s.icon|split:":"|last }}"></use></svg>{% else %}{{ s.label }}{% endif %}</a>{% endfor %}</div>{% endif %}{% endmacro %}
|
||||||
|
{% macro newsletter() %}{% if showNewsletter %}<div class="max-w-sm"><h3 class="coffee-hand text-2xl text-accent">{{ newsletterTitle|default:"Stay in the loop" }}</h3>{% if newsletterDescription %}<p class="coffee-body mt-1 text-sm text-muted-foreground">{{ newsletterDescription }}</p>{% endif %}<div class="mt-3">{% signup_form list=newsletterListSlug|default:"main" source="footer" submit=newsletterButtonText|default:"Subscribe" %}</div></div>{% endif %}{% endmacro %}
|
||||||
|
{% macro legalbar() %}<div class="mt-10 flex flex-col items-center justify-between gap-3 border-t border-dashed border-border pt-6 text-sm text-muted-foreground sm:flex-row"><p class="coffee-body">{% if copyright %}© {{ copyright }}{% else %}© {{ companyName }}{% endif %}</p>{% if legal_items %}<nav class="flex flex-wrap items-center gap-x-4 gap-y-1">{% for l in legal_items %}<a href="{{ l.url|default:'#' }}"{% if l.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="coffee-body transition-colors hover:text-foreground">{{ l.label }}</a>{% endfor %}</nav>{% endif %}</div>{% endmacro %}
|
||||||
|
<footer class="coffee-torn-top w-full bg-muted/50 text-foreground {{ class }}">
|
||||||
|
<div class="{% if fullWidth %}w-full px-4 sm:px-6 lg:px-8{% else %}mx-auto max-w-7xl px-4 sm:px-6 lg:px-8{% endif %} py-16">
|
||||||
|
{% if fl == "minimal" %}
|
||||||
|
<div class="flex flex-col items-center justify-between gap-6 sm:flex-row">
|
||||||
|
<div class="flex items-center gap-3">{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-8 w-auto"/>{% elif companyName %}<span class="coffee-display text-xl text-primary">{{ companyName }}</span>{% endif %}{% if tagline %}<span class="coffee-hand hidden text-lg text-accent sm:inline">{{ tagline }}</span>{% endif %}</div>
|
||||||
|
{{ sociallinks("") }}
|
||||||
|
</div>
|
||||||
|
{{ legalbar() }}
|
||||||
|
{% elif fl == "centered" %}
|
||||||
|
<div class="flex flex-col items-center gap-5 text-center">
|
||||||
|
{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-10 w-auto"/>{% elif companyName %}<span class="coffee-display text-2xl text-primary">{{ companyName }}</span>{% endif %}
|
||||||
|
{% if tagline %}<p class="coffee-hand max-w-md text-xl text-accent">{{ tagline }}</p>{% endif %}
|
||||||
|
{% if columns %}<nav class="flex flex-wrap items-center justify-center gap-x-6 gap-y-2">{% for col in columns %}{% for item in col.items %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="coffee-body text-sm text-muted-foreground transition-colors hover:text-foreground">{{ item.label }}</a>{% endfor %}{% endfor %}</nav>{% endif %}
|
||||||
|
{{ newsletter() }}
|
||||||
|
{{ sociallinks("justify-center") }}
|
||||||
|
</div>
|
||||||
|
{{ legalbar() }}
|
||||||
|
{% else %}
|
||||||
|
<div class="grid grid-cols-2 gap-8 md:grid-cols-4 lg:grid-cols-5">
|
||||||
|
<div class="col-span-2 lg:col-span-2">
|
||||||
|
{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-9 w-auto"/>{% elif companyName %}<span class="coffee-display text-2xl text-primary">{{ companyName }}</span>{% endif %}
|
||||||
|
{% if tagline %}<p class="coffee-hand mt-2 max-w-xs text-xl text-accent">{{ tagline }}</p>{% endif %}
|
||||||
|
{% if address or email or phone %}<address class="coffee-body mt-4 space-y-1 text-sm not-italic text-muted-foreground">{% if address %}<div>{{ address }}</div>{% endif %}{% if email %}<div><a href="mailto:{{ email }}" class="transition-colors hover:text-foreground">{{ email }}</a></div>{% endif %}{% if phone %}<div><a href="tel:{{ phone }}" class="transition-colors hover:text-foreground">{{ phone }}</a></div>{% endif %}</address>{% endif %}
|
||||||
|
{{ sociallinks("mt-5") }}
|
||||||
|
</div>
|
||||||
|
{% for col in columns %}<div><h3 class="coffee-hand text-xl text-accent">{{ col.header }}</h3><ul class="mt-3 space-y-2">{% for item in col.items %}<li><a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="coffee-body text-sm text-muted-foreground transition-colors hover:text-foreground">{{ item.label }}</a></li>{% endfor %}</ul></div>{% endfor %}
|
||||||
|
{% if showNewsletter %}<div class="col-span-2 lg:col-span-1">{{ newsletter() }}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
{{ legalbar() }}
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
9
templates/overrides/coffee/gallery.ninjatpl
Normal file
9
templates/overrides/coffee/gallery.ninjatpl
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{% with layout=layout|default:"grid" gap=gap|default:"md" cols=columns|default:3 %}
|
||||||
|
<div class="bn-gallery my-8{% if class %} {{ class }}{% endif %}" data-bn-gallery data-lightbox="{% if lightbox %}true{% else %}false{% endif %}">
|
||||||
|
<div class="{% if layout == "carousel" %}flex snap-x snap-mandatory overflow-x-auto pb-2 {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% elif layout == "masonry" %}columns-2 {% if cols == 4 %}md:columns-4{% elif cols == 2 %}md:columns-2{% else %}md:columns-3{% endif %} {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% else %}grid grid-cols-2 {% if cols == 4 %}md:grid-cols-4{% elif cols == 2 %}md:grid-cols-2{% else %}md:grid-cols-3{% endif %} {% if gap == "sm" %}gap-2{% elif gap == "lg" %}gap-6{% else %}gap-4{% endif %}{% endif %}">
|
||||||
|
{% for image in images %}
|
||||||
|
<a href="{{ image.src|mediaURL }}" data-bn-gallery-item data-caption="{{ image.caption|default:"" }}" class="group relative block overflow-hidden rounded-sm coffee-frame p-1.5 bg-card focus:outline-none focus-visible:ring-2 focus-visible:ring-ring {% if layout == "carousel" %}w-64 shrink-0 snap-start sm:w-80{% elif layout == "masonry" %}mb-4 w-full break-inside-avoid{% endif %}">{% if layout == "grid" %}{% img image.src alt=image.alt|default:"" class="aspect-square w-full rounded-sm object-cover" %}{% else %}{% img image.src alt=image.alt|default:"" class="h-auto w-full rounded-sm" %}{% endif %}{% if image.caption %}<span class="sr-only">{{ image.caption }}</span>{% endif %}</a>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endwith %}
|
||||||
@ -1 +1,6 @@
|
|||||||
{% if level|integer == 1 %}<h1 class="coffee-display text-5xl leading-tight text-primary {{ textClass }}">{{ text }}</h1>{% elif level|integer == 3 %}<h3 class="coffee-display text-2xl text-primary {{ textClass }}"><span class="coffee-doodle-underline">{{ text }}</span></h3>{% elif level|integer == 4 %}<h4 class="coffee-display text-xl text-primary {{ textClass }}">{{ text }}</h4>{% elif level|integer == 5 %}<h5 class="coffee-display text-lg text-primary {{ textClass }}">{{ text }}</h5>{% elif level|integer == 6 %}<h6 class="coffee-display text-base text-primary {{ textClass }}">{{ text }}</h6>{% else %}<h2 class="coffee-display text-3xl italic text-primary {{ textClass }}"><span class="coffee-doodle-underline">{{ text }}</span></h2>{% endif %}
|
{% if level == 1 %}<h1 class="coffee-display text-4xl md:text-5xl leading-tight text-primary flex-1 {{ textClass }}">{{ text }}</h1>
|
||||||
|
{% elif level == 3 %}<h3 class="coffee-display text-2xl text-primary flex-1 {{ textClass }}"><span class="coffee-doodle-underline">{{ text }}</span></h3>
|
||||||
|
{% elif level == 4 %}<h4 class="coffee-display text-xl text-primary flex-1 {{ textClass }}">{{ text }}</h4>
|
||||||
|
{% elif level == 5 %}<h5 class="coffee-display text-lg font-medium text-primary flex-1 {{ textClass }}">{{ text }}</h5>
|
||||||
|
{% elif level == 6 %}<h6 class="coffee-hand text-xl text-accent flex-1 {{ textClass }}">{{ text }}</h6>
|
||||||
|
{% else %}<h2 class="coffee-display text-3xl md:text-4xl text-primary flex-1 {{ textClass }}"><span class="coffee-doodle-underline">{{ text }}</span></h2>{% endif %}
|
||||||
|
|||||||
36
templates/overrides/coffee/hero.ninjatpl
Normal file
36
templates/overrides/coffee/hero.ninjatpl
Normal file
@ -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 %}<span class="coffee-hand mb-2 inline-block text-2xl text-accent">{{ eyebrowText }}</span>{% endif %}
|
||||||
|
{% if headlineText %}<h1 class="coffee-display text-4xl leading-tight text-primary sm:text-5xl lg:text-6xl">{{ headlineText }}</h1>{% endif %}
|
||||||
|
{% if subText %}<h2 class="coffee-body mt-4 max-w-2xl text-xl text-muted-foreground sm:text-2xl{% if align == "center" %} mx-auto{% endif %}">{{ subText }}</h2>{% endif %}
|
||||||
|
{% if descText %}<p class="coffee-body mt-4 max-w-xl leading-relaxed text-muted-foreground{% if align == "center" %} mx-auto{% endif %}">{{ descText }}</p>{% endif %}
|
||||||
|
{% if pText or sText %}<div class="mt-8 flex flex-col gap-4 sm:flex-row{% if align == "center" %} justify-center{% endif %}">
|
||||||
|
{% if pText and pUrl %}<a href="{{ pUrl }}" class="kraft-tag text-base px-6 py-3">{{ pText }}</a>{% endif %}
|
||||||
|
{% if sText and sUrl %}<a href="{{ sUrl }}" class="kraft-tag kraft-tag--ghost text-base px-6 py-3">{{ sText }}</a>{% endif %}
|
||||||
|
</div>{% endif %}
|
||||||
|
{% if trustLogos %}<div class="mt-10"><p class="coffee-hand mb-3 text-lg text-accent">Regulars love</p><div class="flex flex-wrap items-center gap-6{% if align == "center" %} justify-center{% endif %}">{% for logo in trustLogos %}{% if logo.url %}<a href="{{ logo.url }}" target="_blank" rel="noopener" class="opacity-60 transition-opacity hover:opacity-100">{% img logo.image alt=logo.alt class="h-8 w-auto" %}</a>{% else %}{% img logo.image alt=logo.alt class="h-8 w-auto opacity-60" %}{% endif %}{% endfor %}</div></div>{% endif %}
|
||||||
|
{% endmacro %}
|
||||||
|
{% if variant == "split" %}
|
||||||
|
<section class="hero grid grid-cols-1 overflow-hidden bg-background text-foreground lg:grid-cols-2{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="relative min-h-[320px] bg-secondary {% if mediaPosition == "left" %}order-1 lg:order-1{% else %}order-1 lg:order-2{% endif %}">{% if hasImage %}{% img backgroundImage alt=headlineText class="absolute inset-0 h-full w-full object-cover" layout="hero" %}{% endif %}</div>
|
||||||
|
<div class="flex flex-col justify-center px-6 py-16 lg:px-12 {% if mediaPosition == "left" %}order-2 lg:order-2{% else %}order-2 lg:order-1{% endif %}"><div class="max-w-lg text-left">{{ herocontent("left") }}</div></div>
|
||||||
|
</section>
|
||||||
|
{% else %}
|
||||||
|
{% set align = "center" %}
|
||||||
|
{% if variant == "left-aligned" %}{% set align = "left" %}{% endif %}
|
||||||
|
<section class="hero relative flex flex-col overflow-hidden bg-background text-foreground {% if minHeight == "screen" %}min-h-screen{% elif minHeight == "50vh" %}min-h-[50vh]{% elif minHeight == "auto" %}min-h-[400px]{% else %}min-h-[75vh]{% endif %} {% if verticalAlign == "top" %}justify-start pt-20{% elif verticalAlign == "bottom" %}justify-end pb-20{% else %}justify-center{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||||
|
{% if hasImage %}<div class="absolute inset-0 z-0">{% img backgroundImage alt=headlineText class="h-full w-full object-cover" layout="hero" %}{% if showOverlay %}<div class="absolute inset-0 {% if overlayColor == "light" %}bg-background/60{% elif overlayColor == "primary" %}bg-primary/50{% elif overlayOpacity <= 30 %}bg-black/30{% elif overlayOpacity <= 50 %}bg-black/50{% elif overlayOpacity <= 70 %}bg-black/70{% else %}bg-black/80{% endif %}"></div>{% endif %}</div>{% endif %}
|
||||||
|
<div class="relative z-10 mx-auto w-full max-w-4xl px-4 sm:px-6 lg:px-8 {% if variant == "left-aligned" %}text-left{% else %}text-center{% endif %}">{{ herocontent(align) }}</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
40
templates/overrides/coffee/hours-location.ninjatpl
Normal file
40
templates/overrides/coffee/hours-location.ninjatpl
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-5xl px-4">
|
||||||
|
{% if heading %}<h2 class="coffee-display text-3xl text-primary md:text-4xl"><span class="coffee-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||||
|
<div class="mt-10 grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||||
|
<div>
|
||||||
|
{% if hours %}
|
||||||
|
<table class="coffee-body w-full text-sm" data-coffee-hours>
|
||||||
|
<tbody class="divide-y divide-dashed divide-border">
|
||||||
|
{% for row in hours %}
|
||||||
|
<tr data-day="{{ row.day }}">
|
||||||
|
<th scope="row" class="coffee-display py-2 pr-4 text-left text-base text-primary">{{ row.day }}</th>
|
||||||
|
<td class="py-2 text-right {% if row.closed %}text-muted-foreground{% else %}text-foreground{% endif %} tabular-nums">{% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{% endif %}
|
||||||
|
{% if specialHoursNote %}<p class="coffee-body mt-4 rounded-md border border-dashed border-border bg-muted px-3 py-2 text-sm text-muted-foreground">{{ specialHoursNote }}</p>{% endif %}
|
||||||
|
{% if address %}<div class="mt-6"><div class="coffee-hand text-lg text-accent">Find us</div><address class="coffee-body mt-1 whitespace-pre-line not-italic">{{ address }}</address></div>{% endif %}
|
||||||
|
{% if phone %}<div class="mt-4"><div class="coffee-hand text-lg text-accent">Call ahead</div><a href="tel:{{ phone }}" class="coffee-body mt-1 inline-block font-medium transition-colors hover:text-accent">{{ phone }}</a></div>{% endif %}
|
||||||
|
{% if directionsUrl %}<a href="{{ directionsUrl }}" target="_blank" rel="noopener" class="kraft-tag mt-6 text-sm"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#navigation"></use></svg>Get directions</a>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if mapEmbedUrl %}
|
||||||
|
<div class="coffee-frame aspect-square w-full overflow-hidden rounded-sm bg-secondary md:aspect-auto">
|
||||||
|
<button type="button" data-map-src="{{ mapEmbedUrl }}" onclick="var f=document.createElement('iframe');f.src=this.getAttribute('data-map-src');f.setAttribute('class','h-full w-full border-0');f.setAttribute('loading','lazy');f.setAttribute('referrerpolicy','no-referrer-when-downgrade');f.setAttribute('title','Map');this.parentNode.replaceChild(f,this);" class="coffee-body flex h-full min-h-[16rem] w-full items-center justify-center gap-2 text-sm font-medium transition-colors hover:bg-muted"><svg class="h-5 w-5 coffee-pin" aria-hidden="true"><use href="/icons/lucide.svg#map-pin"></use></svg>{{ mapButtonLabel|default:"Load map" }}</button>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
var days=['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];
|
||||||
|
var today=days[new Date().getDay()];
|
||||||
|
document.querySelectorAll('[data-coffee-hours] tr[data-day]').forEach(function(tr){
|
||||||
|
var d=(tr.getAttribute('data-day')||'').trim();
|
||||||
|
if(d.toLowerCase().indexOf(today.toLowerCase())===0){tr.querySelector('th').classList.add('coffee-hours-today');}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
@ -1,6 +1,8 @@
|
|||||||
<figure class="my-8 flex flex-col items-center coffee-body">
|
{% with size=size|default:"full" align=align|default:"center" %}
|
||||||
<div class="coffee-frame p-2 bg-card rounded-sm relative coffee-torn-bottom">
|
<figure class="my-8 flex flex-1 flex-col {% if align == "left" %}items-start{% elif align == "right" %}items-end{% else %}items-center{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||||
{% if src %}<img src="{{ src }}" alt="{{ alt }}" class="block max-w-full h-auto" loading="lazy">{% else %}<div class="w-full h-48 flex items-center justify-center coffee-body text-sm text-muted-foreground italic px-6">Add an image to render the torn-edge frame.</div>{% endif %}
|
<div class="coffee-frame coffee-torn-bottom relative overflow-hidden p-2 {% if size == "xs" %}max-w-[200px] w-full{% elif size == "sm" %}max-w-[400px] w-full{% elif size == "md" %}max-w-[600px] w-full{% elif size == "lg" %}max-w-[800px] w-full{% else %}w-full{% endif %}">
|
||||||
|
{% if src %}{% img src alt=alt class="block h-auto w-full rounded-sm object-cover" layout=size %}{% else %}<div class="flex h-48 w-full items-center justify-center coffee-hand text-2xl text-muted-foreground">Add an image</div>{% endif %}
|
||||||
</div>
|
</div>
|
||||||
{% if caption %}<figcaption class="coffee-display italic text-center text-sm text-muted-foreground mt-3">{{ caption }}</figcaption>{% endif %}
|
{% if caption %}<figcaption class="coffee-hand mt-3 text-center text-xl text-accent">{{ caption }}</figcaption>{% endif %}
|
||||||
</figure>
|
</figure>
|
||||||
|
{% endwith %}
|
||||||
|
|||||||
15
templates/overrides/coffee/logo-strip.ninjatpl
Normal file
15
templates/overrides/coffee/logo-strip.ninjatpl
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<section class="py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-6xl px-4">
|
||||||
|
{% if heading %}<p class="coffee-hand text-center text-2xl text-accent">{{ heading }}</p>{% endif %}
|
||||||
|
<div class="mt-8 {% if marquee %}group relative overflow-hidden [mask-image:linear-gradient(to_right,transparent,black_8%,black_92%,transparent)]{% endif %}">
|
||||||
|
<div class="flex flex-wrap items-center justify-center gap-x-10 gap-y-6 {% if marquee %}flex-nowrap [animation:logo-marquee_30s_linear_infinite] motion-reduce:[animation:none] group-hover:[animation-play-state:paused]{% endif %}">
|
||||||
|
{% for logo in logos %}
|
||||||
|
{% if logo.url %}<a href="{{ logo.url }}" class="block shrink-0 opacity-80 transition hover:opacity-100">{% endif %}
|
||||||
|
<span class="inline-flex h-8 items-center{% if grayscale %} grayscale transition-[filter] hover:grayscale-0{% endif %}">{% img logo.image alt=logo.alt class="h-8 w-auto object-contain" %}</span>
|
||||||
|
{% if logo.url %}</a>{% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% if marquee %}<style>@keyframes logo-marquee{from{transform:translateX(0)}to{transform:translateX(-50%)}}</style>{% endif %}
|
||||||
31
templates/overrides/coffee/menu-list.ninjatpl
Normal file
31
templates/overrides/coffee/menu-list.ninjatpl
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-5xl px-4">
|
||||||
|
{% if heading %}<h2 class="coffee-display text-3xl text-primary md:text-4xl"><span class="coffee-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||||
|
{% if intro %}<p class="coffee-body mt-3 max-w-2xl text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||||
|
<div class="coffee-chalkboard mt-10 p-6 md:p-10">
|
||||||
|
<div class="grid grid-cols-1 gap-x-12 gap-y-10 {% if columns == 2 %}md:grid-cols-2{% endif %}">
|
||||||
|
{% for section in sections %}
|
||||||
|
<div class="menu-section">
|
||||||
|
{% if section.title %}<h3 class="coffee-hand border-b coffee-chalk-rule pb-2 text-3xl">{{ section.title }}</h3>{% endif %}
|
||||||
|
{% if section.note %}<p class="coffee-body coffee-chalk-muted mt-2 text-sm">{{ section.note }}</p>{% endif %}
|
||||||
|
<ul class="mt-4 space-y-5">
|
||||||
|
{% for item in section.items %}
|
||||||
|
<li class="flex items-baseline gap-4">
|
||||||
|
<div class="min-w-0 flex-1">
|
||||||
|
<div class="flex flex-wrap items-baseline gap-x-2 gap-y-1">
|
||||||
|
<span class="coffee-display text-lg">{{ item.name }}</span>
|
||||||
|
{% for badge in item.badges %}{% if badge.label %}<span class="coffee-hand rounded-full border coffee-chalk-rule px-2 py-0.5 text-sm coffee-chalk-muted">{{ badge.label }}</span>{% endif %}{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if item.description %}<p class="coffee-body coffee-chalk-muted mt-0.5 text-sm">{{ item.description }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% if item.price %}<span class="coffee-mono shrink-0 tabular-nums">{% if currency %}{{ currency }}{% endif %}{{ item.price }}</span>{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if footnote %}<p class="coffee-body coffee-chalk-muted mt-10 text-xs">{{ footnote }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
74
templates/overrides/coffee/navbar.ninjatpl
Normal file
74
templates/overrides/coffee/navbar.ninjatpl
Normal file
@ -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) %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} class="coffee-body px-3 py-2 text-sm font-medium text-foreground/80 transition-colors hover:text-accent {{ item.css_class }}">{{ item.label }}</a>{% endmacro %}
|
||||||
|
{% macro dropdown(item) %}<div class="relative group"><button type="button" class="coffee-body inline-flex items-center gap-1 px-3 py-2 text-sm font-medium text-foreground/80 transition-colors hover:text-accent">{{ item.label }}<svg class="h-4 w-4 transition-transform group-hover:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg></button><div class="absolute left-0 z-50 mt-1 w-56 invisible opacity-0 transition-all duration-200 group-hover:visible group-hover:opacity-100"><div class="coffee-card py-2 shadow-lg">{% for child in item.children %}{% if child.item_type == "header" %}<div class="coffee-hand px-4 py-2 text-base text-accent">{{ child.label }}</div>{% else %}<a href="{{ child.url|default:'#' }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="coffee-body block px-4 py-2 text-sm text-foreground transition-colors hover:bg-muted {{ child.css_class }}">{{ child.label }}</a>{% endif %}{% endfor %}</div></div></div>{% endmacro %}
|
||||||
|
{% macro brand(size) %}<a href="/" class="flex flex-shrink-0 items-center gap-2">{% if logoType == "text" and logoText.text %}<span class="coffee-display text-2xl text-primary {{ logoText.color }}">{{ logoText.text }}</span>{% elif logo %}<img src="{{ logo }}" alt="{{ alt|default:companyName }}" class="{{ size }} w-auto"{% if logoMaxWidth or logoMaxHeight %} style="{% if logoMaxWidth %}max-width:{{ logoMaxWidth }};{% endif %}{% if logoMaxHeight %}max-height:{{ logoMaxHeight }};{% endif %}"{% endif %}/>{% elif companyName %}<span class="coffee-display text-2xl text-primary">{{ companyName }}</span>{% endif %}</a>{% endmacro %}
|
||||||
|
{% macro ctabtn(extra) %}{% if cta_text %}<a href="{{ cta_url|default:'#' }}" class="kraft-tag text-sm {{ extra }}">{{ cta_text }}</a>{% endif %}{% endmacro %}
|
||||||
|
<nav data-bn-navbar data-bn-sticky="{{ sb }}" class="bn-navbar w-full {% if sb == "sticky" or sb == "shrink-on-scroll" %}sticky top-0 z-40 {% endif %}bg-background/95 backdrop-blur border-b border-dashed border-border {{ class }}">
|
||||||
|
{% if showUtilityBar %}<div class="w-full border-b border-border/60 bg-muted/60 text-xs text-muted-foreground {% if not showOnMobile %}hidden sm:block{% endif %}"><div class="mx-auto flex h-8 max-w-7xl items-center justify-between px-4 sm:px-6 lg:px-8"><div class="coffee-body">{{ utilityBarText|safe }}</div><div class="flex items-center gap-4">{% if utilityBarPhone %}<a href="tel:{{ utilityBarPhone }}" class="hover:text-foreground">{{ utilityBarPhone }}</a>{% endif %}{% if utilityBarEmail %}<a href="mailto:{{ utilityBarEmail }}" class="hover:text-foreground">{{ utilityBarEmail }}</a>{% endif %}</div></div></div>{% endif %}
|
||||||
|
<div class="mx-auto max-w-7xl px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="bn-navbar-row flex h-16 items-center justify-between">
|
||||||
|
{% if nv == "centered-logo" %}
|
||||||
|
<div class="hidden flex-1 items-center gap-1 md:flex">{% for item in menu.items %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endfor %}</div>
|
||||||
|
<div class="flex items-center justify-center">{{ brand("h-9") }}</div>
|
||||||
|
<div class="hidden flex-1 items-center justify-end gap-3 md:flex">{{ ctabtn("") }}</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="flex items-center">{{ brand("h-9") }}</div>
|
||||||
|
<div class="hidden items-center gap-1 md:flex">{% for item in menu.items %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endfor %}{{ ctabtn("ml-3") }}</div>
|
||||||
|
{% endif %}
|
||||||
|
<div class="flex items-center md:hidden">
|
||||||
|
<label for="bn-navbar-toggle" data-bn-nav-open aria-controls="bn-navbar-drawer" aria-expanded="false" class="inline-flex cursor-pointer items-center justify-center rounded-md p-2 transition-colors hover:bg-muted"><span class="sr-only">Open menu</span><svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"/></svg></label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<input type="checkbox" id="bn-navbar-toggle" class="bn-navbar-toggle" aria-hidden="true"/>
|
||||||
|
<label for="bn-navbar-toggle" class="bn-navbar-overlay fixed inset-0 z-40 bg-black/50 md:hidden" aria-hidden="true"></label>
|
||||||
|
<div id="bn-navbar-drawer" class="bn-navbar-drawer fixed right-0 top-0 z-50 flex h-full w-80 max-w-[85vw] flex-col bg-background text-foreground shadow-xl md:hidden" role="dialog" aria-modal="true" aria-label="Site menu">
|
||||||
|
<div class="flex items-center justify-between border-b border-dashed border-border p-4">{{ brand("h-8") }}<label for="bn-navbar-toggle" data-bn-nav-close class="cursor-pointer rounded-md p-2 text-muted-foreground hover:bg-muted hover:text-foreground"><span class="sr-only">Close menu</span><svg class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg></label></div>
|
||||||
|
<nav class="flex-1 overflow-y-auto py-4" aria-label="Mobile">{% for item in menu.items %}{% if item.children %}<details class="group/acc"><summary class="coffee-body flex cursor-pointer list-none items-center justify-between px-6 py-3 text-base font-medium transition-colors hover:bg-muted">{{ item.label }}<svg class="h-4 w-4 transition-transform group-open/acc:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg></summary><div class="pb-2 pl-8">{% for child in item.children %}{% if child.item_type != "header" %}<a href="{{ child.url|default:'#' }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="coffee-body block px-2 py-2 text-sm text-muted-foreground transition-colors hover:text-foreground">{{ child.label }}</a>{% endif %}{% endfor %}</div></details>{% else %}<a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="coffee-body block px-6 py-3 text-base font-medium transition-colors hover:bg-muted">{{ item.label }}</a>{% endif %}{% endfor %}</nav>
|
||||||
|
{% if cta_text %}<div class="border-t border-dashed border-border p-4"><a href="{{ cta_url|default:'#' }}" class="kraft-tag w-full justify-center">{{ cta_text }}</a></div>{% endif %}
|
||||||
|
</div>
|
||||||
|
<style>
|
||||||
|
.bn-navbar-toggle{position:absolute;width:1px;height:1px;opacity:0;pointer-events:none;}
|
||||||
|
.bn-navbar-drawer{transform:translateX(100%);transition:transform .3s ease;}
|
||||||
|
.bn-navbar-overlay{opacity:0;pointer-events:none;transition:opacity .3s ease;}
|
||||||
|
.bn-navbar-toggle:checked ~ .bn-navbar-drawer{transform:translateX(0);}
|
||||||
|
.bn-navbar-toggle:checked ~ .bn-navbar-overlay{opacity:1;pointer-events:auto;}
|
||||||
|
.bn-navbar-drawer details > summary::-webkit-details-marker{display:none;}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
(function(){
|
||||||
|
if(window.__bnCoffeeNavInit)return;window.__bnCoffeeNavInit=1;
|
||||||
|
function ready(fn){if(document.readyState!='loading')fn();else document.addEventListener('DOMContentLoaded',fn);}
|
||||||
|
ready(function(){
|
||||||
|
document.querySelectorAll('[data-bn-navbar]').forEach(function(nav){
|
||||||
|
var toggle=nav.querySelector('.bn-navbar-toggle');
|
||||||
|
var drawer=nav.querySelector('.bn-navbar-drawer');
|
||||||
|
var opener=nav.querySelector('[data-bn-nav-open]');
|
||||||
|
if(!toggle||!drawer)return;
|
||||||
|
function focusables(){return drawer.querySelectorAll('a[href],button,label[for],input,summary,[tabindex]:not([tabindex="-1"])');}
|
||||||
|
toggle.addEventListener('change',function(){
|
||||||
|
var open=toggle.checked;
|
||||||
|
document.documentElement.style.overflow=open?'hidden':'';
|
||||||
|
document.body.style.overflow=open?'hidden':'';
|
||||||
|
if(opener)opener.setAttribute('aria-expanded',open?'true':'false');
|
||||||
|
if(open){var f=focusables();if(f.length)f[0].focus();}else if(opener&&opener.focus)opener.focus();
|
||||||
|
});
|
||||||
|
drawer.addEventListener('keydown',function(e){
|
||||||
|
if(e.key==='Escape'){toggle.checked=false;toggle.dispatchEvent(new Event('change'));return;}
|
||||||
|
if(e.key!=='Tab')return;
|
||||||
|
var f=focusables();if(!f.length)return;
|
||||||
|
var first=f[0],last=f[f.length-1];
|
||||||
|
if(e.shiftKey&&document.activeElement===first){e.preventDefault();last.focus();}
|
||||||
|
else if(!e.shiftKey&&document.activeElement===last){e.preventDefault();first.focus();}
|
||||||
|
});
|
||||||
|
drawer.querySelectorAll('a[href]').forEach(function(a){a.addEventListener('click',function(){toggle.checked=false;toggle.dispatchEvent(new Event('change'));});});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
9
templates/overrides/coffee/newsletter-signup.ninjatpl
Normal file
9
templates/overrides/coffee/newsletter-signup.ninjatpl
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<section class="py-12 bg-background text-foreground md:py-16{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-3xl px-4">
|
||||||
|
<div class="coffee-torn-top coffee-torn-bottom rounded-2xl {% if layout == "inline" %}bg-primary px-6 py-10 text-primary-foreground md:px-12{% else %}coffee-card px-6 py-10 md:px-10{% endif %} text-center">
|
||||||
|
{% if title %}<h2 class="coffee-display text-2xl md:text-3xl {% if layout == "inline" %}text-primary-foreground{% else %}text-primary{% endif %}">{{ title }}</h2>{% endif %}
|
||||||
|
{% if description %}<p class="coffee-body mx-auto mt-3 max-w-xl {% if layout == "inline" %}text-primary-foreground/80{% else %}text-muted-foreground{% endif %}">{{ description }}</p>{% endif %}
|
||||||
|
<div class="mx-auto mt-6 max-w-md">{% signup_form list=listSlug source=source submit=submitText success_message=successMessage placeholder=placeholder %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
22
templates/overrides/coffee/page-suggestions.ninjatpl
Normal file
22
templates/overrides/coffee/page-suggestions.ninjatpl
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{# coffee page-suggestions — themed 404 helper. Suggestions (when the platform
|
||||||
|
provides them) list {url|slug, title, excerpt}; otherwise a friendly
|
||||||
|
hand-drawn "lost your way" panel with a route home. #}
|
||||||
|
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-3xl px-4 text-center">
|
||||||
|
<p class="coffee-hand text-3xl text-accent">Lost your way?</p>
|
||||||
|
<h2 class="coffee-display mt-1 text-3xl text-primary md:text-4xl">{{ title|default:"Were you looking for..." }}</h2>
|
||||||
|
{% if suggestions %}
|
||||||
|
<ul class="mt-8 space-y-3 text-left">
|
||||||
|
{% for s in suggestions %}
|
||||||
|
<li class="coffee-card p-4">
|
||||||
|
<a href="{{ s.url|default:s.slug }}" class="coffee-display text-lg text-primary transition-colors hover:text-accent">{{ s.title }}</a>
|
||||||
|
{% if showDescription and s.excerpt %}<p class="coffee-body mt-1 text-sm text-muted-foreground">{{ s.excerpt }}</p>{% endif %}
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% else %}
|
||||||
|
<p class="coffee-body mx-auto mt-4 max-w-xl text-muted-foreground">{{ emptyMessage|default:"That page has wandered off. Head back to the counter and start again." }}</p>
|
||||||
|
{% endif %}
|
||||||
|
<a href="/" class="kraft-tag mt-8"><svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#home"></use></svg>Back home</a>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
14
templates/overrides/coffee/password-reset-form.ninjatpl
Normal file
14
templates/overrides/coffee/password-reset-form.ninjatpl
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{# coffee password-reset-form — request panel in kraft chrome. Endpoint matches
|
||||||
|
the platform (/api/auth/request-password-reset, #reset-message). #}
|
||||||
|
<div class="password-reset-form coffee-card mx-auto max-w-md p-6">
|
||||||
|
<div id="reset-message"></div>
|
||||||
|
<div id="request-reset-panel">
|
||||||
|
<h2 class="coffee-display text-2xl text-primary"><span class="coffee-doodle-underline">Reset password</span></h2>
|
||||||
|
<p class="coffee-body mt-2 text-muted-foreground">Enter your email address and we will send you a link to reset your password.</p>
|
||||||
|
<form hx-post="/api/auth/request-password-reset" hx-target="#reset-message" hx-swap="innerHTML" class="mt-4">
|
||||||
|
<div class="mb-4"><label class="coffee-hand mb-1 block text-lg text-accent">Email</label><input type="email" name="email" required class="coffee-body w-full rounded-md border border-input bg-background px-3 py-2 text-sm focus:outline-none focus:ring-2 focus:ring-ring"></div>
|
||||||
|
<button type="submit" class="kraft-tag w-full justify-center" hx-disabled-elt="this">Send reset link</button>
|
||||||
|
</form>
|
||||||
|
<p class="coffee-body mt-4 text-center text-sm"><a href="/login" class="text-accent underline decoration-dashed">Back to login</a></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
15
templates/overrides/coffee/popular-posts.ninjatpl
Normal file
15
templates/overrides/coffee/popular-posts.ninjatpl
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<section class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
{% if heading %}<h2 class="coffee-hand mb-4 text-2xl text-accent">{{ heading }}</h2>{% endif %}
|
||||||
|
<ol class="space-y-4">
|
||||||
|
{% for post in posts %}
|
||||||
|
<li class="flex items-start gap-3">
|
||||||
|
{% if showRank %}<span class="coffee-display mt-0.5 text-2xl text-accent/60">{{ forloop.Counter }}</span>{% endif %}
|
||||||
|
{% if showThumbnail and post.featured_image_url %}<a href="{{ post.url }}" class="coffee-frame h-12 w-12 flex-shrink-0 overflow-hidden rounded-sm p-0.5">{% img post.featured_image_url alt=post.title class="h-full w-full rounded-sm object-cover" %}</a>{% endif %}
|
||||||
|
<div class="min-w-0">
|
||||||
|
<h3 class="coffee-display text-sm text-primary"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
|
||||||
|
{% if post.published_at %}<p class="coffee-body mt-0.5 text-xs text-muted-foreground">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
</section>
|
||||||
19
templates/overrides/coffee/post-hero.ninjatpl
Normal file
19
templates/overrides/coffee/post-hero.ninjatpl
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{% set center = align != "left" %}
|
||||||
|
{% if post.featured_image_url %}
|
||||||
|
<section class="hero relative flex min-h-[50vh] flex-col justify-end overflow-hidden bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="absolute inset-0 z-0">{% img post.featured_image_url alt=post.featured_image_alt class="h-full w-full object-cover" layout="hero" %}<div class="absolute inset-0 bg-black/60"></div></div>
|
||||||
|
<div class="relative z-10 mx-auto w-full max-w-4xl px-4 py-12 sm:px-6 lg:px-8 {% if center %}text-center{% endif %}">
|
||||||
|
<h1 class="coffee-display text-3xl leading-tight text-white sm:text-4xl lg:text-5xl">{{ post.title }}</h1>
|
||||||
|
{% if showExcerpt and post.excerpt %}<p class="coffee-body mt-4 max-w-2xl text-lg text-white/80{% if center %} mx-auto{% endif %}">{{ post.excerpt }}</p>{% endif %}
|
||||||
|
{% if showMeta %}<div class="coffee-hand mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 text-lg text-white/90{% if center %} justify-center{% endif %}">{% if post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"F j, Y" }}</span>{% endif %}{% if post.reading_time %}<span>{{ post.reading_time }} min</span>{% endif %}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% else %}
|
||||||
|
<section class="bg-background py-14 text-foreground md:py-20{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto w-full max-w-3xl px-4 {% if center %}text-center{% endif %}">
|
||||||
|
<h1 class="coffee-display text-3xl leading-tight text-primary sm:text-4xl lg:text-5xl">{{ post.title }}</h1>
|
||||||
|
{% if showExcerpt and post.excerpt %}<p class="coffee-body mt-4 text-lg text-muted-foreground{% if center %} mx-auto max-w-2xl{% endif %}">{{ post.excerpt }}</p>{% endif %}
|
||||||
|
{% if showMeta %}<div class="coffee-hand mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 text-lg text-accent{% if center %} justify-center{% endif %}">{% if post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span class="text-muted-foreground">{{ post.published_at|date:"F j, Y" }}</span>{% endif %}{% if post.reading_time %}<span class="text-muted-foreground">{{ post.reading_time }} min</span>{% endif %}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
8
templates/overrides/coffee/post-metadata.ninjatpl
Normal file
8
templates/overrides/coffee/post-metadata.ninjatpl
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{% if post %}
|
||||||
|
<div class="coffee-body flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-muted-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
{% if showAuthor and post.author_name %}<span class="flex items-center gap-2">{% if showAvatar and post.author_avatar %}{% img post.author_avatar alt=post.author_name class="h-7 w-7 rounded-full object-cover" %}{% endif %}<span class="coffee-display text-foreground">{{ post.author_name }}</span></span>{% endif %}
|
||||||
|
{% if showDate and post.published_at %}<time datetime="{{ post.published_at|date:"Y-m-d" }}">{{ post.published_at|date:"F j, Y" }}</time>{% endif %}
|
||||||
|
{% if showReadingTime and post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}
|
||||||
|
{% if post.categories %}<span class="flex flex-wrap gap-1.5">{% for cat in post.categories %}<a href="{{ cat.url|default:'#' }}" class="rounded-full bg-muted px-2 py-0.5 text-xs text-muted-foreground transition-colors hover:text-accent">{{ cat.name }}</a>{% endfor %}</span>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
24
templates/overrides/coffee/pricing.ninjatpl
Normal file
24
templates/overrides/coffee/pricing.ninjatpl
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-6xl px-4">
|
||||||
|
{% if heading %}<h2 class="coffee-display text-center text-3xl text-primary md:text-4xl">{{ heading }}</h2>{% endif %}
|
||||||
|
{% if intro %}<p class="coffee-body mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||||
|
<div class="mt-12 grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3">
|
||||||
|
{% for tier in tiers %}
|
||||||
|
<div class="coffee-card relative flex flex-col p-6 {% if tier.highlighted %}ring-2 ring-accent{% endif %}">
|
||||||
|
{% if tier.badge %}<span class="coffee-hand absolute -top-4 left-1/2 -translate-x-1/2 rotate-[-3deg] rounded-full bg-accent px-4 py-1 text-lg text-accent-foreground shadow-sm">{{ tier.badge }}</span>{% endif %}
|
||||||
|
<h3 class="coffee-display text-xl text-primary">{{ tier.name }}</h3>
|
||||||
|
<p class="mt-4 flex items-baseline gap-1">
|
||||||
|
<span class="coffee-display text-4xl text-accent">{% if currency %}{{ currency }}{% else %}${% endif %}{{ tier.price }}</span>
|
||||||
|
{% if tier.period %}<span class="coffee-body text-sm text-muted-foreground">{{ tier.period }}</span>{% endif %}
|
||||||
|
</p>
|
||||||
|
{% if tier.description %}<p class="coffee-body mt-3 text-sm text-muted-foreground">{{ tier.description }}</p>{% endif %}
|
||||||
|
{% if tier.features %}<ul class="mt-6 flex-1 space-y-3 text-sm">
|
||||||
|
{% for feature in tier.features %}<li class="coffee-body flex items-start gap-2"><svg class="mt-0.5 h-4 w-4 shrink-0 text-accent" aria-hidden="true"><use href="/icons/lucide.svg#check"></use></svg><span>{{ feature }}</span></li>{% endfor %}
|
||||||
|
</ul>{% endif %}
|
||||||
|
{% if tier.ctaUrl and tier.ctaLabel %}<a href="{{ tier.ctaUrl }}" class="kraft-tag mt-8 w-full justify-center {% if not tier.highlighted %}kraft-tag--ghost{% endif %}">{{ tier.ctaLabel }}</a>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% if billingNote %}<p class="coffee-body mt-8 text-center text-sm text-muted-foreground">{{ billingNote }}</p>{% endif %}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
6
templates/overrides/coffee/quote.ninjatpl
Normal file
6
templates/overrides/coffee/quote.ninjatpl
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{% with style=style|default:"block" align=align|default:"left" %}
|
||||||
|
<figure class="bn-quote my-8 {% if style == "inline-card" %}coffee-card p-6 md:p-8{% elif style == "pull" %}border-l-4 border-accent pl-6 md:pl-8 py-2{% else %}border-l-2 border-dashed border-border pl-5 py-1{% endif %}{% if align == "center" %} text-center{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||||
|
<blockquote{% if cite %} cite="{{ cite }}"{% endif %} class="coffee-display {% if style == "pull" %}text-2xl md:text-3xl leading-snug{% else %}text-xl md:text-2xl leading-relaxed{% endif %} text-primary">{{ quote|safe }}</blockquote>
|
||||||
|
{% if attribution or role or cite %}<figcaption class="coffee-hand mt-4 text-xl text-accent{% if align == "center" %} flex flex-col items-center{% endif %}">{% if attribution %}<span>{{ attribution }}</span>{% endif %}{% if role %}<span class="{% if attribution %}block {% endif %}text-lg text-muted-foreground">{{ role }}</span>{% endif %}{% if cite %}<a href="{{ cite }}" class="{% if attribution or role %}mt-1 {% endif %}coffee-body inline-block text-sm underline decoration-dashed underline-offset-2 hover:text-foreground">Source</a>{% endif %}</figcaption>{% endif %}
|
||||||
|
</figure>
|
||||||
|
{% endwith %}
|
||||||
16
templates/overrides/coffee/related-posts.ninjatpl
Normal file
16
templates/overrides/coffee/related-posts.ninjatpl
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
{% if posts %}
|
||||||
|
<section class="border-t border-dashed border-border py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-6xl px-4">
|
||||||
|
<h2 class="coffee-display mb-8 text-2xl text-primary">{{ heading|default:"More from the counter" }}</h2>
|
||||||
|
<div class="grid grid-cols-1 gap-6 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% else %}lg:grid-cols-3{% endif %}">
|
||||||
|
{% for post in posts %}
|
||||||
|
<article class="group flex flex-col">
|
||||||
|
{% if post.featured_image_url %}<a href="{{ post.url }}" class="coffee-frame mb-3 block aspect-video overflow-hidden rounded-sm p-1.5">{% img post.featured_image_url alt=post.title class="h-full w-full rounded-sm object-cover transition-transform duration-300 group-hover:scale-105" %}</a>{% endif %}
|
||||||
|
<h3 class="coffee-display text-base text-primary"><a href="{{ post.url }}" class="transition-colors hover:text-accent">{{ post.title }}</a></h3>
|
||||||
|
{% if post.published_at %}<p class="coffee-hand mt-1 text-base text-accent">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
|
||||||
|
</article>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% endif %}
|
||||||
13
templates/overrides/coffee/rich-section.ninjatpl
Normal file
13
templates/overrides/coffee/rich-section.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<section class="py-16 md:py-24 {% if background == "muted" %}bg-muted text-foreground{% elif background == "card" %}bg-card text-card-foreground{% else %}bg-background text-foreground{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto grid max-w-6xl items-center gap-10 px-4 md:grid-cols-2 md:gap-16">
|
||||||
|
<div class="{% if mediaPosition == "left" %}md:order-2{% else %}md:order-1{% endif %}">
|
||||||
|
{% if eyebrow %}<p class="coffee-hand text-2xl text-accent">{{ eyebrow }}</p>{% endif %}
|
||||||
|
{% if heading %}<h2 class="coffee-display mt-1 text-3xl text-primary md:text-4xl"><span class="coffee-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||||
|
{% if body %}<div class="coffee-body mt-4 space-y-4 text-lg text-muted-foreground [&_a]:text-accent [&_a]:underline [&_a]:decoration-dashed [&_strong]:text-primary">{{ body|safe }}</div>{% endif %}
|
||||||
|
{% if ctaUrl and ctaLabel %}<a href="{{ ctaUrl }}" class="kraft-tag mt-6 text-base px-5 py-3">{{ ctaLabel }}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"></use></svg></a>{% endif %}
|
||||||
|
</div>
|
||||||
|
<div class="{% if mediaPosition == "left" %}md:order-1{% else %}md:order-2{% endif %}">
|
||||||
|
{% if media %}<div class="coffee-frame coffee-torn-bottom overflow-hidden rounded-sm p-2">{% img media alt=mediaAlt class="h-auto w-full rounded-sm object-cover" %}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
1
templates/overrides/coffee/rich-text.ninjatpl
Normal file
1
templates/overrides/coffee/rich-text.ninjatpl
Normal file
@ -0,0 +1 @@
|
|||||||
|
{% if html %}<article class="rich-text coffee-body prose prose-lg max-w-none text-foreground [&_h1]:coffee-display [&_h2]:coffee-display [&_h3]:coffee-display [&_h1]:text-primary [&_h2]:text-primary [&_h3]:text-primary [&_a]:text-accent [&_a]:decoration-dashed [&_blockquote]:border-l-2 [&_blockquote]:border-accent [&_blockquote]:italic">{{ html|safe }}</article>{% endif %}
|
||||||
12
templates/overrides/coffee/sidebar-nav.ninjatpl
Normal file
12
templates/overrides/coffee/sidebar-nav.ninjatpl
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
{% set compact = template == "compact" %}
|
||||||
|
<nav class="flex-1 {% if compact %}w-56 py-4 pr-3{% else %}w-64 py-6 pr-4{% endif %}{% if showBorder %} border-r border-dashed border-border{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||||
|
{% if title %}<h3 class="coffee-hand {% if compact %}px-2 mb-3 text-lg{% else %}px-3 mb-4 text-xl{% endif %} text-accent">{{ title }}</h3>{% endif %}
|
||||||
|
<ul class="{% if compact %}space-y-0.5{% else %}space-y-1{% endif %}">
|
||||||
|
{% for item in menu.items %}
|
||||||
|
<li><a href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}{% if item.title %} title="{{ item.title }}"{% endif %} data-bn-sidebar-link class="coffee-body flex items-center rounded-md text-sm text-muted-foreground transition-colors hover:bg-muted hover:text-foreground {% if compact %}px-2 py-1.5{% else %}px-3 py-2{% endif %} {{ item.css_class }}">{% if item.icon %}<svg class="mr-2 h-4 w-4" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"></use></svg>{% endif %}{{ item.label }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
{% if highlightCurrent %}<script>
|
||||||
|
(function(){var p=window.location.pathname;document.querySelectorAll('[data-bn-sidebar-link]').forEach(function(a){if(a.getAttribute('href')===p){a.classList.remove('text-muted-foreground');a.classList.add('text-foreground','bg-muted','font-medium');}});})();
|
||||||
|
</script>{% endif %}
|
||||||
13
templates/overrides/coffee/social-links.ninjatpl
Normal file
13
templates/overrides/coffee/social-links.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
<div class="social-links{% if class %} {{ class }}{% endif %}">
|
||||||
|
{% if heading %}<p class="coffee-hand mb-3 text-xl text-accent">{{ heading }}</p>{% endif %}
|
||||||
|
<ul class="flex {% if style == "stack" %}flex-col gap-2{% else %}flex-wrap items-center gap-3{% endif %}">
|
||||||
|
{% for link in links %}{% if link.url %}
|
||||||
|
<li>
|
||||||
|
<a href="{{ link.url }}" target="_blank" rel="noopener" aria-label="{{ link.label|default:link.network }}" class="inline-flex items-center gap-2 rounded-full border border-dashed border-border text-foreground transition-colors hover:border-accent hover:text-accent {% if style == "stack" %}w-full px-4 py-2{% else %}p-2.5{% endif %}">
|
||||||
|
{% if link.network %}<svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ link.network|split:":"|first }}.svg#{{ link.network|split:":"|last }}"></use></svg>{% endif %}
|
||||||
|
{% if style == "stack" and link.label %}<span class="coffee-body text-sm font-medium">{{ link.label }}</span>{% endif %}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
44
templates/overrides/coffee/stats.ninjatpl
Normal file
44
templates/overrides/coffee/stats.ninjatpl
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}"{% if countUp %} data-stats{% endif %}>
|
||||||
|
<div class="mx-auto max-w-6xl px-4">
|
||||||
|
{% if heading %}<h2 class="coffee-display text-center text-3xl text-primary md:text-4xl">{{ heading }}</h2>{% endif %}
|
||||||
|
{% if intro %}<p class="coffee-body mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||||
|
<dl class="mt-12 grid grid-cols-2 gap-8 {% if columns == 2 %}md:grid-cols-2{% elif columns == 3 %}md:grid-cols-3{% else %}md:grid-cols-4{% endif %}">
|
||||||
|
{% for item in items %}
|
||||||
|
<div class="text-center">
|
||||||
|
<dd class="coffee-display text-4xl text-accent md:text-5xl">{% if item.prefix %}{{ item.prefix }}{% endif %}<span{% if countUp %} data-countup-value="{{ item.value }}"{% endif %}>{{ item.value }}</span>{% if item.suffix %}{{ item.suffix }}{% endif %}</dd>
|
||||||
|
{% if item.label %}<dt class="coffee-hand mt-2 text-xl text-muted-foreground">{{ item.label }}</dt>{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
{% if countUp %}<script>
|
||||||
|
(function(){
|
||||||
|
var sections = document.querySelectorAll('[data-stats]');
|
||||||
|
var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||||
|
function animate(section){
|
||||||
|
section.querySelectorAll('[data-countup-value]').forEach(function(node){
|
||||||
|
var raw = node.getAttribute('data-countup-value') || '';
|
||||||
|
var target = parseFloat(raw.replace(/[^0-9.\-]/g, ''));
|
||||||
|
if (isNaN(target)) return;
|
||||||
|
var decimals = (raw.split('.')[1] || '').length;
|
||||||
|
var start = null, dur = 1200;
|
||||||
|
function step(ts){
|
||||||
|
if (start === null) start = ts;
|
||||||
|
var p = Math.min((ts - start) / dur, 1);
|
||||||
|
var eased = 1 - Math.pow(1 - p, 3);
|
||||||
|
node.textContent = (target * eased).toFixed(decimals);
|
||||||
|
if (p < 1) requestAnimationFrame(step); else node.textContent = target.toFixed(decimals);
|
||||||
|
}
|
||||||
|
requestAnimationFrame(step);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
sections.forEach(function(section){
|
||||||
|
if (reduce || !('IntersectionObserver' in window)) return;
|
||||||
|
var io = new IntersectionObserver(function(entries){
|
||||||
|
entries.forEach(function(e){ if (e.isIntersecting){ animate(section); io.disconnect(); } });
|
||||||
|
}, { threshold: 0.3 });
|
||||||
|
io.observe(section);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>{% endif %}
|
||||||
21
templates/overrides/coffee/team.ninjatpl
Normal file
21
templates/overrides/coffee/team.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-6xl px-4">
|
||||||
|
{% if heading %}<h2 class="coffee-display text-center text-3xl text-primary md:text-4xl"><span class="coffee-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||||
|
{% if intro %}<p class="coffee-body mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||||
|
<div class="mt-12 {% if layout == "list" %}mx-auto max-w-3xl space-y-6{% elif layout == "single" %}mx-auto max-w-xl{% else %}grid grid-cols-1 gap-8 sm:grid-cols-2 {% if columns == 2 %}lg:grid-cols-2{% elif columns == 4 %}lg:grid-cols-4{% else %}lg:grid-cols-3{% endif %}{% endif %}">
|
||||||
|
{% for member in members %}
|
||||||
|
<div class="{% if layout == "list" %}coffee-card flex items-start gap-5 p-5{% else %}text-center{% endif %}">
|
||||||
|
{% if member.photo %}<span class="{% if layout == "list" %}h-20 w-20{% else %}mx-auto h-28 w-28{% endif %} block shrink-0 overflow-hidden rounded-full bg-secondary ring-2 ring-border">{% img member.photo alt=member.name class="h-full w-full object-cover" %}</span>{% endif %}
|
||||||
|
<div class="{% if layout != "list" %}mt-4{% endif %}">
|
||||||
|
{% if member.name %}<h3 class="coffee-display text-lg text-primary">{{ member.name }}</h3>{% endif %}
|
||||||
|
{% if member.role %}<p class="coffee-hand text-lg text-accent">{{ member.role }}</p>{% endif %}
|
||||||
|
{% if member.bio %}<p class="coffee-body mt-2 text-sm text-muted-foreground">{{ member.bio }}</p>{% endif %}
|
||||||
|
{% if member.socials %}<div class="mt-3 flex gap-3 {% if layout != "list" %}justify-center{% endif %}">
|
||||||
|
{% for social in member.socials %}{% if social.url %}<a href="{{ social.url }}" class="text-muted-foreground transition-colors hover:text-accent" aria-label="Social link"><svg class="h-5 w-5" aria-hidden="true"><use href="/icons/{{ social.icon|split:":"|first }}.svg#{{ social.icon|split:":"|last }}"></use></svg></a>{% endif %}{% endfor %}
|
||||||
|
</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
26
templates/overrides/coffee/testimonial.ninjatpl
Normal file
26
templates/overrides/coffee/testimonial.ninjatpl
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-6xl px-4">
|
||||||
|
{% if heading %}<h2 class="coffee-display text-center text-3xl text-primary md:text-4xl"><span class="coffee-doodle-underline">{{ heading }}</span></h2>{% endif %}
|
||||||
|
<div class="mt-12 {% if layout == "single" %}mx-auto max-w-3xl{% elif layout == "carousel" %}flex snap-x snap-mandatory gap-6 overflow-x-auto pb-4{% else %}grid grid-cols-1 gap-6 md:grid-cols-2 lg:grid-cols-3{% endif %}">
|
||||||
|
{% for q in quotes %}
|
||||||
|
<figure class="coffee-card flex flex-col p-6{% if layout == "carousel" %} min-w-[18rem] max-w-sm shrink-0 snap-start{% endif %}">
|
||||||
|
{% if q.rating %}<div class="mb-3 flex gap-0.5" aria-label="Rated {{ q.rating }} out of 5">
|
||||||
|
<svg class="h-4 w-4 {% if q.rating >= 1 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||||
|
<svg class="h-4 w-4 {% if q.rating >= 2 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||||
|
<svg class="h-4 w-4 {% if q.rating >= 3 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||||
|
<svg class="h-4 w-4 {% if q.rating >= 4 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||||
|
<svg class="h-4 w-4 {% if q.rating >= 5 %}text-accent{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||||
|
</div>{% endif %}
|
||||||
|
<blockquote class="coffee-body flex-1 text-lg leading-relaxed text-foreground">{% if q.quote %}“{{ q.quote }}”{% endif %}</blockquote>
|
||||||
|
<figcaption class="mt-5 flex items-center gap-3">
|
||||||
|
{% if q.avatar %}<span class="h-10 w-10 shrink-0 overflow-hidden rounded-full bg-secondary">{% img q.avatar alt=q.name class="h-full w-full object-cover" %}</span>{% endif %}
|
||||||
|
<span>
|
||||||
|
{% if q.name %}<span class="coffee-display block text-base text-primary">{{ q.name }}</span>{% endif %}
|
||||||
|
{% if q.role %}<span class="coffee-hand block text-lg text-accent">{{ q.role }}</span>{% endif %}
|
||||||
|
</span>
|
||||||
|
</figcaption>
|
||||||
|
</figure>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
@ -1 +1 @@
|
|||||||
<div class="coffee-body coffee-dropcap prose max-w-prose text-foreground {{ class }}">{{ text|safe }}</div>
|
<div class="coffee-body prose max-w-none flex-1 text-foreground [&_a]:text-accent [&_a]:underline [&_a]:decoration-dashed [&_strong]:text-primary {{ proseClass }}">{{ text|safe }}</div>
|
||||||
|
|||||||
21
templates/overrides/coffee/timeline.ninjatpl
Normal file
21
templates/overrides/coffee/timeline.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
<section class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||||
|
<div class="mx-auto max-w-4xl px-4">
|
||||||
|
{% if heading %}<h2 class="coffee-display text-center text-3xl text-primary md:text-4xl">{{ heading }}</h2>{% endif %}
|
||||||
|
{% if intro %}<p class="coffee-body mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground">{{ intro }}</p>{% endif %}
|
||||||
|
<ol class="mt-12 {% if orientation == "horizontal" %}grid grid-cols-1 gap-8 sm:grid-cols-2 lg:grid-cols-4{% else %}relative space-y-8 before:absolute before:left-4 before:top-2 before:h-[calc(100%-1rem)] before:w-px before:bg-border{% endif %}">
|
||||||
|
{% for step in steps %}
|
||||||
|
<li class="{% if orientation == "horizontal" %}text-center{% else %}relative pl-14{% endif %}">
|
||||||
|
<span class="coffee-stamp coffee-stamp-sm {% if orientation == "horizontal" %}mx-auto{% else %}absolute left-0 top-0 ring-4 ring-background{% endif %}">
|
||||||
|
{% if marker == "numbered" %}{{ forloop.counter }}{% elif marker == "dot" %}<span class="h-2 w-2 rounded-full bg-accent-foreground"></span>{% else %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#coffee"></use></svg>{% endif %}
|
||||||
|
</span>
|
||||||
|
<div class="{% if orientation == "horizontal" %}mt-4{% else %}pt-1{% endif %}">
|
||||||
|
{% if step.label %}<p class="coffee-hand text-lg text-accent">{{ step.label }}</p>{% endif %}
|
||||||
|
{% if step.title %}<h3 class="coffee-display mt-1 text-lg text-primary">{{ step.title }}</h3>{% endif %}
|
||||||
|
{% if step.text %}<p class="coffee-body mt-1 text-muted-foreground">{{ step.text }}</p>{% endif %}
|
||||||
|
{% if step.media %}<div class="coffee-frame mt-3 overflow-hidden rounded-sm p-1.5">{% img step.media alt=step.title class="h-auto w-full rounded-sm object-cover" %}</div>{% endif %}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ol>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
10
templates/overrides/coffee/utility-bar.ninjatpl
Normal file
10
templates/overrides/coffee/utility-bar.ninjatpl
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{# coffee utility-bar — thin contact strip; left/right item lists {icon,label,link}. #}
|
||||||
|
{% set ubcls = "bg-muted text-muted-foreground border-b border-dashed border-border" %}
|
||||||
|
{% if variant == "primary" %}{% set ubcls = "bg-primary text-primary-foreground" %}{% elif variant == "dark" %}{% set ubcls = "bg-foreground text-background" %}{% endif %}
|
||||||
|
{% macro ubitem(it) %}{% if it.link %}<a href="{{ it.link }}" class="inline-flex items-center gap-1.5 transition-opacity hover:opacity-80">{% if it.icon %}<svg class="h-3.5 w-3.5" aria-hidden="true"><use href="/icons/{{ it.icon|split:":"|first }}.svg#{{ it.icon|split:":"|last }}"></use></svg>{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</a>{% else %}<span class="inline-flex items-center gap-1.5">{% if it.icon %}<svg class="h-3.5 w-3.5" aria-hidden="true"><use href="/icons/{{ it.icon|split:":"|first }}.svg#{{ it.icon|split:":"|last }}"></use></svg>{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</span>{% endif %}{% endmacro %}
|
||||||
|
<div class="coffee-body w-full text-xs {% if not showOnMobile %}hidden sm:block{% endif %} {{ ubcls }} {{ class }}">
|
||||||
|
<div class="mx-auto flex h-9 max-w-7xl items-center justify-between gap-4 px-4 sm:px-6 lg:px-8">
|
||||||
|
<div class="flex flex-wrap items-center gap-4">{% for it in leftItems %}{{ ubitem(it) }}{% endfor %}</div>
|
||||||
|
<div class="flex flex-wrap items-center justify-end gap-4">{% for it in rightItems %}{{ ubitem(it) }}{% endfor %}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
22
templates/overrides/coffee/video-embed.ninjatpl
Normal file
22
templates/overrides/coffee/video-embed.ninjatpl
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{% with aspect=aspect|default:"16:9" %}<figure class="bn-video my-8{% if class %} {{ class }}{% endif %}"><div class="bn-video-frame coffee-frame relative w-full overflow-hidden rounded-sm bg-secondary p-1.5 {% if aspect == "4:3" %}aspect-[4/3]{% elif aspect == "1:1" %}aspect-square{% elif aspect == "21:9" %}aspect-[21/9]{% else %}aspect-video{% endif %}" data-bn-video data-video-url="{{ url|default:"" }}" data-video-media="{% if media %}{{ media|mediaURL }}{% endif %}"><button type="button" data-bn-video-play class="group absolute inset-0 flex h-full w-full items-center justify-center focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ring" aria-label="Play video{% if title %}: {{ title }}{% endif %}">{% if poster %}{% img poster alt=title|default:"" class="absolute inset-0 h-full w-full rounded-sm object-cover" %}{% endif %}<span class="absolute inset-0 bg-foreground/10 transition-colors group-hover:bg-foreground/20" aria-hidden="true"></span><span class="relative inline-flex h-16 w-16 items-center justify-center rounded-full bg-accent text-accent-foreground shadow-lg transition-transform group-hover:scale-105 motion-reduce:transform-none"><svg class="h-7 w-7" aria-hidden="true"><use href="/icons/lucide.svg#play"></use></svg></span></button></div>{% if caption %}<figcaption class="coffee-hand mt-3 text-center text-xl text-accent">{{ caption }}</figcaption>{% endif %}</figure><script>
|
||||||
|
(function(){
|
||||||
|
if(window.__bnVideoFacade)return;window.__bnVideoFacade=true;
|
||||||
|
function yt(u){var m=u.match(/(?:youtube\.com\/(?:watch\?v=|embed\/|shorts\/)|youtu\.be\/)([\w-]{6,})/);return m?m[1]:null;}
|
||||||
|
function vim(u){var m=u.match(/vimeo\.com\/(?:video\/)?(\d+)/);return m?m[1]:null;}
|
||||||
|
document.addEventListener('click',function(e){
|
||||||
|
var btn=e.target.closest('[data-bn-video-play]');if(!btn)return;
|
||||||
|
var frame=btn.closest('[data-bn-video]');if(!frame)return;
|
||||||
|
e.preventDefault();
|
||||||
|
var url=frame.getAttribute('data-video-url')||'',media=frame.getAttribute('data-video-media')||'',node;
|
||||||
|
var vstyle='position:absolute;inset:0;width:100%;height:100%;object-fit:contain;background:rgba(0,0,0,1);';
|
||||||
|
if(media){node=document.createElement('video');node.src=media;node.controls=true;node.autoplay=true;node.setAttribute('playsinline','');node.style.cssText=vstyle;}
|
||||||
|
else{var y=yt(url),v=vim(url),embed;
|
||||||
|
if(y)embed='https://www.youtube-nocookie.com/embed/'+y+'?autoplay=1&rel=0';
|
||||||
|
else if(v)embed='https://player.vimeo.com/video/'+v+'?autoplay=1';
|
||||||
|
if(embed){node=document.createElement('iframe');node.src=embed;node.allow='autoplay; fullscreen; picture-in-picture';node.setAttribute('allowfullscreen','');node.setAttribute('title',btn.getAttribute('aria-label')||'Video');node.style.cssText='position:absolute;inset:0;width:100%;height:100%;border:0;';}
|
||||||
|
else if(url){node=document.createElement('video');node.src=url;node.controls=true;node.autoplay=true;node.setAttribute('playsinline','');node.style.cssText=vstyle;}
|
||||||
|
}
|
||||||
|
if(node){btn.remove();frame.appendChild(node);}
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>{% endwith %}
|
||||||
Loading…
x
Reference in New Issue
Block a user