Compare commits
24 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4cc9f10bd1 | ||
|
|
ff71b93717 | ||
|
|
e3747c2cd8 | ||
|
|
73a139e032 | ||
|
|
7c7f249fa4 | ||
|
|
8419d717e3 | ||
|
|
4e54f71279 | ||
|
|
8c1377d91b | ||
|
|
f4cddccb3c | ||
|
|
d2d27b8dd9 | ||
|
|
59bb1f453a | ||
|
|
a5546afae5 | ||
|
|
ee48e06931 | ||
|
|
d919a967cf | ||
|
|
800c8904d6 | ||
|
|
ba23c3c453 | ||
|
|
40348809a8 | ||
|
|
b4891b95cf | ||
|
|
885dd372e7 | ||
|
|
6dbd76f2e8 | ||
|
|
9fa99a8d40 | ||
|
|
36a4b97bb7 | ||
|
|
abe0b25e9d | ||
|
|
25ac7dbde0 |
5
.gitignore
vendored
@ -1,2 +1,7 @@
|
||||
*.so
|
||||
.superpowers/
|
||||
|
||||
# local Playwright/MCP test + debug artifacts
|
||||
.playwright-mcp/
|
||||
login-page.png
|
||||
*.bnp
|
||||
|
||||
55
Makefile
Normal file
@ -0,0 +1,55 @@
|
||||
# lcars — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
|
||||
#
|
||||
# This theme is a CODELESS .bnp: pure declaration the host runs — blocks
|
||||
# (blocks/blocks.yaml + .ninjatpl), page/system templates, template overrides,
|
||||
# an email wrapper, master pages, presets, fonts and CSS in manifest.yaml. No
|
||||
# Go, no templ, no plugin.wasm, no block/core dependency. `ninja plugin build`
|
||||
# classifies the repo as codeless (no Go at the root) and packs the artifact
|
||||
# with manifest.pb (codeless: true) and NO plugin.wasm.
|
||||
#
|
||||
# Usage:
|
||||
# make build # pack lcars-<version>.bnp (codeless — no wasm)
|
||||
# make verify # validate the built .bnp (loader-identical checks)
|
||||
# make archive-check # prove a clean `git archive HEAD` still builds codeless
|
||||
# make publish # ninja plugin publish --bnp (to the active registry)
|
||||
# make clean # remove build artefacts
|
||||
|
||||
.PHONY: build verify archive-check publish clean help
|
||||
|
||||
PLUGIN_NAME := lcars
|
||||
NINJA := ninja
|
||||
BNP := $(PLUGIN_NAME)-$(shell grep '^version' plugin.mod | sed 's/.*"\(.*\)"/\1/').bnp
|
||||
|
||||
# Pack the codeless .bnp (manifest.pb synthesized from plugin.mod + manifest.yaml
|
||||
# + blocks/ + templates/; no wasm compile, no DESCRIBE probe).
|
||||
build:
|
||||
$(NINJA) plugin build --dir .
|
||||
|
||||
# Validate the built .bnp exactly as the registry/loader will.
|
||||
verify:
|
||||
$(NINJA) plugin verify $(BNP)
|
||||
|
||||
# Prove a clean `git archive HEAD` (what publish ships) still packs codeless.
|
||||
archive-check:
|
||||
@T=$$(mktemp -d /tmp/archive-check-$(PLUGIN_NAME).XXXXXX) && \
|
||||
trap 'rm -rf "$$T"' EXIT && \
|
||||
git archive HEAD | tar -x -C "$$T" && \
|
||||
cd "$$T" && $(NINJA) plugin build --dir . && \
|
||||
echo "archive-check OK"
|
||||
|
||||
# Publish the prebuilt .bnp to the active registry.
|
||||
# Pass --host https://my.blockninja.dev for the dev orchestrator.
|
||||
publish: build
|
||||
$(NINJA) plugin publish --bnp $(BNP)
|
||||
|
||||
# Remove build artefacts.
|
||||
clean:
|
||||
rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
|
||||
|
||||
help:
|
||||
@echo "Targets:"
|
||||
@echo " build Pack the codeless .bnp (no wasm)"
|
||||
@echo " verify Validate the built .bnp (loader-identical checks)"
|
||||
@echo " archive-check Prove a clean git archive HEAD packs codeless"
|
||||
@echo " publish Publish the .bnp to the active registry"
|
||||
@echo " clean Remove build artefacts"
|
||||
25
README.md
Normal file
@ -0,0 +1,25 @@
|
||||
# LCARS
|
||||
|
||||
A retro-futurist theme built as a full library-computer interface. Angular pill rails, numeric readouts, and stardate furniture frame every surface, so the chrome itself becomes the layout. It reads like the console of a 24th-century starship. Amber and lilac panels sit over true black in dark mode.
|
||||
|
||||
## Good for
|
||||
|
||||
We built LCARS for sci-fi media and fandom. Treat these as starting points, not limits. If the mood fits your brand, use it:
|
||||
|
||||
- Sci-fi podcasts and audio shows
|
||||
- Fan sites and episode archives
|
||||
- Tech and gaming communities
|
||||
- Convention and event pages
|
||||
|
||||
## What you get
|
||||
|
||||
- Five color presets, each with a tuned light mode and a tuned dark mode: Ops Amber, Red Alert, Blue Console, Nebula, and Medbay Green.
|
||||
- Two bundled fonts, ready to assign: Antonio for condensed panel headings and Share Tech Mono for readouts and labels. Fonts stay admin-controlled, so you can swap them from the typography settings.
|
||||
- A GO BIG hero with an animated canvas panel-sweep. It honors reduced-motion settings and draws a single static frame when scripts are off.
|
||||
- Four persona blocks: an episode console, a crew roster, a stardate log, and a transmission-status readout.
|
||||
- A demo site that installs when you activate the theme: a home page, an about page, an episode feed with three articles, and a contact page whose form emails your admins.
|
||||
- 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.
|
||||
39
RECOMMENDED_FONTS.md
Normal file
@ -0,0 +1,39 @@
|
||||
# LCARS — recommended fonts
|
||||
|
||||
This theme **bundles** two libre families (OFL-1.1) so it renders on-brand with
|
||||
no admin setup:
|
||||
|
||||
| Slot | Bundled family | Role |
|
||||
|------|----------------|------|
|
||||
| Heading | **Antonio** | Tall condensed grotesque for LCARS labels, headings, buttons, and nav pills. |
|
||||
| Body | **Antonio** | Same family for panel copy; condensed and legible at reading sizes. |
|
||||
| Mono | **Share Tech Mono** | Numeric readouts, stardate stamps, statistics, prices, metadata. Tabular by design. |
|
||||
|
||||
Bundled files live in `assets/fonts/` (latin subsets); licenses in
|
||||
`assets/fonts/LICENSES.md`. The theme CSS resolves every family through
|
||||
`var(--font-heading | --font-body | --font-mono, <fallback>)`, and the fallback
|
||||
stacks name the bundled families, so the identity holds even before an admin
|
||||
touches the typography panel.
|
||||
|
||||
## Optional upgrade — Roboto Condensed for body (Google Fonts tab)
|
||||
|
||||
The locked direction pairs Antonio labels with **Roboto Condensed** (Apache-2.0)
|
||||
for longer readout copy. Roboto Condensed is not bundled here (no libre woff2 was
|
||||
available in the workspace at build time), so the body fallback stack degrades to
|
||||
Antonio then a system condensed sans (`"Arial Narrow", system-ui`). To match the
|
||||
locked direction exactly, assign Roboto Condensed to the **Body** slot from the
|
||||
Google Fonts tab:
|
||||
|
||||
| Slot | Source | Family | Why |
|
||||
|------|--------|--------|-----|
|
||||
| Body | `google:Roboto Condensed` | Roboto Condensed | The locked body face for readout copy. Wider aperture than Antonio at small sizes. |
|
||||
| Mono | `google:Share Tech Mono` | Share Tech Mono | Already bundled; also available in the picker if you prefer the Google source. |
|
||||
|
||||
## How to apply an override
|
||||
|
||||
1. Open **Admin → Theme → Typography**.
|
||||
2. Switch to the **Google Fonts** tab.
|
||||
3. Search and pick the family.
|
||||
4. Assign it to the matching slot (Heading / Body / Mono).
|
||||
5. Save. The pick is stored as `google:<Family>` and binds the `--font-*`
|
||||
variable, overriding the bundled fallback.
|
||||
15
RECOMMENDED_ICONS.md
Normal file
@ -0,0 +1,15 @@
|
||||
# LCARS — required icon packs
|
||||
|
||||
The theme renders icons via the sprite system only
|
||||
(`<svg><use href="/icons/<pack>.svg#<name>"/></svg>` and the `::pack:name::`
|
||||
shorthand). It declares its packs in `manifest.yaml` `required_icon_packs`.
|
||||
Until the loader auto-installs declared packs, install them by hand before
|
||||
activation.
|
||||
|
||||
| Pack | Used for | Install |
|
||||
|------|----------|---------|
|
||||
| **lucide** | All UI accents: arrows, chevrons, checks, contact channels, nav toggle, play controls, mail/phone marks. Bundled with the CMS. | Ships with the CMS. Verify it is enabled at `/admin/icons`. |
|
||||
|
||||
lucide ships with BlockNinja, so no npm download is needed. If a block picks an
|
||||
icon from a different pack, install that pack at `/admin/icons` first (`lucide`
|
||||
is the safe default fallback for any icon-picker field).
|
||||
21
assets/fonts/LICENSES.md
Normal file
@ -0,0 +1,21 @@
|
||||
# Bundled font licenses
|
||||
|
||||
Both bundled families are libre and redistributable. Latin subsets only.
|
||||
|
||||
## Antonio
|
||||
|
||||
- Files: `antonio-latin.woff2`, `antonio-latin-ext.woff2` (variable weight axis; theme uses 300/400/600/700)
|
||||
- Family: Antonio (Vernon Adams / Marcelo Magalhaes)
|
||||
- License: SIL Open Font License 1.1 (OFL-1.1)
|
||||
- Source: https://fonts.google.com/specimen/Antonio — latin and latin-ext subsets as distributed by google-webfonts-helper.
|
||||
- Role: LCARS labels, headings, buttons, nav pills. A tall condensed grotesque that reads as library-computer type.
|
||||
|
||||
## Share Tech Mono
|
||||
|
||||
- File: `share-tech-mono-latin.woff2` (weight 400)
|
||||
- Family: Share Tech Mono (Carrois Apostrophe)
|
||||
- License: SIL Open Font License 1.1 (OFL-1.1)
|
||||
- Source: https://fonts.google.com/specimen/Share+Tech+Mono — latin subset as distributed by google-webfonts-helper.
|
||||
- Role: numeric readouts, stardate stamps, metadata, statistics, prices. Tabular by design.
|
||||
|
||||
The full OFL-1.1 text applies to both. See https://openfontlicense.org.
|
||||
BIN
assets/fonts/share-tech-mono-latin.woff2
Normal file
53
blocks.go
@ -1,53 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
)
|
||||
|
||||
// --- LCARS Header Block ---
|
||||
|
||||
var LCARSHeaderMeta = blocks.BlockMeta{
|
||||
Key: "lcars_header",
|
||||
Title: "LCARS Header",
|
||||
Description: "Top bar with elbow bracket, status indicators, and stardate",
|
||||
Category: blocks.CategoryNavigation,
|
||||
Source: "lcars",
|
||||
}
|
||||
|
||||
var headerDefaults = map[string]any{
|
||||
"title": "LCARS",
|
||||
"subtitle": "Library Computer Access/Retrieval System",
|
||||
"stardate": "",
|
||||
"status": "online",
|
||||
"menu_name": "main",
|
||||
}
|
||||
|
||||
// --- LCARS Sidebar Block ---
|
||||
|
||||
var LCARSSidebarMeta = blocks.BlockMeta{
|
||||
Key: "lcars_sidebar",
|
||||
Title: "LCARS Sidebar",
|
||||
Description: "Left panel with rounded-rectangle navigation buttons",
|
||||
Category: blocks.CategoryNavigation,
|
||||
Source: "lcars",
|
||||
}
|
||||
|
||||
var sidebarDefaults = map[string]any{
|
||||
"items": []any{
|
||||
map[string]any{"label": "Personnel", "url": "#", "color": "primary"},
|
||||
map[string]any{"label": "Operations", "url": "#", "color": "secondary"},
|
||||
map[string]any{"label": "Sciences", "url": "#", "color": "accent"},
|
||||
map[string]any{"label": "Engineering", "url": "#", "color": "primary"},
|
||||
map[string]any{"label": "Medical", "url": "#", "color": "secondary"},
|
||||
},
|
||||
}
|
||||
|
||||
// --- LCARS Panel Block ---
|
||||
|
||||
var LCARSPanelMeta = blocks.BlockMeta{
|
||||
Key: "lcars_panel",
|
||||
Title: "LCARS Panel",
|
||||
Description: "Framed content area with LCARS border treatment",
|
||||
Category: blocks.CategoryLayout,
|
||||
Source: "lcars",
|
||||
}
|
||||
36
blocks/blocks.yaml
Normal file
@ -0,0 +1,36 @@
|
||||
# LCARS theme block definitions (codeless .bnp, WO-TF-020).
|
||||
# Keys are fully qualified (lcars:<key>) because the codeless loader registers
|
||||
# definitions verbatim — there is no PluginBlockRegistry prefixing in the
|
||||
# codeless path. The qualified keys match the data-block attributes and the
|
||||
# master_pages.json / seed references.
|
||||
#
|
||||
# Only true persona furniture stays a theme block. Everything that duplicated a
|
||||
# builtin (the old lcars_header navbar, lcars_panel card, lcars_sidebar rail) is
|
||||
# now a template override of the builtin key or page-template chrome instead.
|
||||
# All four blocks below are content-map-only (no data providers), so they render
|
||||
# fully under the frozen codeless loader.
|
||||
blocks:
|
||||
- key: lcars:episode_console
|
||||
title: Episode Console
|
||||
description: Featured podcast episode as an LCARS playback console with duration readout, stardate, and platform channels.
|
||||
category: content
|
||||
schema: episode_console.schema.json
|
||||
template: episode_console.ninjatpl
|
||||
- key: lcars:crew_roster
|
||||
title: Crew Roster
|
||||
description: Roster of hosts and crew as LCARS ID panels with role and callsign.
|
||||
category: content
|
||||
schema: crew_roster.schema.json
|
||||
template: crew_roster.ninjatpl
|
||||
- key: lcars:stardate_log
|
||||
title: Stardate Log
|
||||
description: Dated log entries on a numeric stardate rail — a captain's-log timeline.
|
||||
category: content
|
||||
schema: stardate_log.schema.json
|
||||
template: stardate_log.ninjatpl
|
||||
- key: lcars:transmission_status
|
||||
title: Transmission Status
|
||||
description: Status board of channels and systems as LCARS readouts with online, standby, or alert states.
|
||||
category: content
|
||||
schema: transmission_status.schema.json
|
||||
template: transmission_status.ninjatpl
|
||||
11
blocks/crew_roster.ninjatpl
Normal file
@ -0,0 +1,11 @@
|
||||
<section class="lcars-section {{ class }}" data-block="lcars:crew_roster"><div class="lcars-well">
|
||||
{% if eyebrow %}<p class="lcars-eyebrow">{{ eyebrow }}</p>{% endif %}
|
||||
{% if heading %}<h2 class="lcars-heading lcars-heading-2" style="margin-bottom:1.75rem;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
|
||||
{% if members %}<div class="lcars-leadership-grid">{% for m in members %}
|
||||
<article class="lcars-card" style="padding:1.25rem;">
|
||||
<div style="display:flex;align-items:center;gap:0.875rem;">{% if m.avatar %}<span class="lcars-avatar" style="width:3.25rem;height:3.25rem;flex:none;overflow:hidden;display:inline-block;">{% img m.avatar alt=m.name|default:"Crew" %}</span>{% else %}<span class="lcars-pill" style="width:3.25rem;height:3.25rem;border-radius:0 1.625rem 1.625rem 0;flex:none;"></span>{% endif %}<div style="min-width:0;">{% if m.name %}<div class="lcars-panel-title" style="font-size:1.0625rem;letter-spacing:0.08em;">{{ m.name }}</div>{% endif %}{% if m.role %}<div class="lcars-stat-label" style="margin-top:0.125rem;">{{ m.role }}</div>{% endif %}</div></div>
|
||||
{% if m.callsign %}<div class="lcars-readout" style="margin-top:0.75rem;font-size:0.8125rem;">Callsign {{ m.callsign }}</div>{% endif %}
|
||||
{% if m.bio %}<p class="lcars-text" style="font-size:0.9375rem;color:hsl(var(--muted-foreground));margin-top:0.5rem;">{{ m.bio }}</p>{% endif %}
|
||||
</article>
|
||||
{% endfor %}</div>{% endif %}
|
||||
</div></section>
|
||||
25
blocks/crew_roster.schema.json
Normal file
@ -0,0 +1,25 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Crew Roster",
|
||||
"description": "Hosts and crew as LCARS ID panels.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eyebrow": { "type": "string", "title": "Eyebrow", "x-editor": "text" },
|
||||
"heading": { "type": "string", "title": "Heading", "x-editor": "text" },
|
||||
"members": {
|
||||
"type": "array",
|
||||
"title": "Crew",
|
||||
"x-editor": "collection",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": { "type": "string", "title": "Name", "x-editor": "text" },
|
||||
"role": { "type": "string", "title": "Role", "x-editor": "text" },
|
||||
"callsign": { "type": "string", "title": "Callsign", "x-editor": "text" },
|
||||
"bio": { "type": "string", "title": "Bio", "x-editor": "textarea" },
|
||||
"avatar": { "type": "string", "title": "Portrait", "x-editor": "media" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
14
blocks/episode_console.ninjatpl
Normal file
@ -0,0 +1,14 @@
|
||||
<section class="lcars-section {{ class }}" data-block="lcars:episode_console"><div class="lcars-well">
|
||||
<div class="lcars-panel lcars-panel-accent" style="border-radius:0 0.75rem 0.75rem 0;padding:1.5rem;">
|
||||
<div class="lcars-hero-split" style="gap:1.75rem;align-items:start;">
|
||||
{% if cover %}<div class="lcars-media-frame" style="aspect-ratio:1 / 1;max-height:22rem;align-self:start;">{% img cover alt=title|default:"Episode cover" %}</div>{% else %}<div class="lcars-console-art"><span class="lcars-console-art-label">No cover art</span><span class="lcars-console-art-scope" aria-hidden="true"><i></i><i></i><i></i><i></i><i></i></span></div>{% endif %}
|
||||
<div class="lcars-stack-sm" style="min-width:0;">
|
||||
<div class="lcars-panel-header"><span class="lcars-pill lcars-pill-accent" aria-hidden="true"></span>{% if eyebrow %}<span class="lcars-panel-title">{{ eyebrow }}</span>{% else %}<span class="lcars-panel-title">Now Broadcasting</span>{% endif %}<span class="lcars-panel-header-bar"></span></div>
|
||||
<div style="display:flex;flex-wrap:wrap;gap:0.75rem;align-items:center;">{% if episodeNumber %}<span class="lcars-badge">EP {{ episodeNumber }}</span>{% endif %}{% if stardate %}<span class="lcars-stardate">Stardate {{ stardate }}</span>{% endif %}{% if duration %}<span class="lcars-readout">{{ duration }}</span>{% endif %}</div>
|
||||
{% if title %}<h2 class="lcars-heading lcars-heading-2">{{ title }}</h2>{% endif %}
|
||||
{% if description %}<p class="lcars-text" style="color:hsl(var(--muted-foreground));">{{ description }}</p>{% endif %}
|
||||
<div class="lcars-btn-row" style="margin-top:0.25rem;">{% if listenUrl %}<a class="lcars-btn-primary" href="{{ listenUrl }}"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#play"/></svg>{{ listenLabel|default:"Listen now" }}</a>{% endif %}{% for p in platforms %}<a class="lcars-btn-outline lcars-btn-sm" href="{{ p.url|default:'#' }}">{{ p.label }}</a>{% endfor %}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div></section>
|
||||
29
blocks/episode_console.schema.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Episode Console",
|
||||
"description": "Featured episode as an LCARS playback console.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eyebrow": { "type": "string", "title": "Eyebrow", "x-editor": "text" },
|
||||
"title": { "type": "string", "title": "Episode title", "x-editor": "text" },
|
||||
"episodeNumber": { "type": "string", "title": "Episode number", "description": "e.g. \"047\"", "x-editor": "text" },
|
||||
"duration": { "type": "string", "title": "Duration readout", "description": "e.g. \"58:12\"", "x-editor": "text" },
|
||||
"stardate": { "type": "string", "title": "Stardate", "description": "e.g. \"78412.3\"", "x-editor": "text" },
|
||||
"description": { "type": "string", "title": "Description", "x-editor": "textarea" },
|
||||
"cover": { "type": "string", "title": "Cover art", "x-editor": "media" },
|
||||
"listenUrl": { "type": "string", "title": "Listen link", "x-editor": "text" },
|
||||
"listenLabel": { "type": "string", "title": "Listen label", "x-editor": "text" },
|
||||
"platforms": {
|
||||
"type": "array",
|
||||
"title": "Platform channels",
|
||||
"x-editor": "collection",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": { "type": "string", "title": "Label", "x-editor": "text" },
|
||||
"url": { "type": "string", "title": "URL", "x-editor": "text" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
blocks/stardate_log.ninjatpl
Normal file
@ -0,0 +1,10 @@
|
||||
<section class="lcars-section {{ class }}" data-block="lcars:stardate_log"><div class="lcars-well">
|
||||
{% if eyebrow %}<p class="lcars-eyebrow">{{ eyebrow }}</p>{% endif %}
|
||||
{% if heading %}<h2 class="lcars-heading lcars-heading-2" style="margin-bottom:1.75rem;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
|
||||
{% if entries %}<div class="lcars-stack">{% for e in entries %}
|
||||
<div class="lcars-timeline-step" style="padding-bottom:1.25rem;">
|
||||
<div class="lcars-timeline-rail"><span class="lcars-timeline-node" style="width:auto;min-width:4.5rem;padding:0 0.75rem;font-size:0.8125rem;">{{ e.stardate|default:"—" }}</span>{% if not forloop.Last %}<span class="lcars-timeline-line"></span>{% endif %}</div>
|
||||
<div style="min-width:0;">{% if e.title %}<div class="lcars-panel-title" style="font-size:1.0625rem;">{{ e.title }}</div>{% endif %}{% if e.text %}<p class="lcars-text" style="font-size:0.9375rem;color:hsl(var(--muted-foreground));margin-top:0.375rem;">{{ e.text }}</p>{% endif %}</div>
|
||||
</div>
|
||||
{% endfor %}</div>{% endif %}
|
||||
</div></section>
|
||||
23
blocks/stardate_log.schema.json
Normal file
@ -0,0 +1,23 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Stardate Log",
|
||||
"description": "Dated log entries on a numeric stardate rail.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eyebrow": { "type": "string", "title": "Eyebrow", "x-editor": "text" },
|
||||
"heading": { "type": "string", "title": "Heading", "x-editor": "text" },
|
||||
"entries": {
|
||||
"type": "array",
|
||||
"title": "Log entries",
|
||||
"x-editor": "collection",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"stardate": { "type": "string", "title": "Stardate", "x-editor": "text" },
|
||||
"title": { "type": "string", "title": "Entry title", "x-editor": "text" },
|
||||
"text": { "type": "string", "title": "Entry", "x-editor": "textarea" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
10
blocks/transmission_status.ninjatpl
Normal file
@ -0,0 +1,10 @@
|
||||
<section class="lcars-section-tight {{ class }}" data-block="lcars:transmission_status"><div class="lcars-well">
|
||||
{% if eyebrow %}<p class="lcars-eyebrow">{{ eyebrow }}</p>{% endif %}
|
||||
{% if heading %}<h2 class="lcars-heading lcars-heading-3" style="margin-bottom:1.25rem;">{{ heading }}</h2>{% endif %}
|
||||
{% if systems %}<div class="lcars-grid lcars-grid-2">{% for s in systems %}
|
||||
<div class="lcars-panel" style="padding:0.875rem 1.25rem;display:flex;align-items:center;justify-content:space-between;gap:1rem;">
|
||||
<div style="min-width:0;">{% if s.label %}<div class="lcars-label">{{ s.label }}</div>{% endif %}{% if s.detail %}<div class="lcars-readout" style="font-size:0.75rem;margin-top:0.25rem;">{{ s.detail }}</div>{% endif %}</div>
|
||||
{% if s.status == "alert" %}<span class="lcars-status lcars-status-alert">Alert</span>{% elif s.status == "standby" %}<span class="lcars-status lcars-status-standby">Standby</span>{% elif s.status == "offline" %}<span class="lcars-status lcars-status-offline">Offline</span>{% else %}<span class="lcars-status lcars-status-online">Online</span>{% endif %}
|
||||
</div>
|
||||
{% endfor %}</div>{% endif %}
|
||||
</div></section>
|
||||
29
blocks/transmission_status.schema.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "Transmission Status",
|
||||
"description": "Status board of channels and systems as LCARS readouts.",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"eyebrow": { "type": "string", "title": "Eyebrow", "x-editor": "text" },
|
||||
"heading": { "type": "string", "title": "Heading", "x-editor": "text" },
|
||||
"systems": {
|
||||
"type": "array",
|
||||
"title": "Systems",
|
||||
"x-editor": "collection",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": { "type": "string", "title": "Label", "x-editor": "text" },
|
||||
"status": {
|
||||
"type": "string",
|
||||
"title": "Status",
|
||||
"x-editor": "select",
|
||||
"enum": ["online", "standby", "alert", "offline"],
|
||||
"default": "online"
|
||||
},
|
||||
"detail": { "type": "string", "title": "Detail readout", "x-editor": "text" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
embed.go
@ -1,60 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/plugin"
|
||||
)
|
||||
|
||||
//go:embed assets/*
|
||||
var assetsFS embed.FS
|
||||
|
||||
//go:embed schemas/*
|
||||
var schemasFS embed.FS
|
||||
|
||||
//go:embed templates/*
|
||||
var templateFS embed.FS
|
||||
|
||||
//go:embed presets.json
|
||||
var presetsData []byte
|
||||
|
||||
//go:embed fonts.json
|
||||
var fontsData []byte
|
||||
|
||||
//go:embed plugin.mod
|
||||
var pluginModBytes []byte
|
||||
|
||||
func Assets() fs.FS {
|
||||
sub, _ := fs.Sub(assetsFS, "assets")
|
||||
return sub
|
||||
}
|
||||
|
||||
func SchemasFS() fs.FS {
|
||||
sub, _ := fs.Sub(schemasFS, "schemas")
|
||||
return sub
|
||||
}
|
||||
|
||||
func TemplatesFS() fs.FS {
|
||||
sub, _ := fs.Sub(templateFS, "templates")
|
||||
return sub
|
||||
}
|
||||
|
||||
func AssetsHandler() http.Handler {
|
||||
return http.FileServer(http.FS(Assets()))
|
||||
}
|
||||
|
||||
func ThemePresets() []byte { return presetsData }
|
||||
|
||||
func BundledFonts() []byte { return fontsData }
|
||||
|
||||
func ThemeCSSManifest() *plugin.CSSManifest {
|
||||
css, err := assetsFS.ReadFile("assets/style.css")
|
||||
if err != nil {
|
||||
return &plugin.CSSManifest{}
|
||||
}
|
||||
return &plugin.CSSManifest{
|
||||
InputCSSAppend: string(css),
|
||||
}
|
||||
}
|
||||
31
fonts.json
@ -3,26 +3,17 @@
|
||||
"name": "Antonio",
|
||||
"family": "Antonio",
|
||||
"variants": [
|
||||
{
|
||||
"weight": "300",
|
||||
"style": "normal",
|
||||
"file": "fonts/antonio-latin.woff2"
|
||||
},
|
||||
{
|
||||
"weight": "400",
|
||||
"style": "normal",
|
||||
"file": "fonts/antonio-latin.woff2"
|
||||
},
|
||||
{
|
||||
"weight": "600",
|
||||
"style": "normal",
|
||||
"file": "fonts/antonio-latin.woff2"
|
||||
},
|
||||
{
|
||||
"weight": "700",
|
||||
"style": "normal",
|
||||
"file": "fonts/antonio-latin.woff2"
|
||||
}
|
||||
{ "weight": "300", "style": "normal", "file": "fonts/antonio-latin.woff2" },
|
||||
{ "weight": "400", "style": "normal", "file": "fonts/antonio-latin.woff2" },
|
||||
{ "weight": "600", "style": "normal", "file": "fonts/antonio-latin.woff2" },
|
||||
{ "weight": "700", "style": "normal", "file": "fonts/antonio-latin.woff2" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Share Tech Mono",
|
||||
"family": "Share Tech Mono",
|
||||
"variants": [
|
||||
{ "weight": "400", "style": "normal", "file": "fonts/share-tech-mono-latin.woff2" }
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
19
go.mod
@ -1,19 +0,0 @@
|
||||
module git.dev.alexdunmow.com/block/lcars
|
||||
|
||||
go 1.26.4
|
||||
|
||||
require git.dev.alexdunmow.com/block/core v0.14.1
|
||||
|
||||
require (
|
||||
connectrpc.com/connect v1.20.0 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/a-h/templ v0.3.1020 // indirect
|
||||
github.com/flosch/pongo2/v6 v6.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/pgx/v5 v5.9.2 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
)
|
||||
54
go.sum
@ -1,54 +0,0 @@
|
||||
connectrpc.com/connect v1.20.0 h1:6TNDAB+WeNd2uolWNlYczB5E0KNNaVMNUEx8JEUsPmQ=
|
||||
connectrpc.com/connect v1.20.0/go.mod h1:A2ygJrukXwWy32vkCAAHNVguZrqZ+jeZ9rGRnGR4dN4=
|
||||
git.dev.alexdunmow.com/block/core v0.14.0 h1:cO6QQQPndhxwh1zbDvDOzebwrFS8Ka3WsxD8JBJLw/Y=
|
||||
git.dev.alexdunmow.com/block/core v0.14.0/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
|
||||
git.dev.alexdunmow.com/block/core v0.14.1 h1:63b25yzWoqrhBd3wdDFhsJzyGNM99wYmdh4WkN35hDk=
|
||||
git.dev.alexdunmow.com/block/core v0.14.1/go.mod h1:S0ZfpGZ9BQhhmuTUd78ailH7m2vo7QRCTIionvCVm9s=
|
||||
github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk=
|
||||
github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/a-h/templ v0.3.1020 h1:ypAT/L5ySWEnZ6Zft/5yfoWXYYkhFNvEFOeeqecg4tw=
|
||||
github.com/a-h/templ v0.3.1020/go.mod h1:A2DlK61v+K+NRoGnhmYbNYVmtYHcFO5/AisMvBdDxTM=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/flosch/pongo2/v6 v6.1.0 h1:A/NJbrQJJD2B2mbpw3DRFwBYG0xpCr3vwFlEr46y1HQ=
|
||||
github.com/flosch/pongo2/v6 v6.1.0/go.mod h1:CuDpFm47R0uGGE7z13/tTlt1Y6zdxvr2RLT5LJhsHEU=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||
github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw=
|
||||
github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
|
||||
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
||||
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.15.0 h1:D0RCU5rMAp+SpgkiNdrjfJ+LX4J1M32V2NeCY7EJ6hc=
|
||||
github.com/rogpeppe/go-internal v1.15.0/go.mod h1:DrUVZyrJU+txYW5/1kwtXQSMFio52ZOxX7yM1VHvnxs=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
484
manifest.yaml
Normal file
@ -0,0 +1,484 @@
|
||||
# LCARS theme — codeless .bnp manifest (WO-TF-020, Wave A).
|
||||
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
|
||||
#
|
||||
# Identity: the full library-computer interface of a sci-fi podcast / media
|
||||
# outfit. Pill rails, elbow brackets, live numeric readouts, stardate stamps
|
||||
# and episode-console furniture frame every surface — the chrome IS the
|
||||
# interface. Dark = canonical LCARS black with orange / amber / lilac panels;
|
||||
# light = daylight-ops cream with the colour bars kept vivid. Motion tier
|
||||
# GO BIG: one dependency-free canvas sweep on the hero / landing only.
|
||||
#
|
||||
# This theme was previously a pongo2 wasm guest; the codeless path renders the
|
||||
# SAME .ninjatpl sources host-side via cms/backend/plugin/pongoengine, so no
|
||||
# engine ships here. Every override maps its classes 1:1 onto the lcars-*
|
||||
# vocabulary defined below; semantic colour tokens only (hsl(var(--token))).
|
||||
theme_presets: presets.json
|
||||
bundled_fonts: fonts.json
|
||||
master_pages: master_pages.json
|
||||
|
||||
required_icon_packs:
|
||||
- lucide
|
||||
|
||||
system_templates:
|
||||
- key: lcars
|
||||
title: LCARS
|
||||
description: "Library-computer interface theme for sci-fi podcast and media sites: elbow-rail chrome, numeric readouts, stardate stamps, dual daylight-ops cream and canonical-black modes."
|
||||
|
||||
page_templates:
|
||||
- system: lcars
|
||||
key: default
|
||||
title: Standard Display
|
||||
description: "Elbow header, left command rail, main panel, and footer bar."
|
||||
slots: [header, sidebar, main, footer]
|
||||
- system: lcars
|
||||
key: full-width
|
||||
title: Full Display
|
||||
description: "Edge-to-edge LCARS frame with no command rail."
|
||||
slots: [header, main, footer]
|
||||
- system: lcars
|
||||
key: full-display
|
||||
title: Full Display (legacy)
|
||||
description: "Pre-existing full-width key, retained for sites already using it."
|
||||
slots: [header, main, footer]
|
||||
- system: lcars
|
||||
key: landing
|
||||
title: Landing
|
||||
description: "Hero readout band, main panel stack, closing CTA, and footer."
|
||||
slots: [hero, main, cta, footer]
|
||||
- system: lcars
|
||||
key: article
|
||||
title: Article
|
||||
description: "Long-form transcript column with a data aside rail."
|
||||
slots: [header, main, aside, footer]
|
||||
- system: lcars
|
||||
key: blog-index
|
||||
title: Episode Index
|
||||
description: "Title readout, featured lead, and an episode-panel grid."
|
||||
slots: [header, featured, main, footer]
|
||||
- system: lcars
|
||||
key: contact
|
||||
title: Hail Channel
|
||||
description: "Contact console: form on the main panel, channel details in the aside rail."
|
||||
slots: [header, main, aside, footer]
|
||||
- system: lcars
|
||||
key: auth
|
||||
title: Access Terminal
|
||||
description: "Centered access panel on a canonical-black terminal ground."
|
||||
slots: [main, footer]
|
||||
|
||||
template_overrides:
|
||||
# Chrome (4)
|
||||
- { template: lcars, block: navbar }
|
||||
- { template: lcars, block: footer }
|
||||
- { template: lcars, block: utility-bar }
|
||||
- { template: lcars, block: announcement-bar }
|
||||
# Marketing (12)
|
||||
- { template: lcars, block: hero }
|
||||
- { template: lcars, block: feature-grid }
|
||||
- { template: lcars, block: rich-section }
|
||||
- { template: lcars, block: stats }
|
||||
- { template: lcars, block: testimonial }
|
||||
- { template: lcars, block: logo-strip }
|
||||
- { template: lcars, block: pricing }
|
||||
- { template: lcars, block: team }
|
||||
- { template: lcars, block: faq }
|
||||
- { template: lcars, block: timeline }
|
||||
- { template: lcars, block: cta }
|
||||
- { template: lcars, block: countdown }
|
||||
# Primitives (11)
|
||||
- { template: lcars, block: heading }
|
||||
- { template: lcars, block: text }
|
||||
- { template: lcars, block: rich-text }
|
||||
- { template: lcars, block: image }
|
||||
- { template: lcars, block: quote }
|
||||
- { template: lcars, block: divider }
|
||||
- { template: lcars, block: button }
|
||||
- { template: lcars, block: card }
|
||||
- { template: lcars, block: gallery }
|
||||
- { template: lcars, block: video-embed }
|
||||
- { template: lcars, block: audio-embed }
|
||||
# Commerce / local (6)
|
||||
- { template: lcars, block: menu-list }
|
||||
- { template: lcars, block: event-list }
|
||||
- { template: lcars, block: contact-form }
|
||||
- { template: lcars, block: contact-card }
|
||||
- { template: lcars, block: hours-location }
|
||||
- { template: lcars, block: social-links }
|
||||
# Blog family (12)
|
||||
- { template: lcars, block: blog-hero }
|
||||
- { template: lcars, block: featured-posts }
|
||||
- { template: lcars, block: related-posts }
|
||||
- { template: lcars, block: popular-posts }
|
||||
- { template: lcars, block: category-list }
|
||||
- { template: lcars, block: post-hero }
|
||||
- { template: lcars, block: post-metadata }
|
||||
- { template: lcars, block: author-bio }
|
||||
- { template: lcars, block: author-bio-hero }
|
||||
- { template: lcars, block: newsletter-signup }
|
||||
- { template: lcars, block: breadcrumbs }
|
||||
- { template: lcars, block: sidebar-nav }
|
||||
# Auth surface (3)
|
||||
- { template: lcars, block: auth-form }
|
||||
- { template: lcars, block: password-reset-form }
|
||||
- { template: lcars, block: auth-status }
|
||||
# System (1)
|
||||
- { template: lcars, block: page-suggestions }
|
||||
|
||||
email_wrappers:
|
||||
- lcars
|
||||
|
||||
css:
|
||||
input_css_append: |
|
||||
/* ============================================================
|
||||
LCARS — appended component CSS (WO-TF-020).
|
||||
Library-computer interface: pill rails, elbow brackets,
|
||||
numeric readouts, stardate stamps. Semantic tokens ONLY —
|
||||
hsl(var(--token)); never literal colours. Class names map
|
||||
1:1 onto the override vocabulary.
|
||||
============================================================ */
|
||||
|
||||
/* Bundled display faces. The host does not consume bundled_fonts
|
||||
(LoadedPlugin.BundledFonts is unconsumed), so the theme declares its own
|
||||
@font-face against the served artifact paths (/templates/lcars/fonts/...).
|
||||
Presets set typography.fontHeading/body/mono to template:lcars:<Family> so
|
||||
--font-heading/--font-body/--font-mono resolve to these families. */
|
||||
@font-face { font-family: 'Antonio'; src: url('/templates/lcars/fonts/antonio-latin.woff2') format('woff2'); font-weight: 100 700; font-style: normal; font-display: swap; }
|
||||
@font-face { font-family: 'Antonio'; src: url('/templates/lcars/fonts/antonio-latin-ext.woff2') format('woff2'); font-weight: 100 700; font-style: normal; font-display: swap; unicode-range: U+0100-02AF, U+0304, U+0308, U+0329, U+1E00-1E9F, U+1EF2-1EFF, U+2020, U+20A0-20AB, U+20AD-20C0, U+2113, U+2C60-2C7F, U+A720-A7FF; }
|
||||
@font-face { font-family: 'Share Tech Mono'; src: url('/templates/lcars/fonts/share-tech-mono-latin.woff2') format('woff2'); font-weight: 400; font-style: normal; font-display: swap; }
|
||||
|
||||
/* --- Base / layout primitives --- */
|
||||
.lcars-page { font-family: var(--font-body, "Antonio", "Roboto Condensed", "Arial Narrow", system-ui, sans-serif); letter-spacing: 0.02em; }
|
||||
.lcars-body { font-family: var(--font-body, "Antonio", "Roboto Condensed", "Arial Narrow", system-ui, sans-serif); }
|
||||
.lcars-mono { font-family: var(--font-mono, "Share Tech Mono", ui-monospace, "SFMono-Regular", monospace); font-variant-numeric: tabular-nums; letter-spacing: 0.05em; }
|
||||
.numeric-tabular { font-variant-numeric: tabular-nums; }
|
||||
.lcars-well { width: 100%; max-width: 1280px; margin-left: auto; margin-right: auto; padding-left: 1.5rem; padding-right: 1.5rem; box-sizing: border-box; }
|
||||
.lcars-measure { max-width: 72ch; }
|
||||
.lcars-section { padding-top: 3rem; padding-bottom: 3rem; }
|
||||
.lcars-section-tight { padding-top: 1.75rem; padding-bottom: 1.75rem; }
|
||||
.lcars-stack > * + * { margin-top: 1.5rem; }
|
||||
.lcars-stack-sm > * + * { margin-top: 0.625rem; }
|
||||
|
||||
/* --- Typography --- */
|
||||
.lcars-display, .lcars-heading { font-family: var(--font-heading, "Antonio", "Roboto Condensed", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: hsl(var(--primary)); line-height: 1.05; margin: 0; }
|
||||
.lcars-heading-h2-accent { border-bottom: 0.1875rem solid hsl(var(--primary)); padding-bottom: 0.25rem; }
|
||||
.lcars-heading-1 { font-size: clamp(2rem, 5vw, 2.75rem); }
|
||||
.lcars-heading-2 { font-size: clamp(1.625rem, 3.5vw, 2rem); }
|
||||
.lcars-heading-3 { font-size: 1.375rem; }
|
||||
.lcars-heading-4 { font-size: 1.125rem; }
|
||||
.lcars-eyebrow { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.8125rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.22em; color: hsl(var(--secondary)); margin: 0 0 0.75rem 0; }
|
||||
.lcars-kicker { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 0.75rem; letter-spacing: 0.18em; text-transform: uppercase; color: hsl(var(--accent)); }
|
||||
.lcars-text { font-family: var(--font-body, "Antonio", "Roboto Condensed", system-ui, sans-serif); font-size: 1.0625rem; line-height: 1.7; color: hsl(var(--foreground)); }
|
||||
.lcars-text a { color: hsl(var(--secondary)); text-decoration: underline; text-underline-offset: 0.2em; }
|
||||
.lcars-text a:hover { color: hsl(var(--primary)); }
|
||||
.lcars-text strong { color: hsl(var(--primary)); font-weight: 700; }
|
||||
.lcars-text code, .lcars-prose code { font-family: var(--font-mono, "Share Tech Mono", monospace); background-color: hsl(var(--muted)); padding: 0.125rem 0.375rem; border-radius: 0.25rem; font-size: 0.875em; color: hsl(var(--accent)); }
|
||||
.lcars-prose { font-family: var(--font-body, "Antonio", "Roboto Condensed", system-ui, sans-serif); color: hsl(var(--foreground)); line-height: 1.7; font-size: 1.0625rem; }
|
||||
.lcars-prose a { color: hsl(var(--secondary)); text-decoration: underline; text-underline-offset: 0.2em; }
|
||||
.lcars-prose a:hover { color: hsl(var(--primary)); }
|
||||
.lcars-prose strong { color: hsl(var(--primary)); }
|
||||
/* Paragraph rhythm inside long-form text/prose (episode transcripts) so
|
||||
multi-paragraph bodies separate cleanly instead of running together. */
|
||||
.lcars-text p, .lcars-prose p { margin: 0 0 1.1rem 0; }
|
||||
.lcars-text p:last-child, .lcars-prose p:last-child { margin-bottom: 0; }
|
||||
.lcars-lede { font-family: var(--font-body, "Antonio", system-ui, sans-serif); font-size: 1.25rem; line-height: 1.6; color: hsl(var(--muted-foreground)); }
|
||||
.lcars-label { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.8125rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.12em; color: hsl(var(--foreground)); }
|
||||
.lcars-readout { font-family: var(--font-mono, "Share Tech Mono", monospace); font-variant-numeric: tabular-nums; letter-spacing: 0.1em; color: hsl(var(--accent)); text-transform: uppercase; }
|
||||
.lcars-stardate { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 0.8125rem; font-weight: 400; letter-spacing: 0.12em; text-transform: uppercase; color: hsl(var(--accent)); }
|
||||
|
||||
/* --- Pills / bars / marks (LCARS signature) --- */
|
||||
.lcars-pill { display: inline-block; height: 1.25rem; min-width: 3rem; border-radius: 0.625rem; background-color: hsl(var(--primary)); flex: none; }
|
||||
.lcars-pill-secondary { background-color: hsl(var(--secondary)); }
|
||||
.lcars-pill-accent { background-color: hsl(var(--accent)); }
|
||||
.lcars-cap { display: inline-block; height: 1.5rem; width: 1.5rem; background-color: hsl(var(--primary)); flex: none; }
|
||||
.lcars-cap-left { border-radius: 0.75rem 0 0 0.75rem; }
|
||||
.lcars-cap-right { border-radius: 0 0.75rem 0.75rem 0; }
|
||||
.lcars-bar-accent { height: 0.5rem; border-radius: 0.25rem; background: linear-gradient(to right, hsl(var(--primary)) 0%, hsl(var(--primary)) 34%, hsl(var(--secondary)) 34%, hsl(var(--secondary)) 67%, hsl(var(--accent)) 67%, hsl(var(--accent)) 100%); }
|
||||
.lcars-rule { border: 0; border-top: 0.1875rem solid hsl(var(--primary)); margin: 0; }
|
||||
|
||||
/* --- Buttons (pill, uppercase, right-rounded) --- */
|
||||
.lcars-btn-primary { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1.25rem; background-color: hsl(var(--primary)); color: hsl(var(--background)); border: 0; border-radius: 0 1.25rem 1.25rem 0; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; text-decoration: none; line-height: 1.3; box-shadow: none; background-image: none; transition: filter 150ms ease; cursor: pointer; }
|
||||
.lcars-btn-primary:hover { filter: brightness(1.15); }
|
||||
.lcars-btn-primary:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
|
||||
.lcars-btn-secondary { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1.25rem; background-color: hsl(var(--secondary)); color: hsl(var(--background)); border: 0; border-radius: 0 1.25rem 1.25rem 0; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; text-decoration: none; line-height: 1.3; transition: filter 150ms ease; cursor: pointer; }
|
||||
.lcars-btn-secondary:hover { filter: brightness(1.15); }
|
||||
.lcars-btn-outline { display: inline-flex; align-items: center; gap: 0.5rem; padding: 0.5rem 1.25rem; background-color: transparent; color: hsl(var(--secondary)); border: 0.125rem solid hsl(var(--secondary)); border-radius: 0 1.25rem 1.25rem 0; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; text-decoration: none; line-height: 1.3; transition: background-color 150ms ease, color 150ms ease; cursor: pointer; }
|
||||
.lcars-btn-outline:hover { background-color: hsl(var(--secondary)); color: hsl(var(--background)); }
|
||||
.lcars-btn-outline:focus-visible { outline: 2px solid hsl(var(--ring)); outline-offset: 2px; }
|
||||
.lcars-btn-ghost { display: inline-flex; align-items: center; gap: 0.375rem; color: hsl(var(--accent)); font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; text-decoration: none; line-height: 1.3; transition: gap 150ms ease; }
|
||||
.lcars-btn-ghost:hover { gap: 0.625rem; }
|
||||
.lcars-btn-sm { padding: 0.375rem 0.875rem; font-size: 0.8125rem; }
|
||||
.lcars-btn-lg { padding: 0.75rem 1.75rem; font-size: 1.0625rem; }
|
||||
.lcars-btn-row { display: flex; flex-wrap: wrap; gap: 0.75rem; }
|
||||
.lcars-icon { width: 1em; height: 1em; display: inline-block; vertical-align: -0.125em; }
|
||||
/* Sized <use> icons carry explicit width/height attributes (SVG geometry
|
||||
guard); this only handles inline flow/alignment, never sizing. */
|
||||
.lcars-inline-icon { display: inline-block; vertical-align: -0.125em; flex: none; }
|
||||
|
||||
/* --- Panels / cards / surfaces --- */
|
||||
.lcars-panel { background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border-left: 0.5rem solid hsl(var(--primary)); border-radius: 0 0.5rem 0.5rem 0; padding: 1.25rem 1.5rem; box-shadow: none; }
|
||||
.lcars-panel-secondary { border-left-color: hsl(var(--secondary)); }
|
||||
.lcars-panel-accent { border-left-color: hsl(var(--accent)); }
|
||||
.lcars-panel-header { display: flex; align-items: center; gap: 0.625rem; margin-bottom: 0.75rem; }
|
||||
.lcars-panel-title { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.9375rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.15em; color: hsl(var(--foreground)); white-space: nowrap; }
|
||||
.lcars-panel-header-bar { flex: 1; height: 0.125rem; background-color: hsl(var(--border)); }
|
||||
.lcars-panel-body { }
|
||||
.lcars-card { background-color: hsl(var(--card)); color: hsl(var(--card-foreground)); border-top: 0.375rem solid hsl(var(--primary)); border-radius: 0 0 0.75rem 0.75rem; padding: 1.5rem; box-shadow: none; overflow: hidden; }
|
||||
.lcars-card-accent { border-top-color: hsl(var(--accent)); }
|
||||
.lcars-surface-muted { background-color: hsl(var(--muted)); }
|
||||
.lcars-surface-card { background-color: hsl(var(--card)); }
|
||||
.lcars-media-frame { display: block; width: 100%; overflow: hidden; background-color: hsl(var(--muted)); border-radius: 0.5rem; }
|
||||
.lcars-media-frame img { display: block; width: 100%; height: 100%; object-fit: cover; }
|
||||
.lcars-img-empty { display: block; width: 100%; aspect-ratio: 16 / 9; max-height: 20rem; background-color: hsl(var(--muted)); border-radius: 0.5rem; }
|
||||
/* Graceful empty-media state: an on-brand LCARS readout panel with a status
|
||||
label, shown instead of a bare grey rectangle when no image ships. Stays
|
||||
compact instead of ballooning into a blank square. */
|
||||
.lcars-console-art { display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 0.9rem; align-self: start; width: 100%; aspect-ratio: 4 / 3; max-height: 15rem; padding: 1.5rem; background-color: hsl(var(--muted)); border-radius: 0.5rem; box-sizing: border-box; }
|
||||
.lcars-console-art-label { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 0.6875rem; font-weight: 400; letter-spacing: 0.24em; text-transform: uppercase; color: hsl(var(--muted-foreground)); }
|
||||
.lcars-console-art-scope { display: flex; flex-direction: column; gap: 0.5rem; width: 100%; max-width: 16rem; }
|
||||
.lcars-console-art-scope i { display: block; height: 0.625rem; border-radius: 0.3125rem; opacity: 0.55; }
|
||||
.lcars-console-art-scope i:nth-child(1) { width: 100%; background-color: hsl(var(--primary)); }
|
||||
.lcars-console-art-scope i:nth-child(2) { width: 72%; background-color: hsl(var(--secondary)); }
|
||||
.lcars-console-art-scope i:nth-child(3) { width: 88%; background-color: hsl(var(--accent)); }
|
||||
.lcars-console-art-scope i:nth-child(4) { width: 60%; background-color: hsl(var(--secondary)); }
|
||||
.lcars-console-art-scope i:nth-child(5) { width: 80%; background-color: hsl(var(--primary)); }
|
||||
|
||||
/* --- Responsive grids --- */
|
||||
.lcars-grid { display: grid; gap: 1.25rem; grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
@media (min-width: 640px) { .lcars-grid-2, .lcars-grid-3, .lcars-grid-4 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (min-width: 1024px) { .lcars-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .lcars-grid-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
|
||||
.lcars-leadership-grid { display: grid; gap: 1.25rem; grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
@media (min-width: 640px) { .lcars-leadership-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (min-width: 1024px) { .lcars-leadership-grid { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||||
|
||||
/* --- Page frame chrome (elbow rails, command sidebar) --- */
|
||||
.lcars-frame { display: flex; flex-direction: column; min-height: 100vh; padding: 0.5rem; gap: 0.375rem; background-color: hsl(var(--background)); }
|
||||
.lcars-elbow-top, .lcars-elbow-bottom { display: flex; align-items: stretch; height: 2.25rem; gap: 0.375rem; }
|
||||
.lcars-elbow-corner { width: 12rem; min-width: 12rem; background-color: hsl(var(--primary)); }
|
||||
.lcars-elbow-tl { border-radius: 0 0 2rem 0; }
|
||||
.lcars-elbow-bl { border-radius: 0 2rem 0 0; }
|
||||
.lcars-bar { flex: 1; }
|
||||
.lcars-bar-top { background-color: hsl(var(--secondary)); border-radius: 0 0 1rem 0; }
|
||||
.lcars-bar-bottom { background-color: hsl(var(--secondary)); border-radius: 0 1rem 0 0; }
|
||||
.lcars-frame-header { display: flex; align-items: center; gap: 0.625rem; }
|
||||
.lcars-frame-caps { display: flex; align-items: center; gap: 0.375rem; }
|
||||
.lcars-content-row { display: grid; grid-template-columns: 12rem 1fr; gap: 0.375rem; flex: 1; }
|
||||
.lcars-rail { display: flex; flex-direction: column; gap: 0.375rem; }
|
||||
.lcars-rail-btn { display: flex; align-items: center; justify-content: flex-end; text-align: right; padding: 0.625rem 1rem; min-height: 3rem; border-radius: 0 0 0 1.5rem; background-color: hsl(var(--secondary)); color: hsl(var(--background)); font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; font-size: 0.8125rem; text-transform: uppercase; letter-spacing: 0.1em; text-decoration: none; transition: filter 150ms ease; }
|
||||
.lcars-rail-btn:nth-child(3n+1) { background-color: hsl(var(--primary)); }
|
||||
.lcars-rail-btn:nth-child(3n+2) { background-color: hsl(var(--secondary)); }
|
||||
.lcars-rail-btn:nth-child(3n) { background-color: hsl(var(--accent)); }
|
||||
.lcars-rail-btn:hover { filter: brightness(1.15); }
|
||||
.lcars-rail-cap { height: 2.25rem; background-color: hsl(var(--primary)); border-radius: 1rem 0 0 0; }
|
||||
.lcars-rail-readout { padding: 0.5rem 1rem; text-align: right; }
|
||||
.lcars-main-panel { min-width: 0; background-color: hsl(var(--background)); }
|
||||
|
||||
/* --- Chrome: navbar --- */
|
||||
.lcars-navbar { display: flex; align-items: center; justify-content: space-between; gap: 1.5rem; padding: 0.5rem 0; }
|
||||
.lcars-brand { display: inline-flex; align-items: center; gap: 0.625rem; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; font-size: 1.25rem; text-transform: uppercase; letter-spacing: 0.14em; color: hsl(var(--primary)); text-decoration: none; }
|
||||
.lcars-brand-logo { height: 2rem; width: auto; }
|
||||
.lcars-nav-links { display: none; align-items: center; gap: 0.375rem; }
|
||||
@media (min-width: 768px) { .lcars-nav-links { display: flex; } }
|
||||
.lcars-nav-link { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.875rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: hsl(var(--background)); background-color: hsl(var(--secondary)); padding: 0.375rem 1rem; border-radius: 0 1rem 1rem 0; text-decoration: none; transition: filter 150ms ease; }
|
||||
.lcars-nav-link:hover { filter: brightness(1.15); }
|
||||
.lcars-nav-toggle { display: inline-flex; }
|
||||
@media (min-width: 768px) { .lcars-nav-toggle { display: none; } }
|
||||
.lcars-nav-drawer { display: none; flex-direction: column; gap: 0.375rem; padding: 0.75rem 0 0.5rem; width: 100%; }
|
||||
@media (min-width: 768px) { .lcars-nav-drawer { display: none !important; } }
|
||||
.lcars-nav-drawer a { padding: 0.625rem 1rem; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: hsl(var(--background)); background-color: hsl(var(--secondary)); border-radius: 0 1rem 1rem 0; text-decoration: none; }
|
||||
|
||||
/* --- Chrome: utility bar + announcement bar --- */
|
||||
.lcars-utility-bar { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 0.8125rem; letter-spacing: 0.08em; padding: 0.375rem 0; }
|
||||
.lcars-utility-bar--muted { background-color: hsl(var(--muted)); color: hsl(var(--muted-foreground)); }
|
||||
.lcars-utility-bar--primary { background-color: hsl(var(--primary)); color: hsl(var(--background)); }
|
||||
.lcars-utility-bar--dark { background-color: hsl(var(--foreground)); color: hsl(var(--background)); }
|
||||
.lcars-utility-row { display: flex; align-items: center; justify-content: space-between; gap: 1rem; flex-wrap: wrap; }
|
||||
.lcars-utility-group { display: flex; align-items: center; gap: 1.25rem; }
|
||||
.lcars-utility-item { display: inline-flex; align-items: center; gap: 0.375rem; color: inherit; text-decoration: none; }
|
||||
.lcars-announce { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.875rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.1em; padding: 0.5rem 0; }
|
||||
.lcars-announce--info { background-color: hsl(var(--secondary)); color: hsl(var(--background)); }
|
||||
.lcars-announce--promo { background-color: hsl(var(--accent)); color: hsl(var(--background)); }
|
||||
.lcars-announce--alert { background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); }
|
||||
.lcars-announce-row { display: flex; align-items: center; justify-content: center; gap: 0.75rem; }
|
||||
.lcars-announce a { color: inherit; text-decoration: underline; text-underline-offset: 2px; }
|
||||
.lcars-announce-close { margin-left: auto; background: none; border: 0; color: inherit; cursor: pointer; line-height: 1; padding: 0 0.25rem; }
|
||||
|
||||
/* --- Chrome: footer --- */
|
||||
.lcars-footer { background-color: hsl(var(--background)); color: hsl(var(--foreground)); border-top: 0.375rem solid hsl(var(--primary)); border-radius: 0.75rem 0.75rem 0 0; padding: 2.5rem 0 2rem; }
|
||||
.lcars-footer-cols { display: grid; gap: 2rem; grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
@media (min-width: 640px) { .lcars-footer-cols { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (min-width: 1024px) { .lcars-footer-cols { grid-template-columns: 2fr repeat(3, 1fr); } }
|
||||
.lcars-footer-head { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.8125rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.14em; color: hsl(var(--secondary)); margin: 0 0 0.875rem; }
|
||||
.lcars-footer-link { display: block; font-size: 0.9375rem; color: hsl(var(--foreground)); text-decoration: none; padding: 0.25rem 0; }
|
||||
.lcars-footer-link:hover { color: hsl(var(--primary)); }
|
||||
.lcars-footer-legal { color: hsl(var(--muted-foreground)); font-size: 0.8125rem; font-family: var(--font-mono, "Share Tech Mono", monospace); letter-spacing: 0.06em; border-top: 1px solid hsl(var(--border)); padding-top: 1.5rem; margin-top: 2.5rem; display: flex; flex-wrap: wrap; gap: 1rem; justify-content: space-between; }
|
||||
|
||||
/* --- Marketing: hero --- */
|
||||
.lcars-hero { padding: 4rem 0; }
|
||||
.lcars-hero-headline { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; line-height: 1.02; font-size: clamp(2.5rem, 6vw, 4.5rem); color: hsl(var(--primary)); margin: 0; }
|
||||
.lcars-hero-sub { font-size: clamp(1.125rem, 2vw, 1.5rem); line-height: 1.5; color: hsl(var(--foreground)); margin: 1.25rem 0 0; }
|
||||
.lcars-hero-split { display: grid; gap: 2.5rem; grid-template-columns: repeat(1, minmax(0, 1fr)); align-items: center; }
|
||||
@media (min-width: 1024px) { .lcars-hero-split { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
|
||||
/* --- Marketing: logo strip --- */
|
||||
.lcars-logo { filter: grayscale(1); opacity: 0.7; transition: filter 200ms ease, opacity 200ms ease; }
|
||||
.lcars-logo:hover { filter: grayscale(0); opacity: 1; }
|
||||
.lcars-logo-row { display: flex; flex-wrap: wrap; align-items: center; gap: 2.5rem; }
|
||||
|
||||
/* --- Marketing: feature grid --- */
|
||||
.lcars-feature { border-top: 0.25rem solid hsl(var(--accent)); border-radius: 0 0 0.5rem 0.5rem; padding: 1rem 1rem 1.25rem; background-color: hsl(var(--card)); }
|
||||
.lcars-feature-title { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; font-size: 1.0625rem; color: hsl(var(--foreground)); margin: 0.75rem 0 0.5rem; }
|
||||
|
||||
/* --- Marketing: stats / metrics --- */
|
||||
.lcars-metric { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: clamp(2.5rem, 5vw, 3.5rem); line-height: 1; font-variant-numeric: tabular-nums; color: hsl(var(--primary)); letter-spacing: 0.02em; }
|
||||
.lcars-stat-figure { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: clamp(2.25rem, 4vw, 3rem); line-height: 1; font-variant-numeric: tabular-nums; color: hsl(var(--accent)); letter-spacing: 0.02em; }
|
||||
.lcars-stat-label { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.8125rem; color: hsl(var(--muted-foreground)); text-transform: uppercase; letter-spacing: 0.1em; margin-top: 0.5rem; }
|
||||
.lcars-numeral-index { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 2rem; line-height: 1; font-variant-numeric: tabular-nums; color: hsl(var(--accent)); }
|
||||
|
||||
/* --- Marketing: pull quote / testimonial --- */
|
||||
.lcars-pull-quote { border-left: 0.375rem solid hsl(var(--accent)); border-radius: 0 0.5rem 0.5rem 0; padding: 1rem 1.5rem; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 1.5rem; line-height: 1.3; font-weight: 700; text-transform: uppercase; letter-spacing: 0.02em; color: hsl(var(--foreground)); }
|
||||
.lcars-quote-block { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 1.375rem; line-height: 1.4; font-weight: 700; color: hsl(var(--foreground)); }
|
||||
.lcars-quote-cite { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 0.8125rem; letter-spacing: 0.08em; text-transform: uppercase; color: hsl(var(--muted-foreground)); margin-top: 0.75rem; }
|
||||
.lcars-avatar { width: 2.75rem; height: 2.75rem; border-radius: 9999px; object-fit: cover; background-color: hsl(var(--muted)); }
|
||||
|
||||
/* --- Marketing: pricing --- */
|
||||
.lcars-price { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 2.5rem; font-variant-numeric: tabular-nums; color: hsl(var(--primary)); letter-spacing: 0.02em; }
|
||||
.lcars-price-period { font-family: var(--font-body, "Antonio", system-ui, sans-serif); font-size: 0.9375rem; color: hsl(var(--muted-foreground)); }
|
||||
.lcars-price-tier { display: flex; flex-direction: column; }
|
||||
.lcars-price-tier--featured { border-top: 0.5rem solid hsl(var(--accent)); }
|
||||
.lcars-badge { display: inline-block; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.6875rem; font-weight: 700; letter-spacing: 0.12em; text-transform: uppercase; padding: 0.25rem 0.625rem; background-color: hsl(var(--accent)); color: hsl(var(--background)); border-radius: 0.625rem; }
|
||||
.lcars-check { color: hsl(var(--accent)); flex: none; }
|
||||
|
||||
/* --- Marketing: faq --- */
|
||||
.lcars-faq-item { border-top: 1px solid hsl(var(--border)); }
|
||||
.lcars-faq-item summary { cursor: pointer; list-style: none; padding: 1.125rem 0; display: flex; align-items: center; justify-content: space-between; gap: 1rem; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.06em; color: hsl(var(--foreground)); }
|
||||
.lcars-faq-item summary::-webkit-details-marker { display: none; }
|
||||
.lcars-faq-item[open] summary .lcars-faq-chevron { transform: rotate(180deg); }
|
||||
.lcars-faq-chevron { transition: transform 150ms ease; color: hsl(var(--accent)); }
|
||||
.lcars-faq-answer { padding: 0 0 1.125rem; color: hsl(var(--muted-foreground)); font-size: 1rem; line-height: 1.65; }
|
||||
|
||||
/* --- Marketing: timeline --- */
|
||||
.lcars-timeline-step { display: grid; grid-template-columns: auto 1fr; gap: 1.25rem; padding-bottom: 2rem; }
|
||||
.lcars-timeline-rail { position: relative; display: flex; flex-direction: column; align-items: center; }
|
||||
.lcars-timeline-node { width: 2.75rem; height: 2.75rem; background-color: hsl(var(--accent)); border-radius: 0 1.375rem 1.375rem 0; display: flex; align-items: center; justify-content: center; font-family: var(--font-mono, "Share Tech Mono", monospace); color: hsl(var(--background)); flex: none; font-variant-numeric: tabular-nums; }
|
||||
.lcars-timeline-line { flex: 1 1 auto; width: 0.1875rem; background-color: hsl(var(--border)); margin-top: 0.5rem; }
|
||||
|
||||
/* --- Marketing: cta strip --- */
|
||||
.lcars-cta-strip { padding: 3rem 0; }
|
||||
.lcars-cta-strip--band { background-color: hsl(var(--primary)); color: hsl(var(--background)); border-radius: 1rem; }
|
||||
.lcars-cta-strip--band .lcars-cta-headline { color: hsl(var(--background)); }
|
||||
.lcars-cta-strip--band .lcars-btn-primary { background-color: hsl(var(--background)); color: hsl(var(--primary)); }
|
||||
.lcars-cta-strip--card { background-color: hsl(var(--muted)); border-radius: 1rem; }
|
||||
.lcars-cta-headline { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; font-size: clamp(1.75rem, 3vw, 2.5rem); line-height: 1.1; margin: 0; color: hsl(var(--primary)); }
|
||||
|
||||
/* --- Marketing: countdown --- */
|
||||
.lcars-count-unit { display: flex; flex-direction: column; align-items: center; }
|
||||
.lcars-count-figure { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: clamp(2rem, 4vw, 3rem); font-variant-numeric: tabular-nums; color: hsl(var(--accent)); line-height: 1; }
|
||||
.lcars-count-box { background-color: hsl(var(--card)); border-top: 0.25rem solid hsl(var(--primary)); padding: 1rem 1.25rem; border-radius: 0 0 0.5rem 0.5rem; min-width: 4.5rem; }
|
||||
.lcars-count-label { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.1em; color: hsl(var(--muted-foreground)); margin-top: 0.375rem; }
|
||||
|
||||
/* --- Primitives: divider / gallery / media --- */
|
||||
.lcars-divider-line { border: 0; border-top: 0.1875rem solid hsl(var(--border)); }
|
||||
.lcars-divider-thick { border-top-width: 0.375rem; border-top-color: hsl(var(--primary)); }
|
||||
.lcars-divider-ornament { display: flex; align-items: center; justify-content: center; gap: 0.5rem; color: hsl(var(--primary)); }
|
||||
.lcars-gallery-grid { display: grid; gap: 0.5rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
@media (min-width: 768px) { .lcars-gallery-grid.lcars-gallery-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } .lcars-gallery-grid.lcars-gallery-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); } }
|
||||
.lcars-video-frame { position: relative; display: block; width: 100%; background-color: hsl(var(--muted)); border-radius: 0.5rem; overflow: hidden; }
|
||||
.lcars-video-play { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; color: hsl(var(--background)); background: none; border: 0; cursor: pointer; }
|
||||
.lcars-video-play-badge { width: 4rem; height: 4rem; border-radius: 9999px; background-color: hsl(var(--primary)); color: hsl(var(--background)); display: flex; align-items: center; justify-content: center; }
|
||||
|
||||
/* --- Commerce / local --- */
|
||||
.lcars-menu-item { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; padding: 0.75rem 0; border-bottom: 1px solid hsl(var(--border)); }
|
||||
.lcars-menu-price { font-family: var(--font-mono, "Share Tech Mono", monospace); font-variant-numeric: tabular-nums; color: hsl(var(--accent)); }
|
||||
.lcars-channel { display: flex; align-items: flex-start; gap: 0.75rem; }
|
||||
.lcars-channel-mark { width: 2.75rem; height: 2.75rem; background-color: hsl(var(--primary)); border-radius: 0 1.375rem 1.375rem 0; display: flex; align-items: center; justify-content: center; color: hsl(var(--background)); flex: none; }
|
||||
.lcars-hours-row { display: flex; align-items: baseline; justify-content: space-between; gap: 1rem; padding: 0.375rem 0; border-bottom: 1px solid hsl(var(--border)); }
|
||||
.lcars-social-row { display: flex; flex-wrap: wrap; gap: 0.5rem; }
|
||||
.lcars-social-link { display: inline-flex; align-items: center; gap: 0.375rem; padding: 0.5rem 0.875rem; background-color: hsl(var(--secondary)); border-radius: 0 1rem 1rem 0; color: hsl(var(--background)); text-decoration: none; font-size: 0.8125rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; transition: filter 150ms ease; }
|
||||
.lcars-social-link:hover { filter: brightness(1.15); }
|
||||
|
||||
/* --- Forms --- */
|
||||
.lcars-form { display: grid; gap: 1.25rem; }
|
||||
.lcars-form-grid { display: grid; gap: 1.25rem; grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.lcars-field { display: flex; flex-direction: column; gap: 0.375rem; }
|
||||
.lcars-field--full { grid-column: 1 / -1; }
|
||||
.lcars-input { width: 100%; box-sizing: border-box; padding: 0.625rem 0.875rem; background-color: hsl(var(--background)); color: hsl(var(--foreground)); border: 0.125rem solid hsl(var(--input)); border-radius: 0.5rem; font-family: var(--font-body, "Antonio", system-ui, sans-serif); font-size: 1rem; }
|
||||
.lcars-input:focus { outline: 2px solid hsl(var(--ring)); outline-offset: 1px; border-color: hsl(var(--ring)); }
|
||||
.lcars-form-note { font-size: 0.8125rem; color: hsl(var(--muted-foreground)); }
|
||||
.lcars-alert-ok { border-left: 0.375rem solid hsl(var(--primary)); background-color: hsl(var(--muted)); padding: 1rem 1.25rem; color: hsl(var(--foreground)); border-radius: 0 0.5rem 0.5rem 0; }
|
||||
.lcars-alert-err { border-left: 0.375rem solid hsl(var(--destructive)); background-color: hsl(var(--muted)); padding: 1rem 1.25rem; color: hsl(var(--foreground)); border-radius: 0 0.5rem 0.5rem 0; }
|
||||
|
||||
/* --- Blog family --- */
|
||||
.lcars-post-card { display: flex; flex-direction: column; gap: 0.75rem; background-color: hsl(var(--card)); border-top: 0.375rem solid hsl(var(--secondary)); border-radius: 0 0 0.5rem 0.5rem; padding: 1rem 1.25rem 1.25rem; }
|
||||
.lcars-post-thumb { display: block; width: 100%; aspect-ratio: 16 / 10; object-fit: cover; background-color: hsl(var(--muted)); border-radius: 0.375rem; }
|
||||
.lcars-post-title { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-weight: 700; text-transform: uppercase; letter-spacing: 0.04em; font-size: 1.1875rem; line-height: 1.2; color: hsl(var(--primary)); text-decoration: none; }
|
||||
.lcars-post-title:hover { color: hsl(var(--secondary)); }
|
||||
.lcars-post-meta { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 0.75rem; letter-spacing: 0.08em; text-transform: uppercase; color: hsl(var(--muted-foreground)); display: flex; flex-wrap: wrap; gap: 0.75rem; }
|
||||
.lcars-post-excerpt { color: hsl(var(--muted-foreground)); font-size: 0.9375rem; line-height: 1.6; }
|
||||
.lcars-post-grid { display: grid; gap: 1.5rem; grid-template-columns: repeat(1, minmax(0, 1fr)); }
|
||||
@media (min-width: 640px) { .lcars-post-grid-2, .lcars-post-grid-3 { grid-template-columns: repeat(2, minmax(0, 1fr)); } }
|
||||
@media (min-width: 1024px) { .lcars-post-grid-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); } }
|
||||
.lcars-tag { display: inline-block; font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.75rem; font-weight: 700; letter-spacing: 0.08em; text-transform: uppercase; padding: 0.1875rem 0.625rem; background-color: hsl(var(--muted)); border-radius: 0.625rem; color: hsl(var(--secondary)); text-decoration: none; }
|
||||
.lcars-tag:hover { background-color: hsl(var(--secondary)); color: hsl(var(--background)); }
|
||||
.lcars-crumbs { display: flex; flex-wrap: wrap; align-items: center; gap: 0.5rem; font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 0.8125rem; letter-spacing: 0.06em; color: hsl(var(--muted-foreground)); }
|
||||
.lcars-crumbs a { color: hsl(var(--secondary)); text-decoration: none; }
|
||||
.lcars-crumbs a:hover { color: hsl(var(--primary)); }
|
||||
.lcars-crumb-sep { color: hsl(var(--accent)); }
|
||||
.lcars-sidebar-link { display: block; padding: 0.5rem 0.875rem; border-radius: 0 1rem 1rem 0; color: hsl(var(--foreground)); text-decoration: none; font-size: 0.9375rem; }
|
||||
.lcars-sidebar-link:hover, .lcars-sidebar-link[aria-current="page"] { background-color: hsl(var(--secondary)); color: hsl(var(--background)); }
|
||||
.lcars-rank { font-family: var(--font-mono, "Share Tech Mono", monospace); font-size: 1.25rem; color: hsl(var(--accent)); font-variant-numeric: tabular-nums; width: 2rem; flex: none; }
|
||||
|
||||
/* --- Status readouts --- */
|
||||
.lcars-status { font-family: var(--font-heading, "Antonio", system-ui, sans-serif); font-size: 0.75rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.15em; padding: 0.25rem 0.75rem; border-radius: 0.75rem; }
|
||||
.lcars-status-online { background-color: hsl(var(--primary)); color: hsl(var(--background)); animation: lcars-pulse 3s ease-in-out infinite; }
|
||||
.lcars-status-standby { background-color: hsl(var(--secondary)); color: hsl(var(--background)); }
|
||||
.lcars-status-alert { background-color: hsl(var(--destructive)); color: hsl(var(--destructive-foreground)); animation: lcars-blink 1s step-end infinite; }
|
||||
.lcars-status-offline { background-color: hsl(var(--muted)); color: hsl(var(--muted-foreground)); }
|
||||
|
||||
/* --- Auth --- */
|
||||
.lcars-auth-radio { position: absolute; width: 1px; height: 1px; opacity: 0; pointer-events: none; }
|
||||
.lcars-auth-panel { display: none; }
|
||||
|
||||
/* --- Motion (GO BIG: canvas sweep on hero/landing only; CSS elsewhere) --- */
|
||||
.lcars-hero-canvas { position: absolute; inset: 0; width: 100%; height: 100%; display: block; pointer-events: none; z-index: 0; }
|
||||
.lcars-hero-canvas-host { position: relative; overflow: hidden; }
|
||||
.lcars-hero-canvas-host > *:not(.lcars-hero-canvas) { position: relative; z-index: 1; }
|
||||
.lcars-reveal { opacity: 0; transform: translateY(0.5rem); animation: lcars-reveal 500ms ease forwards; }
|
||||
@keyframes lcars-reveal { to { opacity: 1; transform: none; } }
|
||||
@keyframes lcars-pulse { 0%, 100% { opacity: 1; } 50% { opacity: 0.6; } }
|
||||
@keyframes lcars-blink { 0%, 100% { opacity: 1; } 50% { opacity: 0.3; } }
|
||||
@keyframes lcars-sweep { 0% { transform: translateX(-100%); } 100% { transform: translateX(100%); } }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.lcars-reveal { animation: none; opacity: 1; transform: none; }
|
||||
.lcars-status-online, .lcars-status-alert { animation: none; }
|
||||
.lcars-btn-ghost, .lcars-logo, .lcars-nav-link, .lcars-faq-chevron { transition: none; }
|
||||
}
|
||||
|
||||
/* --- Responsive: collapse the command rail on small screens --- */
|
||||
@media (max-width: 768px) {
|
||||
.lcars-content-row { grid-template-columns: 1fr; }
|
||||
.lcars-rail { flex-direction: row; flex-wrap: wrap; }
|
||||
.lcars-elbow-corner { width: 4rem; min-width: 4rem; }
|
||||
.lcars-rail-btn { border-radius: 0 1rem 1rem 0; justify-content: center; }
|
||||
.lcars-form-grid { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
/* --- Compatibility layer: classes the overrides carry over from the
|
||||
shared builtin re-skin. Styled LCARS-native so every class resolves. --- */
|
||||
.lcars-content-well { width: 100%; max-width: 1280px; margin-left: auto; margin-right: auto; padding-left: 1.5rem; padding-right: 1.5rem; box-sizing: border-box; }
|
||||
.lcars-swiss-12 { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); column-gap: 1.25rem; row-gap: 1.25rem; }
|
||||
.lcars-article-measure { max-width: 72ch; }
|
||||
.lcars-hairline-top { border-top: 0.1875rem solid hsl(var(--border)); }
|
||||
.lcars-hairline-bottom { border-bottom: 0.1875rem solid hsl(var(--border)); }
|
||||
.lcars-accent-rule { border-top: 0.1875rem solid hsl(var(--primary)); }
|
||||
.lcars-heading-h1 { font-size: clamp(2rem, 5vw, 2.75rem); }
|
||||
.lcars-heading-h2 { font-size: clamp(1.625rem, 3.5vw, 2rem); }
|
||||
.lcars-heading-h3 { font-size: 1.375rem; }
|
||||
.lcars-heading-h4 { font-size: 1.125rem; }
|
||||
.lcars-heading-h5 { font-size: 1rem; }
|
||||
.lcars-heading-h6 { font-size: 0.875rem; }
|
||||
/* LCARS marks: pills and caps replace the Bauhaus geometric marks. */
|
||||
.lcars-mark { display: inline-block; width: 1.5rem; height: 0.75rem; flex: none; background-color: hsl(var(--primary)); border-radius: 0.375rem; }
|
||||
.lcars-mark-circle { border-radius: 9999px; width: 0.75rem; background-color: hsl(var(--primary)); }
|
||||
.lcars-mark-square { border-radius: 0 0.375rem 0.375rem 0; background-color: hsl(var(--secondary)); }
|
||||
.lcars-mark-triangle { border-radius: 0.375rem 0 0 0.375rem; background-color: hsl(var(--accent)); }
|
||||
.lcars-mark-ring { border-radius: 9999px; width: 0.75rem; background: none; border: 2px solid hsl(var(--accent)); }
|
||||
@ -1,57 +0,0 @@
|
||||
package main
|
||||
|
||||
import "git.dev.alexdunmow.com/block/core/plugin"
|
||||
|
||||
func DefaultMasterPages() []plugin.MasterPageDefinition {
|
||||
return []plugin.MasterPageDefinition{
|
||||
{
|
||||
Key: "lcars:standard-display",
|
||||
Title: "LCARS Standard Display",
|
||||
PageTemplates: []string{"default"},
|
||||
Blocks: []plugin.MasterPageBlock{
|
||||
{
|
||||
BlockKey: "lcars:lcars_header",
|
||||
Title: "LCARS Header",
|
||||
Content: map[string]any{"title": "LCARS", "subtitle": "Library Computer Access/Retrieval System"},
|
||||
Slot: "header",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "lcars:lcars_sidebar",
|
||||
Title: "LCARS Sidebar",
|
||||
Content: map[string]any{},
|
||||
Slot: "sidebar",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "slot",
|
||||
Title: "Main Content Slot",
|
||||
Content: map[string]any{"slotName": "main", "placeholder": "Enter display data…"},
|
||||
Slot: "main",
|
||||
SortOrder: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Key: "lcars:full-display",
|
||||
Title: "LCARS Full Display",
|
||||
PageTemplates: []string{"full-display"},
|
||||
Blocks: []plugin.MasterPageBlock{
|
||||
{
|
||||
BlockKey: "lcars:lcars_header",
|
||||
Title: "LCARS Header",
|
||||
Content: map[string]any{"title": "LCARS", "subtitle": "Main Viewer"},
|
||||
Slot: "header",
|
||||
SortOrder: 0,
|
||||
},
|
||||
{
|
||||
BlockKey: "slot",
|
||||
Title: "Main Content Slot",
|
||||
Content: map[string]any{"slotName": "main", "placeholder": "Enter display data…"},
|
||||
Slot: "main",
|
||||
SortOrder: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
53
master_pages.json
Normal file
@ -0,0 +1,53 @@
|
||||
[
|
||||
{
|
||||
"key": "lcars:standard-display",
|
||||
"title": "LCARS Standard Display",
|
||||
"page_templates": ["default"],
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Main Navigation", "content": { "variant": "standard", "menuName": "main", "companyName": "Subspace Signal" }, "slot": "header", "sort_order": 0 },
|
||||
{ "block_key": "html", "title": "Command Rail", "content": { "html": "<nav class=\"lcars-rail\" aria-label=\"Command rail\"><span class=\"lcars-rail-cap\" aria-hidden=\"true\"></span><a class=\"lcars-rail-btn\" href=\"/\">Home</a><a class=\"lcars-rail-btn\" href=\"/about\">About</a><a class=\"lcars-rail-btn\" href=\"/episodes\">Episodes</a><a class=\"lcars-rail-btn\" href=\"/contact\">Hail</a><div class=\"lcars-rail-readout\"><span class=\"lcars-readout\" style=\"font-size:0.6875rem;\">Sig OK</span></div></nav>" }, "slot": "sidebar", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Main Content Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
|
||||
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showSocial": true, "companyName": "Subspace Signal", "tagline": "A weekly transmission on science fiction and the future.", "copyright": "Subspace Signal" }, "slot": "footer", "sort_order": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "lcars:content-display",
|
||||
"title": "LCARS Content Display",
|
||||
"page_templates": ["full-width", "full-display", "article", "blog-index"],
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Main Navigation", "content": { "variant": "standard", "menuName": "main", "companyName": "Subspace Signal" }, "slot": "header", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Main Content Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
|
||||
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showSocial": true, "companyName": "Subspace Signal", "tagline": "A weekly transmission on science fiction and the future.", "copyright": "Subspace Signal" }, "slot": "footer", "sort_order": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "lcars:landing-display",
|
||||
"title": "LCARS Landing Display",
|
||||
"page_templates": ["landing"],
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Main Navigation", "content": { "variant": "standard", "menuName": "main", "companyName": "Subspace Signal" }, "slot": "header", "sort_order": 0 },
|
||||
{ "block_key": "hero", "title": "Landing Hero", "content": { "template": "left-aligned", "eyebrow": "Now broadcasting", "headline": "Subspace Signal", "subheadline": "A weekly transmission on science fiction and the future." }, "slot": "hero", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Main Content Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
|
||||
{ "block_key": "cta", "title": "Closing CTA", "content": { "background": "band", "heading": "Tune the frequency", "text": "New transmission every week. Subscribe on your channel of choice.", "primaryLabel": "Subscribe", "primaryUrl": "/episodes" }, "slot": "cta", "sort_order": 0 },
|
||||
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showSocial": true, "companyName": "Subspace Signal", "copyright": "Subspace Signal" }, "slot": "footer", "sort_order": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "lcars:contact-display",
|
||||
"title": "LCARS Contact Display",
|
||||
"page_templates": ["contact"],
|
||||
"blocks": [
|
||||
{ "block_key": "navbar", "title": "Main Navigation", "content": { "variant": "standard", "menuName": "main", "companyName": "Subspace Signal" }, "slot": "header", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Main Content Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 },
|
||||
{ "block_key": "footer", "title": "Site Footer", "content": { "layout": "columns", "showSocial": true, "companyName": "Subspace Signal", "copyright": "Subspace Signal" }, "slot": "footer", "sort_order": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "lcars:auth-display",
|
||||
"title": "LCARS Auth Display",
|
||||
"page_templates": ["auth"],
|
||||
"blocks": [
|
||||
{ "block_key": "slot", "title": "Auth Slot", "content": { "slotName": "main" }, "slot": "main", "sort_order": 0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -2,11 +2,8 @@
|
||||
name = "lcars"
|
||||
display_name = "LCARS"
|
||||
scope = "@themes"
|
||||
version = "0.2.8"
|
||||
version = "0.5.2"
|
||||
description = "Star Trek-inspired LCARS interface theme for BlockNinja — angular panels, bold accent colors, and the distinctive curved frame layout of the 24th-century starship computer."
|
||||
kind = "theme"
|
||||
categories = ["templates"]
|
||||
tags = ["retro-futurist", "scifi", "lcars", "demo", "showcase", "angular", "amber"]
|
||||
|
||||
[compatibility]
|
||||
block_core = ">=0.11.0 <0.12.0"
|
||||
|
||||
413
presets.json
@ -1,202 +1,277 @@
|
||||
[
|
||||
{
|
||||
"id": "federation",
|
||||
"name": "Federation",
|
||||
"description": "Classic LCARS — gold, periwinkle, and coral on black",
|
||||
"id": "ops-amber",
|
||||
"name": "Ops Amber",
|
||||
"description": "Canonical LCARS. Black ops ground with orange, amber, and lilac panels. Light mode is daylight-ops cream with the colour bars kept vivid.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"typography": {
|
||||
"fontHeading": "template:lcars:Antonio",
|
||||
"fontBody": "template:lcars:Antonio",
|
||||
"fontMono": "template:lcars:Share Tech Mono"
|
||||
},
|
||||
"lightColors": {
|
||||
"background": "0 0% 2%",
|
||||
"foreground": "40 33% 92%",
|
||||
"card": "0 0% 5%",
|
||||
"cardForeground": "40 33% 92%",
|
||||
"popover": "0 0% 5%",
|
||||
"popoverForeground": "40 33% 92%",
|
||||
"primary": "39 90% 65%",
|
||||
"primaryForeground": "0 0% 0%",
|
||||
"secondary": "230 55% 70%",
|
||||
"secondaryForeground": "0 0% 0%",
|
||||
"muted": "0 0% 10%",
|
||||
"mutedForeground": "40 20% 55%",
|
||||
"accent": "10 70% 65%",
|
||||
"accentForeground": "0 0% 0%",
|
||||
"destructive": "0 85% 55%",
|
||||
"destructiveForeground": "0 0% 98%",
|
||||
"border": "0 0% 15%",
|
||||
"input": "0 0% 15%",
|
||||
"ring": "39 90% 65%"
|
||||
"background": "40 38% 95%",
|
||||
"foreground": "28 30% 12%",
|
||||
"card": "40 30% 91%",
|
||||
"cardForeground": "28 30% 12%",
|
||||
"popover": "40 30% 91%",
|
||||
"popoverForeground": "28 30% 12%",
|
||||
"primary": "28 92% 48%",
|
||||
"primaryForeground": "40 38% 96%",
|
||||
"secondary": "38 88% 46%",
|
||||
"secondaryForeground": "40 38% 96%",
|
||||
"muted": "40 24% 88%",
|
||||
"mutedForeground": "28 18% 38%",
|
||||
"accent": "268 52% 52%",
|
||||
"accentForeground": "40 38% 96%",
|
||||
"destructive": "2 74% 46%",
|
||||
"destructiveForeground": "40 38% 96%",
|
||||
"border": "40 22% 82%",
|
||||
"input": "40 22% 82%",
|
||||
"ring": "28 92% 48%"
|
||||
},
|
||||
"darkColors": {
|
||||
"background": "0 0% 2%",
|
||||
"foreground": "40 33% 92%",
|
||||
"card": "0 0% 5%",
|
||||
"cardForeground": "40 33% 92%",
|
||||
"popover": "0 0% 5%",
|
||||
"popoverForeground": "40 33% 92%",
|
||||
"primary": "39 90% 65%",
|
||||
"primaryForeground": "0 0% 0%",
|
||||
"secondary": "230 55% 70%",
|
||||
"secondaryForeground": "0 0% 0%",
|
||||
"muted": "0 0% 10%",
|
||||
"mutedForeground": "40 20% 55%",
|
||||
"accent": "10 70% 65%",
|
||||
"accentForeground": "0 0% 0%",
|
||||
"destructive": "0 85% 55%",
|
||||
"destructiveForeground": "0 0% 98%",
|
||||
"border": "0 0% 15%",
|
||||
"input": "0 0% 15%",
|
||||
"ring": "39 90% 65%"
|
||||
},
|
||||
"mode": "dark"
|
||||
"background": "240 8% 6%",
|
||||
"foreground": "40 30% 92%",
|
||||
"card": "240 8% 11%",
|
||||
"cardForeground": "40 30% 92%",
|
||||
"popover": "240 8% 11%",
|
||||
"popoverForeground": "40 30% 92%",
|
||||
"primary": "28 100% 58%",
|
||||
"primaryForeground": "240 8% 6%",
|
||||
"secondary": "40 96% 62%",
|
||||
"secondaryForeground": "240 8% 6%",
|
||||
"muted": "240 6% 16%",
|
||||
"mutedForeground": "40 14% 66%",
|
||||
"accent": "266 70% 76%",
|
||||
"accentForeground": "240 8% 6%",
|
||||
"destructive": "0 78% 58%",
|
||||
"destructiveForeground": "240 8% 6%",
|
||||
"border": "240 6% 20%",
|
||||
"input": "240 6% 22%",
|
||||
"ring": "28 100% 58%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "red-alert",
|
||||
"name": "Red Alert",
|
||||
"description": "Emergency operations — red and amber on black",
|
||||
"description": "Condition-one console. Signal red and orange over black, with an amber standby channel. Light mode reads as an emergency daylight console.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"typography": {
|
||||
"fontHeading": "template:lcars:Antonio",
|
||||
"fontBody": "template:lcars:Antonio",
|
||||
"fontMono": "template:lcars:Share Tech Mono"
|
||||
},
|
||||
"lightColors": {
|
||||
"background": "0 0% 2%",
|
||||
"foreground": "0 0% 92%",
|
||||
"card": "0 0% 5%",
|
||||
"cardForeground": "0 0% 92%",
|
||||
"popover": "0 0% 5%",
|
||||
"popoverForeground": "0 0% 92%",
|
||||
"primary": "0 80% 50%",
|
||||
"primaryForeground": "0 0% 100%",
|
||||
"secondary": "35 95% 55%",
|
||||
"secondaryForeground": "0 0% 0%",
|
||||
"muted": "0 0% 10%",
|
||||
"mutedForeground": "0 15% 55%",
|
||||
"accent": "35 95% 55%",
|
||||
"accentForeground": "0 0% 0%",
|
||||
"destructive": "0 85% 55%",
|
||||
"destructiveForeground": "0 0% 98%",
|
||||
"border": "0 30% 18%",
|
||||
"input": "0 30% 18%",
|
||||
"ring": "0 80% 50%"
|
||||
"background": "20 34% 95%",
|
||||
"foreground": "8 34% 12%",
|
||||
"card": "20 28% 91%",
|
||||
"cardForeground": "8 34% 12%",
|
||||
"popover": "20 28% 91%",
|
||||
"popoverForeground": "8 34% 12%",
|
||||
"primary": "4 78% 46%",
|
||||
"primaryForeground": "20 34% 96%",
|
||||
"secondary": "24 90% 48%",
|
||||
"secondaryForeground": "20 34% 96%",
|
||||
"muted": "20 22% 88%",
|
||||
"mutedForeground": "8 18% 38%",
|
||||
"accent": "40 92% 46%",
|
||||
"accentForeground": "20 34% 96%",
|
||||
"destructive": "0 80% 42%",
|
||||
"destructiveForeground": "20 34% 96%",
|
||||
"border": "20 20% 82%",
|
||||
"input": "20 20% 82%",
|
||||
"ring": "4 78% 46%"
|
||||
},
|
||||
"darkColors": {
|
||||
"background": "0 0% 2%",
|
||||
"foreground": "0 0% 92%",
|
||||
"card": "0 0% 5%",
|
||||
"cardForeground": "0 0% 92%",
|
||||
"popover": "0 0% 5%",
|
||||
"popoverForeground": "0 0% 92%",
|
||||
"primary": "0 80% 50%",
|
||||
"primaryForeground": "0 0% 100%",
|
||||
"secondary": "35 95% 55%",
|
||||
"secondaryForeground": "0 0% 0%",
|
||||
"muted": "0 0% 10%",
|
||||
"mutedForeground": "0 15% 55%",
|
||||
"accent": "35 95% 55%",
|
||||
"accentForeground": "0 0% 0%",
|
||||
"destructive": "0 85% 55%",
|
||||
"destructiveForeground": "0 0% 98%",
|
||||
"border": "0 30% 18%",
|
||||
"input": "0 30% 18%",
|
||||
"ring": "0 80% 50%"
|
||||
},
|
||||
"mode": "dark"
|
||||
"background": "0 12% 6%",
|
||||
"foreground": "20 30% 92%",
|
||||
"card": "0 12% 11%",
|
||||
"cardForeground": "20 30% 92%",
|
||||
"popover": "0 12% 11%",
|
||||
"popoverForeground": "20 30% 92%",
|
||||
"primary": "4 86% 60%",
|
||||
"primaryForeground": "0 12% 6%",
|
||||
"secondary": "24 100% 58%",
|
||||
"secondaryForeground": "0 12% 6%",
|
||||
"muted": "0 8% 16%",
|
||||
"mutedForeground": "20 14% 66%",
|
||||
"accent": "42 98% 60%",
|
||||
"accentForeground": "0 12% 6%",
|
||||
"destructive": "0 84% 62%",
|
||||
"destructiveForeground": "0 12% 6%",
|
||||
"border": "0 8% 20%",
|
||||
"input": "0 8% 22%",
|
||||
"ring": "4 86% 60%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "sickbay",
|
||||
"name": "Sickbay",
|
||||
"description": "Medical operations — teal, cyan, and blue on black",
|
||||
"id": "blue-console",
|
||||
"name": "Blue Console",
|
||||
"description": "24th-century sciences layout. Periwinkle blue and cyan panels with an orange action channel over black. Light mode is a bright bridge console.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"typography": {
|
||||
"fontHeading": "template:lcars:Antonio",
|
||||
"fontBody": "template:lcars:Antonio",
|
||||
"fontMono": "template:lcars:Share Tech Mono"
|
||||
},
|
||||
"lightColors": {
|
||||
"background": "0 0% 2%",
|
||||
"foreground": "180 20% 90%",
|
||||
"card": "0 0% 5%",
|
||||
"cardForeground": "180 20% 90%",
|
||||
"popover": "0 0% 5%",
|
||||
"popoverForeground": "180 20% 90%",
|
||||
"primary": "174 72% 50%",
|
||||
"primaryForeground": "0 0% 0%",
|
||||
"secondary": "199 80% 55%",
|
||||
"secondaryForeground": "0 0% 0%",
|
||||
"muted": "0 0% 10%",
|
||||
"mutedForeground": "180 15% 50%",
|
||||
"accent": "199 80% 55%",
|
||||
"accentForeground": "0 0% 0%",
|
||||
"destructive": "0 85% 55%",
|
||||
"destructiveForeground": "0 0% 98%",
|
||||
"border": "180 15% 15%",
|
||||
"input": "180 15% 15%",
|
||||
"ring": "174 72% 50%"
|
||||
"background": "215 34% 96%",
|
||||
"foreground": "220 32% 14%",
|
||||
"card": "215 28% 92%",
|
||||
"cardForeground": "220 32% 14%",
|
||||
"popover": "215 28% 92%",
|
||||
"popoverForeground": "220 32% 14%",
|
||||
"primary": "222 62% 48%",
|
||||
"primaryForeground": "215 34% 97%",
|
||||
"secondary": "190 70% 40%",
|
||||
"secondaryForeground": "215 34% 97%",
|
||||
"muted": "215 22% 89%",
|
||||
"mutedForeground": "220 16% 38%",
|
||||
"accent": "28 92% 50%",
|
||||
"accentForeground": "215 34% 97%",
|
||||
"destructive": "2 74% 46%",
|
||||
"destructiveForeground": "215 34% 97%",
|
||||
"border": "215 20% 83%",
|
||||
"input": "215 20% 83%",
|
||||
"ring": "222 62% 48%"
|
||||
},
|
||||
"darkColors": {
|
||||
"background": "0 0% 2%",
|
||||
"foreground": "180 20% 90%",
|
||||
"card": "0 0% 5%",
|
||||
"cardForeground": "180 20% 90%",
|
||||
"popover": "0 0% 5%",
|
||||
"popoverForeground": "180 20% 90%",
|
||||
"primary": "174 72% 50%",
|
||||
"primaryForeground": "0 0% 0%",
|
||||
"secondary": "199 80% 55%",
|
||||
"secondaryForeground": "0 0% 0%",
|
||||
"muted": "0 0% 10%",
|
||||
"mutedForeground": "180 15% 50%",
|
||||
"accent": "199 80% 55%",
|
||||
"accentForeground": "0 0% 0%",
|
||||
"destructive": "0 85% 55%",
|
||||
"destructiveForeground": "0 0% 98%",
|
||||
"border": "180 15% 15%",
|
||||
"input": "180 15% 15%",
|
||||
"ring": "174 72% 50%"
|
||||
},
|
||||
"mode": "dark"
|
||||
"background": "222 20% 7%",
|
||||
"foreground": "210 28% 92%",
|
||||
"card": "222 20% 12%",
|
||||
"cardForeground": "210 28% 92%",
|
||||
"popover": "222 20% 12%",
|
||||
"popoverForeground": "210 28% 92%",
|
||||
"primary": "220 78% 66%",
|
||||
"primaryForeground": "222 20% 7%",
|
||||
"secondary": "188 78% 52%",
|
||||
"secondaryForeground": "222 20% 7%",
|
||||
"muted": "222 16% 17%",
|
||||
"mutedForeground": "210 16% 66%",
|
||||
"accent": "30 100% 60%",
|
||||
"accentForeground": "222 20% 7%",
|
||||
"destructive": "0 80% 60%",
|
||||
"destructiveForeground": "222 20% 7%",
|
||||
"border": "222 14% 22%",
|
||||
"input": "222 14% 24%",
|
||||
"ring": "220 78% 66%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "engineering",
|
||||
"name": "Engineering",
|
||||
"description": "Engineering deck — orange, amber, and tan on black",
|
||||
"id": "nebula",
|
||||
"name": "Nebula",
|
||||
"description": "Deep-space media suite. Lilac and magenta panels with a peach action channel over indigo-black. Light mode keeps the nebula hues on soft violet paper.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"typography": {
|
||||
"fontHeading": "template:lcars:Antonio",
|
||||
"fontBody": "template:lcars:Antonio",
|
||||
"fontMono": "template:lcars:Share Tech Mono"
|
||||
},
|
||||
"lightColors": {
|
||||
"background": "0 0% 2%",
|
||||
"foreground": "35 30% 88%",
|
||||
"card": "0 0% 5%",
|
||||
"cardForeground": "35 30% 88%",
|
||||
"popover": "0 0% 5%",
|
||||
"popoverForeground": "35 30% 88%",
|
||||
"primary": "25 95% 55%",
|
||||
"primaryForeground": "0 0% 0%",
|
||||
"secondary": "40 80% 60%",
|
||||
"secondaryForeground": "0 0% 0%",
|
||||
"muted": "0 0% 10%",
|
||||
"mutedForeground": "30 20% 50%",
|
||||
"accent": "33 65% 70%",
|
||||
"accentForeground": "0 0% 0%",
|
||||
"destructive": "0 85% 55%",
|
||||
"destructiveForeground": "0 0% 98%",
|
||||
"border": "25 20% 16%",
|
||||
"input": "25 20% 16%",
|
||||
"ring": "25 95% 55%"
|
||||
"background": "280 30% 96%",
|
||||
"foreground": "278 30% 14%",
|
||||
"card": "280 24% 92%",
|
||||
"cardForeground": "278 30% 14%",
|
||||
"popover": "280 24% 92%",
|
||||
"popoverForeground": "278 30% 14%",
|
||||
"primary": "270 56% 50%",
|
||||
"primaryForeground": "280 30% 97%",
|
||||
"secondary": "322 66% 48%",
|
||||
"secondaryForeground": "280 30% 97%",
|
||||
"muted": "280 20% 89%",
|
||||
"mutedForeground": "278 16% 40%",
|
||||
"accent": "22 88% 54%",
|
||||
"accentForeground": "280 30% 97%",
|
||||
"destructive": "2 74% 46%",
|
||||
"destructiveForeground": "280 30% 97%",
|
||||
"border": "280 18% 83%",
|
||||
"input": "280 18% 83%",
|
||||
"ring": "270 56% 50%"
|
||||
},
|
||||
"darkColors": {
|
||||
"background": "0 0% 2%",
|
||||
"foreground": "35 30% 88%",
|
||||
"card": "0 0% 5%",
|
||||
"cardForeground": "35 30% 88%",
|
||||
"popover": "0 0% 5%",
|
||||
"popoverForeground": "35 30% 88%",
|
||||
"primary": "25 95% 55%",
|
||||
"primaryForeground": "0 0% 0%",
|
||||
"secondary": "40 80% 60%",
|
||||
"secondaryForeground": "0 0% 0%",
|
||||
"muted": "0 0% 10%",
|
||||
"mutedForeground": "30 20% 50%",
|
||||
"accent": "33 65% 70%",
|
||||
"accentForeground": "0 0% 0%",
|
||||
"destructive": "0 85% 55%",
|
||||
"destructiveForeground": "0 0% 98%",
|
||||
"border": "25 20% 16%",
|
||||
"input": "25 20% 16%",
|
||||
"ring": "25 95% 55%"
|
||||
"background": "268 24% 7%",
|
||||
"foreground": "285 26% 92%",
|
||||
"card": "268 24% 12%",
|
||||
"cardForeground": "285 26% 92%",
|
||||
"popover": "268 24% 12%",
|
||||
"popoverForeground": "285 26% 92%",
|
||||
"primary": "268 74% 74%",
|
||||
"primaryForeground": "268 24% 7%",
|
||||
"secondary": "322 82% 64%",
|
||||
"secondaryForeground": "268 24% 7%",
|
||||
"muted": "268 18% 17%",
|
||||
"mutedForeground": "285 16% 66%",
|
||||
"accent": "24 96% 64%",
|
||||
"accentForeground": "268 24% 7%",
|
||||
"destructive": "0 80% 62%",
|
||||
"destructiveForeground": "268 24% 7%",
|
||||
"border": "268 16% 22%",
|
||||
"input": "268 16% 24%",
|
||||
"ring": "268 74% 74%"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "medbay-green",
|
||||
"name": "Medbay Green",
|
||||
"description": "Sickbay diagnostic layout. Teal and green panels with an amber alert channel over black. Light mode is a clean daylight medbay readout.",
|
||||
"theme": {
|
||||
"mode": "both",
|
||||
"typography": {
|
||||
"fontHeading": "template:lcars:Antonio",
|
||||
"fontBody": "template:lcars:Antonio",
|
||||
"fontMono": "template:lcars:Share Tech Mono"
|
||||
},
|
||||
"mode": "dark"
|
||||
"lightColors": {
|
||||
"background": "150 26% 96%",
|
||||
"foreground": "165 30% 12%",
|
||||
"card": "150 22% 92%",
|
||||
"cardForeground": "165 30% 12%",
|
||||
"popover": "150 22% 92%",
|
||||
"popoverForeground": "165 30% 12%",
|
||||
"primary": "168 62% 34%",
|
||||
"primaryForeground": "150 26% 97%",
|
||||
"secondary": "142 58% 40%",
|
||||
"secondaryForeground": "150 26% 97%",
|
||||
"muted": "150 18% 89%",
|
||||
"mutedForeground": "165 16% 38%",
|
||||
"accent": "40 92% 48%",
|
||||
"accentForeground": "150 26% 97%",
|
||||
"destructive": "2 74% 46%",
|
||||
"destructiveForeground": "150 26% 97%",
|
||||
"border": "150 16% 83%",
|
||||
"input": "150 16% 83%",
|
||||
"ring": "168 62% 34%"
|
||||
},
|
||||
"darkColors": {
|
||||
"background": "168 18% 6%",
|
||||
"foreground": "150 24% 92%",
|
||||
"card": "168 18% 11%",
|
||||
"cardForeground": "150 24% 92%",
|
||||
"popover": "168 18% 11%",
|
||||
"popoverForeground": "150 24% 92%",
|
||||
"primary": "168 72% 52%",
|
||||
"primaryForeground": "168 18% 6%",
|
||||
"secondary": "142 66% 56%",
|
||||
"secondaryForeground": "168 18% 6%",
|
||||
"muted": "168 14% 16%",
|
||||
"mutedForeground": "150 14% 66%",
|
||||
"accent": "42 98% 60%",
|
||||
"accentForeground": "168 18% 6%",
|
||||
"destructive": "0 80% 60%",
|
||||
"destructiveForeground": "168 18% 6%",
|
||||
"border": "168 12% 21%",
|
||||
"input": "168 12% 23%",
|
||||
"ring": "168 72% 52%"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
BIN
preview.png
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 984 KiB |
50
register.go
@ -1,50 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
"git.dev.alexdunmow.com/block/core/templates"
|
||||
"git.dev.alexdunmow.com/block/core/templates/pongo"
|
||||
)
|
||||
|
||||
var engine = pongo.NewEngine(TemplatesFS(), "/templates/lcars/style.css")
|
||||
|
||||
func Register(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
|
||||
tr.RegisterSystemTemplate(templates.SystemTemplateMeta{
|
||||
Key: "lcars",
|
||||
Title: "LCARS",
|
||||
Description: "Star Trek LCARS computer interface theme",
|
||||
})
|
||||
|
||||
if err := tr.RegisterPageTemplate("lcars", templates.PageTemplateMeta{
|
||||
Key: "default",
|
||||
Title: "Standard Display",
|
||||
Description: "Classic LCARS layout with sidebar and header frame",
|
||||
Slots: []string{"header", "sidebar", "main", "footer"},
|
||||
}, engine.MustPageTemplate("default.html")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tr.RegisterPageTemplate("lcars", templates.PageTemplateMeta{
|
||||
Key: "full-display",
|
||||
Title: "Full Display",
|
||||
Description: "Full-width LCARS display without sidebar",
|
||||
Slots: []string{"header", "main", "footer"},
|
||||
}, engine.MustPageTemplate("full_display.html")); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := br.LoadSchemasFromFS(SchemasFS()); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
br.Register(LCARSHeaderMeta, engine.MustBlockTemplateWithDefaults("blocks/header.html", headerDefaults))
|
||||
br.Register(LCARSSidebarMeta, engine.MustBlockTemplateWithDefaults("blocks/sidebar.html", sidebarDefaults))
|
||||
br.Register(LCARSPanelMeta, engine.MustBlockTemplate("blocks/panel.html"))
|
||||
|
||||
br.RegisterTemplateOverride("lcars", "heading", engine.MustTemplateOverride("blocks/heading_override.html"))
|
||||
br.RegisterTemplateOverride("lcars", "text", engine.MustTemplateOverride("blocks/text_override.html"))
|
||||
|
||||
tr.RegisterEmailWrapper("lcars", engine.MustEmailWrapper("email_wrapper.html"))
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -1,24 +0,0 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
|
||||
"git.dev.alexdunmow.com/block/core/blocks"
|
||||
"git.dev.alexdunmow.com/block/core/plugin"
|
||||
"git.dev.alexdunmow.com/block/core/templates"
|
||||
)
|
||||
|
||||
var Registration = plugin.PluginRegistration{
|
||||
Name: "lcars",
|
||||
Version: plugin.ParseModVersion(pluginModBytes),
|
||||
Register: func(tr templates.TemplateRegistry, br blocks.BlockRegistry) error {
|
||||
return Register(tr, br)
|
||||
},
|
||||
Assets: func() http.Handler { return AssetsHandler() },
|
||||
Schemas: func() fs.FS { return SchemasFS() },
|
||||
ThemePresets: func() []byte { return ThemePresets() },
|
||||
BundledFonts: func() []byte { return BundledFonts() },
|
||||
MasterPages: func() []plugin.MasterPageDefinition { return DefaultMasterPages() },
|
||||
CSSManifest: func() *plugin.CSSManifest { return ThemeCSSManifest() },
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "LCARS Header",
|
||||
"description": "Top bar with elbow bracket, status indicators, and stardate",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "Title",
|
||||
"description": "Main display title",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"subtitle": {
|
||||
"type": "string",
|
||||
"title": "Subtitle",
|
||||
"description": "Secondary text below the title",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"stardate": {
|
||||
"type": "string",
|
||||
"title": "Stardate",
|
||||
"description": "Stardate display (leave empty for auto)",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"status": {
|
||||
"type": "string",
|
||||
"title": "Status",
|
||||
"description": "System status indicator",
|
||||
"enum": ["online", "standby", "alert", "offline"],
|
||||
"default": "online",
|
||||
"x-editor": "select"
|
||||
},
|
||||
"menu_name": {
|
||||
"type": "string",
|
||||
"title": "Menu Name",
|
||||
"description": "Navigation menu to display",
|
||||
"x-editor": "text"
|
||||
}
|
||||
},
|
||||
"required": ["title"]
|
||||
}
|
||||
@ -1,29 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "LCARS Panel",
|
||||
"description": "Framed content area with LCARS border treatment",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"title": "Panel Title",
|
||||
"description": "Title shown in the panel header bar",
|
||||
"x-editor": "text"
|
||||
},
|
||||
"content": {
|
||||
"type": "string",
|
||||
"title": "Content",
|
||||
"description": "Panel body content (HTML)",
|
||||
"x-editor": "richtext"
|
||||
},
|
||||
"border_color": {
|
||||
"type": "string",
|
||||
"title": "Border Color",
|
||||
"description": "Which theme color for the panel border",
|
||||
"enum": ["primary", "secondary", "accent"],
|
||||
"default": "primary",
|
||||
"x-editor": "select"
|
||||
}
|
||||
},
|
||||
"required": ["title"]
|
||||
}
|
||||
@ -1,33 +0,0 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"title": "LCARS Sidebar",
|
||||
"description": "Left panel with rounded-rectangle navigation buttons",
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"items": {
|
||||
"type": "array",
|
||||
"title": "Navigation Items",
|
||||
"description": "List of sidebar navigation buttons",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"type": "string",
|
||||
"title": "Label"
|
||||
},
|
||||
"url": {
|
||||
"type": "string",
|
||||
"title": "URL"
|
||||
},
|
||||
"color": {
|
||||
"type": "string",
|
||||
"title": "Color",
|
||||
"enum": ["primary", "secondary", "accent"],
|
||||
"default": "primary"
|
||||
}
|
||||
},
|
||||
"required": ["label", "url"]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
screenshots/01-home-light.png
Normal file
|
After Width: | Height: | Size: 982 KiB |
BIN
screenshots/02-home-dark.png
Normal file
|
After Width: | Height: | Size: 984 KiB |
BIN
screenshots/03-about-light.png
Normal file
|
After Width: | Height: | Size: 986 KiB |
BIN
screenshots/04-about-dark.png
Normal file
|
After Width: | Height: | Size: 987 KiB |
BIN
screenshots/05-episodes-first-contact-light.png
Normal file
|
After Width: | Height: | Size: 256 KiB |
BIN
screenshots/06-episodes-first-contact-dark.png
Normal file
|
After Width: | Height: | Size: 265 KiB |
BIN
seed/media/console.jpg
Normal file
|
After Width: | Height: | Size: 321 KiB |
BIN
seed/media/control-buttons.jpg
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
seed/media/deep-space.jpg
Normal file
|
After Width: | Height: | Size: 97 KiB |
BIN
seed/media/milky-way.jpg
Normal file
|
After Width: | Height: | Size: 313 KiB |
BIN
seed/media/nebula.jpg
Normal file
|
After Width: | Height: | Size: 511 KiB |
BIN
seed/media/starfield.jpg
Normal file
|
After Width: | Height: | Size: 290 KiB |
129
seed/seed.json
Normal file
@ -0,0 +1,129 @@
|
||||
{
|
||||
"demo": true,
|
||||
"settings": {
|
||||
"merge": {
|
||||
"site_name": "Subspace Signal",
|
||||
"tagline": "A weekly transmission on science fiction and the future."
|
||||
}
|
||||
},
|
||||
"media": [
|
||||
{ "id": "435207dd-8607-4fe6-b8fa-fba0c7486e9f", "file": "media/nebula.jpg", "alt": "The Lagoon Nebula glowing violet and gold in deep space" },
|
||||
{ "id": "74eb0f23-fb2b-419a-ab3e-06410aa71d64", "file": "media/milky-way.jpg", "alt": "The Milky Way arcing over a dark horizon" },
|
||||
{ "id": "5d6beec6-a06c-4af7-b1fe-48869f09cef3", "file": "media/starfield.jpg", "alt": "A dense field of stars across the night sky" },
|
||||
{ "id": "78c7e990-2682-4b39-acb4-cb7868e36e41", "file": "media/deep-space.jpg", "alt": "A distant world against the depth of the cosmos" },
|
||||
{ "id": "b1a9259a-919f-4ac7-855f-315f295a6536", "file": "media/console.jpg", "alt": "A futuristic spaceship control console lit in amber" },
|
||||
{ "id": "68341d09-4984-4a9c-ba91-f4005bcb52fd", "file": "media/control-buttons.jpg", "alt": "Illuminated control-panel buttons on a bridge console" }
|
||||
],
|
||||
"data_tables": [
|
||||
{
|
||||
"key": "hail_signals",
|
||||
"name": "Hail signals",
|
||||
"description": "Messages sent from the Subspace Signal contact console.",
|
||||
"primary_key": "id",
|
||||
"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": "Subspace Signal",
|
||||
"template_key": "full-width",
|
||||
"reconcile_blocks": true,
|
||||
"blocks": [
|
||||
{ "block_key": "hero", "title": "Hero", "slot": "main", "sort_order": 1, "content": { "template": "split", "mediaPosition": "right", "backgroundImage": "435207dd-8607-4fe6-b8fa-fba0c7486e9f", "eyebrow": "Now broadcasting", "headline": "Subspace Signal", "subheadline": "A weekly transmission on science fiction, deep space, and the people building the future. Two hosts. One open channel.", "ctaPrimary": { "text": "Listen now", "url": "/episodes" }, "ctaSecondary": { "text": "Meet the crew", "url": "/about" } } },
|
||||
{ "block_key": "lcars:episode_console", "title": "Featured episode", "slot": "main", "sort_order": 2, "content": { "eyebrow": "Latest transmission", "title": "First contact was a podcast", "cover": "74eb0f23-fb2b-419a-ab3e-06410aa71d64", "episodeNumber": "048", "duration": "58:12", "stardate": "78412.3", "description": "We open the season with a simple question. If a signal reached us tomorrow, who would answer, and what would we say back.", "listenUrl": "/episodes/first-contact", "listenLabel": "Listen now", "platforms": [ { "label": "Apple", "url": "/episodes" }, { "label": "Spotify", "url": "/episodes" }, { "label": "RSS", "url": "/episodes" } ] } },
|
||||
{ "block_key": "stats", "title": "Signal log", "slot": "main", "sort_order": 3, "content": { "heading": "Signal log", "columns": 3, "countUp": false, "items": [ { "value": "48", "label": "Episodes aired" }, { "value": "4", "label": "Seasons on air" }, { "value": "26", "label": "Star systems covered" } ] } },
|
||||
{ "block_key": "lcars:crew_roster", "title": "Crew", "slot": "main", "sort_order": 4, "content": { "eyebrow": "On the bridge", "heading": "Your crew", "members": [ { "name": "Vega Okonkwo", "role": "Host", "callsign": "Bridge-1", "bio": "Reads too much hard science fiction and asks the questions engineers dread." }, { "name": "Rell Tavras", "role": "Co-host", "callsign": "Science-2", "bio": "Keeps the physics honest and the metaphors from getting away from us." } ] } },
|
||||
{ "block_key": "cta", "title": "Closing CTA", "slot": "main", "sort_order": 5, "content": { "background": "band", "heading": "Tune the frequency", "text": "New transmission every week. Subscribe on your channel of choice.", "primaryLabel": "Subscribe", "primaryUrl": "/episodes" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/about",
|
||||
"title": "About",
|
||||
"template_key": "default",
|
||||
"reconcile_blocks": true,
|
||||
"blocks": [
|
||||
{ "block_key": "heading", "title": "Heading", "slot": "main", "sort_order": 1, "content": { "text": "About the signal", "level": 1 } },
|
||||
{ "block_key": "text", "title": "Intro", "slot": "main", "sort_order": 2, "content": { "text": "<p>Subspace Signal is a weekly show about science fiction and the real science it borrows from. We read the books, watch the shows, and talk to the people who build the future for a living. Every episode is one open channel between two hosts and you.</p>" } },
|
||||
{ "block_key": "image", "title": "Bridge console", "slot": "main", "sort_order": 3, "content": { "src": "68341d09-4984-4a9c-ba91-f4005bcb52fd", "alt": "Illuminated control-panel buttons on a bridge console", "caption": "The Subspace Signal recording console, Deck 12.", "align": "center" } },
|
||||
{ "block_key": "lcars:transmission_status", "title": "Systems", "slot": "main", "sort_order": 4, "content": { "eyebrow": "Console", "heading": "Channel status", "systems": [ { "label": "Weekly uplink", "status": "online", "detail": "Every Tuesday" }, { "label": "Back catalogue", "status": "online", "detail": "48 episodes" }, { "label": "Listener comms", "status": "standby", "detail": "Open for hails" }, { "label": "Live recordings", "status": "standby", "detail": "Next season" } ] } },
|
||||
{ "block_key": "lcars:stardate_log", "title": "Log", "slot": "main", "sort_order": 5, "content": { "eyebrow": "Ship's log", "heading": "How we got here", "entries": [ { "stardate": "74001.0", "title": "Signal first opened", "text": "Two friends started recording arguments about warp drives in a spare room." }, { "stardate": "76210.5", "title": "First guest aboard", "text": "A working propulsion engineer joined to explain why the arguments were wrong." }, { "stardate": "78412.3", "title": "Season four begins", "text": "New gear, same open channel, more star systems to cover." } ] } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/episodes",
|
||||
"title": "Episodes",
|
||||
"template_key": "blog-index",
|
||||
"reconcile_blocks": true,
|
||||
"blocks": [
|
||||
{ "block_key": "card", "title": "Episode 48", "slot": "main", "sort_order": 1, "content": { "title": "First contact was a podcast", "media": "78c7e990-2682-4b39-acb4-cb7868e36e41", "text": "<p>If a signal reached us tomorrow, who would answer, and what would we say back.</p>", "link": "/episodes/first-contact", "linkLabel": "Listen" } },
|
||||
{ "block_key": "card", "title": "Episode 47", "slot": "main", "sort_order": 2, "content": { "title": "The physics of warp, honestly", "media": "5d6beec6-a06c-4af7-b1fe-48869f09cef3", "text": "<p>A propulsion engineer walks us through what the math actually allows.</p>", "link": "/episodes/warp-honestly", "linkLabel": "Listen" } },
|
||||
{ "block_key": "card", "title": "Episode 46", "slot": "main", "sort_order": 3, "content": { "title": "Terraforming on a budget", "media": "b1a9259a-919f-4ac7-855f-315f295a6536", "text": "<p>What it would really take to make one small world a little more livable.</p>", "link": "/episodes/terraforming", "linkLabel": "Listen" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/episodes/first-contact",
|
||||
"parent_slug": "/episodes",
|
||||
"title": "First contact was a podcast",
|
||||
"template_key": "article",
|
||||
"reconcile_blocks": true,
|
||||
"blocks": [
|
||||
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "First contact was a podcast", "level": 1 } },
|
||||
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "<p>Stardate 78412.3. We open the season with a simple question, the kind that sounds small until you try to answer it out loud. If a signal reached us tomorrow, who would answer, and what would we say back? The honest answer is that nobody is really in charge of the reply, and that is where this whole transmission begins.</p><p>Start with the receiving end. There is no single red phone that rings when the universe calls. A confirmed detection would arrive not as a voice but as a pattern buried in noise, checked and rechecked across observatories on three continents before anyone dared say the word out loud. By the time it was real, it would already have passed through a hundred hands, and not one of them would have the authority to speak for the planet.</p><p>So the argument that fills the first act of tonight's episode is not about aliens at all. It is about us. Who gets to draft humanity's opening line? A committee? A head of state? The astronomer who happened to be on shift? We walk through the protocols that actually exist on paper, and then we walk through how quickly those protocols would dissolve the moment a real signal turned the abstract into the urgent.</p><p>Rell makes the case that the first message should be pure mathematics. Prime numbers, the value of pi, the hydrogen line. Nothing that could be misread, nothing that carries the baggage of a culture or a century. A handshake in the only language two strangers across a galaxy could possibly share. It is a clean idea, and for about four minutes it is winning.</p><p>Then Vega asks the question that undoes it. If the whole point of answering is to say something true about who we are, why would we lead with the one part of ourselves that looks identical to every other species that can count? Mathematics proves we are here. It says nothing about what it is like to be here. And maybe, she argues, that second thing is the message.</p><p>We bring in a linguist who has spent a career thinking about first contact between human languages that share no common root. Her warning is sobering. Every act of translation smuggles in assumptions, she says, and the assumptions you cannot see are the ones that hurt you. The first message is not a greeting. It is a lens, and whoever builds it decides what the other side will see of us first.</p><p>That reframes the engineering problem entirely. Bandwidth stops being the constraint. Intent becomes the constraint. You are not trying to transmit the most information. You are trying to transmit the least misleading version of an entire civilization in a package small enough to survive the crossing intact.</p><p>Halfway through, we do the exercise ourselves. Live, unscripted, we each try to write three lines we would put at the front of the reply. It is harder than it sounds, and both attempts are worse than we expected. Every draft either flatters us or apologizes for us. Nobody writes the plain, unhurried truth on the first try, which turns out to be the most human result of all.</p><p>What we keep circling back to is patience. A conversation across light-years is measured in lifetimes. You will not hear back. Your grandchildren might. That changes the character of the message from a statement into an inheritance, something written to be finished by people you will never meet, in a context you cannot predict.</p><p>We close the way we opened, with the question turned around. Not what would we say to them, but what do we wish someone had said to us, early, before we knew enough to be afraid of the answer. That is the message. That is what we would send. And if the channel stays quiet for another thousand years, at least we will have spent the wait learning how to speak plainly to each other.</p><p>Next week we hand the microphone to a working propulsion engineer and ask him to break our hearts about warp drive. Until then, this is Subspace Signal, and the channel stays open.</p>" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/episodes/warp-honestly",
|
||||
"parent_slug": "/episodes",
|
||||
"title": "The physics of warp, honestly",
|
||||
"template_key": "article",
|
||||
"reconcile_blocks": true,
|
||||
"blocks": [
|
||||
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "The physics of warp, honestly", "level": 1 } },
|
||||
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "<p>Stardate 78295.1. Warp drive makes for a great story and a hard equation. This week we asked a working propulsion engineer to walk us through what the math actually allows, and where the fiction quietly cheats to keep the plot moving.</p><p>The short version is that the energy budget is the whole story. Everything else is set dressing until you solve for that. Bend spacetime enough to matter and the numbers you get back are not large in the everyday sense. They are large in the sense that they compare to the output of stars.</p><p>Our guest is generous about the fiction. He does not want warp drive to be impossible, and he is careful to separate what is forbidden by physics from what is merely absurd by engineering. The first list is short. The second list is long, and it is where the honest heartbreak lives.</p><p>We spend a while on exotic matter, the placeholder every warp metric leans on to make the equations behave. It is not a material you could ever put in a crate. It is a property the math demands and the universe has never once been observed to provide at the scale a ship would need.</p><p>By the end, nobody at the table wants to give up the dream. We just want to hold it honestly. The stars are not close, the shortcuts are not free, and the wanting is the most real part of the whole enterprise. That, at least, the math cannot take away from us.</p>" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/episodes/terraforming",
|
||||
"parent_slug": "/episodes",
|
||||
"title": "Terraforming on a budget",
|
||||
"template_key": "article",
|
||||
"reconcile_blocks": true,
|
||||
"blocks": [
|
||||
{ "block_key": "heading", "title": "Title", "slot": "main", "sort_order": 1, "content": { "text": "Terraforming on a budget", "level": 1 } },
|
||||
{ "block_key": "text", "title": "Body", "slot": "main", "sort_order": 2, "content": { "text": "<p>Stardate 78190.7. Making a whole planet livable is a project measured in centuries. So this week we asked a smaller, more honest question. What is the cheapest first step that would make one small world a little more forgiving to the people who came to stay?</p><p>The answer is not domes or rockets. It is patience, pressure, and a very long supply of the right microbes. The glamorous engineering comes much later, if it comes at all. The early work is closer to gardening than to conquest.</p><p>Our guest, a planetary scientist who models atmospheres for a living, keeps pulling us back to the timescales. Nothing about this happens in a human career. You plant a process and you hand it to people who are not born yet, and you trust them to keep the thermostat pointed the right way.</p><p>We talk about the ethics of it too. A world you are slowly changing might already have something living on it, quiet and slow and easy to miss. The budget that matters most, she argues, is not measured in credits. It is measured in how carefully you look before you begin.</p><p>We end where terraforming actually starts, with a single greenhouse and a stubborn strain of algae. Small, unglamorous, and real. The rest is just that same patience, repeated for a thousand years, until one morning the air outside is one degree kinder than it was.</p>" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/contact",
|
||||
"title": "Hail us",
|
||||
"template_key": "contact",
|
||||
"reconcile_blocks": true,
|
||||
"blocks": [
|
||||
{ "block_key": "contact-form", "title": "Hail console", "slot": "main", "sort_order": 1, "content": { "heading": "Open a channel", "description": "Send a hail. We read every signal and reply within a few days.", "submitLabel": "Transmit", "successMessage": "Signal received. We will reply on this channel shortly.", "formConfig": { "targetTable": "hail_signals", "enabled": true, "captchaEnabled": false }, "fields": [ { "name": "name", "label": "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": "Message", "type": "textarea", "required": true, "width": "full" } ] } },
|
||||
{ "block_key": "contact-card", "title": "Direct channels", "slot": "aside", "sort_order": 1, "content": { "heading": "Direct channels", "columns": 1, "channels": [ { "icon": "lucide:mail", "label": "Email", "value": "hail@subspace.example", "link": "mailto:hail@subspace.example" }, { "icon": "lucide:radio", "label": "Studio line", "value": "+61 2 5550 0148", "link": "tel:+61255500148" } ] } },
|
||||
{ "block_key": "hours-location", "title": "Broadcast hours", "slot": "aside", "sort_order": 2, "content": { "heading": "Broadcast hours", "address": "Deck 12, Signal Studios, Sydney NSW 2000", "hours": [ { "day": "New episode", "hours": "Tuesdays 06:00" }, { "day": "Listener comms", "hours": "Monday to Friday 09:00 to 17:00" }, { "day": "Weekend", "closed": true } ] } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"slug": "/login",
|
||||
"title": "Access terminal",
|
||||
"template_key": "auth",
|
||||
"reconcile_blocks": true,
|
||||
"blocks": [
|
||||
{ "block_key": "auth-form", "title": "Sign in", "slot": "main", "sort_order": 1, "content": { "default_tab": "login", "show_social": false } }
|
||||
]
|
||||
}
|
||||
],
|
||||
"menu_items": [
|
||||
{ "menu": "main", "label": "Home", "page_slug": "/", "sort_order": 1 },
|
||||
{ "menu": "main", "label": "About", "page_slug": "/about", "sort_order": 2 },
|
||||
{ "menu": "main", "label": "Episodes", "page_slug": "/episodes", "sort_order": 3 },
|
||||
{ "menu": "main", "label": "Hail us", "page_slug": "/contact", "sort_order": 4 }
|
||||
]
|
||||
}
|
||||
19
seed/workflows.json
Normal file
@ -0,0 +1,19 @@
|
||||
{
|
||||
"workflows": [
|
||||
{
|
||||
"key": "hail_notify_admins",
|
||||
"name": "Notify admins on hail signal",
|
||||
"description": "Emails site admins whenever the Subspace Signal contact console receives a hail.",
|
||||
"enabled": true,
|
||||
"trigger": { "type": "row_inserted", "table": "hail_signals" },
|
||||
"steps": [
|
||||
{
|
||||
"type": "email",
|
||||
"name": "Email admins",
|
||||
"template": "Form Submission Notification",
|
||||
"recipients": { "mode": "admins", "scope": "all" }
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -1,23 +0,0 @@
|
||||
<header class="lcars-header">
|
||||
<div class="lcars-header-bar">
|
||||
<div class="lcars-header-pill lcars-bg-primary"></div>
|
||||
<div class="lcars-header-title-area">
|
||||
<h1 class="lcars-title">{{ title }}</h1>
|
||||
{% if subtitle %}
|
||||
<span class="lcars-subtitle">{{ subtitle }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="lcars-header-indicators">
|
||||
{% if stardate %}
|
||||
<span class="lcars-stardate">SD {{ stardate }}</span>
|
||||
{% endif %}
|
||||
<span class="lcars-status lcars-status-{{ status|default:"online" }}">
|
||||
{{ status|default:"online"|upper }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="lcars-header-pill lcars-bg-secondary"></div>
|
||||
</div>
|
||||
{% if ctx.isEditor %}
|
||||
<div class="lcars-editor-badge">EDIT MODE</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
@ -1 +0,0 @@
|
||||
<{{ tag|default:"h2" }} class="lcars-heading lcars-heading-{{ level|default:2 }}">{{ text }}</{{ tag|default:"h2" }}>
|
||||
@ -1,14 +0,0 @@
|
||||
<div class="lcars-panel lcars-panel-{{ border_color|default:"primary" }}">
|
||||
{% if title %}
|
||||
<div class="lcars-panel-header">
|
||||
<div class="lcars-panel-header-pill"></div>
|
||||
<span class="lcars-panel-title">{{ title }}</span>
|
||||
<div class="lcars-panel-header-bar"></div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="lcars-panel-body">
|
||||
{% if content %}
|
||||
{{ content|safe }}
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
@ -1,11 +0,0 @@
|
||||
<nav class="lcars-sidebar">
|
||||
{% for item in items %}
|
||||
<a href="{{ item.url }}" class="lcars-sidebar-btn lcars-bg-{{ item.color|default:"primary" }}">
|
||||
<span class="lcars-sidebar-btn-label">{{ item.label }}</span>
|
||||
</a>
|
||||
{% empty %}
|
||||
<div class="lcars-sidebar-btn lcars-bg-muted">
|
||||
<span class="lcars-sidebar-btn-label">No items</span>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
@ -1 +0,0 @@
|
||||
<div class="lcars-prose">{{ html|safe }}</div>
|
||||
@ -1,45 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block body_class %}lcars-page lcars-default bg-background text-foreground antialiased min-h-screen{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="lcars-frame">
|
||||
<div class="lcars-header-area">
|
||||
{{ slots.header|safe }}
|
||||
</div>
|
||||
|
||||
<div class="lcars-body">
|
||||
<div class="lcars-elbow-top">
|
||||
<div class="lcars-elbow-corner lcars-elbow-tl"></div>
|
||||
<div class="lcars-bar lcars-bar-top"></div>
|
||||
</div>
|
||||
|
||||
<div class="lcars-content-area">
|
||||
<aside class="lcars-sidebar-area">
|
||||
{{ slots.sidebar|safe }}
|
||||
</aside>
|
||||
|
||||
<div class="lcars-divider"></div>
|
||||
|
||||
<main class="lcars-main-area">
|
||||
{% if slots.main %}
|
||||
{{ slots.main|safe }}
|
||||
{% else %}
|
||||
<div class="lcars-empty-state">
|
||||
<p class="text-muted-foreground uppercase tracking-widest">No data on file</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<div class="lcars-elbow-bottom">
|
||||
<div class="lcars-elbow-corner lcars-elbow-bl"></div>
|
||||
<div class="lcars-bar lcars-bar-bottom"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="lcars-footer-area">
|
||||
{{ slots.footer|safe }}
|
||||
</footer>
|
||||
</div>
|
||||
{% endblock %}
|
||||
66
templates/email/lcars.ninjatpl
Normal file
@ -0,0 +1,66 @@
|
||||
<!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">
|
||||
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; 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) {
|
||||
.lcars-email-container { width: 100% !important; max-width: 100% !important; }
|
||||
.lcars-email-padding { padding-left: 24px !important; padding-right: 24px !important; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body style="background-color: #0b0b10; margin: 0; padding: 0; font-family: 'Antonio', 'Arial Narrow', Arial, sans-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: 32px 12px; background-color: #0b0b10;">
|
||||
<table role="presentation" class="lcars-email-container" width="600" cellspacing="0" cellpadding="0" border="0" style="max-width: 600px; width: 600px; background-color: #14141b; border-collapse: separate; border-radius: 10px; overflow: hidden;">
|
||||
<!-- Header: dark bar + uppercase title -->
|
||||
<tr>
|
||||
<td class="lcars-email-padding" style="padding: 28px 48px 12px 48px; background-color: #14141b;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr>
|
||||
<td style="width: 56px;"><div style="width: 48px; height: 20px; background-color: #ff8c1a; border-radius: 10px; line-height: 20px; font-size: 0;"> </div></td>
|
||||
<td>{% if logo_url %}<img src="{{ logo_url }}" alt="{{ site_name }}" style="max-height: 34px; width: auto; display: block;">{% elif site_name %}<span style="font-size: 22px; font-weight: 700; text-transform: uppercase; letter-spacing: 3px; color: #ff8c1a; font-family: 'Antonio', 'Arial Narrow', Arial, sans-serif;">{{ site_name }}</span>{% endif %}</td>
|
||||
</tr></table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- LCARS tri-colour bar as solid table cells -->
|
||||
<tr>
|
||||
<td style="padding: 8px 48px 0 48px;">
|
||||
<table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr>
|
||||
<td style="width: 34%; height: 6px; background-color: #ff8c1a; line-height: 6px; font-size: 0;"> </td>
|
||||
<td style="width: 4px; font-size: 0; line-height: 6px;"> </td>
|
||||
<td style="width: 33%; height: 6px; background-color: #f9b23c; line-height: 6px; font-size: 0;"> </td>
|
||||
<td style="width: 4px; font-size: 0; line-height: 6px;"> </td>
|
||||
<td style="width: 33%; height: 6px; background-color: #b89ae6; line-height: 6px; font-size: 0;"> </td>
|
||||
</tr></table>
|
||||
</td>
|
||||
</tr>
|
||||
<!-- Body -->
|
||||
<tr>
|
||||
<td class="lcars-email-padding" style="padding: 28px 48px; color: #e6ddc9; font-size: 16px; line-height: 1.65; font-family: 'Antonio', 'Arial Narrow', Arial, sans-serif;">{{ body|safe }}</td>
|
||||
</tr>
|
||||
<!-- Footer -->
|
||||
<tr>
|
||||
<td class="lcars-email-padding" style="padding: 20px 48px 30px 48px; border-top: 2px solid #2a2a34;">
|
||||
{% if site_name %}<p style="margin: 0 0 6px 0; font-size: 12px; color: #ff8c1a; font-weight: 700; letter-spacing: 2px; text-transform: uppercase; font-family: 'Antonio', 'Arial Narrow', Arial, sans-serif;">{{ site_name }}</p>{% endif %}
|
||||
<p style="margin: 0 0 12px 0; font-size: 12px; color: #8c8a94; font-family: 'Courier New', monospace; letter-spacing: 1px;">You are receiving this transmission because you subscribed to updates.</p>
|
||||
{% if site_url %}<p style="margin: 0 0 8px 0; font-size: 12px;"><a href="{{ site_url }}" style="color: #f9b23c; text-decoration: none;">{{ site_url }}</a></p>{% endif %}
|
||||
{% if unsubscribe_url %}<p style="margin: 0; font-size: 11px;"><a href="{{ unsubscribe_url }}" style="color: #8c8a94; text-decoration: underline;">Unsubscribe</a></p>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,43 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
{% if preview_text %}
|
||||
<span style="display:none;max-height:0;overflow:hidden;">{{ preview_text }}</span>
|
||||
{% endif %}
|
||||
<style>
|
||||
body { margin: 0; padding: 0; background-color: #050505; color: #e0d4b0; font-family: Arial, Helvetica, sans-serif; }
|
||||
.lcars-email { max-width: 600px; margin: 0 auto; padding: 20px; }
|
||||
.lcars-email-header { padding: 16px 20px; }
|
||||
.lcars-email-header table { width: 100%; }
|
||||
.lcars-email-pill { width: 80px; height: 32px; border-radius: 16px; background-color: {{ colors.primary }}; }
|
||||
.lcars-email-title { font-size: 20px; font-weight: bold; text-transform: uppercase; letter-spacing: 3px; color: {{ colors.primary }}; padding-left: 12px; }
|
||||
.lcars-email-bar { height: 4px; border-radius: 2px; margin-bottom: 24px; }
|
||||
.lcars-email-body { padding: 0 20px; line-height: 1.6; font-size: 16px; }
|
||||
.lcars-email-body a { color: {{ colors.secondary }}; text-decoration: underline; }
|
||||
.lcars-email-footer { margin-top: 32px; padding-top: 16px; border-top: 2px solid {{ colors.border }}; font-size: 12px; text-transform: uppercase; letter-spacing: 2px; opacity: 0.6; text-align: center; }
|
||||
.lcars-email-footer a { color: {{ colors.secondary }}; text-decoration: underline; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="lcars-email">
|
||||
<div class="lcars-email-header">
|
||||
<table><tr>
|
||||
<td style="width:80px;"><div class="lcars-email-pill"></div></td>
|
||||
<td class="lcars-email-title">{{ site_name }}</td>
|
||||
</tr></table>
|
||||
</div>
|
||||
|
||||
<div class="lcars-email-bar" style="background: linear-gradient(to right, {{ colors.primary }} 0%, {{ colors.primary }} 30%, {{ colors.secondary }} 30%, {{ colors.secondary }} 60%, {{ colors.muted }} 60%, {{ colors.muted }} 100%);"></div>
|
||||
|
||||
<div class="lcars-email-body">
|
||||
{{ body|safe }}
|
||||
</div>
|
||||
|
||||
<div class="lcars-email-footer">
|
||||
{{ site_name }}{% if unsubscribe_url %} · <a href="{{ unsubscribe_url }}">Unsubscribe</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
@ -1,31 +0,0 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block body_class %}lcars-page lcars-full-display bg-background text-foreground antialiased min-h-screen{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="lcars-frame lcars-frame-full">
|
||||
<div class="lcars-header-area">
|
||||
{{ slots.header|safe }}
|
||||
</div>
|
||||
|
||||
<div class="lcars-body lcars-body-full">
|
||||
<div class="lcars-bar-accent"></div>
|
||||
|
||||
<main class="lcars-main-area lcars-main-full">
|
||||
{% if slots.main %}
|
||||
{{ slots.main|safe }}
|
||||
{% else %}
|
||||
<div class="lcars-empty-state">
|
||||
<p class="text-muted-foreground uppercase tracking-widest">No data on file</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
|
||||
<div class="lcars-bar-accent"></div>
|
||||
</div>
|
||||
|
||||
<footer class="lcars-footer-area">
|
||||
{{ slots.footer|safe }}
|
||||
</footer>
|
||||
</div>
|
||||
{% endblock %}
|
||||
18
templates/lcars/article.ninjatpl
Normal file
@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
{{ head_html|safe }}
|
||||
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
|
||||
{{ admin_banner_html|safe }}
|
||||
<div class="lcars-frame" style="flex:1 1 auto;">
|
||||
<div class="lcars-elbow-top"><div class="lcars-elbow-corner lcars-elbow-tl"></div><div class="lcars-bar lcars-bar-top"></div></div>
|
||||
{% if slots.header %}<header class="lcars-well" style="padding-top:0.25rem;padding-bottom:0.25rem;">{{ slots.header|safe }}</header>{% endif %}
|
||||
<main class="lcars-well" style="flex:1 1 auto;padding-top:1.5rem;padding-bottom:2.5rem;" data-content-well>
|
||||
<article class="lcars-measure lcars-panel" style="margin:0 auto;">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="lcars-readout">No transcript on file.</p>{% endif %}</article>
|
||||
{% if slots.aside %}<aside class="lcars-measure" style="margin:2rem auto 0;border-top:0.1875rem solid hsl(var(--border));padding-top:1.5rem;">{{ slots.aside|safe }}</aside>{% endif %}
|
||||
</main>
|
||||
<div class="lcars-elbow-bottom"><div class="lcars-elbow-corner lcars-elbow-bl"></div><div class="lcars-bar lcars-bar-bottom"></div></div>
|
||||
</div>
|
||||
{% if slots.footer %}<footer style="margin-top:auto;">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
18
templates/lcars/auth.ninjatpl
Normal file
@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
{{ head_html|safe }}
|
||||
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
|
||||
{{ admin_banner_html|safe }}
|
||||
<main style="flex:1 1 auto;width:100%;display:flex;flex-direction:column;align-items:center;justify-content:center;padding:3rem 1.5rem;">
|
||||
<div style="width:100%;max-width:28rem;">
|
||||
<div class="lcars-elbow-top" style="height:1.5rem;margin-bottom:0.375rem;"><div class="lcars-elbow-corner lcars-elbow-tl" style="width:7rem;min-width:7rem;"></div><div class="lcars-bar lcars-bar-top"></div></div>
|
||||
<a href="/" aria-label="{{ site_settings.Title|default:"Home" }}" class="lcars-brand" style="margin-bottom:1.25rem;">{{ site_settings.Title|default:"LCARS" }}</a>
|
||||
<div class="lcars-panel lcars-reveal" style="padding:1.75rem;">
|
||||
{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="lcars-readout">No access console assigned.</p>{% endif %}
|
||||
</div>
|
||||
{% if slots.footer %}<div class="lcars-form-note" style="text-align:center;margin-top:1.25rem;">{{ slots.footer|safe }}</div>{% endif %}
|
||||
</div>
|
||||
</main>
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
19
templates/lcars/blog-index.ninjatpl
Normal file
@ -0,0 +1,19 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
{{ head_html|safe }}
|
||||
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
|
||||
{{ admin_banner_html|safe }}
|
||||
<div class="lcars-frame" style="flex:1 1 auto;">
|
||||
<div class="lcars-elbow-top"><div class="lcars-elbow-corner lcars-elbow-tl"></div><div class="lcars-bar lcars-bar-top"></div></div>
|
||||
{% if slots.header %}<header class="lcars-well" style="padding-top:0.25rem;padding-bottom:0.25rem;">{{ slots.header|safe }}</header>{% endif %}
|
||||
<main class="lcars-well" style="flex:1 1 auto;padding-top:2rem;padding-bottom:2.5rem;" data-content-well>
|
||||
{% if title %}<div style="border-bottom:0.1875rem solid hsl(var(--primary));padding-bottom:0.75rem;margin-bottom:2rem;"><p class="lcars-eyebrow">Broadcast Log</p><h1 class="lcars-heading lcars-heading-1" style="margin:0;">{{ title }}</h1>{% if page_meta.MetaDescription %}<p class="lcars-lede" style="max-width:60ch;margin:0.75rem 0 0;">{{ page_meta.MetaDescription }}</p>{% endif %}</div>{% endif %}
|
||||
{% if slots.featured %}<section style="margin-bottom:2.5rem;">{{ slots.featured|safe }}</section>{% endif %}
|
||||
{% if slots.main %}<section>{{ slots.main|safe }}</section>{% else %}<p class="lcars-readout">No episodes logged yet.</p>{% endif %}
|
||||
</main>
|
||||
<div class="lcars-elbow-bottom"><div class="lcars-elbow-corner lcars-elbow-bl"></div><div class="lcars-bar lcars-bar-bottom"></div></div>
|
||||
</div>
|
||||
{% if slots.footer %}<footer style="margin-top:auto;">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
25
templates/lcars/contact.ninjatpl
Normal file
@ -0,0 +1,25 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
{{ head_html|safe }}
|
||||
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
|
||||
{{ admin_banner_html|safe }}
|
||||
<div class="lcars-frame" style="flex:1 1 auto;">
|
||||
<div class="lcars-elbow-top"><div class="lcars-elbow-corner lcars-elbow-tl"></div><div class="lcars-bar lcars-bar-top"></div></div>
|
||||
{% if slots.header %}<header class="lcars-well" style="padding-top:0.25rem;padding-bottom:0.25rem;">{{ slots.header|safe }}</header>{% endif %}
|
||||
<main class="lcars-well" style="flex:1 1 auto;padding-top:2rem;padding-bottom:2.5rem;" data-content-well>
|
||||
{% if slots.aside %}
|
||||
<div class="lcars-contact-grid" style="display:grid;gap:2rem;grid-template-columns:1fr;">
|
||||
<div style="min-width:0;">{{ slots.main|safe }}</div>
|
||||
<aside class="lcars-contact-aside" style="min-width:0;">{{ slots.aside|safe }}</aside>
|
||||
</div>
|
||||
<style>@media (min-width:1024px){.lcars-contact-grid{grid-template-columns:1.4fr 1fr;}}</style>
|
||||
{% else %}
|
||||
<div class="lcars-measure">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="lcars-readout">No channel configured.</p>{% endif %}</div>
|
||||
{% endif %}
|
||||
</main>
|
||||
<div class="lcars-elbow-bottom"><div class="lcars-elbow-corner lcars-elbow-bl"></div><div class="lcars-bar lcars-bar-bottom"></div></div>
|
||||
</div>
|
||||
{% if slots.footer %}<footer style="margin-top:auto;">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
22
templates/lcars/default.ninjatpl
Normal file
@ -0,0 +1,22 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
{{ head_html|safe }}
|
||||
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
|
||||
{{ admin_banner_html|safe }}
|
||||
<div class="lcars-frame" style="flex:1 1 auto;">
|
||||
<div class="lcars-elbow-top"><div class="lcars-elbow-corner lcars-elbow-tl"></div><div class="lcars-bar lcars-bar-top"></div></div>
|
||||
{% if slots.header %}<header class="lcars-well" style="padding-top:0.25rem;padding-bottom:0.25rem;">{{ slots.header|safe }}</header>{% endif %}
|
||||
{% if slots.sidebar %}
|
||||
<div class="lcars-well lcars-content-row" style="align-items:start;padding-top:0.75rem;padding-bottom:0.75rem;">
|
||||
<aside class="lcars-rail">{{ slots.sidebar|safe }}</aside>
|
||||
<main class="lcars-main-panel" style="min-width:0;">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="lcars-readout" style="padding:2rem 0;">No data on file.</p>{% endif %}</main>
|
||||
</div>
|
||||
{% else %}
|
||||
<main class="lcars-well" style="padding-top:0.75rem;padding-bottom:0.75rem;">{% if slots.main %}{{ slots.main|safe }}{% else %}<p class="lcars-readout" style="padding:2rem 0;">No data on file.</p>{% endif %}</main>
|
||||
{% endif %}
|
||||
<div class="lcars-elbow-bottom"><div class="lcars-elbow-corner lcars-elbow-bl"></div><div class="lcars-bar lcars-bar-bottom"></div></div>
|
||||
</div>
|
||||
{% if slots.footer %}<footer style="margin-top:auto;">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
15
templates/lcars/full-display.ninjatpl
Normal file
@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
{{ head_html|safe }}
|
||||
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
|
||||
{{ admin_banner_html|safe }}
|
||||
<div class="lcars-frame" style="flex:1 1 auto;">
|
||||
<div class="lcars-elbow-top"><div class="lcars-elbow-corner lcars-elbow-tl"></div><div class="lcars-bar lcars-bar-top"></div></div>
|
||||
{% if slots.header %}<header class="lcars-well" style="padding-top:0.25rem;padding-bottom:0.25rem;">{{ slots.header|safe }}</header>{% endif %}
|
||||
<main style="flex:1 1 auto;width:100%;" data-content-well>{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="lcars-well" style="padding-top:3rem;padding-bottom:3rem;"><p class="lcars-readout">No data on file.</p></div>{% endif %}</main>
|
||||
<div class="lcars-elbow-bottom"><div class="lcars-elbow-corner lcars-elbow-bl"></div><div class="lcars-bar lcars-bar-bottom"></div></div>
|
||||
</div>
|
||||
{% if slots.footer %}<footer style="margin-top:auto;">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
15
templates/lcars/full-width.ninjatpl
Normal file
@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
{{ head_html|safe }}
|
||||
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
|
||||
{{ admin_banner_html|safe }}
|
||||
<div class="lcars-frame" style="flex:1 1 auto;">
|
||||
<div class="lcars-elbow-top"><div class="lcars-elbow-corner lcars-elbow-tl"></div><div class="lcars-bar lcars-bar-top"></div></div>
|
||||
{% if slots.header %}<header class="lcars-well" style="padding-top:0.25rem;padding-bottom:0.25rem;">{{ slots.header|safe }}</header>{% endif %}
|
||||
<main style="flex:1 1 auto;width:100%;" data-content-well>{% if slots.main %}{{ slots.main|safe }}{% else %}<div class="lcars-well" style="padding-top:3rem;padding-bottom:3rem;"><p class="lcars-readout">No data on file.</p></div>{% endif %}</main>
|
||||
<div class="lcars-elbow-bottom"><div class="lcars-elbow-corner lcars-elbow-bl"></div><div class="lcars-bar lcars-bar-bottom"></div></div>
|
||||
</div>
|
||||
{% if slots.footer %}<footer style="margin-top:auto;">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
17
templates/lcars/landing.ninjatpl
Normal file
@ -0,0 +1,17 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
{{ head_html|safe }}
|
||||
<body class="lcars-page" style="margin:0;min-height:100vh;display:flex;flex-direction:column;background-color:hsl(var(--background));color:hsl(var(--foreground));">
|
||||
{{ admin_banner_html|safe }}
|
||||
<div class="lcars-frame" style="flex:1 1 auto;">
|
||||
<div class="lcars-elbow-top"><div class="lcars-elbow-corner lcars-elbow-tl"></div><div class="lcars-bar lcars-bar-top"></div></div>
|
||||
{% if slots.header %}<header class="lcars-well" style="padding-top:0.25rem;padding-bottom:0.25rem;">{{ slots.header|safe }}</header>{% endif %}
|
||||
<section style="width:100%;">{{ slots.hero|safe }}</section>
|
||||
<main style="flex:1 1 auto;width:100%;" data-content-well>{% if slots.main %}<div class="lcars-well" style="padding-top:2.5rem;padding-bottom:2.5rem;">{{ slots.main|safe }}</div>{% endif %}</main>
|
||||
<section style="width:100%;">{{ slots.cta|safe }}</section>
|
||||
<div class="lcars-elbow-bottom"><div class="lcars-elbow-corner lcars-elbow-bl"></div><div class="lcars-bar lcars-bar-bottom"></div></div>
|
||||
</div>
|
||||
{% if slots.footer %}<footer style="margin-top:auto;">{{ slots.footer|safe }}</footer>{% endif %}
|
||||
{{ body_end_html|safe }}
|
||||
</body>
|
||||
</html>
|
||||
8
templates/overrides/lcars/announcement-bar.ninjatpl
Normal file
@ -0,0 +1,8 @@
|
||||
{# announcement-bar override — centered notice; carries data-bn-announcement / data-bn-annkey and a data-bn-ann-dismiss close button so the host dismissal JS binds. #}
|
||||
{% if variant == "promo" %}{% set annvar = "lcars-announce--promo" %}{% set annsty = "background:hsl(var(--primary));color:hsl(var(--primary-foreground));" %}{% elif variant == "alert" %}{% set annvar = "lcars-announce--alert" %}{% set annsty = "background:hsl(var(--destructive));color:hsl(var(--destructive-foreground));" %}{% else %}{% set annvar = "lcars-announce--info" %}{% set annsty = "background:hsl(var(--accent));color:hsl(var(--accent-foreground));" %}{% endif %}
|
||||
<div data-bn-announcement data-bn-annkey="{{ variant|default:'info' }}:{{ message|slugify }}" class="lcars-announce {{ annvar }} {{ class }}" style="{{ annsty }}">
|
||||
<div class="lcars-content-well lcars-announce-row" style="position:relative;display:flex;align-items:center;justify-content:center;gap:0.75rem;padding-top:0.5rem;padding-bottom:0.5rem;font-size:0.875rem;text-align:center;">
|
||||
<p style="margin:0;font-weight:500;">{{ message }}{% if linkText and linkUrl %} <a href="{{ linkUrl }}" style="color:inherit;text-decoration:underline;text-underline-offset:2px;">{{ linkText }}</a>{% endif %}</p>
|
||||
{% if dismissable %}<button type="button" data-bn-ann-dismiss aria-label="Dismiss announcement" class="lcars-announce-close" style="position:absolute;right:0.75rem;top:50%;transform:translateY(-50%);display:inline-flex;align-items:center;justify-content:center;padding:0.25rem;background:none;border:0;color:inherit;cursor:pointer;"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#x"/></svg></button>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
14
templates/overrides/lcars/audio-embed.ninjatpl
Normal file
@ -0,0 +1,14 @@
|
||||
{# audio-embed override — hairline card row; carries data-bn-audio / data-audio-url / data-audio-media + data-bn-audio-slot so the host facade JS binds. #}
|
||||
<figure class="lcars-card {{ class }}" data-bn-audio data-audio-url="{{ url|default:'' }}" data-audio-media="{% if media %}{{ media|mediaURL }}{% endif %}" style="margin:2rem 0;padding:0;overflow:hidden;">
|
||||
<div style="display:flex;align-items:center;gap:1rem;padding:1rem;">
|
||||
{% if artwork %}<div class="lcars-media-frame" style="width:4rem;height:4rem;flex:0 0 auto;overflow:hidden;border:1px solid hsl(var(--border));border-radius:4px;">{% img artwork alt=title|default:"" class="lcars-audio-art" %}</div>{% endif %}
|
||||
<div style="min-width:0;flex:1;">
|
||||
{% if title %}<p class="lcars-display" style="margin:0;font-size:0.9375rem;color:hsl(var(--foreground));white-space:nowrap;overflow:hidden;text-overflow:ellipsis;">{{ title }}</p>{% endif %}
|
||||
{% if artist %}<p style="margin:0;font-size:0.8125rem;color:hsl(var(--muted-foreground));">{{ artist }}</p>{% endif %}
|
||||
<div data-bn-audio-slot style="display:flex;align-items:center;gap:0.75rem;margin-top:0.5rem;">
|
||||
<button type="button" data-bn-audio-play class="lcars-btn-primary lcars-btn-sm" aria-label="Play{% if title %}: {{ title }}{% endif %}" style="display:inline-flex;align-items:center;gap:0.375rem;"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#play"/></svg> Play</button>
|
||||
{% if download %}<a href="{% if media %}{{ media|mediaURL }}{% else %}{{ url }}{% endif %}" download class="lcars-btn-ghost" style="display:inline-flex;align-items:center;gap:0.25rem;font-size:0.8125rem;color:hsl(var(--muted-foreground));text-decoration:none;"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#download"/></svg> Download</a>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</figure><style>.lcars-audio-art{display:block;width:100%;height:100%;object-fit:cover;}</style>
|
||||
22
templates/overrides/lcars/auth-form.ninjatpl
Normal file
@ -0,0 +1,22 @@
|
||||
<div class="lcars-stack {{ class }}" data-block="lcars:auth-form">
|
||||
<input type="radio" name="lcars-auth-tab" id="lcars-auth-login" class="lcars-auth-radio" style="display:none;"{% if default_tab != "register" %} checked{% endif %}>
|
||||
<input type="radio" name="lcars-auth-tab" id="lcars-auth-register" class="lcars-auth-radio" style="display:none;"{% if default_tab == "register" %} checked{% endif %}>
|
||||
<div class="lcars-auth-tabs" style="display:flex; gap:1.5rem; border-bottom:1px solid hsl(var(--border));">
|
||||
<label for="lcars-auth-login" class="lcars-auth-tab lcars-label" style="cursor:pointer; padding:0.75rem 0;">Sign in</label>
|
||||
<label for="lcars-auth-register" class="lcars-auth-tab lcars-label" style="cursor:pointer; padding:0.75rem 0;">Create account</label>
|
||||
</div>
|
||||
<div id="auth-message" role="status" aria-live="polite"></div>
|
||||
<form class="lcars-auth-panel lcars-auth-panel-login lcars-form" hx-post="/api/auth/login" hx-target="#auth-message" hx-swap="innerHTML">
|
||||
<div class="lcars-field lcars-field--full"><label class="lcars-label" for="lcars-login-email">Email</label><input class="lcars-input" type="email" name="email" id="lcars-login-email" required></div>
|
||||
<div class="lcars-field lcars-field--full"><label class="lcars-label" for="lcars-login-password">Password</label><input class="lcars-input" type="password" name="password" id="lcars-login-password" required></div>
|
||||
<button type="submit" class="lcars-btn-primary" hx-disabled-elt="this" style="width:100%; justify-content:center;">Sign in</button>
|
||||
<p class="lcars-form-note" style="text-align:center;"><a href="/forgot-password" style="color: hsl(var(--accent)); text-decoration: underline; text-underline-offset: 2px;">Forgot password?</a></p>
|
||||
</form>
|
||||
<form class="lcars-auth-panel lcars-auth-panel-register lcars-form" hx-post="/api/auth/register" hx-target="#auth-message" hx-swap="innerHTML">
|
||||
<div class="lcars-field lcars-field--full"><label class="lcars-label" for="lcars-reg-username">Username</label><input class="lcars-input" type="text" name="username" id="lcars-reg-username" required pattern="[a-zA-Z0-9_-]{3,30}"></div>
|
||||
<div class="lcars-field lcars-field--full"><label class="lcars-label" for="lcars-reg-email">Email</label><input class="lcars-input" type="email" name="email" id="lcars-reg-email" required></div>
|
||||
<div class="lcars-field lcars-field--full"><label class="lcars-label" for="lcars-reg-password">Password</label><input class="lcars-input" type="password" name="password" id="lcars-reg-password" required minlength="8"></div>
|
||||
<button type="submit" class="lcars-btn-primary" hx-disabled-elt="this" style="width:100%; justify-content:center;">Create account</button>
|
||||
</form>
|
||||
<style>.lcars-auth-panel{display:none;} #lcars-auth-login:checked ~ .lcars-auth-panel-login{display:grid;} #lcars-auth-register:checked ~ .lcars-auth-panel-register{display:grid;}</style>
|
||||
</div>
|
||||
1
templates/overrides/lcars/auth-status.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
{% if context.isPublicLoggedIn %}<div class="lcars-btn-row {{ class }}" data-block="lcars:auth-status" style="align-items: center;"><span class="lcars-mono" style="color: hsl(var(--muted-foreground));">{{ context.publicDisplayName|default:context.publicUsername }}</span><a class="lcars-btn-outline lcars-btn-sm" href="/account">Account</a><a class="lcars-btn-ghost lcars-btn-sm" href="/api/auth/logout">Sign out</a></div>{% else %}<div class="lcars-btn-row {{ class }}" data-block="lcars:auth-status" style="align-items: center;"><a class="lcars-btn-outline lcars-btn-sm" href="/login">Sign in</a><a class="lcars-btn-primary lcars-btn-sm" href="/login?tab=register">Create account</a></div>{% endif %}
|
||||
1
templates/overrides/lcars/author-bio-hero.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
{% if context.currentAuthor %}<section class="lcars-section {{ class }}" data-block="lcars:author-bio-hero"><div class="lcars-content-well"><div class="lcars-measure lcars-stack" style="text-align: center; margin-left: auto; margin-right: auto;">{% if showAvatar and context.currentAuthor.avatarUrl %}<img class="lcars-avatar" src="{{ context.currentAuthor.avatarUrl }}" alt="{{ context.currentAuthor.displayName }}" style="width: 6rem; height: 6rem; border-radius: 9999px; object-fit: cover; margin-left: auto; margin-right: auto;">{% else %}<span class="lcars-mark lcars-mark-ring" aria-hidden="true" style="margin-left: auto; margin-right: auto;"></span>{% endif %}<h1 class="lcars-display" style="margin: 0;">{{ context.currentAuthor.displayName }}</h1>{% if context.currentAuthor.bio %}<p class="lcars-lede" style="margin: 0;">{{ context.currentAuthor.bio }}</p>{% endif %}{% if showSocial %}<div class="lcars-btn-row" style="justify-content: center;">{% if context.currentAuthor.websiteUrl %}<a class="lcars-btn-ghost lcars-btn-sm" href="{{ context.currentAuthor.websiteUrl }}" target="_blank" rel="noopener noreferrer" title="Website"><svg class="lcars-icon" width="16" height="16" aria-hidden="true"><use href="/icons/lucide.svg#globe"/></svg></a>{% endif %}{% if context.currentAuthor.twitterHandle %}<a class="lcars-btn-ghost lcars-btn-sm" href="https://twitter.com/{{ context.currentAuthor.twitterHandle|cut:"@" }}" target="_blank" rel="noopener noreferrer" title="X"><svg class="lcars-icon" width="16" height="16" aria-hidden="true"><use href="/icons/simple-icons.svg#x"/></svg></a>{% endif %}{% if context.currentAuthor.linkedinUrl %}<a class="lcars-btn-ghost lcars-btn-sm" href="{{ context.currentAuthor.linkedinUrl }}" target="_blank" rel="noopener noreferrer" title="LinkedIn"><svg class="lcars-icon" width="16" height="16" aria-hidden="true"><use href="/icons/simple-icons.svg#linkedin"/></svg></a>{% endif %}</div>{% endif %}</div></div></section>{% endif %}
|
||||
1
templates/overrides/lcars/author-bio.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
{% if context.currentPost.authorName %}<aside class="lcars-card lcars-card-accent {{ class }}" data-block="lcars:author-bio">{% if heading %}<p class="lcars-kicker" style="margin-top: 0;">{{ heading }}</p>{% endif %}<div style="display: flex; align-items: flex-start; gap: 1rem;">{% if showAvatar and context.currentPost.authorAvatarUrl %}<img class="lcars-avatar" src="{{ context.currentPost.authorAvatarUrl }}" alt="{{ context.currentPost.authorName }}" style="width: 3.5rem; height: 3.5rem; border-radius: 9999px; object-fit: cover; flex-shrink: 0;">{% endif %}<div style="min-width: 0;"><h3 class="lcars-heading" style="font-size: 1.125rem; margin: 0;">{% if context.currentPost.authorSlug %}<a href="/author/{{ context.currentPost.authorSlug }}">{{ context.currentPost.authorName }}</a>{% else %}{{ context.currentPost.authorName }}{% endif %}</h3>{% if context.currentAuthor.bio %}<p class="lcars-text" style="margin: 0.5rem 0 0 0; font-size: 0.9375rem; color: hsl(var(--muted-foreground));">{{ context.currentAuthor.bio }}</p>{% endif %}</div></div></aside>{% endif %}
|
||||
1
templates/overrides/lcars/blog-hero.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
<section class="lcars-section {{ class }}" data-block="lcars:blog-hero"><div class="lcars-content-well"><div class="lcars-stack">{% if title %}<h1 class="lcars-heading" style="font-size: clamp(2rem, 4vw, 3rem); margin: 0;"><span class="lcars-heading-h2-accent">{{ title }}</span></h1>{% endif %}{% if subtitle %}<p class="lcars-lede" style="max-width: 60ch; margin: 0.75rem 0 0 0;">{{ subtitle }}</p>{% endif %}</div>{% if posts %}<div class="lcars-post-grid lcars-post-grid-3" style="margin-top: 2.5rem;">{% for post in posts %}<article class="lcars-post-card{% if forloop.First %} lcars-card-accent{% endif %}">{% if post.featured_image_url %}<a href="{{ post.url }}" class="lcars-post-thumb lcars-media-frame" style="display: block; aspect-ratio: 16 / 9;"><img src="{{ post.featured_image_url }}" alt="{{ post.title }}" style="width: 100%; height: 100%; object-fit: cover;"></a>{% endif %}<h2 class="lcars-post-title"{% if forloop.First %} style="font-size: 1.5rem;"{% endif %}><a href="{{ post.url }}">{{ post.title }}</a></h2>{% if showExcerpt and post.excerpt %}<p class="lcars-post-excerpt">{{ post.excerpt }}</p>{% endif %}<div class="lcars-post-meta">{% if post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if post.published_at %}<span class="lcars-mono numeric-tabular">{{ post.published_at|date:"M j, Y" }}</span>{% endif %}</div></article>{% endfor %}</div>{% endif %}</div></section>
|
||||
1
templates/overrides/lcars/breadcrumbs.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
<nav aria-label="Breadcrumb" class="lcars-crumbs {{ class }}" data-block="lcars:breadcrumbs"><ol style="display: flex; flex-wrap: wrap; align-items: center; gap: 0.25rem; list-style: none; margin: 0; padding: 0;">{% if showHome %}<li style="display: flex; align-items: center;"><a class="lcars-sidebar-link" href="/" style="padding: 0;">{{ homeLabel|default:"Home" }}</a></li>{% endif %}{% for item in breadcrumbs %}<li style="display: flex; align-items: center;">{% if showHome or not forloop.First %}<span class="lcars-crumb-sep" aria-hidden="true">{{ separator|default:"/" }}</span>{% endif %}{% if item.is_current %}{% if showCurrent %}<span aria-current="page" style="color: hsl(var(--foreground)); font-weight: 500;">{{ item.label }}</span>{% endif %}{% else %}<a class="lcars-sidebar-link" href="{{ item.url }}" style="padding: 0;">{{ item.label }}</a>{% endif %}</li>{% endfor %}</ol></nav>
|
||||
6
templates/overrides/lcars/button.ninjatpl
Normal file
@ -0,0 +1,6 @@
|
||||
{% set btnStyle = style|default:"primary" %}
|
||||
{% set btnClass = "lcars-btn-primary" %}
|
||||
{% if btnStyle == "outline" or btnStyle == "secondary" %}{% set btnClass = "lcars-btn-outline" %}{% elif btnStyle == "ghost" or btnStyle == "link" %}{% set btnClass = "lcars-btn-ghost" %}{% endif %}
|
||||
{% set sizeClass = "" %}
|
||||
{% if size == "sm" %}{% set sizeClass = "lcars-btn-sm" %}{% elif size == "lg" %}{% set sizeClass = "lcars-btn-lg" %}{% endif %}
|
||||
<div class="lcars-btn-row {{ class }}" style="{% if align == "center" %}justify-content: center;{% elif align == "right" %}justify-content: flex-end;{% endif %}">{% if newTab %}<a class="{{ btnClass }} {{ sizeClass }}" href="{{ link|default:"#" }}" target="_blank" rel="noopener noreferrer">{% if icon and iconPosition != "right" %}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"/></svg>{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"/></svg>{% endif %}</a>{% else %}<a class="{{ btnClass }} {{ sizeClass }}" href="{{ link|default:"#" }}">{% if icon and iconPosition != "right" %}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"/></svg>{% endif %}<span>{{ label|default:"Learn more" }}</span>{% if icon and iconPosition == "right" %}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ icon|split:":"|first }}.svg#{{ icon|split:":"|last }}"/></svg>{% endif %}</a>{% endif %}</div>
|
||||
1
templates/overrides/lcars/card.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
<article class="lcars-card lcars-card-accent {{ class }}">{% if media %}<div class="lcars-media-frame" style="aspect-ratio: 16 / 9; margin: -1.5rem -1.5rem 1.25rem -1.5rem; border: 0; border-bottom: 1px solid hsl(var(--border));">{% img media alt=title|default:"" class="lcars-card-media" %}</div>{% endif %}{% if title %}<h3 class="lcars-feature-title" style="margin-top: 0; font-size: 1.25rem;">{{ title }}</h3>{% endif %}{% if text %}<div class="lcars-text" style="font-size: 0.9375rem; line-height: 1.6; color: hsl(var(--muted-foreground));">{{ text|safe }}</div>{% endif %}{% if linkLabel and link %}<a class="lcars-btn-ghost" href="{{ link }}" style="margin-top: 1rem;">{{ linkLabel }}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"/></svg></a>{% endif %}</article>
|
||||
1
templates/overrides/lcars/category-list.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
<section class="lcars-section-tight {{ class }}" data-block="lcars:category-list">{% if heading %}<h2 class="lcars-kicker">{{ heading }}</h2>{% endif %}{% if style == "pills" %}<div style="display: flex; flex-wrap: wrap; gap: 0.5rem;">{% for cat in categories %}<a href="{{ cat.url }}" class="lcars-tag">{{ cat.name }}{% if showCount %} <span class="lcars-mono numeric-tabular">{{ cat.post_count }}</span>{% endif %}</a>{% endfor %}</div>{% else %}<ul class="lcars-stack-sm" style="list-style: none; margin: 0; padding: 0;">{% for cat in categories %}<li><a href="{{ cat.url }}" class="lcars-sidebar-link" style="display: flex; align-items: center; justify-content: space-between;"><span>{{ cat.name }}</span>{% if showCount %}<span class="lcars-mono numeric-tabular">{{ cat.post_count }}</span>{% endif %}</a></li>{% endfor %}</ul>{% endif %}</section>
|
||||
14
templates/overrides/lcars/contact-card.ninjatpl
Normal file
@ -0,0 +1,14 @@
|
||||
{# contact-card override — grouped contact channels; each channel a hairline mark + label + value. #}
|
||||
<section class="lcars-section {{ class }}"><div class="lcars-content-well">
|
||||
{% if heading %}<h2 class="lcars-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
|
||||
{% if description %}<p class="lcars-lede" style="max-width:60ch;margin:0 0 2.5rem 0;color:hsl(var(--muted-foreground));">{{ description }}</p>{% endif %}
|
||||
<div class="lcars-grid {% if columns == 3 %}lcars-grid-3{% elif columns == 1 %}{% else %}lcars-grid-2{% endif %}" style="display:grid;gap:1rem;{% if columns == 3 %}grid-template-columns:repeat(auto-fill, minmax(240px, 1fr));{% elif columns == 1 %}grid-template-columns:1fr;{% else %}grid-template-columns:repeat(auto-fill, minmax(280px, 1fr));{% endif %}">
|
||||
{% for channel in channels %}<div class="lcars-channel lcars-card" style="display:flex;align-items:flex-start;gap:1rem;">
|
||||
{% if channel.icon %}<span class="lcars-channel-mark" style="display:inline-flex;align-items:center;justify-content:center;width:2.5rem;height:2.5rem;flex:0 0 auto;border:1px solid hsl(var(--border));border-radius:4px;color:hsl(var(--accent));"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ channel.icon|split:":"|first }}.svg#{{ channel.icon|split:":"|last }}"/></svg></span>{% endif %}
|
||||
<div style="min-width:0;">
|
||||
{% if channel.label %}<div class="lcars-stat-label" style="margin:0;">{{ channel.label }}</div>{% endif %}
|
||||
{% if channel.value %}<div style="margin-top:0.25rem;font-weight:500;word-break:break-word;color:hsl(var(--foreground));">{% if channel.link %}<a href="{{ channel.link }}" style="color:inherit;text-decoration:none;">{{ channel.value }}</a>{% else %}{{ channel.value }}{% endif %}</div>{% endif %}
|
||||
</div>
|
||||
</div>{% endfor %}
|
||||
</div>
|
||||
</div></section>
|
||||
34
templates/overrides/lcars/contact-form.ninjatpl
Normal file
@ -0,0 +1,34 @@
|
||||
{# contact-form override — server-driven; block-id resolution, {% form %} wiring, honeypot, and field loop copied verbatim from the builtin, reskinned with lcars-form primitives. #}
|
||||
{% with bid=_block_id|default:context.blockId %}
|
||||
<section id="contact-form-{{ bid }}" class="lcars-section {{ class }}"><div class="lcars-content-well" style="max-width:640px;">
|
||||
{% if form.submitted %}
|
||||
<div class="lcars-card lcars-card-accent lcars-alert-ok" style="text-align:center;padding:2rem;">
|
||||
<span class="lcars-mark lcars-mark-circle" aria-hidden="true" style="display:inline-block;margin-bottom:1rem;"></span>
|
||||
<p class="lcars-display" style="margin:0;font-size:1.125rem;">{{ form.message|default:successMessage|default:"Thanks — your message has been sent." }}</p>
|
||||
</div>
|
||||
{% else %}
|
||||
{% if heading %}<h2 class="lcars-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
|
||||
{% if description %}<p class="lcars-lede" style="margin:0 0 2rem 0;color:hsl(var(--muted-foreground));">{{ description }}</p>{% endif %}
|
||||
{% if form.errors._form %}<div class="lcars-alert-err" style="margin-bottom:1.5rem;border:1px solid hsl(var(--destructive));border-radius:4px;padding:0.75rem 1rem;font-size:0.875rem;color:hsl(var(--destructive));background:hsl(var(--destructive) / 0.08);">{{ 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="lcars-form-grid" %}
|
||||
{% for field in fields %}
|
||||
<div class="lcars-field {% if field.width == "half" %}{% else %}lcars-field--full{% endif %}">
|
||||
{% if field.label %}<label class="lcars-label" for="cf-{{ bid }}-{{ field.name }}">{{ field.label }}{% if field.required %} <span style="color:hsl(var(--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="lcars-input"></textarea>
|
||||
{% elif field.type == "select" %}
|
||||
<select id="cf-{{ bid }}-{{ field.name }}" name="{{ field.name }}"{% if field.required %} required{% endif %} class="lcars-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="lcars-input">
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
<div class="lcars-field--full" aria-hidden="true" style="display:none;"><label>Leave this field empty<input type="text" name="website" tabindex="-1" autocomplete="off"></label></div>
|
||||
<div class="lcars-field--full"><button type="submit" class="lcars-btn-primary">{{ submitLabel|default:"Send message" }}</button></div>
|
||||
{% endform %}
|
||||
{% endwith %}
|
||||
{% endif %}
|
||||
</div></section>
|
||||
<style>#contact-form-{{ bid }} .lcars-form-grid{display:grid;grid-template-columns:repeat(2, minmax(0, 1fr));gap:1.25rem;margin-top:2rem;}#contact-form-{{ bid }} .lcars-field--full{grid-column:1 / -1;}#contact-form-{{ bid }} .lcars-label{display:block;margin-bottom:0.375rem;font-size:0.8125rem;font-weight:500;color:hsl(var(--foreground));font-family:var(--font-heading, "Antonio", "Antonio", system-ui, sans-serif);}#contact-form-{{ bid }} .lcars-input{width:100%;padding:0.5rem 0.75rem;font-size:0.9375rem;font-family:var(--font-body, "Antonio", system-ui, sans-serif);color:hsl(var(--foreground));background:hsl(var(--background));border:1px solid hsl(var(--input));border-radius:4px;box-sizing:border-box;}#contact-form-{{ bid }} .lcars-input:focus{outline:2px solid hsl(var(--ring));outline-offset:1px;}</style>
|
||||
{% endwith %}
|
||||
44
templates/overrides/lcars/countdown.ninjatpl
Normal file
@ -0,0 +1,44 @@
|
||||
{# countdown override — oversized mono unit boxes; host countdown JS binds via data-* attrs. #}
|
||||
<section class="lcars-section {{ class }}"><div class="lcars-content-well" style="text-align: center;">
|
||||
{% if heading %}<h2 class="lcars-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;">{{ heading }}</h2>{% endif %}
|
||||
{% if intro %}<p class="lcars-lede" style="max-width: 60ch; margin: 0 auto; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
|
||||
<div data-countdown style="margin-top: 2.5rem;">
|
||||
<div data-countdown-timer data-target="{{ target }}" role="timer" aria-label="Time remaining" style="display: flex; align-items: flex-start; justify-content: center; gap: 1rem; flex-wrap: wrap;">
|
||||
<div class="lcars-count-unit {% if variant == "boxes" %}lcars-count-box{% endif %}" style="display: flex; flex-direction: column; align-items: center; {% if variant == "boxes" %}min-width: 5rem; border: 1px solid hsl(var(--border)); background: hsl(var(--card)); padding: 1rem 0.75rem;{% endif %}"><span class="lcars-count-figure numeric-tabular" style="font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(2rem, 5vw, 3rem); font-weight: 500; line-height: 1; color: hsl(var(--foreground));" data-unit="days">--</span><span class="lcars-stat-label" style="margin-top: 0.5rem;">{{ labelDays|default:"Days" }}</span></div>
|
||||
<div class="lcars-count-unit {% if variant == "boxes" %}lcars-count-box{% endif %}" style="display: flex; flex-direction: column; align-items: center; {% if variant == "boxes" %}min-width: 5rem; border: 1px solid hsl(var(--border)); background: hsl(var(--card)); padding: 1rem 0.75rem;{% endif %}"><span class="lcars-count-figure numeric-tabular" style="font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(2rem, 5vw, 3rem); font-weight: 500; line-height: 1; color: hsl(var(--foreground));" data-unit="hours">--</span><span class="lcars-stat-label" style="margin-top: 0.5rem;">{{ labelHours|default:"Hours" }}</span></div>
|
||||
<div class="lcars-count-unit {% if variant == "boxes" %}lcars-count-box{% endif %}" style="display: flex; flex-direction: column; align-items: center; {% if variant == "boxes" %}min-width: 5rem; border: 1px solid hsl(var(--border)); background: hsl(var(--card)); padding: 1rem 0.75rem;{% endif %}"><span class="lcars-count-figure numeric-tabular" style="font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(2rem, 5vw, 3rem); font-weight: 500; line-height: 1; color: hsl(var(--foreground));" data-unit="minutes">--</span><span class="lcars-stat-label" style="margin-top: 0.5rem;">{{ labelMinutes|default:"Minutes" }}</span></div>
|
||||
<div class="lcars-count-unit {% if variant == "boxes" %}lcars-count-box{% endif %}" style="display: flex; flex-direction: column; align-items: center; {% if variant == "boxes" %}min-width: 5rem; border: 1px solid hsl(var(--border)); background: hsl(var(--card)); padding: 1rem 0.75rem;{% endif %}"><span class="lcars-count-figure numeric-tabular" style="font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(2rem, 5vw, 3rem); font-weight: 500; line-height: 1; color: hsl(var(--foreground));" data-unit="seconds">--</span><span class="lcars-stat-label" style="margin-top: 0.5rem;">{{ labelSeconds|default:"Seconds" }}</span></div>
|
||||
</div>
|
||||
<div data-countdown-expired style="display: none; margin-top: 2rem;"><p class="lcars-heading" style="font-size: 1.25rem;">{{ expiredMessage|default:"This has started." }}</p>{% if expiredUrl and expiredLabel %}<a class="lcars-btn-primary" href="{{ expiredUrl }}" style="margin-top: 1rem;">{{ expiredLabel }}</a>{% endif %}</div>
|
||||
</div>
|
||||
<noscript><p class="lcars-text" style="margin-top: 1.5rem; color: hsl(var(--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.style.display = 'block';
|
||||
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>
|
||||
8
templates/overrides/lcars/cta.ninjatpl
Normal file
@ -0,0 +1,8 @@
|
||||
{# cta override — full-width strip; band / card / split variants. #}
|
||||
<section class="lcars-cta-strip {% if background == "band" %}lcars-cta-strip--band{% elif background == "card" %}lcars-cta-strip--card{% endif %} {{ class }}" style="padding-top: 3rem; padding-bottom: 3rem;"><div class="lcars-content-well" style="display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 1.5rem 2.5rem;">
|
||||
<div style="{% if background != "split" %}max-width: 60ch;{% endif %}">
|
||||
{% if heading %}<h2 class="lcars-cta-headline lcars-heading" style="font-size: clamp(1.75rem, 4vw, 2.75rem); margin: 0; color: inherit;">{{ heading }}</h2>{% endif %}
|
||||
{% if text %}<p class="lcars-lede" style="margin: 0.75rem 0 0 0; max-width: 55ch; color: {% if background == "band" %}inherit{% else %}hsl(var(--muted-foreground)){% endif %};">{{ text }}</p>{% endif %}
|
||||
</div>
|
||||
{% if primaryLabel or secondaryLabel %}<div class="lcars-btn-row" style="display: flex; flex-wrap: wrap; gap: 0.75rem; flex: none;">{% if primaryUrl and primaryLabel %}<a class="lcars-btn-primary lcars-btn-lg" href="{{ primaryUrl }}">{{ primaryLabel }}</a>{% endif %}{% if secondaryUrl and secondaryLabel %}<a class="lcars-btn-outline lcars-btn-lg" href="{{ secondaryUrl }}">{{ secondaryLabel }}</a>{% endif %}</div>{% endif %}
|
||||
</div></section>
|
||||
2
templates/overrides/lcars/divider.ninjatpl
Normal file
@ -0,0 +1,2 @@
|
||||
{# divider override — plain hairline / thick spacer rule / centered ornament between two hairlines. #}
|
||||
{% if variant == "ornament" %}<div class="lcars-divider-ornament {{ class }}" role="separator" style="display:flex;align-items:center;gap:1rem;margin:2.5rem auto;{% if width == "narrow" %}max-width:200px;{% elif width == "wide" %}max-width:768px;{% endif %}"><span class="lcars-hairline-top" style="flex:1;height:0;"></span><span style="display:inline-flex;color:hsl(var(--muted-foreground));"><svg width="20" height="20" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ icon|default:"lucide:asterisk"|split:":"|first }}.svg#{{ icon|default:"lucide:asterisk"|split:":"|last }}"/></svg></span><span class="lcars-hairline-top" style="flex:1;height:0;"></span></div>{% elif variant == "spacer-rule" %}<hr class="lcars-divider-line lcars-divider-thick {{ class }}" role="separator" style="border:0;border-top:{% if weight == "thick" %}4px{% elif weight == "medium" %}2px{% else %}1px{% endif %} solid hsl(var(--border));margin:3rem auto;{% if width == "narrow" %}max-width:200px;{% elif width == "wide" %}max-width:768px;{% endif %}">{% else %}<hr class="lcars-divider-line {{ class }}" role="separator" style="border:0;border-top:{% if weight == "thick" %}4px{% elif weight == "medium" %}2px{% else %}1px{% endif %} solid hsl(var(--border));margin:2rem auto;{% if width == "narrow" %}max-width:200px;{% elif width == "wide" %}max-width:768px;{% endif %}">{% endif %}
|
||||
31
templates/overrides/lcars/event-list.ninjatpl
Normal file
@ -0,0 +1,31 @@
|
||||
{# event-list override — dates shown VERBATIM in each event's own timezone (never converted); list rows or card grid. #}
|
||||
<section class="lcars-section {{ class }}"><div class="lcars-content-well">
|
||||
{% if heading %}<h2 class="lcars-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
|
||||
{% if intro %}<p class="lcars-lede" style="max-width:60ch;margin:0 0 2.5rem 0;color:hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
|
||||
{% if layout == "cards" %}
|
||||
<div class="lcars-grid lcars-grid-3" style="margin-top:2.5rem;display:grid;grid-template-columns:repeat(auto-fill, minmax(260px, 1fr));gap:1.5rem;">
|
||||
{% for event in events %}{% if not (hidePast and event.past) %}<article class="lcars-card" style="display:flex;flex-direction:column;padding:0;overflow:hidden;{% if event.past %}opacity:0.6;{% endif %}">
|
||||
{% if event.media %}<div class="lcars-media-frame" style="aspect-ratio:16 / 9;overflow:hidden;border-bottom:1px solid hsl(var(--border));">{% img event.media alt=event.title|default:"" class="lcars-event-media" %}</div>{% endif %}
|
||||
<div style="display:flex;flex-direction:column;gap:0.5rem;padding:1.25rem;flex:1;">
|
||||
<div class="lcars-mono numeric-tabular" style="font-size:0.8125rem;color:hsl(var(--accent));"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}{% if event.end %} – {{ event.end }}{% endif %}</time>{% if event.timezone %} <span style="color:hsl(var(--muted-foreground));">{{ event.timezone }}</span>{% endif %}</div>
|
||||
<h3 class="lcars-feature-title" style="font-size:1.125rem;margin:0;">{{ event.title }}</h3>
|
||||
{% if event.venue %}<p style="margin:0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ event.venue }}</p>{% endif %}
|
||||
{% if event.description %}<p style="margin:0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ event.description }}</p>{% endif %}
|
||||
{% if event.link %}<a class="lcars-btn-ghost" href="{{ event.link }}" style="margin-top:auto;display:inline-flex;align-items:center;gap:0.375rem;">{{ event.linkLabel|default:"Details" }}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"/></svg></a>{% endif %}
|
||||
</div>
|
||||
</article>{% endif %}{% endfor %}
|
||||
</div><style>.lcars-event-media{display:block;width:100%;height:100%;object-fit:cover;}</style>
|
||||
{% else %}
|
||||
<ul class="lcars-hairline-top" style="list-style:none;margin:2.5rem 0 0 0;padding:0;">
|
||||
{% for event in events %}{% if not (hidePast and event.past) %}<li class="lcars-hairline-bottom" style="display:flex;flex-wrap:wrap;align-items:baseline;gap:0.75rem 1.5rem;padding:1.25rem 0;{% if event.past %}opacity:0.6;{% endif %}">
|
||||
<div class="lcars-mono numeric-tabular" style="flex:0 0 12rem;font-size:0.875rem;"><time{% if event.datetime %} datetime="{{ event.datetime }}"{% endif %}>{{ event.start }}</time>{% if event.end %}<div style="font-size:0.75rem;color:hsl(var(--muted-foreground));">to {{ event.end }}</div>{% endif %}{% if event.timezone %}<div style="font-size:0.75rem;color:hsl(var(--muted-foreground));">{{ event.timezone }}</div>{% endif %}</div>
|
||||
<div style="min-width:0;flex:1;">
|
||||
<h3 class="lcars-feature-title" style="font-size:1rem;margin:0;">{{ event.title }}</h3>
|
||||
{% if event.venue %}<p style="margin:0.125rem 0 0 0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ event.venue }}</p>{% endif %}
|
||||
{% if event.description %}<p style="margin:0.25rem 0 0 0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ event.description }}</p>{% endif %}
|
||||
</div>
|
||||
{% if event.link %}<a class="lcars-btn-outline lcars-btn-sm" href="{{ event.link }}" style="flex:0 0 auto;">{{ event.linkLabel|default:"Details" }}</a>{% endif %}
|
||||
</li>{% endif %}{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
</div></section>
|
||||
6
templates/overrides/lcars/faq.ninjatpl
Normal file
@ -0,0 +1,6 @@
|
||||
{# faq override — native hairline-divided disclosure list. #}
|
||||
<section class="lcars-section {{ class }}"><div class="lcars-content-well lcars-measure">
|
||||
{% if heading %}<h2 class="lcars-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
|
||||
{% if intro %}<p class="lcars-lede" style="max-width: 60ch; margin: 0 0 2rem 0; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
|
||||
{% if items %}<div class="lcars-hairline-top" style="margin-top: 1.5rem;">{% for item in items %}<details class="lcars-faq-item lcars-hairline-bottom"{% if variant == "open-list" %} open{% endif %}><summary class="lcars-heading" style="display: flex; justify-content: space-between; align-items: center; gap: 1rem; cursor: pointer; padding: 1rem 0; font-size: 1.125rem; list-style: none;">{{ item.question }}<span class="lcars-faq-chevron" aria-hidden="true" style="color: hsl(var(--muted-foreground)); flex: none;"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#chevron-down"/></svg></span></summary><div class="lcars-faq-answer lcars-text" style="padding-bottom: 1rem; color: hsl(var(--muted-foreground));">{{ item.answer|safe }}</div></details>{% endfor %}</div>{% endif %}
|
||||
</div></section>
|
||||
7
templates/overrides/lcars/feature-grid.ninjatpl
Normal file
@ -0,0 +1,7 @@
|
||||
{# feature-grid override — Swiss cells, each led by a Bauhaus square mark. #}
|
||||
<section class="lcars-section {{ class }}"><div class="lcars-content-well">
|
||||
{% if eyebrow %}<p class="lcars-eyebrow">{{ eyebrow }}</p>{% endif %}
|
||||
{% if heading %}<h2 class="lcars-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
|
||||
{% if intro %}<p class="lcars-lede" style="max-width: 60ch; margin: 0 0 2.5rem 0; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
|
||||
{% if items %}<div class="lcars-grid {% if columns == 2 %}lcars-grid-2{% elif columns == 4 %}lcars-grid-4{% else %}lcars-grid-3{% endif %}">{% for item in items %}<div class="lcars-feature">{% if item.icon %}<span class="lcars-mark lcars-mark-square" aria-hidden="true"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ item.icon|split:":"|first }}.svg#{{ item.icon|split:":"|last }}"/></svg></span>{% else %}<span class="lcars-mark lcars-mark-square" aria-hidden="true"></span>{% endif %}{% if item.title %}<h3 class="lcars-feature-title">{% if item.linkUrl %}<a href="{{ item.linkUrl }}" style="color: inherit; text-decoration: none;">{{ item.title }}</a>{% else %}{{ item.title }}{% endif %}</h3>{% endif %}{% if item.text %}<p class="lcars-text" style="font-size: 0.9375rem; line-height: 1.6; color: hsl(var(--muted-foreground));">{{ item.text }}</p>{% endif %}{% if item.linkUrl and item.linkLabel %}<a class="lcars-btn-ghost" href="{{ item.linkUrl }}" style="margin-top: 0.75rem;">{{ item.linkLabel }}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#arrow-right"/></svg></a>{% endif %}</div>{% endfor %}</div>{% endif %}
|
||||
</div></section>
|
||||
1
templates/overrides/lcars/featured-posts.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
<section class="lcars-section {{ class }}" data-block="lcars:featured-posts"><div class="lcars-content-well">{% if heading %}<h2 class="lcars-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 2rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}<div class="lcars-post-grid {% if columns == 2 %}lcars-post-grid-2{% else %}lcars-post-grid-3{% endif %}">{% for post in posts %}<article class="lcars-post-card">{% if showFeaturedImage and post.featured_image_url %}<a href="{{ post.url }}" class="lcars-post-thumb lcars-media-frame" style="display: block; aspect-ratio: 16 / 9;"><img src="{{ post.featured_image_url }}" alt="{{ post.title }}" style="width: 100%; height: 100%; object-fit: cover;"></a>{% endif %}<h3 class="lcars-post-title"><a href="{{ post.url }}">{{ post.title }}</a></h3>{% if showExcerpt and post.excerpt %}<p class="lcars-post-excerpt">{{ post.excerpt }}</p>{% endif %}<div class="lcars-post-meta">{% if showAuthor and post.author_name %}<span>{{ post.author_name }}</span>{% endif %}{% if showDate and post.published_at %}<span class="lcars-mono numeric-tabular">{{ post.published_at|date:"M j, Y" }}</span>{% endif %}{% if post.reading_time %}<span class="lcars-mono numeric-tabular">{{ post.reading_time }} min</span>{% endif %}</div></article>{% endfor %}</div></div></section>
|
||||
17
templates/overrides/lcars/footer.ninjatpl
Normal file
@ -0,0 +1,17 @@
|
||||
{# footer override — Swiss-grid footer chrome from CONTENT fields (brand, contact, newsletter, legal); menu columns / social / legal rows render from the footer providers (empty today, forward-correct). #}
|
||||
<footer class="lcars-footer {{ class }}"><div class="lcars-content-well">
|
||||
<div class="lcars-footer-cols" style="display:grid;grid-template-columns:repeat(auto-fit,minmax(180px,1fr));gap:2.5rem;align-items:start;">
|
||||
<div class="lcars-stack" style="grid-column:span 2;min-width:220px;display:flex;flex-direction:column;gap:1rem;">
|
||||
<a href="/" style="display:inline-flex;align-items:center;gap:0.5rem;text-decoration:none;color:hsl(var(--foreground));">{% if logo %}{% img logo alt=companyName|default:"Home" class="lcars-footer-logo" %}{% else %}<span class="lcars-mark lcars-mark-square" aria-hidden="true"></span><span class="lcars-display" style="font-size:1.125rem;">{{ companyName|default:context.site.title|default:"Studio" }}</span>{% endif %}</a>
|
||||
{% if tagline %}<p class="lcars-text" style="font-size:0.9375rem;max-width:34ch;margin:0;color:hsl(var(--muted-foreground));">{{ tagline }}</p>{% endif %}
|
||||
{% if address or email or phone %}<address class="lcars-mono numeric-tabular" style="font-style:normal;font-size:0.8125rem;line-height:1.8;color:hsl(var(--muted-foreground));">{% if address %}<div>{{ address }}</div>{% endif %}{% if email %}<div><a href="mailto:{{ email }}" class="lcars-footer-link" style="color:inherit;text-decoration:none;">{{ email }}</a></div>{% endif %}{% if phone %}<div><a href="tel:{{ phone }}" class="lcars-footer-link" style="color:inherit;text-decoration:none;">{{ phone }}</a></div>{% endif %}</address>{% endif %}
|
||||
{% if showNewsletter %}<div class="lcars-stack-sm" style="margin-top:0.5rem;display:flex;flex-direction:column;gap:0.5rem;"><p class="lcars-footer-head" style="margin:0;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.08em;color:hsl(var(--foreground));">{{ newsletterTitle|default:"Stay in the loop" }}</p>{% if newsletterDescription %}<p class="lcars-text" style="margin:0;font-size:0.8125rem;color:hsl(var(--muted-foreground));">{{ newsletterDescription }}</p>{% endif %}{% signup_form list=newsletterListSlug|default:"main" source="footer" submit=newsletterButtonText|default:"Subscribe" %}</div>{% endif %}
|
||||
</div>
|
||||
{% for col in columns %}<div class="lcars-stack-sm"><p class="lcars-footer-head" style="margin:0 0 0.75rem;font-size:0.75rem;text-transform:uppercase;letter-spacing:0.08em;color:hsl(var(--foreground));">{{ col.header }}</p><ul style="list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:0.5rem;">{% for item in col.items %}<li><a class="lcars-footer-link" href="{{ item.url|default:'#' }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} style="font-size:0.875rem;color:hsl(var(--muted-foreground));text-decoration:none;">{{ item.label }}</a></li>{% endfor %}</ul></div>{% endfor %}
|
||||
</div>
|
||||
<div class="lcars-footer-legal" style="display:flex;flex-wrap:wrap;align-items:center;justify-content:space-between;gap:1rem;">
|
||||
<p style="margin:0;">{% if copyright %}© {{ copyright }}{% else %}© {{ companyName }}{% endif %}</p>
|
||||
{% if legal_items %}<nav style="display:flex;flex-wrap:wrap;gap:1.25rem;">{% for l in legal_items %}<a href="{{ l.url|default:'#' }}"{% if l.open_in_new_tab %} target="_blank" rel="noopener"{% endif %} class="lcars-footer-link" style="font-size:0.8125rem;color:hsl(var(--muted-foreground));text-decoration:none;">{{ l.label }}</a>{% endfor %}</nav>{% endif %}
|
||||
{% if showSocial and social_items %}<div class="lcars-social-row" style="display:flex;align-items:center;gap:0.75rem;">{% for s in social_items %}<a href="{{ s.url }}" target="_blank" rel="noopener" title="{{ s.label }}" class="lcars-social-link" style="display:inline-flex;color:hsl(var(--muted-foreground));">{% if s.icon %}<svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/{{ s.icon|split:":"|first }}.svg#{{ s.icon|split:":"|last }}"/></svg>{% endif %}</a>{% endfor %}</div>{% endif %}
|
||||
</div>
|
||||
</div><style>.lcars-footer-logo{max-height:2rem;width:auto;display:block;}.lcars-footer-link:hover{color:hsl(var(--foreground));}</style></footer>
|
||||
6
templates/overrides/lcars/gallery.ninjatpl
Normal file
@ -0,0 +1,6 @@
|
||||
{# gallery override — Swiss grid of hairline-framed images; carries data-bn-gallery / data-lightbox / data-bn-gallery-item so the host lightbox JS binds. #}
|
||||
{% if columns == 4 %}{% set gcols = "lcars-gallery-4" %}{% set gn = 4 %}{% elif columns == 2 %}{% set gcols = "" %}{% set gn = 2 %}{% else %}{% set gcols = "lcars-gallery-3" %}{% set gn = 3 %}{% endif %}
|
||||
{% if gap == "sm" %}{% set ggap = "0.5rem" %}{% elif gap == "lg" %}{% set ggap = "1.5rem" %}{% else %}{% set ggap = "1rem" %}{% endif %}
|
||||
<div class="lcars-gallery-grid {{ gcols }} {{ class }}" data-bn-gallery data-lightbox="{% if lightbox %}true{% else %}false{% endif %}" style="margin:1.5rem 0;display:grid;grid-template-columns:repeat({{ gn }}, minmax(0, 1fr));gap:{{ ggap }};">
|
||||
{% for image in images %}<a href="{{ image.src|mediaURL }}" data-bn-gallery-item data-caption="{{ image.caption|default:'' }}" class="lcars-media-frame" style="display:block;overflow:hidden;border:1px solid hsl(var(--border));border-radius:4px;background:hsl(var(--muted));">{% img image.src alt=image.alt|default:"" class="lcars-gallery-img" %}</a>{% endfor %}
|
||||
</div><style>.lcars-gallery-grid .lcars-gallery-img{display:block;width:100%;height:100%;aspect-ratio:1;object-fit:cover;}</style>
|
||||
1
templates/overrides/lcars/heading.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
{% if level|integer == 1 %}<h1 class="lcars-heading lcars-heading-h1 {{ textClass }}" style="font-size: clamp(2rem, 4vw, 3rem); line-height: 1.1;">{{ text }}</h1>{% elif level|integer == 3 %}<h3 class="lcars-heading lcars-heading-h3 {{ textClass }}" style="font-size: 1.5rem; line-height: 1.3;">{{ text }}</h3>{% elif level|integer == 4 %}<h4 class="lcars-heading lcars-heading-h4 {{ textClass }}" style="font-size: 1.25rem; line-height: 1.3;">{{ text }}</h4>{% elif level|integer == 5 %}<h5 class="lcars-heading lcars-heading-h5 {{ textClass }}" style="font-size: 1.125rem; line-height: 1.4;">{{ text }}</h5>{% elif level|integer == 6 %}<h6 class="lcars-heading lcars-heading-h6 {{ textClass }}" style="font-size: 1rem; line-height: 1.4;">{{ text }}</h6>{% else %}<h2 class="lcars-heading lcars-heading-h2 {{ textClass }}" style="font-size: clamp(1.5rem, 3vw, 2.25rem); line-height: 1.2;"><span class="lcars-heading-h2-accent">{{ text }}</span></h2>{% endif %}
|
||||
57
templates/overrides/lcars/hero.ninjatpl
Normal file
@ -0,0 +1,57 @@
|
||||
{# hero override — LCARS command-deck hero. GO BIG motion tier: one dependency-free canvas panel-sweep behind the content, lazy-inited, prefers-reduced-motion honored, with the fully-styled hero as the static no-JS fallback. All content fields preserved verbatim from the builtin. Styled-text fields accept {text,color} OR a bare string. #}
|
||||
{% set eyebrowText = eyebrow|get:"text"|default:eyebrow %}
|
||||
{% set headlineText = headline|get:"text"|default:headline %}
|
||||
{% set subText = subheadline|get:"text"|default:subheadline %}
|
||||
{% set descText = description|get:"text"|default:description %}
|
||||
{% set pText = ctaPrimary.text %}
|
||||
{% set pUrl = ctaPrimary.url %}
|
||||
{% set sText = ctaSecondary.text %}
|
||||
{% set sUrl = ctaSecondary.url %}
|
||||
{% set variant = template|default:"centered" %}
|
||||
{% macro herobody() %}
|
||||
{% if eyebrowText %}<p class="lcars-eyebrow">{{ eyebrowText }}</p>{% endif %}
|
||||
{% if headlineText %}<h1 class="lcars-hero-headline lcars-display" style="margin: 0;">{{ headlineText }}</h1>{% endif %}
|
||||
{% if subText %}<p class="lcars-hero-sub lcars-lede" style="margin: 1.25rem 0 0 0; max-width: 60ch;">{{ subText }}</p>{% endif %}
|
||||
{% if descText %}<p class="lcars-text" style="margin: 1rem 0 0 0; color: hsl(var(--muted-foreground)); max-width: 60ch;">{{ descText }}</p>{% endif %}
|
||||
{% if pText or sText %}<div class="lcars-btn-row" style="margin-top: 2rem;">{% if pText and pUrl %}<a class="lcars-btn-primary lcars-btn-lg" href="{{ pUrl }}">{{ pText }}</a>{% endif %}{% if sText and sUrl %}<a class="lcars-btn-outline lcars-btn-lg" href="{{ sUrl }}">{{ sText }}</a>{% endif %}</div>{% endif %}
|
||||
{% if trustLogos %}<div class="lcars-hairline-top" style="margin-top: 3rem; padding-top: 1.5rem;"><p class="lcars-kicker lcars-eyebrow" style="margin-bottom: 1rem; color: hsl(var(--muted-foreground));">Trusted by</p><div class="lcars-logo-row">{% for logo in trustLogos %}{% if logo.url %}<a href="{{ logo.url }}">{% img logo.image alt=logo.alt class="lcars-logo" %}</a>{% else %}{% img logo.image alt=logo.alt class="lcars-logo" %}{% endif %}{% endfor %}</div></div>{% endif %}
|
||||
{% endmacro %}
|
||||
{% if variant == "split" %}
|
||||
<section class="lcars-hero lcars-hero-split lcars-hero-canvas-host {{ class }}"><canvas class="lcars-hero-canvas" data-lcars-hero-canvas aria-hidden="true"></canvas><div class="lcars-content-well">{% if backgroundImage %}<div class="lcars-grid lcars-grid-2" style="align-items: center;"><div>{{ herobody() }}</div><div class="lcars-media-frame" style="aspect-ratio: 4 / 3; max-height: 26rem; {% if mediaPosition == "left" %}order: -1;{% endif %}">{% img backgroundImage alt=headlineText class="lcars-hero-media" %}</div></div>{% else %}<div style="max-width: 60ch;">{{ herobody() }}</div>{% endif %}</div></section>
|
||||
{% else %}
|
||||
<section class="lcars-hero lcars-hero-canvas-host {{ class }}" style="{% if variant == "left-aligned" %}text-align: left;{% else %}text-align: center;{% endif %}"><canvas class="lcars-hero-canvas" data-lcars-hero-canvas aria-hidden="true"></canvas><div class="lcars-content-well" style="{% if variant != "left-aligned" %}display: flex; flex-direction: column; align-items: center;{% endif %}"><div style="max-width: 60ch; width: 100%;">{{ herobody() }}</div></div></section>
|
||||
{% endif %}
|
||||
<script>
|
||||
(function(){
|
||||
if(window.__lcarsHeroInit)return;window.__lcarsHeroInit=1;
|
||||
function reduced(){return window.matchMedia&&window.matchMedia('(prefers-reduced-motion: reduce)').matches;}
|
||||
function tok(el,name){var v=getComputedStyle(el).getPropertyValue(name);return (v&&v.trim())?('hsl('+v.trim()+')'):'currentColor';}
|
||||
function start(cv){
|
||||
if(cv.__on)return;cv.__on=1;
|
||||
var ctx=cv.getContext&&cv.getContext('2d');if(!ctx)return;
|
||||
var host=cv.parentElement;
|
||||
var cols=[tok(host,'--primary'),tok(host,'--secondary'),tok(host,'--accent')];
|
||||
var bars=[];
|
||||
function seed(w,h){bars=[];var n=Math.max(4,Math.floor(h/46));for(var i=0;i<n;i++){bars.push({y:(i+0.5)*(h/n),w:40+Math.random()*160,x:Math.random()*w,v:(0.25+Math.random()*0.9)*(Math.random()<0.5?-1:1),c:cols[i%3]});}}
|
||||
function size(){var r=host.getBoundingClientRect();var dpr=Math.min(window.devicePixelRatio||1,2);cv.width=Math.max(1,r.width*dpr);cv.height=Math.max(1,r.height*dpr);ctx.setTransform(dpr,0,0,dpr,0,0);seed(r.width,r.height);}
|
||||
size();
|
||||
var still=reduced();
|
||||
function frame(){
|
||||
var r=host.getBoundingClientRect();ctx.clearRect(0,0,r.width,r.height);ctx.globalAlpha=0.16;
|
||||
for(var i=0;i<bars.length;i++){var b=bars[i];ctx.fillStyle=b.c;var h=14;var rad=h/2;var x=b.x,y=b.y-h/2;
|
||||
ctx.beginPath();ctx.moveTo(x+rad,y);ctx.arcTo(x+b.w,y,x+b.w,y+h,rad);ctx.arcTo(x+b.w,y+h,x,y+h,rad);ctx.arcTo(x,y+h,x,y,rad);ctx.arcTo(x,y,x+b.w,y,rad);ctx.closePath();ctx.fill();
|
||||
if(!still){b.x+=b.v;if(b.x>r.width)b.x=-b.w;if(b.x<-b.w)b.x=r.width;}}
|
||||
if(!still)cv.__raf=requestAnimationFrame(frame);
|
||||
}
|
||||
frame();
|
||||
var rt;window.addEventListener('resize',function(){clearTimeout(rt);rt=setTimeout(size,150);},{passive:true});
|
||||
}
|
||||
function init(){
|
||||
var nodes=document.querySelectorAll('[data-lcars-hero-canvas]');
|
||||
if(!('IntersectionObserver' in window)){nodes.forEach(start);return;}
|
||||
var io=new IntersectionObserver(function(es){es.forEach(function(e){if(e.isIntersecting){start(e.target);io.unobserve(e.target);}});},{rootMargin:'120px'});
|
||||
nodes.forEach(function(n){io.observe(n);});
|
||||
}
|
||||
if(document.readyState!='loading')init();else document.addEventListener('DOMContentLoaded',init);
|
||||
})();
|
||||
</script>
|
||||
18
templates/overrides/lcars/hours-location.ninjatpl
Normal file
@ -0,0 +1,18 @@
|
||||
{# hours-location override — hairline hours table + address; click-to-load map preserves the builtin data-map-src / onclick wiring. #}
|
||||
<section class="lcars-section {{ class }}"><div class="lcars-content-well">
|
||||
{% if heading %}<h2 class="lcars-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
|
||||
<div class="lcars-grid lcars-grid-2" style="display:grid;grid-template-columns:repeat(auto-fit, minmax(280px, 1fr));gap:2.5rem;margin-top:2.5rem;">
|
||||
<div>
|
||||
{% if hours %}<table style="width:100%;border-collapse:collapse;font-size:0.9375rem;"><tbody>{% for row in hours %}<tr class="lcars-hours-row lcars-hairline-bottom"><th scope="row" style="text-align:left;padding:0.5rem 1rem 0.5rem 0;font-weight:500;">{{ row.day }}</th><td class="numeric-tabular" style="text-align:right;padding:0.5rem 0;color:{% if row.closed %}hsl(var(--muted-foreground)){% else %}hsl(var(--foreground)){% endif %};">{% if row.closed %}Closed{% else %}{{ row.hours }}{% endif %}</td></tr>{% endfor %}</tbody></table>{% endif %}
|
||||
{% if specialHoursNote %}<p class="lcars-surface-muted" style="margin-top:1rem;border:1px solid hsl(var(--border));background:hsl(var(--muted));padding:0.5rem 0.75rem;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ specialHoursNote }}</p>{% endif %}
|
||||
{% if address %}<div style="margin-top:1.5rem;"><div class="lcars-stat-label" style="margin:0;">Address</div><address style="margin-top:0.25rem;white-space:pre-line;font-style:normal;color:hsl(var(--foreground));">{{ address }}</address></div>{% endif %}
|
||||
{% if phone %}<div style="margin-top:1rem;"><div class="lcars-stat-label" style="margin:0;">Phone</div><a href="tel:{{ phone }}" style="display:inline-block;margin-top:0.25rem;color:hsl(var(--foreground));text-decoration:none;">{{ phone }}</a></div>{% endif %}
|
||||
{% if directionsUrl %}<a href="{{ directionsUrl }}" target="_blank" rel="noopener noreferrer" class="lcars-btn-outline" style="margin-top:1.5rem;display:inline-flex;align-items:center;gap:0.5rem;"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#navigation"/></svg>Get directions</a>{% endif %}
|
||||
</div>
|
||||
{% if mapEmbedUrl %}
|
||||
<div class="lcars-media-frame" style="width:100%;min-height:16rem;aspect-ratio:4 / 3;overflow:hidden;border:1px solid hsl(var(--border));border-radius:4px;background:hsl(var(--muted));">
|
||||
<button type="button" data-map-src="{{ mapEmbedUrl }}" onclick="var f=document.createElement('iframe');f.src=this.getAttribute('data-map-src');f.setAttribute('style','width:100%;height:100%;border:0;');f.setAttribute('loading','lazy');f.setAttribute('referrerpolicy','no-referrer-when-downgrade');f.setAttribute('title','Map');this.parentNode.replaceChild(f,this);" style="display:flex;align-items:center;justify-content:center;gap:0.5rem;width:100%;height:100%;min-height:16rem;border:0;background:none;cursor:pointer;color:hsl(var(--foreground));font-family:var(--font-heading, 'Antonio', 'Antonio', system-ui, sans-serif);font-weight:500;"><svg width="20" height="20" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#map-pin"/></svg>{{ mapButtonLabel|default:"Load map" }}</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div></section>
|
||||
5
templates/overrides/lcars/image.ninjatpl
Normal file
@ -0,0 +1,5 @@
|
||||
{# image override — hairline-framed figure with alignment and an optional caption. #}
|
||||
{% if src %}{% if align == "left" %}{% set ialign = "flex-start" %}{% elif align == "right" %}{% set ialign = "flex-end" %}{% else %}{% set ialign = "center" %}{% endif %}<figure class="{{ class }}" style="margin:1.5rem 0;display:flex;flex-direction:column;align-items:{{ ialign }};">
|
||||
<div class="lcars-media-frame" style="max-width:100%;overflow:hidden;border:1px solid hsl(var(--border));border-radius:4px;line-height:0;">{% img src alt=alt|default:"" class="lcars-image-el" %}</div>
|
||||
{% if caption %}<figcaption class="lcars-stat-label" style="margin-top:0.5rem;text-align:{% if align == "center" %}center{% else %}left{% endif %};">{{ caption }}</figcaption>{% endif %}
|
||||
</figure><style>.lcars-media-frame > .lcars-image-el,.lcars-media-frame > img{display:block;max-width:100%;height:auto;}</style>{% endif %}
|
||||
5
templates/overrides/lcars/logo-strip.ninjatpl
Normal file
@ -0,0 +1,5 @@
|
||||
{# logo-strip override — quiet greyscale client row under a kicker. #}
|
||||
<section class="lcars-section-tight {{ class }}" style="padding-top: 3rem; padding-bottom: 3rem;"><div class="lcars-content-well">
|
||||
{% if heading %}<p class="lcars-kicker lcars-eyebrow" style="text-align: center; color: hsl(var(--muted-foreground)); margin-bottom: 0;">{{ heading }}</p>{% endif %}
|
||||
<div class="lcars-logo-row" style="display: flex; flex-wrap: wrap; align-items: center; justify-content: center; gap: 2.5rem; margin-top: 1.5rem;">{% for logo in logos %}{% if logo.url %}<a href="{{ logo.url }}">{% img logo.image alt=logo.alt class="lcars-logo" %}</a>{% else %}{% img logo.image alt=logo.alt class="lcars-logo" %}{% endif %}{% endfor %}</div>
|
||||
</div></section>
|
||||
21
templates/overrides/lcars/menu-list.ninjatpl
Normal file
@ -0,0 +1,21 @@
|
||||
{# menu-list override — sectioned price list; tabular-numeral prices right-aligned, dietary badges as hairline pills. #}
|
||||
<section class="lcars-section {{ class }}"><div class="lcars-content-well">
|
||||
{% if heading %}<h2 class="lcars-heading" style="font-size:clamp(1.75rem, 3vw, 2.25rem);margin:0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
|
||||
{% if intro %}<p class="lcars-lede" style="max-width:60ch;margin:0 0 2.5rem 0;color:hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
|
||||
<div class="lcars-grid {% if columns == 2 %}lcars-grid-2{% endif %}" style="display:grid;gap:2.5rem 3rem;{% if columns == 2 %}grid-template-columns:repeat(2, minmax(0, 1fr));{% endif %}">
|
||||
{% for section in sections %}<div class="lcars-menu-section" style="break-inside:avoid;">
|
||||
{% if section.title %}<h3 class="lcars-display" style="font-size:1.25rem;border-bottom:1px solid hsl(var(--border));padding-bottom:0.5rem;margin:0 0 1rem 0;">{{ section.title }}</h3>{% endif %}
|
||||
{% if section.note %}<p style="margin:0 0 1rem 0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ section.note }}</p>{% endif %}
|
||||
<ul style="list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:1.25rem;">
|
||||
{% for item in section.items %}<li class="lcars-menu-item" style="display:flex;align-items:baseline;gap:1rem;">
|
||||
<div style="min-width:0;flex:1;">
|
||||
<div style="display:flex;flex-wrap:wrap;align-items:baseline;gap:0.5rem;"><span class="lcars-display" style="font-size:1rem;color:hsl(var(--foreground));">{{ item.name }}</span>{% for badge in item.badges %}{% if badge.label %}<span style="display:inline-block;border:1px solid hsl(var(--border));border-radius:4px;padding:0.05rem 0.4rem;font-size:0.6875rem;text-transform:uppercase;letter-spacing:0.05em;color:hsl(var(--muted-foreground));">{{ badge.label }}</span>{% endif %}{% endfor %}</div>
|
||||
{% if item.description %}<p style="margin:0.25rem 0 0 0;font-size:0.875rem;color:hsl(var(--muted-foreground));">{{ item.description }}</p>{% endif %}
|
||||
</div>
|
||||
{% if item.price %}<span class="lcars-menu-price lcars-mono numeric-tabular" style="flex:0 0 auto;color:hsl(var(--foreground));">{% if currency %}{{ currency }}{% endif %}{{ item.price }}</span>{% endif %}
|
||||
</li>{% endfor %}
|
||||
</ul>
|
||||
</div>{% endfor %}
|
||||
</div>
|
||||
{% if footnote %}<p class="lcars-stat-label" style="margin-top:2.5rem;">{{ footnote }}</p>{% endif %}
|
||||
</div></section>
|
||||
1
templates/overrides/lcars/navbar.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
<nav class="lcars-navbar {{ class }}" aria-label="Primary"><a class="lcars-brand" href="/"><span class="lcars-mark lcars-mark-square" aria-hidden="true"></span>{% if logoType == "image" and logo %}{% img logo alt=alt|default:"Home" class="lcars-brand-logo" %}{% else %}<span>{{ logoText|get:"text"|default:logoText|default:context.site.title|default:"Studio" }}</span>{% endif %}</a><div class="lcars-nav-links">{% for item in menu.items %}<a class="lcars-nav-link" href="{{ item.url }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}>{{ item.label|default:item.title }}</a>{% endfor %}{% if cta.text and cta.url %}<a class="lcars-btn-primary lcars-btn-sm" href="{{ cta.url }}">{{ cta.text }}</a>{% endif %}</div><input type="checkbox" id="lcars-nav-toggle-cb" class="lcars-nav-cb" style="display:none;"><label for="lcars-nav-toggle-cb" class="lcars-nav-toggle lcars-btn-outline lcars-btn-sm" aria-label="Toggle menu"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#menu"/></svg></label><div class="lcars-nav-drawer">{% for item in menu.items %}<a href="{{ item.url }}"{% if item.open_in_new_tab %} target="_blank" rel="noopener"{% endif %}>{{ item.label|default:item.title }}</a>{% endfor %}{% if cta.text and cta.url %}<a href="{{ cta.url }}" class="lcars-btn-primary lcars-btn-sm" style="margin-top:0.75rem;">{{ cta.text }}</a>{% endif %}</div><style>.lcars-nav-cb:checked ~ .lcars-nav-drawer{display:flex;}</style></nav>
|
||||
1
templates/overrides/lcars/newsletter-signup.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
<section class="lcars-section {{ class }}" data-block="lcars:newsletter-signup"><div class="lcars-content-well"><div class="{% if layout == "inline" %}lcars-panel{% else %}lcars-card lcars-card-accent{% endif %}" style="text-align: center;">{% if title %}<h2 class="lcars-heading" style="font-size: clamp(1.5rem, 2.5vw, 2rem); margin: 0;"><span class="lcars-heading-h2-accent">{{ title }}</span></h2>{% endif %}{% if description %}<p class="lcars-lede" style="max-width: 52ch; margin: 0.75rem auto 0 auto;">{{ description }}</p>{% endif %}<div class="lcars-form" style="max-width: 28rem; margin: 1.5rem auto 0 auto;">{% signup_form list=listSlug source=source submit=submitText success_message=successMessage placeholder=placeholder %}</div></div></div></section>
|
||||
1
templates/overrides/lcars/page-suggestions.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
<section class="lcars-section {{ class }}" data-block="lcars:page-suggestions"><div class="lcars-content-well"><div class="lcars-measure lcars-stack"><span class="lcars-mark lcars-mark-triangle" aria-hidden="true"></span><h2 class="lcars-heading" style="font-size: clamp(1.5rem, 2.5vw, 2rem); margin: 0;"><span class="lcars-heading-h2-accent">{{ title|default:"Were you looking for..." }}</span></h2>{% if suggestions %}<ul class="lcars-stack-sm" style="list-style: none; margin: 0; padding: 0;">{% for s in suggestions %}<li><a class="lcars-sidebar-link" href="/{{ s.slug }}" style="display: block;"><span class="lcars-post-title" style="font-size: 1rem;">{{ s.title }}</span>{% if showDescription and s.excerpt %}<span class="lcars-text" style="display: block; font-size: 0.875rem; color: hsl(var(--muted-foreground));">{{ s.excerpt }}</span>{% endif %}</a></li>{% endfor %}</ul>{% else %}{% if emptyMessage %}<p class="lcars-text" style="margin: 0; color: hsl(var(--muted-foreground));">{{ emptyMessage }}</p>{% endif %}<div class="lcars-btn-row"><a class="lcars-btn-outline lcars-btn-sm" href="/"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#home"/></svg>Home</a><a class="lcars-btn-outline lcars-btn-sm" href="/blog"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#newspaper"/></svg>Blog</a></div>{% endif %}</div></div></section>
|
||||
11
templates/overrides/lcars/password-reset-form.ninjatpl
Normal file
@ -0,0 +1,11 @@
|
||||
<div class="lcars-stack {{ class }}" data-block="lcars:password-reset-form">
|
||||
<span class="lcars-mark lcars-mark-ring" aria-hidden="true"></span>
|
||||
<h2 class="lcars-heading" style="font-size: 1.5rem; margin: 0;">Reset password</h2>
|
||||
<p class="lcars-text" style="margin: 0; font-size: 0.9375rem; color: hsl(var(--muted-foreground));">Enter your email and we will send a link to reset your password.</p>
|
||||
<div id="reset-message" role="status" aria-live="polite"></div>
|
||||
<form class="lcars-form" hx-post="/api/auth/request-password-reset" hx-target="#reset-message" hx-swap="innerHTML">
|
||||
<div class="lcars-field lcars-field--full"><label class="lcars-label" for="lcars-reset-email">Email</label><input class="lcars-input" type="email" name="email" id="lcars-reset-email" required></div>
|
||||
<button type="submit" class="lcars-btn-primary" hx-disabled-elt="this" style="width:100%; justify-content:center;">Send reset link</button>
|
||||
</form>
|
||||
<a class="lcars-btn-ghost lcars-btn-sm" href="/login" style="align-self: flex-start;"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#arrow-left"/></svg>Back to sign in</a>
|
||||
</div>
|
||||
1
templates/overrides/lcars/popular-posts.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
<section class="lcars-section-tight {{ class }}" data-block="lcars:popular-posts">{% if heading %}<h2 class="lcars-kicker">{{ heading }}</h2>{% endif %}<ol class="lcars-stack-sm" style="list-style: none; margin: 0; padding: 0;">{% for post in posts %}<li style="display: flex; gap: 0.75rem; align-items: flex-start;">{% if showRank %}<span class="lcars-rank numeric-tabular">{{ forloop.Counter }}</span>{% endif %}{% if showThumbnail and post.featured_image_url %}<a href="{{ post.url }}" class="lcars-post-thumb lcars-media-frame" style="width: 3rem; height: 3rem; flex-shrink: 0;"><img src="{{ post.featured_image_url }}" alt="{{ post.title }}" style="width: 100%; height: 100%; object-fit: cover;"></a>{% endif %}<div style="min-width: 0;"><h3 class="lcars-post-title" style="font-size: 0.9375rem;"><a href="{{ post.url }}">{{ post.title }}</a></h3>{% if post.published_at %}<p class="lcars-post-meta"><span class="lcars-mono numeric-tabular">{{ post.published_at|date:"M j, Y" }}</span></p>{% endif %}</div></li>{% endfor %}</ol></section>
|
||||
1
templates/overrides/lcars/post-hero.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
<section class="lcars-section {{ class }}" data-block="lcars:post-hero"><div class="lcars-content-well">{% if context.currentPost.featuredImageUrl %}<div class="lcars-media-frame" style="aspect-ratio: 21 / 9; margin-bottom: 2.5rem;"><img src="{{ context.currentPost.featuredImageUrl }}" alt="{{ context.currentPost.featuredImageAlt|default:context.currentPost.title }}" style="width: 100%; height: 100%; object-fit: cover;"></div>{% endif %}<div class="lcars-measure lcars-stack" style="{% if align == "left" %}text-align: left;{% else %}margin-left: auto; margin-right: auto; text-align: center;{% endif %}"><span class="lcars-mark lcars-mark-square" aria-hidden="true"{% if align != "left" %} style="margin-left: auto; margin-right: auto;"{% endif %}></span><h1 class="lcars-display" style="margin: 0;">{{ context.currentPost.title }}</h1>{% if showExcerpt and context.currentPost.excerpt %}<p class="lcars-lede" style="margin: 0;">{{ context.currentPost.excerpt }}</p>{% endif %}{% if showMeta %}<div class="lcars-post-meta"{% if align != "left" %} style="justify-content: center;"{% endif %}>{% if context.currentPost.authorName %}<span>{{ context.currentPost.authorName }}</span>{% endif %}{% if context.currentPost.publishedAt %}<span class="lcars-mono numeric-tabular">{{ context.currentPost.publishedAt }}</span>{% endif %}{% if context.currentPost.readingTimeMinutes %}<span class="lcars-mono numeric-tabular">{{ context.currentPost.readingTimeMinutes }} min read</span>{% endif %}</div>{% endif %}</div></div></section>
|
||||
1
templates/overrides/lcars/post-metadata.ninjatpl
Normal file
@ -0,0 +1 @@
|
||||
<div class="lcars-post-meta {{ class }}" data-block="lcars:post-metadata">{% if showAuthor and context.currentPost.authorName %}<span style="display: inline-flex; align-items: center; gap: 0.5rem;">{% if showAvatar and context.currentPost.authorAvatarUrl %}<img class="lcars-avatar" src="{{ context.currentPost.authorAvatarUrl }}" alt="{{ context.currentPost.authorName }}" style="width: 1.75rem; height: 1.75rem; border-radius: 9999px; object-fit: cover;">{% endif %}<span style="color: hsl(var(--foreground)); font-weight: 500;">{{ context.currentPost.authorName }}</span></span>{% endif %}{% if showDate and context.currentPost.publishedAt %}<time class="lcars-mono numeric-tabular" datetime="{{ context.currentPost.publishedAtIso }}">{{ context.currentPost.publishedAt }}</time>{% endif %}{% if showReadingTime and context.currentPost.readingTimeMinutes %}<span class="lcars-mono numeric-tabular">{{ context.currentPost.readingTimeMinutes }} min read</span>{% endif %}</div>
|
||||
14
templates/overrides/lcars/pricing.ninjatpl
Normal file
@ -0,0 +1,14 @@
|
||||
{# pricing override — flat hairline tiers, mono price figures, accent-top featured tier. #}
|
||||
<section class="lcars-section {{ class }}"><div class="lcars-content-well">
|
||||
{% if heading %}<h2 class="lcars-heading" style="font-size: clamp(1.75rem, 3vw, 2.25rem); margin: 0 0 0.75rem 0;"><span class="lcars-heading-h2-accent">{{ heading }}</span></h2>{% endif %}
|
||||
{% if intro %}<p class="lcars-lede" style="max-width: 60ch; margin: 0 0 2.5rem 0; color: hsl(var(--muted-foreground));">{{ intro }}</p>{% endif %}
|
||||
{% if tiers %}<div class="lcars-grid lcars-grid-3">{% for tier in tiers %}<div class="lcars-card lcars-price-tier {% if tier.highlighted %}lcars-price-tier--featured lcars-card-accent{% endif %}" style="display: flex; flex-direction: column;">
|
||||
{% if tier.badge %}<span class="lcars-badge" style="display: inline-block; align-self: flex-start; background: hsl(var(--accent)); color: hsl(var(--accent-foreground)); font-size: 0.6875rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.08em; padding: 0.25rem 0.5rem; border-radius: 4px; margin-bottom: 0.75rem;">{{ tier.badge }}</span>{% endif %}
|
||||
{% if tier.name %}<h3 class="lcars-feature-title" style="margin: 0;">{{ tier.name }}</h3>{% endif %}
|
||||
<p style="display: flex; align-items: baseline; gap: 0.25rem; margin: 1rem 0 0 0;"><span class="lcars-price numeric-tabular" style="font-family: var(--font-mono, ui-monospace, monospace); font-size: clamp(2rem, 4vw, 2.75rem); font-weight: 500; line-height: 1; color: hsl(var(--foreground));">{% if currency %}{{ currency }}{% else %}${% endif %}{{ tier.price }}</span>{% if tier.period %}<span class="lcars-price-period lcars-stat-label" style="margin-top: 0;">{{ tier.period }}</span>{% endif %}</p>
|
||||
{% if tier.description %}<p class="lcars-text" style="font-size: 0.9375rem; color: hsl(var(--muted-foreground)); margin-top: 0.75rem;">{{ tier.description }}</p>{% endif %}
|
||||
{% if tier.features %}<ul class="lcars-stack-sm" style="list-style: none; padding: 0; margin: 1.5rem 0 0 0; display: flex; flex-direction: column; gap: 0.625rem; flex: 1;">{% for feature in tier.features %}<li class="lcars-check" style="display: flex; gap: 0.5rem; align-items: flex-start; font-size: 0.9375rem; color: hsl(var(--foreground));"><span style="color: hsl(var(--accent)); flex: none;"><svg width="16" height="16" class="lcars-inline-icon" aria-hidden="true"><use href="/icons/lucide.svg#check"/></svg></span><span>{{ feature }}</span></li>{% endfor %}</ul>{% endif %}
|
||||
{% if tier.ctaUrl and tier.ctaLabel %}{% if tier.highlighted %}<a class="lcars-btn-primary" href="{{ tier.ctaUrl }}" style="margin-top: 1.5rem; width: 100%; box-sizing: border-box; text-align: center;">{{ tier.ctaLabel }}</a>{% else %}<a class="lcars-btn-outline" href="{{ tier.ctaUrl }}" style="margin-top: 1.5rem; width: 100%; box-sizing: border-box; text-align: center;">{{ tier.ctaLabel }}</a>{% endif %}{% endif %}
|
||||
</div>{% endfor %}</div>{% endif %}
|
||||
{% if billingNote %}<p class="lcars-stat-label" style="text-align: center; margin-top: 2rem;">{{ billingNote }}</p>{% endif %}
|
||||
</div></section>
|
||||
6
templates/overrides/lcars/quote.ninjatpl
Normal file
@ -0,0 +1,6 @@
|
||||
{# quote override — pull quote (accent top rule) / block quote (accent left rule) / inline card. #}
|
||||
{% if style == "pull" %}{% set qcls = "lcars-pull-quote" %}{% set qsty = "margin:2rem 0;" %}{% elif style == "inline-card" %}{% set qcls = "lcars-quote-block lcars-card lcars-card-accent" %}{% set qsty = "margin:2rem 0;" %}{% else %}{% set qcls = "lcars-quote-block" %}{% set qsty = "margin:2rem 0;border-left:2px solid hsl(var(--accent));padding-left:1.25rem;" %}{% endif %}
|
||||
<figure class="{{ qcls }} {{ class }}" style="{{ qsty }}{% if align == "center" %}text-align:center;{% endif %}">
|
||||
<blockquote{% if cite %} cite="{{ cite }}"{% endif %} style="margin:0;border:0;padding:0;{% if style != "pull" %}font-size:1.25rem;line-height:1.5;color:hsl(var(--foreground));{% endif %}">{{ quote|safe }}</blockquote>
|
||||
{% if attribution or role or cite %}<figcaption class="lcars-quote-cite" style="margin-top:1rem;font-size:0.875rem;color:hsl(var(--muted-foreground));">{% if attribution %}<span class="lcars-display" style="font-size:0.9375rem;color:hsl(var(--foreground));">{{ attribution }}</span>{% endif %}{% if role %}<span style="display:block;">{{ role }}</span>{% endif %}{% if cite %}<a href="{{ cite }}" style="display:inline-block;margin-top:0.25rem;color:inherit;text-decoration:underline;text-underline-offset:2px;">Source</a>{% endif %}</figcaption>{% endif %}
|
||||
</figure>
|
||||