Compare commits
4 Commits
efbfaea74c
...
10cecadef6
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
10cecadef6 | ||
|
|
28a201a33a | ||
|
|
ae082bede0 | ||
|
|
a66113fce0 |
24
README.md
Normal file
24
README.md
Normal file
@ -0,0 +1,24 @@
|
||||
# Editorial
|
||||
|
||||
A reading-first theme built around a serif baseline grid and a generous measure. Light mode is warm book paper with dark ink. Dark mode drops to a low-glare charcoal with sepia type for late-night reading. Drop caps, running marginalia, and small-caps labels do the design work, so the writing leads and the page stays quiet.
|
||||
|
||||
## Good for
|
||||
|
||||
We built Editorial for people who write at length. Treat these as starting points, not limits. If the mood fits your brand, use it:
|
||||
|
||||
- Personal essayists and columnists
|
||||
- Independent newsletters and subscriptions
|
||||
- Longform blogs and reported features
|
||||
- Author and portfolio sites
|
||||
|
||||
## What you get
|
||||
|
||||
- Four color presets, each with a tuned light mode and a tuned dark mode: Book Paper, Sepia Review, Legal Pad, and Nightdesk.
|
||||
- Playfair Display for headings and Source Serif 4 for body text, both bundled and ready. Fonts stay admin-controlled, so you can swap them from the typography settings.
|
||||
- Four persona blocks: a drop-cap essay opener, running marginalia side notes, a reading list, and a field-notes aside.
|
||||
- A demo site that installs when you activate the theme: a home page, an about page, a blog with three essays, and a contact page whose form emails your admins. It comes seeded as a newsletter called The Margin, by Frances Hale.
|
||||
- Every core block restyled to match, plus a themed login screen and a themed 404 page.
|
||||
|
||||
## Install
|
||||
|
||||
Install from the BlockNinja registry, then pick a preset and assign your fonts in the admin. Activate the demo content if you want a fully dressed starting point.
|
||||
@ -1,39 +1,44 @@
|
||||
# Editorial — recommended Google Fonts
|
||||
# Editorial — fonts
|
||||
|
||||
This theme ships with no bundled fonts in the current build pass
|
||||
(`fonts.json = []`). Admins assign fonts via the BlockNinja typography panel.
|
||||
The theme's CSS routes every `font-family` through the host-provided
|
||||
`--font-heading`, `--font-body`, and `--font-mono` custom properties with
|
||||
fallback stacks taken from spec §5, so the layout is usable before any font
|
||||
is picked.
|
||||
This theme **bundles** a Latin subset of two open-licence (SIL OFL 1.1)
|
||||
serif families, so it looks correct the moment it is activated — no admin
|
||||
setup required. The bundled faces are declared in `fonts.json` and land in
|
||||
the typography picker as `source=template` rows.
|
||||
|
||||
For the intended editorial aesthetic, open the typography panel in admin and
|
||||
add each of the following from the **Google Fonts** tab:
|
||||
| Slot | Bundled family | Weights / styles | Role |
|
||||
|---------|-------------------|---------------------------|-------------------------------------------------------------|
|
||||
| Heading | Playfair Display | 700 normal, 900 italic | Didone display face for the masthead wordmark and headlines |
|
||||
| Body | Source Serif 4 | 400 normal, 400 italic | Long-read body type; italic for marginalia and captions |
|
||||
|
||||
| Slot | Family | Source | Weights / styles | Why |
|
||||
|-----------|-----------------------|--------------------|------------------------------------------------------------------|----------------------------------------------------------------------|
|
||||
| Heading | Playfair Display | `google:Playfair Display` | 700 normal, 900 italic | Didone display face for the masthead wordmark and headlines. |
|
||||
| Body | Source Serif 4 | `google:Source Serif 4` | 400 normal, 400 italic, 600 normal | Long-read body type; italic for marginalia and figcaptions. |
|
||||
| UI / caption (body slot) | Inter | `google:Inter` | 500 normal, 600 normal | Byline names, timestamps, and small-caps section labels. |
|
||||
| Mono | JetBrains Mono | `google:JetBrains Mono` | 400 normal | Code, datelines on financial copy, ISSN in the colophon. |
|
||||
The bundled woff2 files and their licence record live in `assets/fonts/`
|
||||
(`OFL.txt`). Every `font-family` in the theme is routed through
|
||||
`var(--font-heading, ...)`, `var(--font-body, ...)` and `var(--font-mono, ...)`
|
||||
with a fallback stack, so the layout is usable before any font is assigned
|
||||
and the admin font picker stays in full control.
|
||||
|
||||
## How to assign
|
||||
## Assigning the bundled faces
|
||||
|
||||
1. In the BlockNinja admin, open **Settings → Theme → Typography**.
|
||||
2. Click the **Google Fonts** tab.
|
||||
3. Search for the family above (e.g. "Playfair Display"), then click **Add**.
|
||||
4. Once added, the family appears in the picker for that slot.
|
||||
5. Assign **Playfair Display** to the *Heading* slot.
|
||||
6. Assign **Source Serif 4** to the *Body* slot.
|
||||
7. Assign **JetBrains Mono** to the *Mono* slot.
|
||||
8. (Optional) For UI / caption elements that ride the body slot but want a
|
||||
sans-serif treatment, override per-block via the editor.
|
||||
2. In the **Bundled** tab, assign **Playfair Display** to the *Heading* slot.
|
||||
3. Assign **Source Serif 4** to the *Body* slot.
|
||||
4. Leave *Mono* as the system default, or assign **JetBrains Mono** from the
|
||||
Google Fonts tab — it is used for the masthead kicker, datelines and the
|
||||
colophon ISSN.
|
||||
|
||||
## Wave-2 (out of scope here)
|
||||
## Optional upgrades (Google Fonts tab)
|
||||
|
||||
If the editorial brand is going to be locked in to a specific commercial
|
||||
display face (e.g. *Migra* or *PP Editorial New* instead of *Playfair
|
||||
Display*), licence and bundle the woff2 set, then update `fonts.json` to
|
||||
declare the family + variants. The CSS does not change — the same
|
||||
`var(--font-heading)` consumer pattern picks up the bundled face once an
|
||||
admin assigns it.
|
||||
- **Spectral** (`google:Spectral`) — a refined small-caps serif. Assign it to
|
||||
the body slot if you want the small-caps section labels to carry true
|
||||
small-caps rather than the synthesized `font-variant: small-caps` fallback.
|
||||
- **Newsreader** (`google:Newsreader`) — an alternative reading serif for the
|
||||
body slot; swaps in without any CSS change via `var(--font-body)`.
|
||||
- **JetBrains Mono** (`google:JetBrains Mono`) — for the *Mono* slot
|
||||
(kicker / dateline / ISSN).
|
||||
|
||||
## Bundling a commercial display face (out of scope for Wave A)
|
||||
|
||||
If the brand locks in a licensed display face (e.g. *Migra* or *PP Editorial
|
||||
New*) in place of Playfair Display, licence and subset the woff2 set, drop it
|
||||
in `assets/fonts/`, and add the family + variants to `fonts.json`. The CSS
|
||||
does not change — the same `var(--font-heading)` consumer picks up whichever
|
||||
face an admin assigns.
|
||||
|
||||
25
RECOMMENDED_ICONS.md
Normal file
25
RECOMMENDED_ICONS.md
Normal file
@ -0,0 +1,25 @@
|
||||
# Recommended Icons — Editorial
|
||||
|
||||
Editorial is a typographic theme. It leans on hairline rules and small-caps
|
||||
labels, not iconography, so it references only a single bundled pack.
|
||||
`plugin.mod` declares it in `required_icon_packs`; until the loader
|
||||
auto-installs declared packs, an admin should confirm it is enabled before
|
||||
activation.
|
||||
|
||||
## Required pack
|
||||
|
||||
- **lucide** — ships bundled with the CMS (v0.460). Enabled by default. No
|
||||
action needed in most installs; if icons render as empty boxes, open
|
||||
`/admin/icons` and confirm **Lucide** is installed and enabled.
|
||||
|
||||
## Icons used by the theme chrome and overrides
|
||||
|
||||
All via the sprite pipeline (`<svg><use href="/icons/lucide.svg#<name>"/></svg>`
|
||||
or the `::lucide:<name>::` shorthand):
|
||||
|
||||
`arrow-right`, `chevron-down`, `chevron-left`, `chevron-right`, `x`, `check`,
|
||||
`play`, `map-pin`, `diamond`, `minus`.
|
||||
|
||||
Block content that stores an icon (feature-grid items, social links, contact
|
||||
channels) may reference any installed pack as a `pack:name` string; the
|
||||
schemas default to `lucide` so the theme renders without extra setup.
|
||||
24
assets/fonts/OFL.txt
Normal file
24
assets/fonts/OFL.txt
Normal file
@ -0,0 +1,24 @@
|
||||
Bundled fonts — license record (SIL Open Font License, Version 1.1)
|
||||
====================================================================
|
||||
|
||||
The woff2 files in this directory are Latin subsets of the following
|
||||
open-source families, both distributed under the SIL Open Font License 1.1
|
||||
(https://openfontlicense.org). Subsetting and format conversion (TTF to
|
||||
woff2) are expressly permitted by the OFL; the derived subsets remain under
|
||||
the same license.
|
||||
|
||||
Playfair Display
|
||||
playfair-display-700.woff2, playfair-display-900-italic.woff2
|
||||
Copyright (c) Claus Eggers Sorensen (https://www.forthehearts.net), with
|
||||
Reserved Font Name "Playfair Display". Display face for masthead wordmarks
|
||||
and headlines.
|
||||
|
||||
Source Serif 4
|
||||
source-serif-4-400.woff2, source-serif-4-400-italic.woff2
|
||||
Copyright (c) Adobe (https://github.com/adobe-fonts/source-serif), with
|
||||
Reserved Font Name "Source Serif". Long-read body face; italic for
|
||||
marginalia and figure captions.
|
||||
|
||||
The full SIL Open Font License 1.1 text applies to each family and is
|
||||
available at https://openfontlicense.org. This file records the license and
|
||||
copyright attribution as required for redistribution under the OFL.
|
||||
BIN
assets/fonts/playfair-display-700.woff2
Normal file
BIN
assets/fonts/playfair-display-700.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/playfair-display-900-italic.woff2
Normal file
BIN
assets/fonts/playfair-display-900-italic.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/source-serif-4-400-italic.woff2
Normal file
BIN
assets/fonts/source-serif-4-400-italic.woff2
Normal file
Binary file not shown.
BIN
assets/fonts/source-serif-4-400.woff2
Normal file
BIN
assets/fonts/source-serif-4-400.woff2
Normal file
Binary file not shown.
@ -1,48 +1,33 @@
|
||||
# Editorial theme block definitions (codeless .bnp, WO-WZ-021).
|
||||
# Keys are fully qualified (editorial:<key>) because the codeless loader
|
||||
# registers definitions verbatim — there is no PluginBlockRegistry prefixing
|
||||
# in the codeless path (unlike the wasm/templ path). The qualified keys match
|
||||
# the hardcoded data-block attributes and the master_pages.json references.
|
||||
# Editorial theme block definitions (codeless .bnp, WO-TF-017).
|
||||
#
|
||||
# These are the theme's TRUE persona blocks — furniture with no builtin
|
||||
# equivalent. Everything else the theme used to ship as a block (masthead,
|
||||
# byline, pull quote, section label, colophon) is now a re-skin OVERRIDE of a
|
||||
# builtin (navbar, post-metadata, quote, divider, footer) and lives in
|
||||
# templates/overrides/editorial/. Keys are fully qualified (editorial:<key>)
|
||||
# because the codeless loader registers definitions verbatim.
|
||||
blocks:
|
||||
- key: editorial:masthead
|
||||
title: Masthead
|
||||
description: Newspaper-style masthead with Playfair wordmark and hairline rule
|
||||
category: navigation
|
||||
schema: masthead.schema.json
|
||||
template: masthead.ninjatpl
|
||||
- key: editorial:byline
|
||||
title: Byline
|
||||
description: Author byline with optional photo, dateline, and read time
|
||||
category: theme
|
||||
schema: byline.schema.json
|
||||
template: byline.ninjatpl
|
||||
- key: editorial:pullquote
|
||||
title: Pull Quote
|
||||
description: Large oxblood pull quote in Playfair italic
|
||||
category: theme
|
||||
schema: pullquote.schema.json
|
||||
template: pullquote.ninjatpl
|
||||
- key: editorial:dropcap_intro
|
||||
title: Drop Cap Intro
|
||||
description: Opening paragraph with an oxblood Playfair drop cap
|
||||
description: Opening paragraph whose first letter renders as an oxblood Playfair drop cap
|
||||
category: theme
|
||||
schema: dropcap_intro.schema.json
|
||||
template: dropcap_intro.ninjatpl
|
||||
- key: editorial:marginalia
|
||||
title: Marginalia Rail
|
||||
description: Right-rail italic notes for longform articles
|
||||
description: Right-rail italic notes for longform articles; collapses inline on narrow viewports
|
||||
category: theme
|
||||
schema: marginalia.schema.json
|
||||
template: marginalia.ninjatpl
|
||||
- key: editorial:section_label
|
||||
title: Section Label
|
||||
description: Small-caps section label with optional hairline divider
|
||||
- key: editorial:reading_list
|
||||
title: Reading List
|
||||
description: A curated further-reading list — small-caps heading over hairline-ruled links with notes
|
||||
category: theme
|
||||
schema: section_label.schema.json
|
||||
template: section_label.ninjatpl
|
||||
- key: editorial:colophon
|
||||
title: Colophon
|
||||
description: Editorial footer with ISSN, subscribe stub, and link list
|
||||
category: navigation
|
||||
schema: colophon.schema.json
|
||||
template: colophon.ninjatpl
|
||||
schema: reading_list.schema.json
|
||||
template: reading_list.ninjatpl
|
||||
- key: editorial:field_notes
|
||||
title: Field Notes
|
||||
description: Dated notebook entries in small caps, separated by hairline rules
|
||||
category: theme
|
||||
schema: field_notes.schema.json
|
||||
template: field_notes.ninjatpl
|
||||
|
||||
@ -1 +0,0 @@
|
||||
<div class="editorial-column" data-block="editorial:byline"><div class="editorial-byline">{% if showPhoto != "false" and showPhoto != false and authorSlug %}<div class="editorial-byline-photo" aria-hidden="true"></div>{% endif %}<div class="flex-1">{% if authorSlug %}<div class="editorial-byline-author">By {{ authorSlug }}</div>{% else %}<div class="editorial-byline-author">By Editorial Staff</div>{% endif %}<div class="editorial-byline-meta">{% if dateline %}<span>{{ dateline }}</span>{% endif %}{% if showReadTime != "false" and showReadTime != false %}{% if dateline %}<span class="px-1">·</span>{% endif %}<span>5 min read</span>{% endif %}</div></div></div></div>
|
||||
@ -1,36 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Byline",
|
||||
"description": "Article byline with author name, optional photo, optional read-time, and dateline.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"authorSlug": {
|
||||
"type": "string",
|
||||
"title": "Author Slug",
|
||||
"description": "Slug of the author whose name and photo should be rendered.",
|
||||
"x-editor": "slug"
|
||||
},
|
||||
"showPhoto": {
|
||||
"type": "string",
|
||||
"title": "Show Photo",
|
||||
"description": "Render the author's avatar to the left of the name.",
|
||||
"x-editor": "select",
|
||||
"enum": ["false", "true"],
|
||||
"default": "true"
|
||||
},
|
||||
"showReadTime": {
|
||||
"type": "string",
|
||||
"title": "Show Read Time",
|
||||
"description": "Include the read-time estimate next to the dateline.",
|
||||
"x-editor": "select",
|
||||
"enum": ["false", "true"],
|
||||
"default": "true"
|
||||
},
|
||||
"dateline": {
|
||||
"type": "string",
|
||||
"title": "Dateline",
|
||||
"description": "Optional explicit dateline (e.g. \"London, March 14\"). Rendered in small caps.",
|
||||
"x-editor": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
<div class="editorial-wide editorial-colophon" data-block="editorial:colophon">{% if showSignup == true or showSignup == "true" %}<div class="mb-4"><div class="editorial-section-label mb-1">Subscribe</div><form class="flex gap-2 max-w-md"><input type="email" placeholder="you@example.com" class="flex-1 px-3 py-1 border editorial-hairline" aria-label="Email address"><button type="submit" class="editorial-button">Subscribe</button></form></div>{% endif %}{% if links %}<nav aria-label="Footer"><ul class="flex flex-wrap gap-x-4 gap-y-1">{% for link in links %}<li><a href="{{ link.url|default:"#" }}" class="editorial-button" style="border:0;padding:0;letter-spacing:0.05em;">{{ link.text }}</a></li>{% endfor %}</ul></nav>{% endif %}{% if issn %}<div class="mt-3"><span class="editorial-colophon-issn">ISSN {{ issn }}</span></div>{% endif %}</div>
|
||||
@ -1,46 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Colophon",
|
||||
"description": "Editorial footer with print-convention ISSN field, optional subscribe stub, and a list of footer links.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"showSignup": {
|
||||
"type": "string",
|
||||
"title": "Show Subscribe Stub",
|
||||
"description": "Render the newsletter subscribe stub above the link list.",
|
||||
"x-editor": "select",
|
||||
"enum": ["false", "true"],
|
||||
"default": "false"
|
||||
},
|
||||
"issn": {
|
||||
"type": "string",
|
||||
"title": "ISSN",
|
||||
"description": "Print-convention ISSN number. Rendered in JetBrains Mono small caps.",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"links": {
|
||||
"type": "array",
|
||||
"title": "Footer Links",
|
||||
"description": "Inline footer link list.",
|
||||
"default": [],
|
||||
"x-editor": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"text": {
|
||||
"type": "string",
|
||||
"title": "Link Text",
|
||||
"description": "Display text.",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"title": "URL",
|
||||
"description": "External URL.",
|
||||
"x-editor": "link"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
blocks/field_notes.ninjatpl
Normal file
14
blocks/field_notes.ninjatpl
Normal file
@ -0,0 +1,14 @@
|
||||
{# field_notes (editorial) — dated notebook entries: small-caps dateline over a short note, hairline-ruled. Fields: title, notes[].date/note. #}
|
||||
<section class="editorial-column my-10" data-block="editorial:field_notes">
|
||||
{% if title %}<div class="editorial-section-label">{{ title }}</div><hr class="editorial-section-label-rule">{% endif %}
|
||||
{% if notes %}
|
||||
<div>
|
||||
{% for entry in notes %}
|
||||
<article class="py-4" style="border-top:1px solid hsl(var(--border));">
|
||||
{% if entry.date %}<div class="editorial-meta mb-1">{{ entry.date }}</div>{% endif %}
|
||||
{% if entry.note %}<div class="editorial-text prose-editorial">{{ entry.note|safe }}</div>{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
39
blocks/field_notes.schema.json
Normal file
39
blocks/field_notes.schema.json
Normal file
@ -0,0 +1,39 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Field Notes",
|
||||
"description": "Dated notebook entries — a small-caps date over a short note, separated by hairline rules. The essayist's running observations.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "Heading",
|
||||
"description": "Small-caps section heading (e.g. \"Field notes\").",
|
||||
"x-editor": "text",
|
||||
"default": "Field notes"
|
||||
},
|
||||
"notes": {
|
||||
"type": "array",
|
||||
"title": "Notes",
|
||||
"description": "The dated entries, newest first.",
|
||||
"default": [],
|
||||
"x-editor": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"date": {
|
||||
"type": "string",
|
||||
"title": "Date",
|
||||
"description": "The dateline, rendered in small caps (e.g. \"March 14\").",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"note": {
|
||||
"type": "string",
|
||||
"title": "Note",
|
||||
"description": "The entry body. Rich text is permitted.",
|
||||
"x-editor": "richtext"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
<div class="editorial-wide" data-block="editorial:masthead">{% if compact == true or compact == "true" %}<div class="flex items-center justify-between py-3"><a href="/" class="editorial-masthead-wordmark text-2xl">{{ title|default:"Editorial" }}</a>{% if menuName %}<span class="editorial-masthead-kicker">{{ menuName }}</span>{% endif %}</div><hr class="editorial-masthead-rule">{% else %}<div class="text-center py-6">{% if kicker %}<div class="editorial-masthead-kicker mb-2">{{ kicker }}</div>{% endif %}<a href="/" class="editorial-masthead-wordmark inline-block text-5xl md:text-6xl">{{ title|default:"Editorial" }}</a><hr class="editorial-masthead-rule mt-4">{% if menuName %}<nav class="editorial-section-label mt-3" aria-label="Sections"><span class="text-xs">Sections: {{ menuName }}</span></nav>{% endif %}</div>{% endif %}</div>
|
||||
@ -1,34 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Masthead",
|
||||
"description": "Newspaper-style masthead with wordmark, optional kicker, hairline rule, and section navigation menu.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "Wordmark",
|
||||
"description": "The publication name. Rendered in Playfair Display 900 italic.",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"kicker": {
|
||||
"type": "string",
|
||||
"title": "Kicker",
|
||||
"description": "Small mono caption above the wordmark (e.g. issue label or established date).",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"menuName": {
|
||||
"type": "string",
|
||||
"title": "Section Menu",
|
||||
"description": "Menu providing the section navigation links rendered beneath the masthead.",
|
||||
"x-editor": "menu-select"
|
||||
},
|
||||
"compact": {
|
||||
"type": "string",
|
||||
"title": "Compact Mode",
|
||||
"description": "Use the compact masthead variant (single line) on article pages.",
|
||||
"x-editor": "select",
|
||||
"enum": ["false", "true"],
|
||||
"default": "false"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
{% if quote %}<aside class="editorial-column" data-block="editorial:pullquote"><blockquote class="editorial-pullquote">{{ quote|safe }}{% if attribution %}<cite class="editorial-pullquote-attribution">{{ attribution }}</cite>{% endif %}</blockquote></aside>{% endif %}
|
||||
@ -1,20 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Pull Quote",
|
||||
"description": "Large oxblood pull quote rendered in Playfair Display italic with optional attribution.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"quote": {
|
||||
"type": "string",
|
||||
"title": "Quote",
|
||||
"description": "The pull quote text. Rich text is permitted so authors can emphasise inline.",
|
||||
"x-editor": "richtext"
|
||||
},
|
||||
"attribution": {
|
||||
"type": "string",
|
||||
"title": "Attribution",
|
||||
"description": "Optional source attribution rendered in small caps below the quote.",
|
||||
"x-editor": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
16
blocks/reading_list.ninjatpl
Normal file
16
blocks/reading_list.ninjatpl
Normal file
@ -0,0 +1,16 @@
|
||||
{# reading_list (editorial) — curated further-reading list: small-caps heading over hairline-ruled entries with optional source + note. Fields: title, intro, items[].label/url/source/note. #}
|
||||
<section class="editorial-column my-10" data-block="editorial:reading_list">
|
||||
{% if title %}<div class="editorial-section-label">{{ title }}</div><hr class="editorial-section-label-rule">{% endif %}
|
||||
{% if intro %}<p class="editorial-text text-sm mb-4" style="color:hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
|
||||
{% if items %}
|
||||
<ul class="m-0 p-0" style="list-style:none;">
|
||||
{% for item in items %}
|
||||
<li class="py-3" style="border-top:1px solid hsl(var(--border));">
|
||||
{% if item.url %}<a href="{{ item.url }}" class="editorial-link" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif);font-size:1.1rem;">{{ item.label }}</a>{% else %}<span style="font-family:var(--font-heading, "Playfair Display", Georgia, serif);font-size:1.1rem;color:hsl(var(--foreground));">{{ item.label }}</span>{% endif %}
|
||||
{% if item.source %}<span class="editorial-meta ml-2">{{ item.source }}</span>{% endif %}
|
||||
{% if item.note %}<p class="editorial-text text-sm mt-1" style="color:hsl(var(--muted-foreground));">{{ item.note }}</p>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</section>
|
||||
57
blocks/reading_list.schema.json
Normal file
57
blocks/reading_list.schema.json
Normal file
@ -0,0 +1,57 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Reading List",
|
||||
"description": "A curated further-reading list rendered under a small-caps heading, each entry a hairline-ruled link with an optional note and source.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "Heading",
|
||||
"description": "Small-caps section heading (e.g. \"Further reading\").",
|
||||
"x-editor": "text",
|
||||
"default": "Further reading"
|
||||
},
|
||||
"intro": {
|
||||
"type": "string",
|
||||
"title": "Intro",
|
||||
"description": "Optional short line of context above the list.",
|
||||
"x-editor": "textarea"
|
||||
},
|
||||
"items": {
|
||||
"type": "array",
|
||||
"title": "Entries",
|
||||
"description": "The reading-list entries.",
|
||||
"default": [],
|
||||
"x-editor": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string",
|
||||
"title": "Title",
|
||||
"description": "The title of the piece.",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"title": "URL",
|
||||
"description": "Where the entry links to.",
|
||||
"x-editor": "link"
|
||||
},
|
||||
"source": {
|
||||
"type": "string",
|
||||
"title": "Source",
|
||||
"description": "Optional author or publication, rendered in small caps.",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"note": {
|
||||
"type": "string",
|
||||
"title": "Note",
|
||||
"description": "Optional one-line note on why it is worth reading.",
|
||||
"x-editor": "text"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
{% if label %}<div class="editorial-column" data-block="editorial:section_label"><div class="editorial-section-label">{{ label }}</div>{% if divider != "false" and divider != false %}<hr class="editorial-section-label-rule">{% endif %}</div>{% endif %}
|
||||
@ -1,22 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Section Label",
|
||||
"description": "Small-caps section label followed by an optional hairline divider rule.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string",
|
||||
"title": "Label",
|
||||
"description": "Section label text. Rendered uppercase with small-caps.",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"divider": {
|
||||
"type": "string",
|
||||
"title": "Divider",
|
||||
"description": "Whether to render a hairline rule beneath the label.",
|
||||
"x-editor": "select",
|
||||
"enum": ["true", "false"],
|
||||
"default": "true"
|
||||
}
|
||||
}
|
||||
}
|
||||
19
fonts.json
19
fonts.json
@ -1 +1,18 @@
|
||||
[]
|
||||
[
|
||||
{
|
||||
"name": "Playfair Display",
|
||||
"family": "Playfair Display",
|
||||
"variants": [
|
||||
{ "weight": "700", "style": "normal", "file": "fonts/playfair-display-700.woff2" },
|
||||
{ "weight": "900", "style": "italic", "file": "fonts/playfair-display-900-italic.woff2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Source Serif 4",
|
||||
"family": "Source Serif 4",
|
||||
"variants": [
|
||||
{ "weight": "400", "style": "normal", "file": "fonts/source-serif-4-400.woff2" },
|
||||
{ "weight": "400", "style": "italic", "file": "fonts/source-serif-4-400-italic.woff2" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
549
manifest.yaml
549
manifest.yaml
@ -1,5 +1,10 @@
|
||||
# Editorial theme — codeless .bnp manifest (WO-WZ-021 Phase B).
|
||||
# Editorial theme — codeless .bnp manifest (Theme Fleet Wave A, WO-TF-017).
|
||||
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
|
||||
#
|
||||
# Persona: a personal essayist / independent newsletter. A reading experience
|
||||
# above all — serif baseline grid, drop caps, running marginalia, small-caps
|
||||
# section labels, author byline foregrounded. Typography does the design work;
|
||||
# ornament is limited to hairline rules and generous whitespace.
|
||||
theme_presets: presets.json
|
||||
bundled_fonts: fonts.json
|
||||
master_pages: master_pages.json
|
||||
@ -7,68 +12,191 @@ master_pages: master_pages.json
|
||||
system_templates:
|
||||
- key: editorial
|
||||
title: Editorial
|
||||
description: "Didone display type, narrow measure, and hairline rules for newsrooms, longform writers, and opinion columnists."
|
||||
description: "A personal essayist's reading room: serif baseline grid, drop caps, running marginalia and small-caps labels. Built for longform writers and independent newsletters."
|
||||
|
||||
page_templates:
|
||||
- system: editorial
|
||||
key: default
|
||||
title: Default
|
||||
description: "Standard page with masthead, narrow main column, footer."
|
||||
description: "Standard page: masthead, narrow reading column, colophon."
|
||||
slots: [header, main, footer]
|
||||
- system: editorial
|
||||
key: full-width
|
||||
title: Full Width
|
||||
description: "Edge-to-edge for photo essays and cover images."
|
||||
slots: [header, main, footer]
|
||||
- system: editorial
|
||||
key: landing
|
||||
title: Section Front
|
||||
description: "Front-page-style stack: masthead, lead story, river, footer."
|
||||
title: Front Page
|
||||
description: "Front-page stack: masthead, lead essay, river of stories, colophon."
|
||||
slots: [masthead, lead, river, footer]
|
||||
- system: editorial
|
||||
key: article
|
||||
title: Article
|
||||
description: "Single-column long read with byline, drop cap, marginalia rail."
|
||||
description: "The reading centrepiece: byline, drop-cap opener, single column with a marginalia rail."
|
||||
slots: [header, byline, main, marginalia, footer]
|
||||
- system: editorial
|
||||
key: full-width
|
||||
title: Full Width
|
||||
description: "Edge-to-edge for photo essays and data graphics."
|
||||
slots: [header, main, footer]
|
||||
key: blog-index
|
||||
title: Archive
|
||||
description: "Essay archive: masthead, featured essay, and a river of entries."
|
||||
slots: [header, featured, main, footer]
|
||||
- system: editorial
|
||||
key: contact
|
||||
title: Correspondence
|
||||
description: "Contact layout with an optional aside for hours and location."
|
||||
slots: [header, main, aside, footer]
|
||||
- system: editorial
|
||||
key: auth
|
||||
title: Members
|
||||
description: "Centred members shell with wordmark and hairline rule."
|
||||
slots: [main, footer]
|
||||
|
||||
template_overrides:
|
||||
# Chrome
|
||||
- { template: editorial, block: navbar }
|
||||
- { template: editorial, block: footer }
|
||||
- { template: editorial, block: utility-bar }
|
||||
- { template: editorial, block: announcement-bar }
|
||||
# Marketing
|
||||
- { template: editorial, block: hero }
|
||||
- { template: editorial, block: feature-grid }
|
||||
- { template: editorial, block: rich-section }
|
||||
- { template: editorial, block: stats }
|
||||
- { template: editorial, block: testimonial }
|
||||
- { template: editorial, block: logo-strip }
|
||||
- { template: editorial, block: pricing }
|
||||
- { template: editorial, block: team }
|
||||
- { template: editorial, block: faq }
|
||||
- { template: editorial, block: timeline }
|
||||
- { template: editorial, block: cta }
|
||||
- { template: editorial, block: countdown }
|
||||
# Primitives
|
||||
- { template: editorial, block: heading }
|
||||
- { template: editorial, block: text }
|
||||
- { template: editorial, block: button }
|
||||
- { template: editorial, block: rich-text }
|
||||
- { template: editorial, block: image }
|
||||
- { template: editorial, block: quote }
|
||||
- { template: editorial, block: divider }
|
||||
- { template: editorial, block: button }
|
||||
- { template: editorial, block: card }
|
||||
- { template: editorial, block: gallery }
|
||||
- { template: editorial, block: video-embed }
|
||||
- { template: editorial, block: audio-embed }
|
||||
# Commerce / local
|
||||
- { template: editorial, block: menu-list }
|
||||
- { template: editorial, block: event-list }
|
||||
- { template: editorial, block: contact-form }
|
||||
- { template: editorial, block: contact-card }
|
||||
- { template: editorial, block: hours-location }
|
||||
- { template: editorial, block: social-links }
|
||||
# Blog family
|
||||
- { template: editorial, block: blog-hero }
|
||||
- { template: editorial, block: featured-posts }
|
||||
- { template: editorial, block: related-posts }
|
||||
- { template: editorial, block: popular-posts }
|
||||
- { template: editorial, block: category-list }
|
||||
- { template: editorial, block: post-hero }
|
||||
- { template: editorial, block: post-metadata }
|
||||
- { template: editorial, block: author-bio }
|
||||
- { template: editorial, block: author-bio-hero }
|
||||
- { template: editorial, block: newsletter-signup }
|
||||
- { template: editorial, block: breadcrumbs }
|
||||
- { template: editorial, block: sidebar-nav }
|
||||
# Auth surface
|
||||
- { template: editorial, block: auth-form }
|
||||
- { template: editorial, block: password-reset-form }
|
||||
- { template: editorial, block: auth-status }
|
||||
# System
|
||||
- { template: editorial, block: page-suggestions }
|
||||
|
||||
email_wrappers:
|
||||
- editorial
|
||||
|
||||
css:
|
||||
input_css_append: |
|
||||
/* === Editorial theme — utility layer === */
|
||||
/* === Editorial theme — utility layer ===
|
||||
*
|
||||
* Personal-essayist identity: hairline rules, small-caps labels, drop caps,
|
||||
* marginalia, narrow reading measure. Colour is ALWAYS consumed via
|
||||
* hsl(var(--token)); font families ALWAYS via var(--font-heading|body|mono,
|
||||
* <fallback>). No literal colours, no hardcoded families.
|
||||
*/
|
||||
|
||||
@layer utilities {
|
||||
:root {
|
||||
--editorial-measure: 64ch;
|
||||
--editorial-rule: 1px solid hsl(var(--border));
|
||||
}
|
||||
|
||||
/* --- Narrow editorial column with indented continuation paragraphs --- */
|
||||
/* --- Reading column + page widths --- */
|
||||
.editorial-column {
|
||||
max-width: var(--editorial-measure);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.editorial-wide {
|
||||
max-width: 72rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.editorial-article-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
max-width: 80rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
@media (min-width: 1024px) {
|
||||
.editorial-article-grid {
|
||||
grid-template-columns: minmax(0, 64ch) 16rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* --- Home "river" reading column + vertical rhythm ---
|
||||
* CSS-defined (not JIT Tailwind) so width + spacing always compile.
|
||||
* Keeps the essay list at a comfortable measure and stops stacked
|
||||
* cards from fusing into one seam-y block. */
|
||||
.editorial-river {
|
||||
max-width: 48rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
.editorial-flow > * + * { margin-top: 2rem; }
|
||||
|
||||
/* --- Hero display title — responsive size via media queries so it is
|
||||
* independent of the host Tailwind JIT (large text-* combos in theme
|
||||
* templates are not reliably scanned). --- */
|
||||
.editorial-hero-title {
|
||||
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.015em;
|
||||
line-height: 1.1;
|
||||
color: hsl(var(--foreground));
|
||||
font-size: 2.25rem;
|
||||
}
|
||||
@media (min-width: 640px) { .editorial-hero-title { font-size: 3rem; } }
|
||||
@media (min-width: 1024px) { .editorial-hero-title { font-size: 3.5rem; } }
|
||||
|
||||
/* --- Narrow prose column with indented continuation paragraphs --- */
|
||||
.prose-editorial {
|
||||
max-width: var(--editorial-measure);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.65;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.prose-editorial p {
|
||||
margin: 0 0 1em 0;
|
||||
text-indent: 0;
|
||||
}
|
||||
|
||||
.prose-editorial p + p {
|
||||
text-indent: 1.5em;
|
||||
}
|
||||
|
||||
.prose-editorial p { margin: 0 0 1em 0; }
|
||||
.prose-editorial p + p { text-indent: 1.5em; }
|
||||
.prose-editorial h1,
|
||||
.prose-editorial h2,
|
||||
.prose-editorial h3,
|
||||
@ -78,16 +206,176 @@ css:
|
||||
letter-spacing: -0.01em;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.prose-editorial a {
|
||||
color: hsl(var(--accent));
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.prose-editorial blockquote {
|
||||
hanging-punctuation: first;
|
||||
font-style: italic;
|
||||
border-left: var(--editorial-rule);
|
||||
padding-left: 1rem;
|
||||
margin-left: 0;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* --- Drop cap — oxblood accent first letter, ≥ 3x body size --- */
|
||||
/* --- Small-caps / tracked labels (kickers, eyebrows, meta) --- */
|
||||
.editorial-caps {
|
||||
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.16em;
|
||||
font-size: 0.72rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.editorial-meta {
|
||||
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
font-size: 0.72rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
.editorial-section-label {
|
||||
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
|
||||
text-transform: uppercase;
|
||||
font-variant: small-caps;
|
||||
font-feature-settings: "smcp";
|
||||
letter-spacing: 0.15em;
|
||||
font-size: 0.8rem;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
.editorial-section-label-rule {
|
||||
border: 0;
|
||||
border-top: var(--editorial-rule);
|
||||
margin: 0.5rem 0 1.5rem 0;
|
||||
}
|
||||
|
||||
/* --- Hairline rules --- */
|
||||
hr.editorial-rule,
|
||||
.editorial-rule,
|
||||
.editorial-hairline {
|
||||
border: 0;
|
||||
border-top: var(--editorial-rule);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
/* --- Heading (Playfair, tight tracking, optional kicker) --- */
|
||||
.editorial-heading {
|
||||
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.015em;
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.15;
|
||||
}
|
||||
.editorial-heading-kicker {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.18em;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
.editorial-underline {
|
||||
position: relative;
|
||||
padding-bottom: 0.35em;
|
||||
}
|
||||
.editorial-underline::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
height: 1px;
|
||||
background: hsl(var(--border));
|
||||
}
|
||||
|
||||
/* --- Body text override --- */
|
||||
.editorial-text {
|
||||
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.7;
|
||||
}
|
||||
.editorial-text p + p { text-indent: 1.5em; margin-top: 0; }
|
||||
|
||||
/* --- Inline accent link --- */
|
||||
.editorial-link {
|
||||
color: hsl(var(--accent));
|
||||
text-decoration: underline;
|
||||
text-decoration-thickness: 1px;
|
||||
text-underline-offset: 3px;
|
||||
transition: color 150ms ease, text-decoration-color 150ms ease;
|
||||
}
|
||||
.editorial-link:hover,
|
||||
.editorial-link:focus-visible { color: hsl(var(--foreground)); }
|
||||
|
||||
/* --- Button (hairline border, no fill, accent on hover) --- */
|
||||
.editorial-button {
|
||||
display: inline-block;
|
||||
padding: 0.55rem 1.1rem;
|
||||
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
|
||||
font-size: 0.78rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.14em;
|
||||
background-color: transparent;
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: color 150ms ease, border-color 150ms ease;
|
||||
}
|
||||
.editorial-button:hover,
|
||||
.editorial-button:focus {
|
||||
color: hsl(var(--accent));
|
||||
border-color: hsl(var(--accent));
|
||||
}
|
||||
.editorial-button:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* --- Panel / card (hairline frame, no shadow) --- */
|
||||
.editorial-panel,
|
||||
.editorial-card {
|
||||
border: 1px solid hsl(var(--border));
|
||||
background-color: hsl(var(--card));
|
||||
transition: border-color 150ms ease;
|
||||
}
|
||||
.editorial-panel:hover,
|
||||
.editorial-card:hover { border-color: hsl(var(--accent)); }
|
||||
|
||||
/* --- Form field (hairline border, accent focus) --- */
|
||||
.editorial-input {
|
||||
width: 100%;
|
||||
border: 1px solid hsl(var(--input));
|
||||
background-color: hsl(var(--background));
|
||||
color: hsl(var(--foreground));
|
||||
padding: 0.55rem 0.75rem;
|
||||
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
.editorial-input::placeholder { color: hsl(var(--muted-foreground)); }
|
||||
.editorial-input:focus {
|
||||
outline: none;
|
||||
border-color: hsl(var(--accent));
|
||||
box-shadow: 0 0 0 1px hsl(var(--accent));
|
||||
}
|
||||
|
||||
/* --- Figure / image caption --- */
|
||||
.editorial-figure { margin: 1.5rem 0; }
|
||||
.editorial-figure img { display: block; width: 100%; height: auto; }
|
||||
.editorial-figure figcaption {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: var(--editorial-rule);
|
||||
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
|
||||
font-style: italic;
|
||||
font-size: 0.85rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* --- Drop cap — oxblood accent first letter --- */
|
||||
.editorial-dropcap p:first-of-type::first-letter {
|
||||
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
|
||||
font-weight: 900;
|
||||
@ -102,22 +390,25 @@ css:
|
||||
}
|
||||
|
||||
/* --- Marginalia rail (right-side italic notes) --- */
|
||||
|
||||
.editorial-marginalia {
|
||||
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
|
||||
font-style: italic;
|
||||
font-size: 12px;
|
||||
font-size: 0.8rem;
|
||||
line-height: 1.5;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.editorial-marginalia aside {
|
||||
margin: 0 0 1.5rem 0;
|
||||
padding-left: 0.75rem;
|
||||
border-left: var(--editorial-rule);
|
||||
opacity: 0;
|
||||
animation: editorial-fade-in 600ms ease forwards;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@keyframes editorial-fade-in { to { opacity: 1; } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.editorial-marginalia aside { animation: none; opacity: 1; }
|
||||
}
|
||||
@media (max-width: 1023px) {
|
||||
.editorial-marginalia {
|
||||
margin-top: 1.5rem;
|
||||
border-top: var(--editorial-rule);
|
||||
@ -126,7 +417,6 @@ css:
|
||||
}
|
||||
|
||||
/* --- Masthead wordmark --- */
|
||||
|
||||
.editorial-masthead-wordmark {
|
||||
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
|
||||
font-weight: 900;
|
||||
@ -134,15 +424,13 @@ css:
|
||||
letter-spacing: -0.02em;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.editorial-masthead-kicker {
|
||||
font-family: var(--font-mono, "JetBrains Mono", "SF Mono", ui-monospace, monospace);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.editorial-masthead-rule {
|
||||
border: 0;
|
||||
border-top: var(--editorial-rule);
|
||||
@ -150,58 +438,28 @@ css:
|
||||
}
|
||||
|
||||
/* --- Pull quote — Playfair italic in oxblood --- */
|
||||
|
||||
.editorial-pullquote {
|
||||
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
|
||||
font-style: italic;
|
||||
font-size: 38px;
|
||||
font-size: 2rem;
|
||||
line-height: 1.2;
|
||||
color: hsl(var(--accent));
|
||||
margin: 2.5rem auto;
|
||||
max-width: var(--editorial-measure);
|
||||
border: none;
|
||||
}
|
||||
|
||||
.editorial-pullquote-attribution {
|
||||
display: block;
|
||||
margin-top: 1rem;
|
||||
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
|
||||
font-style: normal;
|
||||
font-size: 14px;
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* --- Section label — small-caps with hairline rule --- */
|
||||
|
||||
.editorial-section-label {
|
||||
font-family: var(--font-body, "Source Serif 4", Georgia, serif);
|
||||
text-transform: uppercase;
|
||||
font-variant: small-caps;
|
||||
font-feature-settings: "smcp";
|
||||
letter-spacing: 0.15em;
|
||||
font-size: 13px;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.editorial-section-label-rule {
|
||||
border: 0;
|
||||
border-top: var(--editorial-rule);
|
||||
margin: 0.5rem 0 1.5rem 0;
|
||||
}
|
||||
|
||||
/* --- Hairline rules --- */
|
||||
|
||||
hr.editorial-rule,
|
||||
.editorial-hairline {
|
||||
border: 0;
|
||||
border-top: var(--editorial-rule);
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
/* --- Byline --- */
|
||||
|
||||
.editorial-byline {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -210,7 +468,6 @@ css:
|
||||
border-top: var(--editorial-rule);
|
||||
border-bottom: var(--editorial-rule);
|
||||
}
|
||||
|
||||
.editorial-byline-photo {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
@ -219,166 +476,48 @@ css:
|
||||
flex-shrink: 0;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.editorial-byline-author {
|
||||
font-family: var(--font-mono, "JetBrains Mono", monospace), var(--font-body, "Source Serif 4", Georgia, serif);
|
||||
/* The spec calls for Inter for UI bylines. Fallback to Inter then system. */
|
||||
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
|
||||
font-weight: 500;
|
||||
font-size: 15px;
|
||||
font-size: 0.95rem;
|
||||
color: hsl(var(--foreground));
|
||||
}
|
||||
|
||||
.editorial-byline-meta {
|
||||
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
|
||||
font-size: 12px;
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* --- Override: heading (Playfair, tight tracking, optional small-caps kicker) --- */
|
||||
|
||||
.editorial-heading {
|
||||
font-family: var(--font-heading, "Playfair Display", Georgia, serif);
|
||||
font-weight: 700;
|
||||
letter-spacing: -0.015em;
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.editorial-heading-kicker {
|
||||
display: block;
|
||||
margin-bottom: 0.5rem;
|
||||
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
font-variant: small-caps;
|
||||
font-feature-settings: "smcp";
|
||||
letter-spacing: 0.18em;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* --- Override: text body (Source Serif, hanging quotes) --- */
|
||||
|
||||
.editorial-text {
|
||||
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.7;
|
||||
max-width: var(--editorial-measure);
|
||||
}
|
||||
|
||||
.editorial-text p + p {
|
||||
text-indent: 1.5em;
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.editorial-text blockquote {
|
||||
hanging-punctuation: first;
|
||||
font-style: italic;
|
||||
border-left: var(--editorial-rule);
|
||||
padding-left: 1rem;
|
||||
margin-left: 0;
|
||||
}
|
||||
|
||||
/* --- Override: button (hairline border, no fill, accent on hover) --- */
|
||||
|
||||
.editorial-button {
|
||||
display: inline-block;
|
||||
padding: 0.5rem 1rem;
|
||||
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
|
||||
font-size: 13px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
background-color: transparent;
|
||||
color: hsl(var(--foreground));
|
||||
border: 1px solid hsl(var(--border));
|
||||
text-decoration: none;
|
||||
cursor: pointer;
|
||||
transition: color 150ms ease, border-color 150ms ease;
|
||||
}
|
||||
|
||||
.editorial-button:hover,
|
||||
.editorial-button:focus {
|
||||
color: hsl(var(--accent));
|
||||
border-color: hsl(var(--accent));
|
||||
}
|
||||
|
||||
.editorial-button:focus-visible {
|
||||
outline: 2px solid hsl(var(--ring));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* --- Override: image (Source Serif italic caption with hairline above) --- */
|
||||
|
||||
.editorial-figure {
|
||||
margin: 1.5rem 0;
|
||||
}
|
||||
|
||||
.editorial-figure img {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
.editorial-figure figcaption {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.5rem;
|
||||
border-top: var(--editorial-rule);
|
||||
font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);
|
||||
font-style: italic;
|
||||
font-size: 14px;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
/* --- Colophon --- */
|
||||
|
||||
.editorial-colophon {
|
||||
border-top: var(--editorial-rule);
|
||||
padding-top: 1.5rem;
|
||||
font-family: var(--font-body, "Inter", -apple-system, sans-serif);
|
||||
font-size: 13px;
|
||||
font-size: 0.8rem;
|
||||
color: hsl(var(--muted-foreground));
|
||||
}
|
||||
|
||||
.editorial-colophon-issn {
|
||||
font-family: var(--font-mono, "JetBrains Mono", monospace);
|
||||
font-size: 11px;
|
||||
font-family: var(--font-mono, "JetBrains Mono", ui-monospace, monospace);
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
/* --- Page layout — narrow editorial column --- */
|
||||
|
||||
.editorial-column {
|
||||
max-width: var(--editorial-measure);
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
/* --- Reading-progress underline (CSS-only atmosphere) --- */
|
||||
.editorial-progress {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 2px;
|
||||
background: hsl(var(--accent));
|
||||
transform-origin: 0 50%;
|
||||
animation: editorial-progress linear both;
|
||||
animation-timeline: scroll(root block);
|
||||
z-index: 50;
|
||||
}
|
||||
|
||||
.editorial-wide {
|
||||
max-width: 96rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
.editorial-article-grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
gap: 1.5rem;
|
||||
max-width: 80rem;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1024px) {
|
||||
.editorial-article-grid {
|
||||
grid-template-columns: minmax(0, 64ch) 16rem;
|
||||
@keyframes editorial-progress { from { transform: scaleX(0); } to { transform: scaleX(1); } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.editorial-progress { animation: none; transform: scaleX(0); }
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,12 +2,24 @@
|
||||
{
|
||||
"key": "editorial:default-master",
|
||||
"title": "Editorial Default Master",
|
||||
"page_templates": ["default", "landing", "full-width"],
|
||||
"page_templates": ["default", "full-width", "blog-index", "contact"],
|
||||
"blocks": [
|
||||
{ "block_key": "editorial:masthead", "title": "Masthead", "content": { "menuName": "main", "kicker": "EST. 2026" }, "slot": "header", "sort_order": 0 },
|
||||
{ "block_key": "navbar", "title": "Section Nav", "content": { "menuName": "sections" }, "slot": "header", "sort_order": 1 },
|
||||
{ "block_key": "navbar", "title": "Masthead Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "The Margin" } }, "slot": "header", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Featured", "content": { "slotName": "featured" }, "slot": "featured", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Main Content", "content": { "slotName": "main", "placeholder": "Page content" }, "slot": "main", "sort_order": 0 },
|
||||
{ "block_key": "editorial:colophon", "title": "Colophon", "content": { "showSignup": true, "issn": "0000-0000" }, "slot": "footer", "sort_order": 0 }
|
||||
{ "block_key": "slot", "title": "Aside", "content": { "slotName": "aside" }, "slot": "aside", "sort_order": 0 },
|
||||
{ "block_key": "footer", "title": "Colophon", "content": { "layout": "centered", "companyName": "The Margin", "tagline": "A newsletter about paying attention.", "showNewsletter": true, "newsletterTitle": "Subscribe", "newsletterDescription": "New essays, every other Sunday.", "copyright": "The Margin" }, "slot": "footer", "sort_order": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "editorial:landing-master",
|
||||
"title": "Editorial Front Page Master",
|
||||
"page_templates": ["landing"],
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Masthead Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "The Margin" } }, "slot": "masthead", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Lead", "content": { "slotName": "lead", "placeholder": "Lead essay" }, "slot": "lead", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "River", "content": { "slotName": "river", "placeholder": "Recent essays" }, "slot": "river", "sort_order": 0 },
|
||||
{ "block_key": "footer", "title": "Colophon", "content": { "layout": "centered", "companyName": "The Margin", "tagline": "A newsletter about paying attention.", "showNewsletter": true, "newsletterTitle": "Subscribe", "newsletterDescription": "New essays, every other Sunday.", "copyright": "The Margin" }, "slot": "footer", "sort_order": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
@ -15,11 +27,11 @@
|
||||
"title": "Editorial Article Master",
|
||||
"page_templates": ["article"],
|
||||
"blocks": [
|
||||
{ "block_key": "editorial:masthead", "title": "Masthead", "content": { "menuName": "main", "compact": true }, "slot": "header", "sort_order": 0 },
|
||||
{ "block_key": "editorial:byline", "title": "Byline", "content": { "showPhoto": true, "showReadTime": true }, "slot": "byline", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Article Body", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
|
||||
{ "block_key": "editorial:marginalia", "title": "Marginalia Rail", "content": { "items": [] }, "slot": "marginalia", "sort_order": 0 },
|
||||
{ "block_key": "editorial:colophon", "title": "Colophon", "content": { "showSignup": true }, "slot": "footer", "sort_order": 0 }
|
||||
{ "block_key": "navbar", "title": "Masthead Navigation", "content": { "menuName": "main", "logoType": "text", "logoText": { "text": "The Margin" } }, "slot": "header", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Byline", "content": { "slotName": "byline" }, "slot": "byline", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Article Body", "content": { "slotName": "main", "placeholder": "Essay body" }, "slot": "main", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Marginalia Rail", "content": { "slotName": "marginalia" }, "slot": "marginalia", "sort_order": 0 },
|
||||
{ "block_key": "footer", "title": "Colophon", "content": { "layout": "centered", "companyName": "The Margin", "tagline": "A newsletter about paying attention.", "copyright": "The Margin" }, "slot": "footer", "sort_order": 0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@ -2,8 +2,8 @@
|
||||
name = "editorial"
|
||||
display_name = "Editorial"
|
||||
scope = "@themes"
|
||||
version = "0.2.0"
|
||||
description = "Didone display type, narrow measure, and hairline rules for newsrooms, longform writers, and opinion columnists."
|
||||
version = "0.2.1"
|
||||
description = "A personal essayist's reading room: warm book-paper light, sepia-on-charcoal dark, serif baseline grid, drop caps, running marginalia and small-caps labels for longform writers and independent newsletters."
|
||||
kind = "theme"
|
||||
categories = ["templates"]
|
||||
tags = ["editorial", "serif", "news", "longform", "magazine", "law", "finance", "opinion"]
|
||||
tags = ["editorial", "serif", "essay", "newsletter", "longform", "reading", "typographic", "blog", "writer"]
|
||||
|
||||
247
presets.json
247
presets.json
@ -1,88 +1,201 @@
|
||||
[
|
||||
{
|
||||
"id": "broadsheet",
|
||||
"name": "Broadsheet",
|
||||
"description": "Default light: cream paper, ink black, oxblood accent. Newsroom front page.",
|
||||
"id": "book-paper",
|
||||
"name": "Book Paper",
|
||||
"description": "The default. Warm off-white paper and dark serif ink by day, low-glare charcoal with sepia type by night. Oxblood accent throughout.",
|
||||
"theme": {
|
||||
"mode": "light",
|
||||
"mode": "both",
|
||||
"lightColors": {
|
||||
"background": "36 30% 96%",
|
||||
"foreground": "20 14% 10%",
|
||||
"card": "36 30% 98%",
|
||||
"cardForeground": "20 14% 10%",
|
||||
"popover": "36 30% 98%",
|
||||
"popoverForeground": "20 14% 10%",
|
||||
"primary": "20 14% 10%",
|
||||
"primaryForeground": "36 30% 96%",
|
||||
"secondary": "36 20% 90%",
|
||||
"secondaryForeground": "20 14% 10%",
|
||||
"muted": "36 20% 92%",
|
||||
"mutedForeground": "20 10% 38%",
|
||||
"accent": "0 55% 28%",
|
||||
"accentForeground": "36 30% 96%",
|
||||
"destructive": "0 72% 42%",
|
||||
"destructiveForeground": "36 30% 96%",
|
||||
"border": "30 12% 82%",
|
||||
"input": "30 12% 82%",
|
||||
"ring": "0 55% 28%"
|
||||
"background": "38 32% 95%",
|
||||
"foreground": "24 16% 12%",
|
||||
"card": "38 34% 97%",
|
||||
"cardForeground": "24 16% 12%",
|
||||
"popover": "38 34% 97%",
|
||||
"popoverForeground": "24 16% 12%",
|
||||
"primary": "24 16% 12%",
|
||||
"primaryForeground": "38 32% 95%",
|
||||
"secondary": "36 22% 89%",
|
||||
"secondaryForeground": "24 16% 14%",
|
||||
"muted": "36 22% 91%",
|
||||
"mutedForeground": "26 10% 38%",
|
||||
"accent": "3 58% 34%",
|
||||
"accentForeground": "38 32% 95%",
|
||||
"destructive": "0 70% 42%",
|
||||
"destructiveForeground": "38 32% 95%",
|
||||
"border": "32 16% 82%",
|
||||
"input": "32 16% 80%",
|
||||
"ring": "3 58% 34%"
|
||||
},
|
||||
"darkColors": {
|
||||
"background": "26 10% 10%",
|
||||
"foreground": "36 22% 82%",
|
||||
"card": "26 10% 13%",
|
||||
"cardForeground": "36 22% 82%",
|
||||
"popover": "26 10% 13%",
|
||||
"popoverForeground": "36 22% 82%",
|
||||
"primary": "36 22% 82%",
|
||||
"primaryForeground": "26 10% 10%",
|
||||
"secondary": "26 8% 18%",
|
||||
"secondaryForeground": "36 20% 80%",
|
||||
"muted": "26 8% 16%",
|
||||
"mutedForeground": "34 12% 58%",
|
||||
"accent": "6 62% 56%",
|
||||
"accentForeground": "26 10% 10%",
|
||||
"destructive": "0 68% 52%",
|
||||
"destructiveForeground": "36 22% 88%",
|
||||
"border": "28 8% 22%",
|
||||
"input": "28 8% 20%",
|
||||
"ring": "6 62% 56%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "nightdesk",
|
||||
"name": "Nightdesk",
|
||||
"description": "Dark mode for after-hours reading. Inverted broadsheet with deeper accent.",
|
||||
"id": "sepia-review",
|
||||
"name": "Sepia Review",
|
||||
"description": "Warm sepia in both modes for a soft, magazine-like read. Amber paper and umber ink by day, deep tobacco ground by night.",
|
||||
"theme": {
|
||||
"mode": "dark",
|
||||
"mode": "both",
|
||||
"lightColors": {
|
||||
"background": "34 44% 93%",
|
||||
"foreground": "22 30% 16%",
|
||||
"card": "34 46% 96%",
|
||||
"cardForeground": "22 30% 16%",
|
||||
"popover": "34 46% 96%",
|
||||
"popoverForeground": "22 30% 16%",
|
||||
"primary": "22 30% 16%",
|
||||
"primaryForeground": "34 44% 93%",
|
||||
"secondary": "32 30% 87%",
|
||||
"secondaryForeground": "22 30% 18%",
|
||||
"muted": "32 28% 89%",
|
||||
"mutedForeground": "24 18% 40%",
|
||||
"accent": "14 62% 38%",
|
||||
"accentForeground": "34 44% 94%",
|
||||
"destructive": "0 70% 42%",
|
||||
"destructiveForeground": "34 44% 94%",
|
||||
"border": "30 22% 78%",
|
||||
"input": "30 22% 76%",
|
||||
"ring": "14 62% 38%"
|
||||
},
|
||||
"darkColors": {
|
||||
"background": "24 10% 8%",
|
||||
"foreground": "36 20% 92%",
|
||||
"card": "24 10% 11%",
|
||||
"cardForeground": "36 20% 92%",
|
||||
"popover": "24 10% 11%",
|
||||
"popoverForeground": "36 20% 92%",
|
||||
"primary": "36 20% 92%",
|
||||
"primaryForeground": "24 10% 8%",
|
||||
"secondary": "24 8% 16%",
|
||||
"secondaryForeground": "36 20% 92%",
|
||||
"muted": "24 8% 14%",
|
||||
"mutedForeground": "36 12% 62%",
|
||||
"accent": "0 60% 52%",
|
||||
"accentForeground": "24 10% 8%",
|
||||
"destructive": "0 72% 50%",
|
||||
"destructiveForeground": "36 20% 92%",
|
||||
"border": "24 8% 20%",
|
||||
"input": "24 8% 18%",
|
||||
"ring": "0 60% 52%"
|
||||
"background": "24 18% 11%",
|
||||
"foreground": "34 30% 80%",
|
||||
"card": "24 18% 14%",
|
||||
"cardForeground": "34 30% 80%",
|
||||
"popover": "24 18% 14%",
|
||||
"popoverForeground": "34 30% 80%",
|
||||
"primary": "34 30% 80%",
|
||||
"primaryForeground": "24 18% 11%",
|
||||
"secondary": "24 14% 19%",
|
||||
"secondaryForeground": "34 28% 78%",
|
||||
"muted": "24 14% 17%",
|
||||
"mutedForeground": "32 16% 60%",
|
||||
"accent": "18 68% 54%",
|
||||
"accentForeground": "24 18% 10%",
|
||||
"destructive": "0 68% 52%",
|
||||
"destructiveForeground": "34 30% 86%",
|
||||
"border": "26 14% 23%",
|
||||
"input": "26 14% 21%",
|
||||
"ring": "18 68% 54%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "legal-pad",
|
||||
"name": "Legal Pad",
|
||||
"description": "Warm cream for law and finance verticals. Tighter saturation, deeper ink.",
|
||||
"description": "Deeper cream and near-black ink for law, finance and long essays. Tight saturation by day, cool ink-black night desk.",
|
||||
"theme": {
|
||||
"mode": "light",
|
||||
"mode": "both",
|
||||
"lightColors": {
|
||||
"background": "48 40% 94%",
|
||||
"foreground": "28 30% 14%",
|
||||
"card": "48 40% 97%",
|
||||
"cardForeground": "28 30% 14%",
|
||||
"popover": "48 40% 97%",
|
||||
"popoverForeground": "28 30% 14%",
|
||||
"primary": "28 30% 14%",
|
||||
"primaryForeground": "48 40% 94%",
|
||||
"secondary": "44 30% 86%",
|
||||
"secondaryForeground": "28 30% 14%",
|
||||
"muted": "44 28% 90%",
|
||||
"mutedForeground": "28 18% 36%",
|
||||
"accent": "10 60% 30%",
|
||||
"accentForeground": "48 40% 94%",
|
||||
"destructive": "0 72% 42%",
|
||||
"destructiveForeground": "48 40% 94%",
|
||||
"border": "40 20% 78%",
|
||||
"input": "40 20% 80%",
|
||||
"ring": "10 60% 30%"
|
||||
"background": "48 42% 93%",
|
||||
"foreground": "30 28% 13%",
|
||||
"card": "48 44% 96%",
|
||||
"cardForeground": "30 28% 13%",
|
||||
"popover": "48 44% 96%",
|
||||
"popoverForeground": "30 28% 13%",
|
||||
"primary": "30 28% 13%",
|
||||
"primaryForeground": "48 42% 93%",
|
||||
"secondary": "46 30% 86%",
|
||||
"secondaryForeground": "30 28% 15%",
|
||||
"muted": "46 28% 89%",
|
||||
"mutedForeground": "30 16% 36%",
|
||||
"accent": "10 58% 32%",
|
||||
"accentForeground": "48 42% 94%",
|
||||
"destructive": "0 70% 42%",
|
||||
"destructiveForeground": "48 42% 94%",
|
||||
"border": "42 22% 77%",
|
||||
"input": "42 22% 75%",
|
||||
"ring": "10 58% 32%"
|
||||
},
|
||||
"darkColors": {
|
||||
"background": "220 12% 9%",
|
||||
"foreground": "44 20% 84%",
|
||||
"card": "220 12% 12%",
|
||||
"cardForeground": "44 20% 84%",
|
||||
"popover": "220 12% 12%",
|
||||
"popoverForeground": "44 20% 84%",
|
||||
"primary": "44 20% 84%",
|
||||
"primaryForeground": "220 12% 9%",
|
||||
"secondary": "220 10% 17%",
|
||||
"secondaryForeground": "44 18% 82%",
|
||||
"muted": "220 10% 15%",
|
||||
"mutedForeground": "40 12% 58%",
|
||||
"accent": "8 64% 55%",
|
||||
"accentForeground": "220 12% 9%",
|
||||
"destructive": "0 68% 52%",
|
||||
"destructiveForeground": "44 20% 88%",
|
||||
"border": "220 10% 21%",
|
||||
"input": "220 10% 19%",
|
||||
"ring": "8 64% 55%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "nightdesk",
|
||||
"name": "Nightdesk",
|
||||
"description": "Cooler neutral paper and a crisp after-hours reading mode. A brighter red accent for the late edition.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"lightColors": {
|
||||
"background": "40 20% 96%",
|
||||
"foreground": "220 14% 14%",
|
||||
"card": "40 22% 98%",
|
||||
"cardForeground": "220 14% 14%",
|
||||
"popover": "40 22% 98%",
|
||||
"popoverForeground": "220 14% 14%",
|
||||
"primary": "220 14% 14%",
|
||||
"primaryForeground": "40 20% 96%",
|
||||
"secondary": "40 14% 90%",
|
||||
"secondaryForeground": "220 14% 16%",
|
||||
"muted": "40 14% 92%",
|
||||
"mutedForeground": "220 8% 40%",
|
||||
"accent": "352 60% 42%",
|
||||
"accentForeground": "40 20% 96%",
|
||||
"destructive": "0 72% 44%",
|
||||
"destructiveForeground": "40 20% 96%",
|
||||
"border": "36 12% 83%",
|
||||
"input": "36 12% 81%",
|
||||
"ring": "352 60% 42%"
|
||||
},
|
||||
"darkColors": {
|
||||
"background": "222 16% 9%",
|
||||
"foreground": "40 16% 86%",
|
||||
"card": "222 16% 12%",
|
||||
"cardForeground": "40 16% 86%",
|
||||
"popover": "222 16% 12%",
|
||||
"popoverForeground": "40 16% 86%",
|
||||
"primary": "40 16% 86%",
|
||||
"primaryForeground": "222 16% 9%",
|
||||
"secondary": "222 12% 17%",
|
||||
"secondaryForeground": "40 14% 84%",
|
||||
"muted": "222 12% 15%",
|
||||
"mutedForeground": "40 10% 60%",
|
||||
"accent": "352 70% 58%",
|
||||
"accentForeground": "222 16% 9%",
|
||||
"destructive": "0 70% 54%",
|
||||
"destructiveForeground": "40 16% 90%",
|
||||
"border": "222 12% 22%",
|
||||
"input": "222 12% 20%",
|
||||
"ring": "352 70% 58%"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
196
seed/seed.json
Normal file
196
seed/seed.json
Normal file
@ -0,0 +1,196 @@
|
||||
{
|
||||
"demo": true,
|
||||
"settings": {
|
||||
"merge": {
|
||||
"companyName": "The Margin",
|
||||
"tagline": "A newsletter about paying attention."
|
||||
}
|
||||
},
|
||||
"data_tables": [
|
||||
{
|
||||
"key": "contact_submissions",
|
||||
"name": "Letters",
|
||||
"description": "Seeded contact-form submissions for the The Margin demo. A submission becomes a row here and emails the site admins.",
|
||||
"columns": [
|
||||
{ "key": "name", "label": "Name", "type": "text", "required": true },
|
||||
{ "key": "email", "label": "Email", "type": "email", "required": true },
|
||||
{ "key": "subject", "label": "Subject", "type": "text", "required": false },
|
||||
{ "key": "message", "label": "Message", "type": "text", "required": true }
|
||||
]
|
||||
}
|
||||
],
|
||||
"pages": [
|
||||
{
|
||||
"slug": "/",
|
||||
"title": "The Margin",
|
||||
"template_key": "landing",
|
||||
"blocks": [
|
||||
{
|
||||
"block_key": "hero",
|
||||
"title": "Lead",
|
||||
"slot": "lead",
|
||||
"sort_order": 1,
|
||||
"content": {
|
||||
"template": "centered",
|
||||
"eyebrow": { "text": "Issue 12" },
|
||||
"headline": { "text": "The Margin" },
|
||||
"subheadline": { "text": "A newsletter about paying attention." },
|
||||
"description": { "text": "Essays on reading, attention, and the slow work of noticing. New writing lands every other Sunday, free to your inbox." },
|
||||
"ctaPrimary": { "text": "Read the latest", "url": "/blog", "style": "primary" },
|
||||
"ctaSecondary": { "text": "About the writer", "url": "/about", "style": "outline" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"block_key": "heading",
|
||||
"title": "Recent heading",
|
||||
"slot": "river",
|
||||
"sort_order": 1,
|
||||
"content": { "text": "Recent essays", "level": 2 }
|
||||
},
|
||||
{
|
||||
"block_key": "card",
|
||||
"title": "Essay: On Attention",
|
||||
"slot": "river",
|
||||
"sort_order": 2,
|
||||
"content": { "layout": "vertical", "title": "On attention", "text": "<p>What it costs to look at one thing for a long time, and what it gives back.</p>", "link": "/blog/on-attention", "linkLabel": "Read the essay" }
|
||||
},
|
||||
{
|
||||
"block_key": "card",
|
||||
"title": "Essay: The Slow Read",
|
||||
"slot": "river",
|
||||
"sort_order": 3,
|
||||
"content": { "layout": "vertical", "title": "The slow read", "text": "<p>A case for reading less, and reading it twice.</p>", "link": "/blog/the-slow-read", "linkLabel": "Read the essay" }
|
||||
},
|
||||
{
|
||||
"block_key": "card",
|
||||
"title": "Essay: Notes on Noticing",
|
||||
"slot": "river",
|
||||
"sort_order": 4,
|
||||
"content": { "layout": "vertical", "title": "Notes on noticing", "text": "<p>Small habits that keep the world from going quiet.</p>", "link": "/blog/notes-on-noticing", "linkLabel": "Read the essay" }
|
||||
},
|
||||
{
|
||||
"block_key": "newsletter-signup",
|
||||
"title": "Subscribe",
|
||||
"slot": "river",
|
||||
"sort_order": 5,
|
||||
"content": { "title": "Subscribe", "description": "New essays every other Sunday. No noise, no schedule creep.", "listSlug": "main", "source": "home", "submitText": "Subscribe", "successMessage": "You are on the list. Look for the next issue on Sunday.", "placeholder": "you@example.com", "layout": "card" }
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/about",
|
||||
"title": "About",
|
||||
"template_key": "default",
|
||||
"blocks": [
|
||||
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "About", "level": 1 } },
|
||||
{ "block_key": "rich-text", "title": "Bio", "slot": "main", "sort_order": 2, "content": { "html": "<p>The Margin is a newsletter by Frances Hale. It is about reading, attention, and the slow work of noticing.</p><p>I write one essay every other week. Some are short. Some take the long way around. All of them start in the margins of a book.</p><p>You can read the archive for free. Members get the full run and the occasional letter.</p>" } },
|
||||
{ "block_key": "editorial:reading_list", "title": "Shelf", "slot": "main", "sort_order": 3, "content": { "title": "On the shelf", "intro": "Books I keep going back to.", "items": [ { "label": "The Writing Life", "source": "Annie Dillard", "note": "On the daily cost of the work." }, { "label": "Ways of Seeing", "source": "John Berger", "note": "How looking is never neutral." }, { "label": "The Art of Stillness", "source": "Pico Iyer", "note": "A short book about going nowhere." } ] } },
|
||||
{ "block_key": "quote", "title": "Quote", "slot": "main", "sort_order": 4, "content": { "style": "block", "align": "center", "quote": "Attention is the beginning of devotion.", "attribution": "Mary Oliver" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/blog",
|
||||
"title": "The archive",
|
||||
"template_key": "blog-index",
|
||||
"blocks": [
|
||||
{ "block_key": "card", "title": "Featured: On Attention", "slot": "featured", "sort_order": 1, "content": { "layout": "horizontal", "title": "On attention", "text": "<p>What it costs to look at one thing for a long time, and what it gives back. The lead essay this issue.</p>", "link": "/blog/on-attention", "linkLabel": "Read the essay" } },
|
||||
{ "block_key": "card", "title": "Post: The Slow Read", "slot": "main", "sort_order": 1, "content": { "layout": "vertical", "title": "The slow read", "text": "<p>A case for reading less, and reading it twice.</p>", "link": "/blog/the-slow-read", "linkLabel": "Read on" } },
|
||||
{ "block_key": "card", "title": "Post: Notes on Noticing", "slot": "main", "sort_order": 2, "content": { "layout": "vertical", "title": "Notes on noticing", "text": "<p>Small habits that keep the world from going quiet.</p>", "link": "/blog/notes-on-noticing", "linkLabel": "Read on" } },
|
||||
{ "block_key": "card", "title": "Post: The Second Draft", "slot": "main", "sort_order": 3, "content": { "layout": "vertical", "title": "The second draft", "text": "<p>Where the real writing happens, once the first draft is out of the way.</p>", "link": "/blog/on-attention", "linkLabel": "Read on" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/blog/on-attention",
|
||||
"title": "On attention",
|
||||
"template_key": "article",
|
||||
"parent_slug": "/blog",
|
||||
"blocks": [
|
||||
{ "block_key": "text", "title": "Byline", "slot": "byline", "sort_order": 1, "content": { "text": "<p>By Frances Hale. Issue 12.</p>" } },
|
||||
{ "block_key": "editorial:dropcap_intro", "title": "Lead", "slot": "main", "sort_order": 1, "content": { "lead": "<p>There is a kind of looking that costs something. You give a thing your whole attention and the rest of the world goes quiet for a while. It is not comfortable. It is not always rewarded. But it is the only way I know to see anything clearly.</p>" } },
|
||||
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>I started keeping a notebook of things I had failed to notice. A tree I passed every day for a year. The way a friend says my name. The list grew fast, which told me something about how little I had been paying attention.</p><p>Attention is not a switch. It is a muscle, and it gets tired. The trick is not to force it but to make room for it. Read slower. Walk without a phone. Let one thing be the whole thing for an hour.</p><p>What you get back is not efficiency. It is the world, a little louder than before.</p>" } },
|
||||
{ "block_key": "editorial:marginalia", "title": "Marginalia", "slot": "marginalia", "sort_order": 1, "content": { "items": [ { "anchor": "On notebooks", "note": "I use a cheap one. A nice notebook makes me careful, and careful is the enemy here." }, { "anchor": "On walking", "note": "The phone stays home. That is the whole method." } ] } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/blog/the-slow-read",
|
||||
"title": "The slow read",
|
||||
"template_key": "article",
|
||||
"parent_slug": "/blog",
|
||||
"blocks": [
|
||||
{ "block_key": "text", "title": "Byline", "slot": "byline", "sort_order": 1, "content": { "text": "<p>By Frances Hale. Issue 11.</p>" } },
|
||||
{ "block_key": "editorial:dropcap_intro", "title": "Lead", "slot": "main", "sort_order": 1, "content": { "lead": "<p>Most of what I read once, I forget by the weekend. The books that stay with me are the ones I read twice. So I have started reading less, on purpose, and going back.</p>" } },
|
||||
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>The second reading is a different book. You know where it is going, so you stop rushing. You notice the joins. You see what the writer set up on page ten and paid off on page two hundred.</p><p>This is slower, and that is the point. A shelf of books read once is a list. A shelf of books read twice is a conversation.</p>" } },
|
||||
{ "block_key": "editorial:marginalia", "title": "Marginalia", "slot": "marginalia", "sort_order": 1, "content": { "items": [ { "anchor": "A rule", "note": "One new book for every reread. It keeps the shelf honest." } ] } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/blog/notes-on-noticing",
|
||||
"title": "Notes on noticing",
|
||||
"template_key": "article",
|
||||
"parent_slug": "/blog",
|
||||
"blocks": [
|
||||
{ "block_key": "text", "title": "Byline", "slot": "byline", "sort_order": 1, "content": { "text": "<p>By Frances Hale. Issue 10.</p>" } },
|
||||
{ "block_key": "editorial:dropcap_intro", "title": "Lead", "slot": "main", "sort_order": 1, "content": { "lead": "<p>Noticing is a habit before it is a talent. Here are the small ones that keep my world from going quiet, collected over a slow year of trying.</p>" } },
|
||||
{ "block_key": "rich-text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "html": "<p>Name one thing you saw today that you had not seen before. Do it out loud, at dinner, and make someone else do it too.</p><p>Carry a small notebook. Write the thing down before you explain it to yourself. The explanation can wait.</p><p>None of this is hard. That is what makes it easy to skip. Do it anyway, and the days stop blurring together.</p>" } },
|
||||
{ "block_key": "editorial:field_notes", "title": "Field notes", "slot": "main", "sort_order": 3, "content": { "title": "Field notes", "notes": [ { "date": "March 14", "note": "<p>A heron on the river wall, standing so still I mistook it for a post.</p>" }, { "date": "March 9", "note": "<p>The bakery changed its bread and told no one. I noticed on the second bite.</p>" } ] } },
|
||||
{ "block_key": "editorial:marginalia", "title": "Marginalia", "slot": "marginalia", "sort_order": 1, "content": { "items": [ { "anchor": "Try it", "note": "The dinner question works on children better than on adults. Adults think it is a test." } ] } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/contact",
|
||||
"title": "Say hello",
|
||||
"template_key": "contact",
|
||||
"blocks": [
|
||||
{
|
||||
"block_key": "contact-form",
|
||||
"title": "Write to me",
|
||||
"slot": "main",
|
||||
"sort_order": 1,
|
||||
"content": {
|
||||
"heading": "Write to me",
|
||||
"description": "A note, a question, a book you think I should read. I answer everything, though it can take a week.",
|
||||
"submitLabel": "Send",
|
||||
"successMessage": "Thank you. Your note has been received. I will write back soon.",
|
||||
"fields": [
|
||||
{ "name": "name", "label": "Your name", "type": "text", "required": true, "width": "half" },
|
||||
{ "name": "email", "label": "Email", "type": "email", "required": true, "width": "half" },
|
||||
{ "name": "subject", "label": "Subject", "type": "text", "required": false, "width": "full" },
|
||||
{ "name": "message", "label": "Your note", "type": "textarea", "required": true, "width": "full" }
|
||||
],
|
||||
"formConfig": { "enabled": true, "captchaEnabled": false, "targetTable": "contact_submissions" }
|
||||
}
|
||||
},
|
||||
{
|
||||
"block_key": "contact-card",
|
||||
"title": "Other ways to reach me",
|
||||
"slot": "aside",
|
||||
"sort_order": 1,
|
||||
"content": {
|
||||
"heading": "Elsewhere",
|
||||
"description": "The inbox is best. These work too.",
|
||||
"columns": 1,
|
||||
"channels": [
|
||||
{ "icon": "lucide:mail", "label": "Email", "value": "frances@themargin.example", "link": "mailto:frances@themargin.example" },
|
||||
{ "icon": "lucide:rss", "label": "Feed", "value": "Subscribe by RSS", "link": "/feed" }
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/login",
|
||||
"title": "Members",
|
||||
"template_key": "auth",
|
||||
"blocks": [
|
||||
{ "block_key": "auth-form", "title": "Sign in", "slot": "main", "sort_order": 1, "content": { "default_tab": "login", "show_social": false } },
|
||||
{ "block_key": "text", "title": "Foot", "slot": "footer", "sort_order": 1, "content": { "text": "<p>Members get the full archive and the occasional letter.</p>" } }
|
||||
]
|
||||
}
|
||||
],
|
||||
"menu_items": [
|
||||
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
|
||||
{ "menu": "main", "label": "Essays", "page_slug": "/blog", "sort_order": 2 },
|
||||
{ "menu": "main", "label": "About", "page_slug": "/about", "sort_order": 3 },
|
||||
{ "menu": "main", "label": "Say hello", "page_slug": "/contact", "sort_order": 4 }
|
||||
]
|
||||
}
|
||||
19
seed/workflows.json
Normal file
19
seed/workflows.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"workflows": [
|
||||
{
|
||||
"key": "contact_notify_admins",
|
||||
"name": "Letter to admins",
|
||||
"description": "On a contact-form submission, email the site admins.",
|
||||
"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,15 +1,16 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
<body class="antialiased min-h-screen flex flex-col bg-background text-foreground" style="font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);">
|
||||
<div class="editorial-progress" aria-hidden="true"></div>
|
||||
{{ admin_banner_html|safe }}
|
||||
<header class="w-full">{{ slots.header|safe }}</header>
|
||||
<div class="editorial-column w-full py-4">{{ slots.byline|safe }}</div>
|
||||
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||
{% if slots.byline %}<div class="editorial-column w-full pt-6 pb-2">{{ slots.byline|safe }}</div>{% endif %}
|
||||
<div class="editorial-article-grid flex-grow py-8">
|
||||
<main class="prose-editorial">{% if slots.main %}{{ slots.main|safe }}{% else %}<p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p>{% endif %}</main>
|
||||
<aside>{{ slots.marginalia|safe }}</aside>
|
||||
<main class="editorial-dropcap prose-editorial">{% if slots.main %}{{ slots.main|safe }}{% else %}<p style="color: hsl(var(--muted-foreground));">This essay has no content yet.</p>{% endif %}</main>
|
||||
<aside class="editorial-marginalia">{{ slots.marginalia|safe }}</aside>
|
||||
</div>
|
||||
<footer class="w-full mt-auto pt-8 pb-6">{{ slots.footer|safe }}</footer>
|
||||
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
18
templates/editorial/auth.ninjatpl
Normal file
18
templates/editorial/auth.ninjatpl
Normal file
@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="antialiased min-h-screen flex flex-col bg-background text-foreground" style="font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);">
|
||||
{{ admin_banner_html|safe }}
|
||||
<main class="flex-grow w-full flex flex-col items-center justify-center px-4 py-12">
|
||||
<div class="w-full max-w-md">
|
||||
<a href="/" class="block text-center" aria-label="{{ site_settings.Title }} home">
|
||||
{% if site_settings.Logo %}<img src="{{ site_settings.Logo }}" alt="{{ site_settings.LogoAlt }}" class="mx-auto h-12 w-auto">{% else %}<span class="editorial-masthead-wordmark text-3xl">{{ site_settings.Title|default:"Editorial" }}</span>{% endif %}
|
||||
</a>
|
||||
<hr class="editorial-rule mx-auto mt-4 mb-8" style="max-width:10rem;">
|
||||
<div>{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p class="italic" style="color:hsl(var(--muted-foreground));">No form assigned to this page.</p></div>{% endif %}</div>
|
||||
{% if slots.footer %}<div class="mt-6 text-center text-sm" style="color:hsl(var(--muted-foreground));">{{ slots.footer|safe }}</div>{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
23
templates/editorial/blog-index.ninjatpl
Normal file
23
templates/editorial/blog-index.ninjatpl
Normal file
@ -0,0 +1,23 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="antialiased min-h-screen flex flex-col bg-background text-foreground" style="font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);">
|
||||
{{ admin_banner_html|safe }}
|
||||
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||
<main class="flex-grow w-full">
|
||||
<div class="editorial-wide py-10 md:py-14">
|
||||
{% if title %}
|
||||
<div class="text-center">
|
||||
<h1 class="editorial-heading text-4xl md:text-5xl">{{ title }}</h1>
|
||||
{% if page_meta.MetaDescription %}<p class="mt-3 italic" style="color:hsl(var(--muted-foreground));">{{ page_meta.MetaDescription }}</p>{% endif %}
|
||||
<hr class="editorial-rule mx-auto mt-6" style="max-width:8rem;">
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if slots.featured %}<section class="mt-12">{{ slots.featured|safe }}</section>{% endif %}
|
||||
{% if slots.main %}<section class="mt-12">{{ slots.main|safe }}</section>{% else %}<div class="py-20 text-center"><p class="italic" style="color:hsl(var(--muted-foreground));">No essays published yet.</p></div>{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
22
templates/editorial/contact.ninjatpl
Normal file
22
templates/editorial/contact.ninjatpl
Normal file
@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="antialiased min-h-screen flex flex-col bg-background text-foreground" style="font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);">
|
||||
{{ admin_banner_html|safe }}
|
||||
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||
<main class="flex-grow w-full">
|
||||
<div class="editorial-wide py-10 md:py-14">
|
||||
{% if slots.aside %}
|
||||
<div class="grid gap-12 md:grid-cols-[2fr_1fr]">
|
||||
<div>{% if slots.main %}{{ slots.main|safe }}{% endif %}</div>
|
||||
<aside class="w-full md:border-l md:border-[hsl(var(--border))] md:pl-10">{{ slots.aside|safe }}</aside>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="mx-auto max-w-2xl">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p class="italic" style="color:hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
@ -1,11 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
<body class="antialiased min-h-screen flex flex-col bg-background text-foreground" style="font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);">
|
||||
{{ admin_banner_html|safe }}
|
||||
<header class="w-full">{{ slots.header|safe }}</header>
|
||||
<main class="flex-grow editorial-column w-full py-8 prose-editorial">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="py-20 text-center"><p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}</main>
|
||||
<footer class="w-full mt-auto pt-8 pb-6">{{ slots.footer|safe }}</footer>
|
||||
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||
<main class="flex-grow w-full py-10 md:py-14">
|
||||
{% if slots.main %}<div class="editorial-column prose-editorial">{{ slots.main|safe }}</div>{% else %}<div class="editorial-column py-20 text-center"><p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}
|
||||
</main>
|
||||
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
<body class="antialiased min-h-screen flex flex-col bg-background text-foreground" style="font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);">
|
||||
{{ admin_banner_html|safe }}
|
||||
<header class="w-full">{{ slots.header|safe }}</header>
|
||||
{% if slots.header %}<header class="w-full">{{ slots.header|safe }}</header>{% endif %}
|
||||
<main class="flex-grow w-full">{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="editorial-wide py-20 text-center"><p style="color: hsl(var(--muted-foreground));">No content blocks assigned to this page.</p></div>{% endif %}</main>
|
||||
<footer class="w-full mt-auto pt-8 pb-6">{{ slots.footer|safe }}</footer>
|
||||
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,12 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<html lang="en" class="{% if theme_mode == "dark" %}dark{% endif %}">
|
||||
{{ head_html|safe }}
|
||||
<body class="antialiased min-h-screen flex flex-col" style="background-color: hsl(var(--background)); color: hsl(var(--foreground));">
|
||||
<body class="antialiased min-h-screen flex flex-col bg-background text-foreground" style="font-family: var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif);">
|
||||
{{ admin_banner_html|safe }}
|
||||
<header class="w-full">{{ slots.masthead|safe }}</header>
|
||||
<section class="w-full editorial-wide py-6">{{ slots.lead|safe }}</section>
|
||||
<section class="w-full editorial-wide py-6">{{ slots.river|safe }}</section>
|
||||
<footer class="w-full mt-auto pt-8 pb-6">{{ slots.footer|safe }}</footer>
|
||||
{% if slots.masthead %}<header class="w-full">{{ slots.masthead|safe }}</header>{% endif %}
|
||||
<main class="flex-grow w-full">
|
||||
{% if slots.lead %}<section class="editorial-wide py-10 md:py-14">{{ slots.lead|safe }}</section>{% endif %}
|
||||
{% if slots.river %}<section class="pb-10 md:pb-16"><div class="editorial-river editorial-flow"><hr class="editorial-rule">{{ slots.river|safe }}</div></section>{% endif %}
|
||||
</main>
|
||||
{% if slots.footer %}<footer class="w-full mt-auto">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,14 +1,74 @@
|
||||
<!doctype html><html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta name="x-apple-disable-message-reformatting"><meta http-equiv="X-UA-Compatible" content="IE=edge"><title>{{ site_name }}</title><style type="text/css">
|
||||
<!doctype html>
|
||||
<html lang="en" xmlns="http://www.w3.org/1999/xhtml" xmlns:o="urn:schemas-microsoft-com:office:office">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<meta name="x-apple-disable-message-reformatting">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<!--[if mso]>
|
||||
<noscript>
|
||||
<xml>
|
||||
<o:OfficeDocumentSettings>
|
||||
<o:PixelsPerInch>96</o:PixelsPerInch>
|
||||
</o:OfficeDocumentSettings>
|
||||
</xml>
|
||||
</noscript>
|
||||
<![endif]-->
|
||||
<title>{{ site_name }}</title>
|
||||
<style type="text/css">
|
||||
body, table, td, p, a, li, blockquote {
|
||||
-webkit-text-size-adjust: 100%;
|
||||
-ms-text-size-adjust: 100%;
|
||||
}
|
||||
table, td { mso-table-lspace: 0pt; mso-table-rspace: 0pt; }
|
||||
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; outline: none; text-decoration: none; }
|
||||
img { -ms-interpolation-mode: bicubic; border: 0; height: auto; line-height: 100%; outline: none; text-decoration: none; }
|
||||
body { margin: 0 !important; padding: 0 !important; width: 100% !important; }
|
||||
a[x-apple-data-detectors] { color: inherit !important; text-decoration: none !important; }
|
||||
@media only screen and (max-width: 620px) {
|
||||
.email-container { width: 100% !important; max-width: 100% !important; }
|
||||
.content-padding { padding-left: 24px !important; padding-right: 24px !important; }
|
||||
}
|
||||
</style></head><body style="background-color: {{ colors.background|default:"#f7f1e4" }}; margin: 0; padding: 0; font-family: var(--font-body, &#39;Source Serif 4&#39;, Georgia, serif);">{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td align="center" style="padding: 48px 10px; background-color: {{ colors.background|default:"#f7f1e4" }};"><table role="presentation" class="email-container" width="580" cellspacing="0" cellpadding="0" border="0" style="max-width: 580px; background-color: {{ colors.card|default:"#fbf8ec" }};"><!-- Masthead --><tr><td align="center" style="padding: 32px 40px 16px 40px;">{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 48px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-family: var(--font-heading, &#39;Playfair Display&#39;, Georgia, serif); font-style: italic; font-weight: 900; font-size: 36px; color: {{ colors.foreground|default:"#1d1714" }}; letter-spacing: -0.02em;">{{ site_name }}</h1>{% endif %}</td></tr><!-- Hairline rule --><tr><td style="padding: 0 40px;"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td style="border-top: 1px solid {{ colors.border|default:"#d6cdbf" }}; font-size: 0; line-height: 0; height: 1px;"> </td></tr></table></td></tr><!-- Body --><tr><td class="content-padding" style="padding: 32px 48px 40px 48px; color: {{ colors.foreground|default:"#1d1714" }}; font-size: 16px; line-height: 1.7; font-family: var(--font-body, &#39;Source Serif 4&#39;, Georgia, serif);">{{ body|safe }}</td></tr><!-- Footer --><tr><td style="padding: 24px 48px 40px 48px; border-top: 1px solid {{ colors.border|default:"#d6cdbf" }};"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td align="center" style="font-family: var(--font-body, &#39;Inter&#39;, sans-serif); font-size: 12px; color: {{ colors.mutedForeground|default:"#6e6357" }}; text-transform: uppercase; letter-spacing: 0.1em;"><p style="margin: 0 0 8px;">{{ site_name }}</p>{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px;"><a href="{{ unsubscribe_url }}" style="color: #702021; text-decoration: underline;">Unsubscribe</a></p>{% endif %}</td></tr></table></td></tr></table></td></tr></table></body></html>
|
||||
</style>
|
||||
</head>
|
||||
<body style="background-color: {{ colors.background|default:"#f2ebda" }}; margin: 0; padding: 0; font-family: 'Source Serif 4', 'Source Serif Pro', Georgia, serif;">
|
||||
{% if preview_text %}<div style="display: none; max-height: 0; overflow: hidden; mso-hide: all;">{{ preview_text }}</div>{% endif %}
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td align="center" style="padding: 48px 10px; background-color: {{ colors.background|default:"#f2ebda" }};">
|
||||
<table role="presentation" class="email-container" width="580" cellspacing="0" cellpadding="0" border="0" style="max-width: 580px; background-color: {{ colors.card|default:"#f7f2e6" }}; border: 1px solid {{ colors.border|default:"#d6cdbc" }};">
|
||||
<!-- Masthead -->
|
||||
<tr>
|
||||
<td align="center" style="padding: 32px 40px 16px 40px;">
|
||||
{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 48px; width: auto; display: block;">{% elif site_name %}<h1 style="margin: 0; font-family: 'Playfair Display', Georgia, serif; font-style: italic; font-weight: 900; font-size: 34px; color: {{ colors.foreground|default:"#211a14" }}; letter-spacing: -0.02em;">{{ site_name }}</h1>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Hairline rule -->
|
||||
<tr>
|
||||
<td style="padding: 0 40px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td style="border-top: 1px solid {{ colors.border|default:"#d6cdbc" }}; font-size: 0; line-height: 0; height: 1px;"> </td></tr></table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Body -->
|
||||
<tr>
|
||||
<td class="content-padding" style="padding: 32px 48px 40px 48px; color: {{ colors.foreground|default:"#211a14" }}; font-size: 16px; line-height: 1.7; font-family: 'Source Serif 4', 'Source Serif Pro', Georgia, serif;">{{ body|safe }}</td>
|
||||
</tr>
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td style="padding: 24px 48px 40px 48px; border-top: 1px solid {{ colors.border|default:"#d6cdbc" }};">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0">
|
||||
<tr>
|
||||
<td align="center" style="font-family: 'Source Serif 4', Georgia, serif; font-size: 12px; color: {{ colors.mutedForeground|default:"#6b6154" }}; text-transform: uppercase; letter-spacing: 0.12em;">
|
||||
<p style="margin: 0 0 8px;">{{ site_name }}</p>
|
||||
{% if site_url %}<p style="margin: 0 0 8px;"><a href="{{ site_url }}" style="color: {{ colors.accent|default:"#8a2e28" }}; text-decoration: none; border-bottom: 1px solid {{ colors.accent|default:"#8a2e28" }};">{{ site_url }}</a></p>{% endif %}
|
||||
{% if unsubscribe_url %}<p style="margin: 12px 0 0; font-size: 11px;"><a href="{{ unsubscribe_url }}" style="color: {{ colors.mutedForeground|default:"#6b6154" }}; text-decoration: underline;">Unsubscribe</a></p>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
21
templates/overrides/editorial/announcement-bar.ninjatpl
Normal file
21
templates/overrides/editorial/announcement-bar.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
||||
{# announcement-bar override — quiet notice on a hairline panel, small-caps message. Dismiss <script>, data-bn-annkey and localStorage keying preserved verbatim. Fields mirror builtin: variant, message, linkText, linkUrl, dismissable. #}
|
||||
{% set anncls = "editorial-panel" %}
|
||||
{% if variant == "promo" %}{% set anncls = "editorial-panel" %}{% elif variant == "alert" %}{% set anncls = "bg-destructive text-destructive-foreground" %}{% endif %}
|
||||
<div data-block-override="editorial:announcement-bar" data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="bn-announcement w-full border-b border-[hsl(var(--border))] {{ anncls }} {{ class }}">
|
||||
<div class="max-w-7xl mx-auto px-10 py-2 relative flex items-center justify-center gap-2 text-sm text-center">
|
||||
<p class="editorial-caps text-xs">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" class="underline 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 -translate-y-1/2 inline-flex items-center justify-center p-1 rounded hover:bg-black/10 transition-colors"><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 %}
|
||||
16
templates/overrides/editorial/audio-embed.ninjatpl
Normal file
16
templates/overrides/editorial/audio-embed.ninjatpl
Normal file
@ -0,0 +1,16 @@
|
||||
{# audio-embed override — editorial hairline player card. Fields + facade JS mirror builtin verbatim: url, media, artwork, title, artist, download. #}
|
||||
<figure data-block-override="editorial:audio-embed" class="bn-audio editorial-panel my-8 overflow-hidden bg-card text-card-foreground{% 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 bg-muted">{% 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="editorial-caps truncate text-sm" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ title }}</p>{% endif %}{% if artist %}<p class="truncate text-sm italic" style="color:hsl(var(--muted-foreground));">{{ artist }}</p>{% endif %}<div class="mt-2 flex items-center gap-3" data-bn-audio-slot><button type="button" data-bn-audio-play class="editorial-button inline-flex items-center gap-2 text-sm" aria-label="Play{% if title %}: {{ title }}{% endif %}">::lucide:play:sm:: Play</button>{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="editorial-link inline-flex items-center gap-1 text-sm">::lucide:download:sm:: 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>
|
||||
33
templates/overrides/editorial/auth-form.ninjatpl
Normal file
33
templates/overrides/editorial/auth-form.ninjatpl
Normal file
@ -0,0 +1,33 @@
|
||||
{# auth-form (editorial) — typographic login/register in an editorial-panel with small-caps labels, hairline tab rules and oxblood active tab. Wiring UNTOUCHABLE: single shared #auth-message, /api/auth/login (email,password) and /api/auth/register (username,email,password), hx-target #auth-message, captcha injected from _captcha_widget. #}
|
||||
{% if context.isPublicLoggedIn %}
|
||||
<div data-block-override="editorial:auth-form" class="auth-form-logged-in mx-auto max-w-md text-center py-8">
|
||||
<p class="italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);">You are signed in as <strong style="color:hsl(var(--foreground));">{{ context.publicUsername }}</strong></p>
|
||||
<a href="/account" class="editorial-link mt-3 inline-block">My account</a>
|
||||
</div>
|
||||
{% else %}
|
||||
<div data-block-override="editorial:auth-form" class="auth-form editorial-panel mx-auto max-w-md bg-card p-8">
|
||||
<div id="auth-message"></div>
|
||||
<div class="mb-6 flex border-b border-[hsl(var(--border))]">
|
||||
<button type="button" onclick="document.getElementById('login-panel').classList.remove('hidden');document.getElementById('register-panel').classList.add('hidden');this.setAttribute('data-active','1');this.nextElementSibling.removeAttribute('data-active');" data-auth-tab {% if default_tab != "register" %}data-active="1"{% endif %} class="editorial-caps flex-1 py-2 text-center text-xs border-b-2 border-transparent data-[active]:border-[hsl(var(--accent))] data-[active]:text-[hsl(var(--accent))]">Sign in</button>
|
||||
<button type="button" onclick="document.getElementById('register-panel').classList.remove('hidden');document.getElementById('login-panel').classList.add('hidden');this.setAttribute('data-active','1');this.previousElementSibling.removeAttribute('data-active');" data-auth-tab {% if default_tab == "register" %}data-active="1"{% endif %} class="editorial-caps flex-1 py-2 text-center text-xs border-b-2 border-transparent data-[active]:border-[hsl(var(--accent))] data-[active]:text-[hsl(var(--accent))]">Register</button>
|
||||
</div>
|
||||
<div id="login-panel" class="{% if default_tab == "register" %}hidden{% endif %}">
|
||||
<form hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
|
||||
<div class="mb-3"><label class="editorial-caps text-muted-foreground block text-xs mb-1">Email</label><input type="email" name="email" required class="w-full editorial-input" /></div>
|
||||
<div class="mb-4"><label class="editorial-caps text-muted-foreground block text-xs mb-1">Password</label><input type="password" name="password" required class="w-full editorial-input" /></div>
|
||||
{% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %}
|
||||
<button type="submit" class="editorial-button w-full" hx-disabled-elt="this">Sign in</button>
|
||||
<p class="text-center mt-3 text-sm"><a href="/forgot-password" class="editorial-link">Forgot password?</a></p>
|
||||
</form>
|
||||
</div>
|
||||
<div id="register-panel" class="{% if default_tab != "register" %}hidden{% endif %}">
|
||||
<form hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
|
||||
<div class="mb-3"><label class="editorial-caps text-muted-foreground block text-xs mb-1">Username</label><input type="text" name="username" required pattern="[a-zA-Z0-9_-]{3,30}" class="w-full editorial-input" /></div>
|
||||
<div class="mb-3"><label class="editorial-caps text-muted-foreground block text-xs mb-1">Email</label><input type="email" name="email" required class="w-full editorial-input" /></div>
|
||||
<div class="mb-4"><label class="editorial-caps text-muted-foreground block text-xs mb-1">Password</label><input type="password" name="password" required minlength="8" class="w-full editorial-input" /></div>
|
||||
{% if _captcha_widget %}{{ _captcha_widget|safe }}{% endif %}
|
||||
<button type="submit" class="editorial-button w-full" hx-disabled-elt="this">Create account</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
31
templates/overrides/editorial/auth-status.ninjatpl
Normal file
31
templates/overrides/editorial/auth-status.ninjatpl
Normal file
@ -0,0 +1,31 @@
|
||||
{# auth-status (editorial) — header login state; small-caps links, hairline monogram and quiet editorial-panel dropdown. Driven by context.isPublicLoggedIn / context.publicDisplayName / context.publicUsername; logout posts /api/auth/logout. #}
|
||||
{% if context.isPublicLoggedIn %}
|
||||
<div data-block-override="editorial:auth-status" class="auth-status relative">
|
||||
<button type="button" onclick="this.nextElementSibling.classList.toggle('hidden')" class="editorial-caps flex items-center gap-2 text-xs opacity-90 hover:opacity-100 px-3 py-1.5 transition-colors">
|
||||
<span class="inline-flex h-7 w-7 items-center justify-center rounded-full text-[0.7rem] font-bold" style="border:1px solid hsl(var(--border));color:hsl(var(--accent));">{{ context.publicDisplayName|default:context.publicUsername|first|upper }}</span>
|
||||
<span>{{ context.publicDisplayName|default:context.publicUsername }}</span>
|
||||
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg>
|
||||
</button>
|
||||
<div id="auth-dropdown-menu" class="hidden absolute right-0 top-full mt-1 w-48 editorial-panel bg-card py-1 z-50">
|
||||
<a href="/account" class="block px-4 py-2 text-sm text-foreground hover:bg-accent">My profile</a>
|
||||
<a href="/account/reviews" class="block px-4 py-2 text-sm text-foreground hover:bg-accent">My reviews</a>
|
||||
<hr class="my-1 editorial-rule"/>
|
||||
<button hx-post="/api/auth/logout" hx-swap="none" class="block w-full text-left px-4 py-2 text-sm text-destructive hover:bg-accent">Sign out</button>
|
||||
</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>
|
||||
</div>
|
||||
{% else %}
|
||||
<div data-block-override="editorial:auth-status" class="auth-status flex items-center gap-2">
|
||||
<a href="/login" class="editorial-caps text-xs px-3 py-1.5 opacity-80 hover:opacity-100 hover:text-[hsl(var(--accent))] transition-colors">Sign in</a>
|
||||
<a href="/login?tab=register" class="editorial-button text-xs">Reserve</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
22
templates/overrides/editorial/author-bio-hero.ninjatpl
Normal file
22
templates/overrides/editorial/author-bio-hero.ninjatpl
Normal file
@ -0,0 +1,22 @@
|
||||
{# author-bio-hero (editorial) — author-page header framed by hairline rules over generous whitespace. #}
|
||||
{% if author %}
|
||||
<section data-block-override="editorial:author-bio-hero" class="editorial-rule 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-6 h-24 w-24 overflow-hidden rounded-full bg-muted ring-1 ring-[hsl(var(--border))]">{% img author.avatar_url alt=author.display_name class="h-full w-full object-cover" %}</div>{% endif %}
|
||||
<div class="mb-4 flex items-center justify-center gap-4"><span class="editorial-rule w-12"></span><span class="editorial-rule w-12"></span></div>
|
||||
<h1 class="editorial-heading text-3xl md:text-4xl text-foreground" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif)">{{ author.display_name }}</h1>
|
||||
{% if author.job_title or author.company %}<p class="editorial-caps mt-2 text-sm text-muted-foreground">{{ author.job_title }}{% if author.job_title and author.company %} · {% endif %}{{ author.company }}</p>{% endif %}
|
||||
{% if author.bio %}<p class="mt-5 max-w-2xl leading-relaxed text-muted-foreground" style="font-family:var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif)">{{ author.bio }}</p>{% endif %}
|
||||
<div class="mt-6 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-6 flex items-center justify-center gap-4">
|
||||
{% if author.website_url %}<a href="{{ author.website_url }}" target="_blank" rel="noopener noreferrer" class="text-muted-foreground transition-colors hover:text-[hsl(var(--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 noreferrer" class="text-muted-foreground transition-colors hover:text-[hsl(var(--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 noreferrer" class="text-muted-foreground transition-colors hover:text-[hsl(var(--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 noreferrer" class="text-muted-foreground transition-colors hover:text-[hsl(var(--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 %}
|
||||
14
templates/overrides/editorial/author-bio.ninjatpl
Normal file
14
templates/overrides/editorial/author-bio.ninjatpl
Normal file
@ -0,0 +1,14 @@
|
||||
{# author-bio (editorial) — byline card in an editorial-panel with hairline rule and small-caps label. #}
|
||||
{% if post.author_name %}
|
||||
<aside data-block-override="editorial:author-bio" class="editorial-panel bg-card p-6 text-card-foreground{% if class %} {{ class }}{% endif %}">
|
||||
{% if heading %}<p class="editorial-caps mb-4 text-xs text-muted-foreground">{{ 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-muted ring-1 ring-[hsl(var(--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="editorial-caps text-base text-card-foreground" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif)">{% if post.author_slug %}<a href="/author/{{ post.author_slug }}" class="transition-colors hover:text-[hsl(var(--accent))]">{{ post.author_name }}</a>{% else %}{{ post.author_name }}{% endif %}</h3>
|
||||
<span class="editorial-rule mt-2 block w-10"></span>
|
||||
{% if post.author_bio %}<p class="mt-2 text-sm text-muted-foreground" style="font-family:var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif)">{{ post.author_bio }}</p>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</aside>
|
||||
{% endif %}
|
||||
24
templates/overrides/editorial/blog-hero.ninjatpl
Normal file
24
templates/overrides/editorial/blog-hero.ninjatpl
Normal file
@ -0,0 +1,24 @@
|
||||
{# blog-hero (editorial) — blog-index header + featured posts from the `posts` provider; typographic, hairline rules. #}
|
||||
<section data-block-override="editorial:blog-hero" 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="mx-auto max-w-2xl text-center">
|
||||
{% if title %}<h1 class="editorial-caps editorial-underline text-4xl md:text-5xl text-foreground" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif)">{{ title }}</h1>{% endif %}
|
||||
{% if subtitle %}<p class="mt-4 text-lg text-muted-foreground" style="font-family:var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif)">{{ subtitle }}</p>{% endif %}
|
||||
</div>
|
||||
{% if posts %}
|
||||
<div class="mt-12 grid grid-cols-1 gap-8 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-8{% endif %}">
|
||||
{% if post.featured_image_url %}<a href="{{ post.url }}" class="mb-4 block overflow-hidden bg-muted{% 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 object-cover transition-transform duration-500 group-hover:scale-105" %}</a>{% endif %}
|
||||
<div class="flex flex-col justify-center{% if forloop.First %} md:w-1/2{% endif %}">
|
||||
<span class="editorial-rule mb-4 block w-12"></span>
|
||||
<h2 class="editorial-caps {% if forloop.First %}text-2xl{% else %}text-lg{% endif %} leading-snug text-foreground" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif)"><a href="{{ post.url }}" class="transition-colors hover:text-[hsl(var(--accent))]">{{ post.title }}</a></h2>
|
||||
{% if showExcerpt and post.excerpt %}<p class="mt-3 text-sm text-muted-foreground" style="font-family:var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif)">{{ post.excerpt }}</p>{% endif %}
|
||||
<div class="mt-4 flex flex-wrap items-center gap-x-3 text-xs text-muted-foreground">{% if post.author_name %}<span class="editorial-caps text-[hsl(var(--accent))]">{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
13
templates/overrides/editorial/breadcrumbs.ninjatpl
Normal file
13
templates/overrides/editorial/breadcrumbs.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
||||
{# breadcrumbs (editorial) — small-caps trail with hairline oxblood separators; current item plain. #}
|
||||
{% set arrows = template == "arrows" %}
|
||||
<nav aria-label="Breadcrumb" data-block-override="editorial:breadcrumbs" class="py-3{% if class %} {{ class }}{% endif %}">
|
||||
<ol class="flex flex-wrap items-center gap-1 text-xs">
|
||||
{% if showHome %}<li class="flex items-center"><a href="/" class="editorial-caps text-muted-foreground transition-colors hover:text-[hsl(var(--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-3.5 w-3.5 text-[hsl(var(--accent))]" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>{% else %}<span class="mx-2 text-[hsl(var(--accent))]">{{ separator|default:"/" }}</span>{% endif %}{% endif %}
|
||||
{% if item.is_current %}{% if showCurrent %}<span class="editorial-caps text-foreground" aria-current="page">{{ item.label }}</span>{% endif %}{% else %}<a href="{{ item.url }}" class="editorial-caps text-muted-foreground transition-colors hover:text-[hsl(var(--accent))]">{{ item.label }}</a>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</nav>
|
||||
@ -1 +1,10 @@
|
||||
{% if text %}{% if url %}<a href="{{ url }}" class="inline-block" aria-label="{{ ariaLabel }}"><button type="button" class="editorial-button">{{ text }}</button></a>{% else %}<button type="button" class="editorial-button" aria-label="{{ ariaLabel }}">{{ text }}</button>{% endif %}{% endif %}
|
||||
{# button override — hairline uppercase button; ghost/link/secondary styles render as an inline oxblood underline link. Fields: link, style, label, size, icon, iconPosition, newTab, align. #}
|
||||
{% with align=align|default:"left" %}
|
||||
<div data-block-override="editorial:button" class="bn-button flex {% if align == "center" %}justify-center{% elif align == "right" %}justify-end{% else %}justify-start{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||
{% if style == "ghost" or style == "link" or style == "secondary" %}
|
||||
<a href="{{ link|default:'#' }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="editorial-link inline-flex items-center gap-2 text-sm">{% if icon and iconPosition != "right" %}::{{ icon }}:sm::{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}::{{ icon }}:sm::{% endif %}</a>
|
||||
{% else %}
|
||||
<a href="{{ link|default:'#' }}"{% if newTab %} target="_blank" rel="noopener"{% endif %} class="editorial-button inline-flex items-center gap-2">{% if icon and iconPosition != "right" %}::{{ icon }}:sm::{% endif %}{{ label|default:"Learn more" }}{% if icon and iconPosition == "right" %}::{{ icon }}:sm::{% endif %}</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endwith %}
|
||||
|
||||
4
templates/overrides/editorial/card.ninjatpl
Normal file
4
templates/overrides/editorial/card.ninjatpl
Normal file
@ -0,0 +1,4 @@
|
||||
{# card override — editorial hairline panel; accent border on hover, oxblood link. Fields mirror builtin: layout, link, media, title, text, linkLabel. Media via {% img %}. #}
|
||||
{% with layout=layout|default:"vertical" %}
|
||||
<article data-block-override="editorial:card" class="bn-card editorial-panel overflow-hidden bg-card text-card-foreground transition-colors hover:border-[hsl(var(--accent))] {% 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-muted {% 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="editorial-heading text-lg" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ title }}</h3>{% endif %}{% if text %}<div class="text-sm leading-relaxed" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);">{{ text|safe }}</div>{% endif %}{% if linkLabel and link %}<span class="mt-2 inline-flex items-center gap-1 text-sm editorial-link">{{ linkLabel }}::lucide:arrow-right:sm::</span>{% endif %}</div>{% if link %}</a>{% endif %}</article>
|
||||
{% endwith %}
|
||||
18
templates/overrides/editorial/category-list.ninjatpl
Normal file
18
templates/overrides/editorial/category-list.ninjatpl
Normal file
@ -0,0 +1,18 @@
|
||||
{# category-list (editorial) — from the `categories` provider. hairline list or bordered chips. #}
|
||||
{% set pills = style == "pills" %}
|
||||
<section data-block-override="editorial:category-list" class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
{% if heading %}<h2 class="editorial-caps mb-5 text-xs text-muted-foreground">{{ heading }}</h2>{% endif %}
|
||||
{% if pills %}
|
||||
<div class="flex flex-wrap gap-2">
|
||||
{% for cat in categories %}
|
||||
<a href="{{ cat.url }}" class="editorial-caps inline-flex items-center gap-1.5 rounded-none border border-[hsl(var(--border))] bg-card px-4 py-1.5 text-xs text-card-foreground transition-colors hover:border-[hsl(var(--accent))] hover:text-[hsl(var(--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="editorial-rule flex items-center justify-between px-3 py-2.5 text-sm text-muted-foreground transition-colors hover:text-[hsl(var(--accent))]"><span class="editorial-caps">{{ cat.name }}</span>{% if showCount %}<span class="rounded-none border border-[hsl(var(--border))] px-2 py-0.5 text-xs text-muted-foreground">{{ cat.post_count }}</span>{% endif %}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</section>
|
||||
17
templates/overrides/editorial/contact-card.ninjatpl
Normal file
17
templates/overrides/editorial/contact-card.ninjatpl
Normal file
@ -0,0 +1,17 @@
|
||||
<section data-block-override="editorial:contact-card" 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="editorial-caps editorial-underline text-3xl md:text-4xl text-foreground" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ heading }}</h2>{% endif %}
|
||||
{% if description %}<p class="mt-4 mx-auto max-w-2xl text-lg text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ description }}</p>{% endif %}
|
||||
<div class="mt-12 grid grid-cols-1 gap-6 text-left {% 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="editorial-panel flex items-start gap-4 rounded-none bg-card p-6 text-card-foreground">
|
||||
{% if channel.icon %}<span class="inline-flex h-11 w-11 shrink-0 items-center justify-center rounded-none border border-[hsl(var(--border))] text-[hsl(var(--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="editorial-caps text-xs tracking-[0.16em] text-muted-foreground">{{ channel.label }}</div>{% endif %}
|
||||
{% if channel.value %}<div class="mt-1 break-words text-lg text-foreground" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{% if channel.link %}<a href="{{ channel.link }}" class="editorial-link transition-colors hover:text-[hsl(var(--accent))]">{{ channel.value }}</a>{% else %}{{ channel.value }}{% endif %}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
43
templates/overrides/editorial/contact-form.ninjatpl
Normal file
43
templates/overrides/editorial/contact-form.ninjatpl
Normal file
@ -0,0 +1,43 @@
|
||||
{# contact-form override — editorial enquiry form. ALL form wiring preserved verbatim from builtin: bid, {% form %} action/hx attrs, honeypot, field loop, form.submitted/errors, submit target. Only inputs restyled to hairline editorial fields. #}
|
||||
{% with bid=_block_id|default:context.blockId %}
|
||||
<section data-block-override="editorial:contact-form" 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="editorial-panel bg-card p-10 text-center">
|
||||
<div class="mx-auto mb-4 inline-flex h-12 w-12 items-center justify-center rounded-full text-[hsl(var(--accent))]" style="box-shadow:inset 0 0 0 1px hsl(var(--accent));">::lucide:check:lg::</div>
|
||||
<p class="text-lg italic" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);">{{ form.message|default:successMessage|default:"Thank you. Your table request has been received." }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="text-center">
|
||||
{% if heading %}<h2 class="editorial-heading text-3xl md:text-4xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ heading }}</h2>{% endif %}
|
||||
{% if description %}<p class="mt-3 text-lg italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);">{{ description }}</p>{% endif %}
|
||||
<span class="editorial-rule w-16 mx-auto mt-5 mb-2 block" aria-hidden="true"></span>
|
||||
</div>
|
||||
{% if form.errors._form %}<div class="mt-6 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="editorial-caps text-muted-foreground mb-1.5 block text-xs">{{ 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="editorial-input"></textarea>
|
||||
{% elif field.type == "select" %}
|
||||
<select id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}"{% if field.required %} required{% endif %} class="editorial-input">
|
||||
{% 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="editorial-input">
|
||||
{% 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 flex justify-center">
|
||||
<button type="submit" class="editorial-button inline-flex items-center justify-center">{{ submitLabel|default:"Request a table" }}</button>
|
||||
</div>
|
||||
{% endform %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endwith %}
|
||||
48
templates/overrides/editorial/countdown.ninjatpl
Normal file
48
templates/overrides/editorial/countdown.ninjatpl
Normal file
@ -0,0 +1,48 @@
|
||||
<section data-block-override="editorial:countdown" 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="editorial-heading text-3xl tracking-tight md:text-4xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-lg text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ 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" %}editorial-panel min-w-[4.5rem] bg-card px-3 py-4 text-card-foreground{% endif %}"><span class="text-4xl font-bold tabular-nums text-[hsl(var(--accent))] sm:text-5xl" style="font-family:var(--font-mono)" data-unit="days">--</span><span class="editorial-caps mt-1 text-xs text-muted-foreground">{{ labelDays|default:"Days" }}</span></div>
|
||||
<div class="flex flex-col items-center {% if variant == "boxes" %}editorial-panel min-w-[4.5rem] bg-card px-3 py-4 text-card-foreground{% endif %}"><span class="text-4xl font-bold tabular-nums text-[hsl(var(--accent))] sm:text-5xl" style="font-family:var(--font-mono)" data-unit="hours">--</span><span class="editorial-caps mt-1 text-xs text-muted-foreground">{{ labelHours|default:"Hours" }}</span></div>
|
||||
<div class="flex flex-col items-center {% if variant == "boxes" %}editorial-panel min-w-[4.5rem] bg-card px-3 py-4 text-card-foreground{% endif %}"><span class="text-4xl font-bold tabular-nums text-[hsl(var(--accent))] sm:text-5xl" style="font-family:var(--font-mono)" data-unit="minutes">--</span><span class="editorial-caps mt-1 text-xs text-muted-foreground">{{ labelMinutes|default:"Minutes" }}</span></div>
|
||||
<div class="flex flex-col items-center {% if variant == "boxes" %}editorial-panel min-w-[4.5rem] bg-card px-3 py-4 text-card-foreground{% endif %}"><span class="text-4xl font-bold tabular-nums text-[hsl(var(--accent))] sm:text-5xl" style="font-family:var(--font-mono)" data-unit="seconds">--</span><span class="editorial-caps mt-1 text-xs text-muted-foreground">{{ labelSeconds|default:"Seconds" }}</span></div>
|
||||
</div>
|
||||
<div class="mt-8 hidden" data-countdown-expired>
|
||||
<p class="editorial-heading text-xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ expiredMessage|default:"This has started." }}</p>
|
||||
{% if expiredUrl and expiredLabel %}<a href="{{ expiredUrl }}" class="editorial-button mt-4 inline-flex items-center gap-2">{{ 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="mt-6 text-muted-foreground">Counting down to <time datetime="{{ target }}">{{ target }}</time>.</p></noscript>
|
||||
</div>
|
||||
</section>
|
||||
<script>
|
||||
(function(){
|
||||
var timers = document.querySelectorAll('[data-countdown-timer]');
|
||||
timers.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/editorial/cta.ninjatpl
Normal file
14
templates/overrides/editorial/cta.ninjatpl
Normal file
@ -0,0 +1,14 @@
|
||||
<section data-block-override="editorial:cta" 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" %}editorial-panel px-6 py-14 text-center text-card-foreground md:px-12{% elif background == "card" %}editorial-panel bg-card px-6 py-14 text-center text-card-foreground md:px-12{% else %}editorial-panel flex flex-col items-center gap-8 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="editorial-heading text-3xl tracking-tight md:text-4xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ heading }}</h2>{% endif %}
|
||||
{% if text %}<p class="mt-3 text-lg {% if background == "band" %}text-muted-foreground{% else %}text-muted-foreground{% endif %}" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ 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="editorial-button inline-flex items-center gap-2">{{ 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="editorial-link inline-flex items-center">{{ secondaryLabel }}</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
12
templates/overrides/editorial/divider.ninjatpl
Normal file
12
templates/overrides/editorial/divider.ninjatpl
Normal file
@ -0,0 +1,12 @@
|
||||
{# divider override — editorial hairline rules. Fields mirror builtin: variant (line|ornament|spacer-rule), weight, width, icon. ornament renders an icon flanked by hairline rules; spacer-rule a short centred hairline; line a full hairline rule. #}
|
||||
{% with variant=variant|default:"line" weight=weight|default:"thin" width=width|default:"full" %}
|
||||
{% if variant == "ornament" %}
|
||||
<div data-block-override="editorial:divider" class="bn-divider mx-auto my-10 flex flex-col items-center gap-2 {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator">
|
||||
<div class="flex w-full items-center gap-4"><span class="editorial-rule flex-1"></span><span style="color:hsl(var(--accent));">::{{ icon|default:"lucide:diamond" }}:sm::</span><span class="editorial-rule flex-1"></span></div>
|
||||
</div>
|
||||
{% elif variant == "spacer-rule" %}
|
||||
<div data-block-override="editorial:divider" class="bn-divider mx-auto py-10 flex justify-center {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator"><span class="{% if weight == "thick" %}editorial-rule{% else %}editorial-rule{% endif %} w-16"></span></div>
|
||||
{% else %}
|
||||
<div data-block-override="editorial:divider" class="bn-divider mx-auto my-8 flex justify-center {% if width == "narrow" %}max-w-[200px]{% elif width == "wide" %}max-w-3xl{% else %}w-full{% endif %}{% if class %} {{ class }}{% endif %}" role="separator"><span class="{% if weight == "thick" %}editorial-rule{% else %}editorial-rule{% endif %} w-full"></span></div>
|
||||
{% endif %}
|
||||
{% endwith %}
|
||||
41
templates/overrides/editorial/event-list.ninjatpl
Normal file
41
templates/overrides/editorial/event-list.ninjatpl
Normal file
@ -0,0 +1,41 @@
|
||||
{# event-list override — editorial listings: hairline panels/rules, oxblood dates, Playfair titles. Fields mirror builtin: heading, intro, layout, hidePast, events[].* . #}
|
||||
<section data-block-override="editorial:event-list" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-6xl px-4 text-center">
|
||||
{% if heading %}<h2 class="editorial-caps editorial-underline text-3xl md:text-4xl text-foreground" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mt-4 mx-auto max-w-2xl text-lg text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ intro }}</p>{% endif %}
|
||||
{% if layout == "cards" %}
|
||||
<div class="mt-12 grid grid-cols-1 gap-8 text-left sm:grid-cols-2 lg:grid-cols-3">
|
||||
{% for event in events %}{% if not (hidePast and event.past) %}
|
||||
<article class="editorial-panel flex flex-col overflow-hidden rounded-none bg-card text-card-foreground{% if event.past %} opacity-60{% endif %}">
|
||||
{% if event.media %}<div class="aspect-video overflow-hidden bg-muted">{% 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-6">
|
||||
<div class="editorial-caps text-xs tracking-[0.18em] text-[hsl(var(--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="text-xl text-foreground" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ event.title }}</h3>
|
||||
{% if event.venue %}<p class="editorial-caps text-xs tracking-[0.14em] text-muted-foreground">{{ event.venue }}</p>{% endif %}
|
||||
{% if event.description %}<p class="text-sm text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ event.description }}</p>{% endif %}
|
||||
{% if event.link %}<a href="{{ event.link }}" class="editorial-link mt-auto inline-flex items-center gap-2 pt-3 text-sm text-[hsl(var(--accent))]">{{ 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-12 divide-y divide-[hsl(var(--border))] border-y-2 border-[hsl(var(--border))] text-left">
|
||||
{% for event in events %}{% if not (hidePast and event.past) %}
|
||||
<li class="flex flex-col gap-3 py-6 sm:flex-row sm:items-center sm:gap-8{% if event.past %} opacity-60{% endif %}">
|
||||
<div class="shrink-0 sm:w-52">
|
||||
<div class="editorial-caps text-sm tracking-[0.16em] text-[hsl(var(--accent))]"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}</time></div>
|
||||
{% if event.end %}<div class="text-xs text-muted-foreground">to {{ event.end }}</div>{% endif %}
|
||||
{% if event.timezone %}<div class="text-xs text-muted-foreground">{{ event.timezone }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="min-w-0 flex-1">
|
||||
<h3 class="text-lg text-foreground" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ event.title }}</h3>
|
||||
{% if event.venue %}<p class="editorial-caps text-xs tracking-[0.14em] text-muted-foreground">{{ event.venue }}</p>{% endif %}
|
||||
{% if event.description %}<p class="mt-1 text-sm text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ event.description }}</p>{% endif %}
|
||||
</div>
|
||||
{% if event.link %}<a href="{{ event.link }}" class="editorial-button inline-flex shrink-0 items-center gap-2">{{ event.linkLabel|default:"Details" }}</a>{% endif %}
|
||||
</li>
|
||||
{% endif %}{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
17
templates/overrides/editorial/faq.ninjatpl
Normal file
17
templates/overrides/editorial/faq.ninjatpl
Normal file
@ -0,0 +1,17 @@
|
||||
<section data-block-override="editorial:faq" 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="editorial-caps text-center text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ intro }}</p>{% endif %}
|
||||
<div class="mt-10 divide-y divide-[hsl(var(--border))] border-y border-[hsl(var(--border))]">
|
||||
{% for item in items %}
|
||||
<details class="group py-2"{% if variant == "open-list" %} open{% endif %}>
|
||||
<summary class="flex cursor-pointer list-none items-center justify-between gap-4 py-3 text-lg font-medium marker:content-none [&::-webkit-details-marker]:hidden" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">
|
||||
<span>{{ item.question }}</span>
|
||||
<svg class="h-5 w-5 shrink-0 text-[hsl(var(--accent))] transition-transform duration-200 group-open:rotate-180" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"></use></svg>
|
||||
</summary>
|
||||
<div class="pb-4 text-muted-foreground [&_a]:text-[hsl(var(--accent))] [&_a]:underline" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ item.answer|safe }}</div>
|
||||
</details>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
17
templates/overrides/editorial/feature-grid.ninjatpl
Normal file
17
templates/overrides/editorial/feature-grid.ninjatpl
Normal file
@ -0,0 +1,17 @@
|
||||
<section data-block-override="editorial:feature-grid" 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 %}<div class="mb-3 flex items-center justify-center gap-4"><span class="editorial-rule w-12"></span><p class="editorial-caps text-xs text-muted-foreground">{{ eyebrow }}</p><span class="editorial-rule w-12"></span></div>{% endif %}
|
||||
{% if heading %}<h2 class="editorial-caps mt-2 text-center text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ 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" %}editorial-panel bg-card p-6 text-card-foreground hover:border-[hsl(var(--accent))] transition-shadow{% elif layout == "centered" %}text-center{% endif %}">
|
||||
{% if item.icon %}<div class="mb-4 inline-flex h-11 w-11 items-center justify-center border border-[hsl(var(--border))] bg-muted text-[hsl(var(--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="editorial-heading text-lg text-foreground" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{% if item.linkUrl %}<a href="{{ item.linkUrl }}" class="transition-colors hover:text-[hsl(var(--accent))]">{{ item.title }}</a>{% else %}{{ item.title }}{% endif %}</h3>
|
||||
{% if item.text %}<p class="mt-2 text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ item.text }}</p>{% endif %}
|
||||
{% if item.linkUrl and item.linkLabel %}<a href="{{ item.linkUrl }}" class="editorial-link mt-3 inline-flex items-center gap-1 text-sm">{{ 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>
|
||||
24
templates/overrides/editorial/featured-posts.ninjatpl
Normal file
24
templates/overrides/editorial/featured-posts.ninjatpl
Normal file
@ -0,0 +1,24 @@
|
||||
{# featured-posts (editorial) — grid of hairline-framed cards from the `posts` provider #}
|
||||
{# (params: limit, featured=true). Card: image, title, excerpt, meta. #}
|
||||
<section data-block-override="editorial:featured-posts" 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="editorial-caps editorial-underline mb-10 text-center text-2xl md:text-3xl text-foreground" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif)">{{ heading }}</h2>{% endif %}
|
||||
<div class="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 %}">
|
||||
{% for post in posts %}
|
||||
<article class="editorial-panel group flex flex-col overflow-hidden bg-card text-card-foreground transition-shadow">
|
||||
{% if showFeaturedImage and post.featured_image_url %}<a href="{{ post.url }}" class="block aspect-video overflow-hidden bg-muted">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" %}</a>{% endif %}
|
||||
<div class="flex flex-1 flex-col p-6 text-center">
|
||||
<h3 class="editorial-heading text-lg leading-snug text-card-foreground" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif)"><a href="{{ post.url }}" class="transition-colors hover:text-[hsl(var(--accent))]">{{ post.title }}</a></h3>
|
||||
<span class="editorial-rule mx-auto mt-3 block w-10"></span>
|
||||
{% if showExcerpt and post.excerpt %}<p class="mt-3 flex-1 text-sm text-muted-foreground" style="font-family:var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif)">{{ post.excerpt }}</p>{% endif %}
|
||||
<div class="mt-4 flex flex-wrap items-center justify-center gap-x-3 gap-y-1 text-xs text-muted-foreground">
|
||||
{% if showAuthor and post.author_name %}<span class="editorial-caps text-[hsl(var(--accent))]">{{ post.author_name }}</span>{% endif %}
|
||||
{% if showDate and post.published_at %}<span>{{ post.published_at|date:"M j, Y" }}</span>{% endif %}
|
||||
{% if post.reading_time %}<span>{{ post.reading_time }} min read</span>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
40
templates/overrides/editorial/footer.ninjatpl
Normal file
40
templates/overrides/editorial/footer.ninjatpl
Normal file
@ -0,0 +1,40 @@
|
||||
{# footer override — quiet editorial colophon: hairline rules, small-caps labels, address, reservations, social on a card panel. Structure/fields/signup_form preserved from builtin; reskinned to editorial. #}
|
||||
{% set fl = layout %}
|
||||
{% if not fl %}{% if template == "centered" %}{% set fl = "centered" %}{% else %}{% set fl = "columns" %}{% endif %}{% endif %}
|
||||
{% set fbg = backgroundColor|default:"bg-card text-card-foreground" %}
|
||||
{% macro sociallinks(cls) %}{% if social_items %}<div class="flex items-center gap-4 {{ cls }}">{% for s in social_items %}<a href="{{ s.url }}" target="_blank" rel="noopener" title="{{ s.label }}" class="text-muted-foreground hover:text-[hsl(var(--accent))] transition-colors"><span class="sr-only">{{ s.label }}</span>{% if s.icon %}::{{ s.icon }}::{% else %}{{ s.label }}{% endif %}</a>{% endfor %}</div>{% endif %}{% endmacro %}
|
||||
{% macro newsletter() %}{% if showNewsletter %}<div class="max-w-sm"><h3 class="editorial-caps text-sm text-muted-foreground">{{ newsletterTitle|default:"Join the guest list" }}</h3>{% if newsletterDescription %}<p class="mt-1 text-sm italic 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 pt-6 border-t border-[hsl(var(--border))] flex flex-col sm:flex-row items-center justify-between gap-3 text-sm text-muted-foreground"><p>{% 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="hover:text-[hsl(var(--accent))] transition-colors">{{ l.label }}</a>{% endfor %}</nav>{% endif %}</div>{% endmacro %}
|
||||
<footer data-block-override="editorial:footer" class="w-full border-t border-[hsl(var(--border))] {{ fbg }} {{ class }}">
|
||||
<div class="{% if fullWidth %}w-full px-4 sm:px-6 lg:px-8{% else %}max-w-7xl mx-auto px-4 sm:px-6 lg:px-8{% endif %} py-16">
|
||||
{% if fl == "minimal" %}
|
||||
<div class="flex flex-col sm:flex-row items-center justify-between gap-6">
|
||||
<div class="flex items-center gap-3">{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-8 w-auto"/>{% elif companyName %}<span class="editorial-caps text-lg" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);">{{ companyName }}</span>{% endif %}{% if tagline %}<span class="text-sm italic text-muted-foreground hidden sm:inline">{{ tagline }}</span>{% endif %}</div>
|
||||
{{ sociallinks("") }}
|
||||
</div>
|
||||
{{ legalbar() }}
|
||||
{% elif fl == "centered" %}
|
||||
<div class="flex flex-col items-center text-center gap-5">
|
||||
{% if logo %}<img src="{{ logo }}" alt="{{ companyName }}" class="h-10 w-auto"/>{% elif companyName %}<span class="editorial-caps text-2xl text-[hsl(var(--accent))]" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);">{{ companyName }}</span>{% endif %}
|
||||
<span class="editorial-rule w-12"></span>
|
||||
{% if tagline %}<p class="max-w-md text-sm italic text-muted-foreground">{{ 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="editorial-caps text-xs text-muted-foreground hover:text-[hsl(var(--accent))] transition-colors">{{ item.label }}</a>{% endfor %}{% endfor %}</nav>{% endif %}
|
||||
{{ newsletter() }}
|
||||
{{ sociallinks("justify-center") }}
|
||||
</div>
|
||||
{{ legalbar() }}
|
||||
{% else %}
|
||||
<div class="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-5 gap-8">
|
||||
<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="editorial-caps text-xl text-[hsl(var(--accent))]" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);">{{ companyName }}</span>{% endif %}
|
||||
{% if tagline %}<p class="mt-3 max-w-xs text-sm italic text-muted-foreground">{{ tagline }}</p>{% endif %}
|
||||
{% if address or email or phone %}<address class="mt-4 not-italic text-sm text-muted-foreground space-y-1">{% if address %}<div>{{ address }}</div>{% endif %}{% if email %}<div><a href="mailto:{{ email }}" class="hover:text-[hsl(var(--accent))] transition-colors">{{ email }}</a></div>{% endif %}{% if phone %}<div><a href="tel:{{ phone }}" class="hover:text-[hsl(var(--accent))] transition-colors">{{ phone }}</a></div>{% endif %}</address>{% endif %}
|
||||
{{ sociallinks("mt-5") }}
|
||||
</div>
|
||||
{% for col in columns %}<div><h3 class="editorial-caps text-xs text-muted-foreground">{{ 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="text-sm text-muted-foreground hover:text-[hsl(var(--accent))] transition-colors">{{ 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>
|
||||
52
templates/overrides/editorial/gallery.ninjatpl
Normal file
52
templates/overrides/editorial/gallery.ninjatpl
Normal file
@ -0,0 +1,52 @@
|
||||
{# gallery override — editorial grid: plain hairline framing, generous whitespace, unchanged lightbox. Fields + lightbox JS mirror builtin verbatim: layout, gap, columns, lightbox, images[].src/.alt/.caption. #}
|
||||
{% with layout=layout|default:"grid" gap=gap|default:"md" cols=columns|default:3 %}<div data-block-override="editorial:gallery" 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 bg-muted focus:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 {% 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 object-cover" %}{% else %}{% img image.src alt=image.alt|default:"" class="h-auto w-full" %}{% endif %}{% if image.caption %}<span class="sr-only">{{ image.caption }}</span>{% endif %}</a>{% endfor %}</div></div>{% if lightbox %}<script>
|
||||
(function(){
|
||||
if(window.__bnLightbox)return;window.__bnLightbox=true;
|
||||
var reduce=window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
||||
var ov,imgEl,capEl,items=[],idx=0,lastFocus=null;
|
||||
function build(){
|
||||
ov=document.createElement('div');
|
||||
ov.setAttribute('role','dialog');ov.setAttribute('aria-modal','true');ov.setAttribute('aria-label','Image viewer');ov.tabIndex=-1;
|
||||
ov.style.cssText='position:fixed;inset:0;z-index:9999;display:none;align-items:center;justify-content:center;background:rgba(0,0,0,0.9);padding:2rem;';
|
||||
var btn='position:absolute;width:2.75rem;height:2.75rem;display:inline-flex;align-items:center;justify-content:center;border-radius:9999px;border:none;background:hsl(var(--background));color:hsl(var(--foreground));cursor:pointer;';
|
||||
ov.innerHTML=
|
||||
'<button type="button" data-lb-close aria-label="Close" style="'+btn+'top:1rem;right:1rem;"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#x"/></svg></button>'+
|
||||
'<button type="button" data-lb-prev aria-label="Previous image" style="'+btn+'left:1rem;top:50%;transform:translateY(-50%);"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#chevron-left"/></svg></button>'+
|
||||
'<figure style="margin:0;max-width:92vw;max-height:92vh;display:flex;flex-direction:column;align-items:center;gap:0.75rem;"><img data-lb-img alt="" style="max-width:92vw;max-height:82vh;object-fit:contain;border-radius:0.5rem;'+(reduce?'':'transition:opacity .2s ease;')+'"><figcaption data-lb-cap style="color:hsl(var(--background));opacity:.85;font-size:.875rem;text-align:center;max-width:60ch;"></figcaption></figure>'+
|
||||
'<button type="button" data-lb-next aria-label="Next image" style="'+btn+'right:1rem;top:50%;transform:translateY(-50%);"><svg style="width:1.25rem;height:1.25rem" aria-hidden="true"><use href="/icons/lucide.svg#chevron-right"/></svg></button>';
|
||||
document.body.appendChild(ov);
|
||||
imgEl=ov.querySelector('[data-lb-img]');capEl=ov.querySelector('[data-lb-cap]');
|
||||
ov.addEventListener('click',function(e){if(e.target===ov||e.target.closest('[data-lb-close]'))close();});
|
||||
ov.querySelector('[data-lb-prev]').addEventListener('click',function(e){e.stopPropagation();show(idx-1);});
|
||||
ov.querySelector('[data-lb-next]').addEventListener('click',function(e){e.stopPropagation();show(idx+1);});
|
||||
}
|
||||
function show(i){
|
||||
if(!items.length)return;
|
||||
idx=(i+items.length)%items.length;
|
||||
var a=items[idx];var im=a.querySelector('img');
|
||||
imgEl.src=(im&&(im.currentSrc||im.src))||a.getAttribute('href');
|
||||
imgEl.alt=im?im.alt:'';
|
||||
var cap=a.getAttribute('data-caption')||'';capEl.textContent=cap;capEl.style.display=cap?'block':'none';
|
||||
}
|
||||
function open(g,link){
|
||||
if(!ov)build();
|
||||
items=Array.prototype.slice.call(g.querySelectorAll('[data-bn-gallery-item]'));
|
||||
lastFocus=document.activeElement;ov.style.display='flex';
|
||||
var multi=items.length>1;
|
||||
ov.querySelector('[data-lb-prev]').style.display=multi?'inline-flex':'none';
|
||||
ov.querySelector('[data-lb-next]').style.display=multi?'inline-flex':'none';
|
||||
show(items.indexOf(link));ov.focus();
|
||||
}
|
||||
function close(){if(ov)ov.style.display='none';if(lastFocus&&lastFocus.focus)lastFocus.focus();}
|
||||
document.addEventListener('click',function(e){
|
||||
var link=e.target.closest('[data-bn-gallery][data-lightbox="true"] [data-bn-gallery-item]');
|
||||
if(!link)return;e.preventDefault();open(link.closest('[data-bn-gallery]'),link);
|
||||
});
|
||||
document.addEventListener('keydown',function(e){
|
||||
if(!ov||ov.style.display==='none')return;
|
||||
if(e.key==='Escape'){e.preventDefault();close();}
|
||||
else if(e.key==='ArrowLeft')show(idx-1);
|
||||
else if(e.key==='ArrowRight')show(idx+1);
|
||||
});
|
||||
})();
|
||||
</script>{% endif %}{% endwith %}
|
||||
@ -1 +1,7 @@
|
||||
{% if level|integer == 1 %}<div class="editorial-column">{% if kicker %}<span class="editorial-heading-kicker">{{ kicker }}</span>{% endif %}<h1 class="editorial-heading text-5xl md:text-7xl mt-2 {{ textClass }}">{{ text }}</h1></div>{% elif level|integer == 3 %}<h3 class="editorial-heading text-2xl md:text-3xl editorial-column {{ textClass }}">{{ text }}</h3>{% elif level|integer == 4 %}<h4 class="editorial-heading text-xl md:text-2xl editorial-column {{ textClass }}">{{ text }}</h4>{% elif level|integer == 5 %}<h5 class="editorial-heading text-lg editorial-column {{ textClass }}">{{ text }}</h5>{% elif level|integer == 6 %}<h6 class="editorial-heading text-base editorial-column {{ textClass }}">{{ text }}</h6>{% else %}<h2 class="editorial-heading text-3xl md:text-4xl editorial-column {{ textClass }}">{{ text }}</h2>{% endif %}
|
||||
{# heading override — editorial display serif with a hairline underline. Fields: text, level, textClass (mirrors builtin). #}
|
||||
{% if level == 1 %}<h1 data-block-override="editorial:heading" class="editorial-heading editorial-underline text-4xl md:text-5xl flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h1>
|
||||
{% elif level == 3 %}<h3 data-block-override="editorial:heading" class="editorial-heading editorial-underline text-2xl md:text-3xl flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h3>
|
||||
{% elif level == 4 %}<h4 data-block-override="editorial:heading" class="editorial-heading editorial-underline text-xl md:text-2xl flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h4>
|
||||
{% elif level == 5 %}<h5 data-block-override="editorial:heading" class="editorial-heading editorial-underline text-lg md:text-xl flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h5>
|
||||
{% elif level == 6 %}<h6 data-block-override="editorial:heading" class="editorial-heading editorial-underline text-base md:text-lg flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h6>
|
||||
{% else %}<h2 data-block-override="editorial:heading" class="editorial-heading editorial-underline text-3xl md:text-4xl flex-1 {{ textClass }}" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ text }}</h2>{% endif %}
|
||||
|
||||
39
templates/overrides/editorial/hero.ninjatpl
Normal file
39
templates/overrides/editorial/hero.ninjatpl
Normal file
@ -0,0 +1,39 @@
|
||||
{# hero override — editorial masthead: hairline eyebrow rules, small-caps Playfair headline, roomy reading measure. Structure/fields/{% img %} calls preserved from builtin. Styled-text fields accept {text,color} or a 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 %}<div class="mb-5 flex items-center gap-4 {% if align == "center" %}justify-center{% endif %}"><span class="editorial-rule w-10"></span><span class="editorial-caps text-xs text-muted-foreground">{{ eyebrowText }}</span><span class="editorial-rule w-10"></span></div>{% endif %}
|
||||
{% if headlineText %}<h1 class="editorial-hero-title" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ headlineText }}</h1>{% endif %}
|
||||
{% if subText %}<h2 class="mt-5 text-xl sm:text-2xl italic max-w-2xl{% if align == "center" %} mx-auto{% endif %}" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);">{{ subText }}</h2>{% endif %}
|
||||
{% if descText %}<p class="mt-4 max-w-xl leading-relaxed{% if align == "center" %} mx-auto{% endif %}" style="color:hsl(var(--muted-foreground));">{{ descText }}</p>{% endif %}
|
||||
{% if pText or sText %}<div class="mt-8 flex flex-col sm:flex-row gap-4{% if align == "center" %} justify-center{% endif %}">
|
||||
{% if pText and pUrl %}<a href="{{ pUrl }}" class="editorial-button inline-flex items-center justify-center gap-2">{{ pText }}</a>{% endif %}
|
||||
{% if sText and sUrl %}<a href="{{ sUrl }}" class="editorial-link inline-flex items-center justify-center">{{ sText }}</a>{% endif %}
|
||||
</div>{% endif %}
|
||||
{% if trustLogos %}<div class="mt-10">
|
||||
<p class="editorial-caps text-xs text-muted-foreground mb-3">Acclaim</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 noreferrer" 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 data-block-override="editorial:hero" class="hero grid grid-cols-1 lg:grid-cols-2 overflow-hidden bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="relative min-h-[320px] bg-muted {% 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 data-block-override="editorial:hero" class="hero relative flex flex-col overflow-hidden bg-background text-foreground {% if hasImage %}{% 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 %}{% 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 %}"><div class="editorial-panel px-6 py-10 sm:px-10 sm:py-14">{{ herocontent(align) }}</div></div>
|
||||
</section>
|
||||
{% endif %}
|
||||
31
templates/overrides/editorial/hours-location.ninjatpl
Normal file
31
templates/overrides/editorial/hours-location.ninjatpl
Normal file
@ -0,0 +1,31 @@
|
||||
{# hours-location override (editorial) — typographic hours ledger + hairline-framed map. Map facade <button onclick> preserved verbatim. Fields mirror builtin: heading, hours[].day/.hours/.closed, specialHoursNote, address, phone, directionsUrl, mapEmbedUrl, mapButtonLabel. #}
|
||||
<section data-block-override="editorial:hours-location" 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="editorial-heading text-3xl md:text-4xl text-center" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ heading }}</h2>{% endif %}
|
||||
<div class="mt-10 grid grid-cols-1 gap-10 md:grid-cols-2">
|
||||
<div class="editorial-panel bg-card p-6 md:p-8">
|
||||
{% if hours %}
|
||||
<table class="w-full text-sm">
|
||||
<tbody class="divide-y divide-[hsl(var(--border))]">
|
||||
{% for row in hours %}
|
||||
<tr>
|
||||
<th scope="row" class="editorial-caps py-2 pr-4 text-left text-xs font-medium" style="color:hsl(var(--foreground));">{{ row.day }}</th>
|
||||
<td class="py-2 text-right tabular-nums {% if row.closed %}italic{% endif %}" style="{% if row.closed %}color:hsl(var(--muted-foreground));{% else %}color:hsl(var(--accent));{% endif %}">{% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if specialHoursNote %}<p class="mt-4 border-t border-[hsl(var(--border))] pt-3 text-sm italic" style="color:hsl(var(--muted-foreground));">{{ specialHoursNote }}</p>{% endif %}
|
||||
{% if address %}<div class="mt-6"><div class="editorial-caps text-xs" style="color:hsl(var(--muted-foreground));">Address</div><address class="mt-1 whitespace-pre-line not-italic" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);">{{ address }}</address></div>{% endif %}
|
||||
{% if phone %}<div class="mt-4"><div class="editorial-caps text-xs" style="color:hsl(var(--muted-foreground));">Reservations</div><a href="tel:{{ phone }}" class="editorial-link mt-1 inline-block">{{ phone }}</a></div>{% endif %}
|
||||
{% if directionsUrl %}<a href="{{ directionsUrl }}" target="_blank" rel="noopener noreferrer" class="editorial-button mt-6 inline-flex items-center gap-2">::lucide:navigation:sm:: Get directions</a>{% endif %}
|
||||
</div>
|
||||
{% if mapEmbedUrl %}
|
||||
<div class="aspect-square w-full overflow-hidden bg-muted 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="editorial-caps flex h-full min-h-[16rem] w-full items-center justify-center gap-2 text-sm transition-colors hover:bg-accent hover:text-accent-foreground" style="color:hsl(var(--muted-foreground));">::lucide:map-pin:md:: {{ mapButtonLabel|default:"Load map" }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -1 +1,11 @@
|
||||
{% if src %}<figure class="editorial-figure editorial-column"><img src="{{ src }}" alt="{{ alt }}">{% if caption %}<figcaption>{{ caption }}</figcaption>{% endif %}</figure>{% endif %}
|
||||
{# image override — plain editorial plate, no ornament frame. Fields mirror builtin: src, alt, caption, size, align, aspectRatio, objectFit. Media resolved via {% img %} (media filter is not registered in the block render path). #}
|
||||
{% with size=size|default:"full" align=align|default:"center" objectFit=objectFit|default:"cover" %}
|
||||
<figure data-block-override="editorial:image" class="my-8 flex-1 {% if align == "left" %}flex justify-start{% elif align == "right" %}flex justify-end{% else %}flex justify-center{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||
{% if src %}<div class="{% 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 %}">
|
||||
<div class="relative overflow-hidden bg-muted {% if aspectRatio == "square" %}aspect-square{% elif aspectRatio == "video" %}aspect-video{% elif aspectRatio == "portrait" %}aspect-[3/4]{% elif aspectRatio == "wide" %}aspect-[21/9]{% endif %}">
|
||||
{% if objectFit == "contain" %}{% img src alt=alt class="w-full h-full object-contain" layout=size %}{% elif objectFit == "fill" %}{% img src alt=alt class="w-full h-full object-fill" layout=size %}{% elif objectFit == "none" %}{% img src alt=alt class="w-full h-full object-none" layout=size %}{% else %}{% img src alt=alt class="w-full h-full object-cover" layout=size %}{% endif %}
|
||||
</div>
|
||||
</div>{% endif %}
|
||||
{% if caption %}<figcaption class="mt-3 text-center text-sm italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Source Serif 4', Georgia, serif);">{{ caption }}</figcaption>{% endif %}
|
||||
</figure>
|
||||
{% endwith %}
|
||||
|
||||
15
templates/overrides/editorial/logo-strip.ninjatpl
Normal file
15
templates/overrides/editorial/logo-strip.ninjatpl
Normal file
@ -0,0 +1,15 @@
|
||||
<section data-block-override="editorial:logo-strip" 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 %}<div class="mb-8 flex items-center justify-center gap-4"><span class="editorial-rule w-12"></span><p class="editorial-caps text-xs text-muted-foreground">{{ heading }}</p><span class="editorial-rule w-12"></span></div>{% 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 %}
|
||||
30
templates/overrides/editorial/menu-list.ninjatpl
Normal file
30
templates/overrides/editorial/menu-list.ninjatpl
Normal file
@ -0,0 +1,30 @@
|
||||
{# menu-list override — editorial menu: Playfair section titles, hairline leader rules, generous columns. Fields mirror builtin: heading, intro, columns, sections[].title/.note/.items[].name/.badges/.description/.price, currency, footnote. #}
|
||||
<section data-block-override="editorial:menu-list" class="py-16 md:py-24 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-5xl px-4 text-center">
|
||||
{% if heading %}<h2 class="editorial-heading text-3xl md:text-4xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mx-auto mt-3 max-w-2xl text-lg italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);">{{ intro }}</p>{% endif %}
|
||||
<span class="editorial-rule w-16 mx-auto mt-6 mb-4 block" aria-hidden="true"></span>
|
||||
<div class="mt-6 grid grid-cols-1 gap-x-14 gap-y-12 text-left {% if columns == 2 %}md:grid-cols-2{% endif %}">
|
||||
{% for section in sections %}
|
||||
<div class="menu-section">
|
||||
{% if section.title %}<h3 class="editorial-caps editorial-underline pb-2 text-center text-xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--accent));">{{ section.title }}</h3>{% endif %}
|
||||
{% if section.note %}<p class="mt-2 text-center text-sm italic" style="color:hsl(var(--muted-foreground));">{{ section.note }}</p>{% endif %}
|
||||
<ul class="mt-5 space-y-5">
|
||||
{% for item in section.items %}
|
||||
<li>
|
||||
<div class="flex items-baseline gap-3">
|
||||
<span class="shrink-0 font-medium" style="color:hsl(var(--foreground));">{{ item.name }}</span>
|
||||
<span class="min-w-6 flex-1 translate-y-[-2px] border-b border-dotted border-[hsl(var(--border))]" aria-hidden="true"></span>
|
||||
{% if item.price %}<span class="shrink-0 tabular-nums" style="color:hsl(var(--accent));font-family:var(--font-heading, 'Playfair Display', Georgia, serif);">{% if currency %}{{ currency }}{% endif %}{{ item.price }}</span>{% endif %}
|
||||
</div>
|
||||
{% if item.badges %}<div class="mt-1 flex flex-wrap gap-1.5">{% for badge in item.badges %}{% if badge.label %}<span class="editorial-caps inline-block border border-[hsl(var(--border))] px-2 py-0.5 text-[0.65rem]" style="color:hsl(var(--muted-foreground));">{{ badge.label }}</span>{% endif %}{% endfor %}</div>{% endif %}
|
||||
{% if item.description %}<p class="mt-1 text-sm italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);">{{ item.description }}</p>{% endif %}
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if footnote %}<p class="mt-12 text-xs" style="color:hsl(var(--muted-foreground));">{{ footnote }}</p>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
104
templates/overrides/editorial/navbar.ninjatpl
Normal file
104
templates/overrides/editorial/navbar.ninjatpl
Normal file
@ -0,0 +1,104 @@
|
||||
{# navbar override — editorial masthead. Structure, fixed drawer ids, focus-trap JS and all data-* preserved verbatim from the builtin; presentation reskinned to editorial (hairline rules, small-caps labels). Fields identical: variant/stickyBehavior/megaMenu/logoType/logoText/logo/companyName/cta/menu.items/... #}
|
||||
{% set nv = variant %}
|
||||
{% if not nv %}{% if template == "centered" %}{% set nv = "centered-logo" %}{% elif template == "transparent" %}{% set nv = "transparent-over-hero" %}{% else %}{% set nv = "standard" %}{% endif %}{% endif %}
|
||||
{% set sb = stickyBehavior %}
|
||||
{% if not sb %}{% if sticky %}{% set sb = "sticky" %}{% else %}{% set sb = "none" %}{% endif %}{% endif %}
|
||||
{% set use_mega = megaMenu or template == "mega" %}
|
||||
{% set is_transparent = nv == "transparent-over-hero" %}
|
||||
{% set cta_text = cta.text|default:ctaLabel %}
|
||||
{% set cta_url = cta.url|default:ctaUrl %}
|
||||
{% set bg = backgroundColor|default:"bg-background" %}
|
||||
{% 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="editorial-caps px-3 py-2 text-xs text-foreground/80 hover:text-[hsl(var(--accent))] transition-colors {{ item.css_class }}">{{ item.label }}</a>{% endmacro %}
|
||||
{% macro dropdown(item) %}<div class="relative group">
|
||||
<button type="button" class="editorial-caps px-3 py-2 text-xs text-foreground/80 hover:text-[hsl(var(--accent))] transition-colors inline-flex items-center gap-1">{{ item.label }}<svg class="h-4 w-4 transition-transform group-hover:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg></button>
|
||||
{% if use_mega %}<div class="absolute left-1/2 -translate-x-1/2 mt-1 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50"><div class="editorial-panel p-6 bg-card grid gap-8" style="grid-template-columns:repeat({{ item.children|length|default:1 }},minmax(10rem,1fr));min-width:min(90vw,42rem)">{% for child in item.children %}<div class="space-y-1">{% if child.item_type == "header" %}<div class="editorial-caps px-2 py-1 text-[0.65rem] text-muted-foreground border-b border-[hsl(var(--border))] mb-2">{{ child.label }}</div>{% else %}<a href="{{ child.url|default:'#' }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="block px-2 py-2 hover:bg-accent transition-colors {{ child.css_class }}">{% if child.icon %}<span class="mr-1.5 text-[hsl(var(--accent))]">::{{ child.icon }}::</span>{% endif %}<span class="text-sm font-medium text-foreground">{{ child.label }}</span>{% if child.description %}<span class="block text-xs text-muted-foreground mt-0.5">{{ child.description }}</span>{% endif %}</a>{% endif %}</div>{% endfor %}</div></div>
|
||||
{% else %}<div class="absolute left-0 mt-1 w-56 opacity-0 invisible group-hover:opacity-100 group-hover:visible transition-all duration-200 z-50"><div class="editorial-panel py-2 bg-card">{% for child in item.children %}{% if child.item_type == "header" %}<div class="editorial-caps px-4 py-2 text-[0.65rem] text-muted-foreground">{{ child.label }}</div>{% else %}<a href="{{ child.url|default:'#' }}"{% if child.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="block px-4 py-2 text-sm text-foreground hover:bg-accent transition-colors {{ child.css_class }}">{{ child.label }}</a>{% endif %}{% endfor %}</div></div>{% endif %}
|
||||
</div>{% endmacro %}
|
||||
{% macro brand(size) %}<a href="/" class="flex-shrink-0 flex items-center">{% if logoType == "text" and logoText.text %}<span class="editorial-caps text-xl {{ logoText.color }}" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ 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="editorial-caps text-xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ companyName }}</span>{% endif %}</a>{% endmacro %}
|
||||
{% macro ctabtn(extra) %}{% if cta_text %}<a href="{{ cta_url|default:'#' }}" class="editorial-button inline-flex items-center {{ extra }}">{{ cta_text }}</a>{% endif %}{% endmacro %}
|
||||
<nav data-block-override="editorial:navbar" data-bn-navbar data-bn-nav-variant="{{ nv }}" data-bn-sticky="{{ sb }}"{% if is_transparent %} data-bn-transparent="1"{% endif %} class="bn-navbar w-full {% if sb == "sticky" or sb == "shrink-on-scroll" %}sticky top-0 z-40 {% endif %}{% if is_transparent %}absolute top-0 left-0 z-40 text-white{% else %}{{ bg }} border-b border-[hsl(var(--border))]{% endif %} {{ class }}">
|
||||
{% if showUtilityBar %}<div class="w-full text-xs {% if not showOnMobile %}hidden sm:block{% endif %} editorial-panel"><div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 flex items-center justify-between h-8"><div>{{ utilityBarText|safe }}</div><div class="flex items-center gap-4">{% if utilityBarPhone %}<a href="tel:{{ utilityBarPhone }}" class="hover:text-[hsl(var(--accent))] transition-colors">{{ utilityBarPhone }}</a>{% endif %}{% if utilityBarEmail %}<a href="mailto:{{ utilityBarEmail }}" class="hover:text-[hsl(var(--accent))] transition-colors">{{ utilityBarEmail }}</a>{% endif %}</div></div></div>{% endif %}
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="bn-navbar-row flex items-center justify-between h-16">
|
||||
{% if nv == "centered-logo" %}
|
||||
<div class="hidden md:flex items-center gap-1 flex-1">{% 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 md:flex items-center gap-3 flex-1 justify-end">{{ ctabtn("") }}</div>
|
||||
{% elif nv == "split" %}
|
||||
<div class="hidden md:flex items-center gap-1 flex-1">{% for item in menu.items %}{% if forloop.Counter0 * 2 < menu.items|length %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endif %}{% endfor %}</div>
|
||||
<div class="flex items-center justify-center">{{ brand("h-9") }}</div>
|
||||
<div class="hidden md:flex items-center gap-1 flex-1 justify-end">{% for item in menu.items %}{% if forloop.Counter0 * 2 >= menu.items|length %}{% if item.children %}{{ dropdown(item) }}{% else %}{{ navlink(item) }}{% endif %}{% endif %}{% endfor %}{{ ctabtn("ml-3") }}</div>
|
||||
{% else %}
|
||||
<div class="flex items-center">{{ brand("h-8") }}</div>
|
||||
<div class="hidden md:flex items-center gap-1">{% 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 items-center justify-center p-2 rounded-md cursor-pointer hover:bg-accent/50 transition-colors"><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>
|
||||
{# --- Mobile off-canvas drawer: pure-CSS checkbox baseline (works with no JS) --- #}
|
||||
<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 bg-black/50 z-40 md:hidden" aria-hidden="true"></label>
|
||||
<div id="bn-navbar-drawer" class="bn-navbar-drawer fixed top-0 right-0 h-full w-80 max-w-[85vw] bg-background text-foreground z-50 shadow-xl md:hidden flex flex-col border-l border-[hsl(var(--border))]" role="dialog" aria-modal="true" aria-label="Site menu">
|
||||
<div class="flex items-center justify-between p-4 border-b border-[hsl(var(--border))]">{{ brand("h-8") }}<label for="bn-navbar-toggle" data-bn-nav-close class="p-2 rounded-md cursor-pointer text-muted-foreground hover:text-[hsl(var(--accent))] hover:bg-accent"><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="editorial-caps flex items-center justify-between px-6 py-3 text-sm cursor-pointer hover:bg-accent transition-colors list-none">{{ item.label }}<svg class="h-4 w-4 transition-transform group-open/acc:rotate-180" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/></svg></summary><div class="pl-8 pb-2">{% 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="block px-2 py-2 text-sm text-muted-foreground hover:text-[hsl(var(--accent))] transition-colors">{{ 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="editorial-caps block px-6 py-3 text-sm hover:bg-accent transition-colors">{{ item.label }}</a>{% endif %}{% endfor %}</nav>
|
||||
{% if cta_text %}<div class="p-4 border-t border-[hsl(var(--border))]"><a href="{{ cta_url|default:'#' }}" class="editorial-button block w-full text-center">{{ cta_text }}</a></div>{% endif %}
|
||||
</div>
|
||||
</nav>
|
||||
<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;}
|
||||
[data-bn-sticky="shrink-on-scroll"].bn-navbar-shrunk .bn-navbar-row{height:3rem;}
|
||||
[data-bn-sticky="shrink-on-scroll"] .bn-navbar-row{transition:height .2s ease;}
|
||||
[data-bn-transparent].bn-navbar-solid{position:sticky;color:inherit;background:hsl(var(--background));border-bottom:1px solid hsl(var(--primary) / 0.4);}
|
||||
</style>
|
||||
<script>
|
||||
(function(){
|
||||
if(window.__bnNavInit)return;window.__bnNavInit=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){
|
||||
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'));});});
|
||||
}
|
||||
var sb=nav.getAttribute('data-bn-sticky');
|
||||
var transparent=nav.getAttribute('data-bn-transparent')==='1';
|
||||
if(sb==='shrink-on-scroll'||transparent){
|
||||
var overHero=false;
|
||||
if(transparent){var next=nav.nextElementSibling;overHero=!!next&&next.getBoundingClientRect().height>=240;if(!overHero)nav.classList.add('bn-navbar-solid');}
|
||||
var onScroll=function(){
|
||||
var y=window.scrollY||window.pageYOffset;
|
||||
if(sb==='shrink-on-scroll')nav.classList.toggle('bn-navbar-shrunk',y>40);
|
||||
if(transparent&&overHero)nav.classList.toggle('bn-navbar-solid',y>40);
|
||||
};
|
||||
window.addEventListener('scroll',onScroll,{passive:true});onScroll();
|
||||
}
|
||||
});
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
13
templates/overrides/editorial/newsletter-signup.ninjatpl
Normal file
13
templates/overrides/editorial/newsletter-signup.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
||||
{# newsletter-signup (editorial) — the {% signup_form %} tag carries the #}
|
||||
{# untouchable subscribe wiring (/api/lists/subscribe HTMX post + honeypot); #}
|
||||
{# this template wraps it in a quiet editorial-panel — no loud filled band. #}
|
||||
<section data-block-override="editorial:newsletter-signup" 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="editorial-panel {% if layout == "inline" %}bg-card px-6 py-12 text-card-foreground md:px-12{% else %}bg-card px-6 py-12 text-card-foreground md:px-10{% endif %} text-center">
|
||||
<div class="mb-5 flex justify-center"><span class="editorial-rule w-12"></span></div>
|
||||
{% if title %}<h2 class="editorial-heading text-2xl md:text-3xl" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif)">{{ title }}</h2>{% endif %}
|
||||
{% if description %}<p class="mx-auto mt-4 max-w-xl {% if layout == "inline" %}text-muted-foreground{% else %}text-muted-foreground{% endif %}" style="font-family:var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif)">{{ 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>
|
||||
21
templates/overrides/editorial/page-suggestions.ninjatpl
Normal file
21
templates/overrides/editorial/page-suggestions.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
||||
{# page-suggestions (editorial) — 404 suggestions with underlined heading and oxblood markers. Fields: title, showDescription, emptyMessage; suggestions[] (.slug/.title/.excerpt). #}
|
||||
{% if suggestions %}
|
||||
<div data-block-override="editorial:page-suggestions" class="page-suggestions py-10">
|
||||
{% if title %}<h2 class="editorial-caps editorial-underline mx-auto w-fit text-xl text-center" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">{{ title }}</h2>{% endif %}
|
||||
<ul class="mt-6 space-y-3 max-w-lg mx-auto">
|
||||
{% for suggestion in suggestions %}
|
||||
<li class="flex items-start gap-3">
|
||||
<span class="mt-0.5 shrink-0" style="color:hsl(var(--accent));">::lucide:chevron-right:sm::</span>
|
||||
<div>
|
||||
<a href="{{ suggestion.slug }}" class="editorial-link font-medium">{{ suggestion.title }}</a>
|
||||
{% if showDescription and suggestion.excerpt %}<p class="mt-1 text-sm italic" style="color:hsl(var(--muted-foreground));">{{ suggestion.excerpt }}</p>{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
{% elif emptyMessage %}
|
||||
<div data-block-override="editorial:page-suggestions" class="page-suggestions py-10 text-center">
|
||||
<p class="italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);">{{ emptyMessage }}</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
13
templates/overrides/editorial/password-reset-form.ninjatpl
Normal file
13
templates/overrides/editorial/password-reset-form.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
||||
{# password-reset-form (editorial) — request stage in an editorial-panel with small-caps label. Wiring UNTOUCHABLE: /api/auth/request-password-reset (email), hx-target #reset-message. #}
|
||||
<div data-block-override="editorial:password-reset-form" class="password-reset-form editorial-panel mx-auto max-w-md bg-card p-8">
|
||||
<div id="reset-message"></div>
|
||||
<div id="request-reset-panel" class="text-center">
|
||||
<h2 class="editorial-heading text-2xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif);color:hsl(var(--foreground));">Reset password</h2>
|
||||
<p class="mt-3 text-sm italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);">Enter your email and we will send a link to reset your password.</p>
|
||||
<form hx-post="/api/auth/request-password-reset" hx-target="#reset-message" hx-swap="innerHTML" class="mt-6 text-left">
|
||||
<div class="mb-4"><label class="editorial-caps text-muted-foreground block text-xs mb-1">Email</label><input type="email" name="email" required class="w-full editorial-input" /></div>
|
||||
<button type="submit" class="editorial-button w-full" hx-disabled-elt="this">Send reset link</button>
|
||||
</form>
|
||||
<p class="text-center mt-4 text-sm"><a href="/login" class="editorial-link">Back to sign in</a></p>
|
||||
</div>
|
||||
</div>
|
||||
16
templates/overrides/editorial/popular-posts.ninjatpl
Normal file
16
templates/overrides/editorial/popular-posts.ninjatpl
Normal file
@ -0,0 +1,16 @@
|
||||
{# popular-posts (editorial) — compact ranked list from the `posts` provider, hairline-ruled. #}
|
||||
<section data-block-override="editorial:popular-posts" class="bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
{% if heading %}<h2 class="editorial-caps mb-5 text-xs text-muted-foreground">{{ heading }}</h2>{% endif %}
|
||||
<ol class="space-y-4">
|
||||
{% for post in posts %}
|
||||
<li class="editorial-rule flex items-start gap-3 pt-4">
|
||||
{% if showRank %}<span class="mt-0.5 text-lg font-bold tabular-nums text-[hsl(var(--accent))]" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif)">{{ forloop.Counter }}</span>{% endif %}
|
||||
{% if showThumbnail and post.featured_image_url %}<a href="{{ post.url }}" class="h-12 w-12 flex-shrink-0 overflow-hidden bg-muted">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover" %}</a>{% endif %}
|
||||
<div class="min-w-0">
|
||||
<h3 class="editorial-caps text-sm leading-snug text-foreground" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif)"><a href="{{ post.url }}" class="transition-colors hover:text-[hsl(var(--accent))]">{{ post.title }}</a></h3>
|
||||
{% if post.published_at %}<p class="mt-1 text-xs text-muted-foreground">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</section>
|
||||
23
templates/overrides/editorial/post-hero.ninjatpl
Normal file
23
templates/overrides/editorial/post-hero.ninjatpl
Normal file
@ -0,0 +1,23 @@
|
||||
{# post-hero (editorial) — current post from the `post` provider. When the post #}
|
||||
{# has a featured image it becomes an overlaid banner; otherwise a clean text hero. #}
|
||||
{% set center = align != "left" %}
|
||||
{% if post.featured_image_url %}
|
||||
<section data-block-override="editorial:post-hero" 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/70"></div></div>
|
||||
<div class="relative z-10 mx-auto w-full max-w-4xl px-4 py-14 sm:px-6 lg:px-8 {% if center %}text-center{% endif %}">
|
||||
{% if center %}<div class="mb-4 flex items-center justify-center gap-4"><span class="editorial-rule w-12"></span><span class="editorial-rule w-12"></span></div>{% endif %}
|
||||
<h1 class="editorial-heading text-3xl leading-tight text-white sm:text-4xl lg:text-5xl" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif)">{{ post.title }}</h1>
|
||||
{% if showExcerpt and post.excerpt %}<p class="mt-5 max-w-2xl text-lg text-white/80{% if center %} mx-auto{% endif %}" style="font-family:var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif)">{{ post.excerpt }}</p>{% endif %}
|
||||
{% if showMeta %}<div class="mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-white/80{% if center %} justify-center{% endif %}">{% if post.author_name %}<span class="editorial-caps text-white">{{ 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 read</span>{% endif %}</div>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% else %}
|
||||
<section data-block-override="editorial:post-hero" class="bg-background py-16 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 %}">
|
||||
{% if center %}<div class="mb-4 flex items-center justify-center gap-4"><span class="editorial-rule w-12"></span><span class="editorial-rule w-12"></span></div>{% endif %}
|
||||
<h1 class="editorial-caps editorial-underline text-3xl leading-tight sm:text-4xl lg:text-5xl text-foreground" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif)">{{ post.title }}</h1>
|
||||
{% if showExcerpt and post.excerpt %}<p class="mt-5 text-lg text-muted-foreground{% if center %} mx-auto max-w-2xl{% endif %}" style="font-family:var(--font-body, "Source Serif 4", "Source Serif Pro", Georgia, serif)">{{ post.excerpt }}</p>{% endif %}
|
||||
{% if showMeta %}<div class="mt-6 flex flex-wrap items-center gap-x-3 gap-y-1 text-sm text-muted-foreground{% if center %} justify-center{% endif %}">{% if post.author_name %}<span class="editorial-caps text-[hsl(var(--accent))]">{{ 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 read</span>{% endif %}</div>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
9
templates/overrides/editorial/post-metadata.ninjatpl
Normal file
9
templates/overrides/editorial/post-metadata.ninjatpl
Normal file
@ -0,0 +1,9 @@
|
||||
{# post-metadata (editorial) — author/date/reading-time row from the `post` provider, hairline chips. #}
|
||||
{% if post %}
|
||||
<div data-block-override="editorial:post-metadata" class="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 ring-1 ring-[hsl(var(--border))]" %}{% endif %}<span class="editorial-caps text-[hsl(var(--accent))]">{{ 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="editorial-caps rounded-none border border-[hsl(var(--border))] px-2 py-0.5 text-xs text-muted-foreground transition-colors hover:text-[hsl(var(--accent))]">{{ cat.name }}</a>{% endfor %}</span>{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
25
templates/overrides/editorial/pricing.ninjatpl
Normal file
25
templates/overrides/editorial/pricing.ninjatpl
Normal file
@ -0,0 +1,25 @@
|
||||
<section data-block-override="editorial:pricing" 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="editorial-caps text-center text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ 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="relative flex flex-col p-6 {% if tier.highlighted %}editorial-panel border-[hsl(var(--accent))] bg-card text-card-foreground{% else %}rounded-none border border-[hsl(var(--border))] bg-card text-card-foreground{% endif %}">
|
||||
{% if tier.badge %}<span class="editorial-caps absolute -top-3 left-1/2 -translate-x-1/2 border border-[hsl(var(--accent))] bg-card px-3 py-1 text-xs text-[hsl(var(--accent))]">{{ tier.badge }}</span>{% endif %}
|
||||
<h3 class="editorial-caps text-center text-lg text-foreground" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ tier.name }}</h3>
|
||||
<div class="mx-auto my-3 flex items-center justify-center"><span class="editorial-rule w-12"></span></div>
|
||||
<p class="flex items-baseline justify-center gap-1">
|
||||
<span class="text-4xl font-bold tracking-tight text-[hsl(var(--accent))]" style="font-family:var(--font-heading)">{% if currency %}{{ currency }}{% else %}${% endif %}{{ tier.price }}</span>
|
||||
{% if tier.period %}<span class="text-sm text-muted-foreground">{{ tier.period }}</span>{% endif %}
|
||||
</p>
|
||||
{% if tier.description %}<p class="mt-3 text-center text-sm text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ 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="flex items-start gap-2"><svg class="mt-0.5 h-4 w-4 shrink-0 text-[hsl(var(--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 %}{% if tier.highlighted %}<a href="{{ tier.ctaUrl }}" class="editorial-button mt-8 inline-flex w-full items-center justify-center">{{ tier.ctaLabel }}</a>{% else %}<a href="{{ tier.ctaUrl }}" class="editorial-link mt-8 inline-flex w-full items-center justify-center">{{ tier.ctaLabel }}</a>{% endif %}{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% if billingNote %}<p class="mt-8 text-center text-sm text-muted-foreground">{{ billingNote }}</p>{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
8
templates/overrides/editorial/quote.ninjatpl
Normal file
8
templates/overrides/editorial/quote.ninjatpl
Normal file
@ -0,0 +1,8 @@
|
||||
{# quote override — editorial pull-quote: large italic Source Serif, hairline panel or oxblood accent rule; centred quotes get a short centred hairline. Fields mirror builtin: quote, style, align, cite, attribution, role. #}
|
||||
{% with style=style|default:"block" align=align|default:"left" %}
|
||||
<figure data-block-override="editorial:quote" class="bn-quote my-10 {% if style == "inline-card" %}editorial-panel bg-card p-8 md:p-10{% elif style == "pull" %}border-l-2 border-[hsl(var(--accent))] pl-6 md:pl-8 py-2{% else %}px-2 py-1{% endif %}{% if align == "center" %} text-center{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||
{% if align == "center" %}<span class="editorial-rule w-16 mx-auto mb-5 block" aria-hidden="true"></span>{% endif %}
|
||||
<blockquote{% if cite %} cite="{{ cite }}"{% endif %} class="{% if style == "pull" %}text-2xl md:text-3xl leading-snug{% else %}text-xl md:text-2xl leading-relaxed{% endif %} italic" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);color:hsl(var(--foreground));">{{ quote|safe }}</blockquote>
|
||||
{% if attribution or role or cite %}<figcaption class="mt-4 text-sm{% if align == "center" %} flex flex-col items-center{% endif %}" style="color:hsl(var(--muted-foreground));">{% if attribution %}<span class="editorial-caps text-xs" style="color:hsl(var(--accent));">{{ attribution }}</span>{% endif %}{% if role %}<span class="{% if attribution %}block {% endif %}italic">{{ role }}</span>{% endif %}{% if cite %}<a href="{{ cite }}" class="editorial-link {% if attribution or role %}mt-1 {% endif %}inline-block text-xs">Source</a>{% endif %}</figcaption>{% endif %}
|
||||
</figure>
|
||||
{% endwith %}
|
||||
17
templates/overrides/editorial/related-posts.ninjatpl
Normal file
17
templates/overrides/editorial/related-posts.ninjatpl
Normal file
@ -0,0 +1,17 @@
|
||||
{# related-posts (editorial) — hairline-topped grid from the `posts` provider. #}
|
||||
{% if posts %}
|
||||
<section data-block-override="editorial:related-posts" class="editorial-rule py-12 md:py-16 bg-background text-foreground{% if class %} {{ class }}{% endif %}">
|
||||
<div class="mx-auto max-w-6xl px-4">
|
||||
<h2 class="editorial-caps editorial-underline mb-10 text-center text-2xl md:text-3xl text-foreground" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif)">{{ heading|default:"Related posts" }}</h2>
|
||||
<div class="grid grid-cols-1 gap-8 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 text-center">
|
||||
{% if post.featured_image_url %}<a href="{{ post.url }}" class="mb-4 block aspect-video overflow-hidden bg-muted">{% img post.featured_image_url alt=post.title class="h-full w-full object-cover transition-transform duration-500 group-hover:scale-105" %}</a>{% endif %}
|
||||
<h3 class="editorial-caps text-base leading-snug text-foreground" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif)"><a href="{{ post.url }}" class="transition-colors hover:text-[hsl(var(--accent))]">{{ post.title }}</a></h3>
|
||||
{% if post.published_at %}<p class="mt-2 text-xs text-muted-foreground">{{ post.published_at|date:"M j, Y" }}</p>{% endif %}
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
13
templates/overrides/editorial/rich-section.ninjatpl
Normal file
13
templates/overrides/editorial/rich-section.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
||||
<section data-block-override="editorial:rich-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 %}<div class="mb-3 flex items-center gap-4"><span class="editorial-rule w-12"></span><p class="editorial-caps text-xs text-muted-foreground">{{ eyebrow }}</p></div>{% endif %}
|
||||
{% if heading %}<h2 class="editorial-caps editorial-underline mt-2 text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ heading }}</h2>{% endif %}
|
||||
{% if body %}<div class="mt-4 space-y-4 text-lg text-muted-foreground [&_a]:text-[hsl(var(--accent))] [&_a]:underline [&_strong]:text-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ body|safe }}</div>{% endif %}
|
||||
{% if ctaUrl and ctaLabel %}<a href="{{ ctaUrl }}" class="editorial-button mt-6 inline-flex items-center gap-2">{{ 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="overflow-hidden">{% img media alt=mediaAlt class="w-full h-auto object-cover" %}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
2
templates/overrides/editorial/rich-text.ninjatpl
Normal file
2
templates/overrides/editorial/rich-text.ninjatpl
Normal file
@ -0,0 +1,2 @@
|
||||
{# rich-text override — long-form Source Serif editorial column with oxblood links and hairline blockquotes. Field: html (mirrors builtin). #}
|
||||
{% if html %}<article data-block-override="editorial:rich-text" class="rich-text prose prose-lg max-w-none [&_a]:text-[hsl(var(--accent))] [&_a]:no-underline [&_a]:border-b [&_a:hover]:text-accent [&_blockquote]:border-l-2 [&_blockquote]:border-[hsl(var(--border))] [&_blockquote]:italic" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);color:hsl(var(--foreground));font-size:1.1rem;line-height:1.75;">{{ html|safe }}</article>{% endif %}
|
||||
13
templates/overrides/editorial/sidebar-nav.ninjatpl
Normal file
13
templates/overrides/editorial/sidebar-nav.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
||||
{# sidebar-nav (editorial) — small-caps vertical menu with hairline divider rule and oxblood link/icon accents. #}
|
||||
{% set compact = template == "compact" %}
|
||||
<nav data-block-override="editorial:sidebar-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-[hsl(var(--border))]{% endif %}{% if class %} {{ class }}{% endif %}">
|
||||
{% if title %}<h3 class="editorial-caps editorial-underline {% if compact %}px-2 mb-4 text-xs text-muted-foreground{% else %}px-3 mb-5 text-sm text-foreground{% endif %}" style="font-family:var(--font-heading, "Playfair Display", Georgia, serif)">{{ 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="editorial-caps flex items-center rounded-none text-sm text-muted-foreground transition-colors hover:text-[hsl(var(--accent))] hover:bg-accent {% 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 text-[hsl(var(--accent))]" 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-accent','font-medium');}});})();
|
||||
</script>{% endif %}
|
||||
13
templates/overrides/editorial/social-links.ninjatpl
Normal file
13
templates/overrides/editorial/social-links.ninjatpl
Normal file
@ -0,0 +1,13 @@
|
||||
<div data-block-override="editorial:social-links" class="social-links{% if class %} {{ class }}{% endif %}">
|
||||
{% if heading %}<p class="editorial-caps mb-4 text-xs tracking-[0.18em] text-muted-foreground">{{ 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 noreferrer" aria-label="{{ link.label|default:link.network }}" class="inline-flex items-center gap-2 rounded-none border border-[hsl(var(--border))] text-foreground transition-colors hover:border-[hsl(var(--accent))] hover:text-[hsl(var(--accent))] {% if style == "stack" %}w-full px-4 py-2.5{% else %}p-2.5{% endif %}">
|
||||
{% if link.network %}<svg class="h-5 w-5 text-[hsl(var(--accent))]" 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="editorial-caps text-xs tracking-[0.14em]">{{ link.label }}</span>{% endif %}
|
||||
</a>
|
||||
</li>
|
||||
{% endif %}{% endfor %}
|
||||
</ul>
|
||||
</div>
|
||||
48
templates/overrides/editorial/stats.ninjatpl
Normal file
48
templates/overrides/editorial/stats.ninjatpl
Normal file
@ -0,0 +1,48 @@
|
||||
<section data-block-override="editorial:stats" 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="editorial-caps text-center text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ intro }}</p>{% endif %}
|
||||
<dl class="mt-12 grid grid-cols-2 gap-8 divide-x divide-[hsl(var(--border))] {% 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="px-2 text-center">
|
||||
<dd class="text-4xl font-bold tracking-tight text-[hsl(var(--accent))] md:text-5xl" style="font-family:var(--font-heading)">{% 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="editorial-caps mt-2 text-xs 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){
|
||||
var nodes = section.querySelectorAll('[data-countup-value]');
|
||||
nodes.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/editorial/team.ninjatpl
Normal file
21
templates/overrides/editorial/team.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
||||
<section data-block-override="editorial:team" 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="editorial-caps text-center text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ 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" %}editorial-panel flex items-start gap-5 bg-card p-5 text-card-foreground{% 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 border border-[hsl(var(--border))] bg-muted">{% 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="editorial-heading text-lg text-foreground" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ member.name }}</h3>{% endif %}
|
||||
{% if member.role %}<p class="editorial-caps text-xs text-muted-foreground">{{ member.role }}</p>{% endif %}
|
||||
{% if member.bio %}<p class="mt-2 text-sm text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ 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-[hsl(var(--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>
|
||||
27
templates/overrides/editorial/testimonial.ninjatpl
Normal file
27
templates/overrides/editorial/testimonial.ninjatpl
Normal file
@ -0,0 +1,27 @@
|
||||
<section data-block-override="editorial:testimonial" 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="editorial-caps text-center text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ heading }}</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="editorial-panel flex flex-col bg-card p-6 text-card-foreground{% if layout == "carousel" %} min-w-[18rem] max-w-sm shrink-0 snap-start{% endif %}">
|
||||
{% if q.rating %}<div class="mb-3 flex justify-center gap-0.5" aria-label="Rated {{ q.rating }} out of 5">
|
||||
<svg class="h-4 w-4 {% if q.rating >= 1 %}text-[hsl(var(--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-[hsl(var(--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-[hsl(var(--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-[hsl(var(--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-[hsl(var(--accent))]{% else %}text-muted-foreground/30{% endif %}" aria-hidden="true"><use href="/icons/lucide.svg#star"></use></svg>
|
||||
</div>{% endif %}
|
||||
<blockquote class="flex-1 text-center text-lg leading-relaxed" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{% if q.quote %}“{{ q.quote }}”{% endif %}</blockquote>
|
||||
<div class="mx-auto my-4 flex items-center justify-center"><span class="editorial-rule w-16"></span></div>
|
||||
<figcaption class="flex items-center justify-center gap-3">
|
||||
{% if q.avatar %}<span class="h-10 w-10 shrink-0 overflow-hidden border border-[hsl(var(--border))] bg-muted">{% img q.avatar alt=q.name class="h-full w-full object-cover" %}</span>{% endif %}
|
||||
<span class="text-center">
|
||||
{% if q.name %}<span class="editorial-caps block text-sm text-foreground">{{ q.name }}</span>{% endif %}
|
||||
{% if q.role %}<span class="block text-sm text-muted-foreground">{{ q.role }}</span>{% endif %}
|
||||
</span>
|
||||
</figcaption>
|
||||
</figure>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@ -1 +1,2 @@
|
||||
<div class="editorial-text editorial-column prose-editorial {{ class }}">{{ text|safe }}</div>
|
||||
{# text override — Source Serif reading measure with oxblood underlined links. Fields: text, proseClass (mirrors builtin). Dropcap is reserved for the article template, not forced on every text block. #}
|
||||
<div data-block-override="editorial:text" class="prose max-w-none flex-1 [&_a]:text-[hsl(var(--accent))] [&_a]:no-underline [&_a]:border-b [&_a]:border-[hsl(var(--border))] [&_a:hover]:text-accent {{ proseClass }}" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);color:hsl(var(--foreground));font-size:1.075rem;line-height:1.75;">{{ text|safe }}</div>
|
||||
|
||||
21
templates/overrides/editorial/timeline.ninjatpl
Normal file
21
templates/overrides/editorial/timeline.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
||||
<section data-block-override="editorial:timeline" 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="editorial-caps text-center text-3xl text-foreground md:text-4xl" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="mx-auto mt-4 max-w-2xl text-center text-lg text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ 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-[hsl(var(--border))]{% endif %}">
|
||||
{% for step in steps %}
|
||||
<li class="{% if orientation == "horizontal" %}text-center{% else %}relative pl-14{% endif %}">
|
||||
<span class="editorial-caps flex h-8 w-8 items-center justify-center border border-[hsl(var(--accent))] bg-card text-sm text-[hsl(var(--accent))] {% 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 bg-[hsl(var(--accent))]"></span>{% else %}<svg class="h-4 w-4" aria-hidden="true"><use href="/icons/lucide.svg#flag"></use></svg>{% endif %}
|
||||
</span>
|
||||
<div class="{% if orientation == "horizontal" %}mt-4{% else %}pt-1{% endif %}">
|
||||
{% if step.label %}<p class="editorial-caps text-xs text-muted-foreground">{{ step.label }}</p>{% endif %}
|
||||
{% if step.title %}<h3 class="editorial-caps mt-1 text-lg text-foreground" style="font-family:var(--font-heading, 'Playfair Display', Georgia, serif)">{{ step.title }}</h3>{% endif %}
|
||||
{% if step.text %}<p class="mt-1 text-muted-foreground" style="font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif)">{{ step.text }}</p>{% endif %}
|
||||
{% if step.media %}<div class="mt-3 overflow-hidden">{% img step.media alt=step.title class="w-full h-auto object-cover" %}</div>{% endif %}
|
||||
</div>
|
||||
</li>
|
||||
{% endfor %}
|
||||
</ol>
|
||||
</div>
|
||||
</section>
|
||||
10
templates/overrides/editorial/utility-bar.ninjatpl
Normal file
10
templates/overrides/editorial/utility-bar.ninjatpl
Normal file
@ -0,0 +1,10 @@
|
||||
{# utility-bar override — quiet contact strip on a hairline-bordered panel, small-caps labels. Fields mirror builtin: variant, showOnMobile, leftItems/rightItems {icon,label,link}. #}
|
||||
{% set ubcls = "editorial-panel" %}
|
||||
{% if variant == "primary" %}{% set ubcls = "editorial-panel" %}{% elif variant == "dark" %}{% set ubcls = "editorial-panel" %}{% endif %}
|
||||
{% macro ubitem(it) %}{% if it.link %}<a href="{{ it.link }}" class="editorial-caps inline-flex items-center gap-1.5 hover:text-[hsl(var(--accent))] transition-colors">{% if it.icon %}::{{ it.icon }}::{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</a>{% else %}<span class="editorial-caps inline-flex items-center gap-1.5">{% if it.icon %}::{{ it.icon }}::{% endif %}{% if it.label %}<span>{{ it.label }}</span>{% endif %}</span>{% endif %}{% endmacro %}
|
||||
<div data-block-override="editorial:utility-bar" class="w-full text-[0.7rem] {% if not showOnMobile %}hidden sm:block{% endif %} {{ ubcls }} {{ class }}">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 h-9 flex items-center justify-between gap-4">
|
||||
<div class="flex items-center gap-5 flex-wrap">{% for it in leftItems %}{{ ubitem(it) }}{% endfor %}</div>
|
||||
<div class="flex items-center gap-5 flex-wrap justify-end">{% for it in rightItems %}{{ ubitem(it) }}{% endfor %}</div>
|
||||
</div>
|
||||
</div>
|
||||
23
templates/overrides/editorial/video-embed.ninjatpl
Normal file
23
templates/overrides/editorial/video-embed.ninjatpl
Normal file
@ -0,0 +1,23 @@
|
||||
{# video-embed override — editorial hairline frame; oxblood play medallion. Fields + facade JS mirror builtin verbatim: aspect, url, media, poster, title, caption. #}
|
||||
{% with aspect=aspect|default:"16:9" %}<figure data-block-override="editorial:video-embed" class="bn-video my-8{% if class %} {{ class }}{% endif %}"><div class="bn-video-frame relative w-full overflow-hidden bg-muted {% 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 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-background/90 text-[hsl(var(--accent))] transition-transform group-hover:scale-105 motion-reduce:transform-none">::lucide:play:xl::</span></button></div>{% if caption %}<figcaption class="mt-3 text-center text-sm italic" style="color:hsl(var(--muted-foreground));font-family:var(--font-body, 'Source Serif 4', 'Source Serif Pro', Georgia, serif);">{{ 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