feat: convert to codeless .bnp — templates + manifest, Go/templ deleted (WO-WZ-021)
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
9fa99a8d40
commit
6dbd76f2e8
40
Makefile
40
Makefile
@ -1,26 +1,27 @@
|
||||
# lcars — build & publish helpers (wasm .bnp workflow)
|
||||
# lcars — build & publish helpers (CODELESS .bnp workflow, WO-WZ-021)
|
||||
#
|
||||
# The theme compiles to a wasip1 reactor-mode wasm module packed into a .bnp
|
||||
# artifact (`ninja plugin build`). The legacy .so path is retired (WO-WZ-015):
|
||||
# the CMS is wasm-native and can no longer load .so plugins. Distribution is
|
||||
# publish-to-registry -> InstallFromRegistry (hot-load, no restart).
|
||||
# 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 # compile wasm + pack lcars-<version>.bnp
|
||||
# make verify # validate the built .bnp (layout/abi/name/size)
|
||||
# make archive-check # prove a clean `git archive HEAD` compiles to wasm
|
||||
# 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 templ # regenerate templ Go files
|
||||
# make clean # remove build artefacts
|
||||
|
||||
.PHONY: build verify archive-check publish templ clean help
|
||||
.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
|
||||
|
||||
# Compile to wasm and pack the .bnp (GOOS=wasip1 GOARCH=wasm, DESCRIBE probe,
|
||||
# tar.zst of plugin.wasm + plugin.mod + manifest.pb + schemas/ + assets/).
|
||||
# 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 .
|
||||
|
||||
@ -28,12 +29,12 @@ build:
|
||||
verify:
|
||||
$(NINJA) plugin verify $(BNP)
|
||||
|
||||
# Prove a clean `git archive HEAD` (what publish ships) compiles to wasm.
|
||||
# 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" && GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o "$$T/check.wasm" . && \
|
||||
cd "$$T" && $(NINJA) plugin build --dir . && \
|
||||
echo "archive-check OK"
|
||||
|
||||
# Publish the prebuilt .bnp to the active registry.
|
||||
@ -41,19 +42,14 @@ archive-check:
|
||||
publish: build
|
||||
$(NINJA) plugin publish --bnp $(BNP)
|
||||
|
||||
# Regenerate templ Go files locally.
|
||||
templ:
|
||||
templ generate
|
||||
|
||||
# Remove build artefacts.
|
||||
clean:
|
||||
rm -f $(PLUGIN_NAME)-*.bnp $(PLUGIN_NAME).so plugin.wasm
|
||||
rm -f $(PLUGIN_NAME)-*.bnp plugin.wasm
|
||||
|
||||
help:
|
||||
@echo "Targets:"
|
||||
@echo " build Compile wasm + pack the .bnp"
|
||||
@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 compiles to wasm"
|
||||
@echo " archive-check Prove a clean git archive HEAD packs codeless"
|
||||
@echo " publish Publish the .bnp to the active registry"
|
||||
@echo " templ Regenerate templ Go files"
|
||||
@echo " clean Remove build artefacts"
|
||||
|
||||
53
blocks.go
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",
|
||||
}
|
||||
28
blocks/blocks.yaml
Normal file
28
blocks/blocks.yaml
Normal file
@ -0,0 +1,28 @@
|
||||
# LCARS theme block definitions (codeless .bnp, WO-WZ-021).
|
||||
# Keys are fully qualified (lcars:<key>) because the codeless loader registers
|
||||
# definitions verbatim — there is no PluginBlockRegistry prefixing in the
|
||||
# codeless path (unlike the retired wasm/pongo-guest path). The qualified keys
|
||||
# match the master_pages.json block references.
|
||||
#
|
||||
# header and sidebar previously used MustBlockTemplateWithDefaults; the codeless
|
||||
# path has no defaults-merge, so their defaults are baked into the templates via
|
||||
# |default filters (title/subtitle) and the {% empty %} branch (sidebar items).
|
||||
blocks:
|
||||
- key: lcars:lcars_header
|
||||
title: LCARS Header
|
||||
description: Top bar with elbow bracket, status indicators, and stardate
|
||||
category: navigation
|
||||
schema: lcars_header.schema.json
|
||||
template: lcars_header.ninjatpl
|
||||
- key: lcars:lcars_sidebar
|
||||
title: LCARS Sidebar
|
||||
description: Left panel with rounded-rectangle navigation buttons
|
||||
category: navigation
|
||||
schema: lcars_sidebar.schema.json
|
||||
template: lcars_sidebar.ninjatpl
|
||||
- key: lcars:lcars_panel
|
||||
title: LCARS Panel
|
||||
description: Framed content area with LCARS border treatment
|
||||
category: layout
|
||||
schema: lcars_panel.schema.json
|
||||
template: lcars_panel.ninjatpl
|
||||
@ -2,9 +2,9 @@
|
||||
<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>
|
||||
<h1 class="lcars-title">{{ title|default:"LCARS" }}</h1>
|
||||
{% if subtitle|default:"Library Computer Access/Retrieval System" %}
|
||||
<span class="lcars-subtitle">{{ subtitle|default:"Library Computer Access/Retrieval System" }}</span>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="lcars-header-indicators">
|
||||
23
blocks/lcars_sidebar.ninjatpl
Normal file
23
blocks/lcars_sidebar.ninjatpl
Normal file
@ -0,0 +1,23 @@
|
||||
<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 %}
|
||||
<a href="#" class="lcars-sidebar-btn lcars-bg-primary">
|
||||
<span class="lcars-sidebar-btn-label">Personnel</span>
|
||||
</a>
|
||||
<a href="#" class="lcars-sidebar-btn lcars-bg-secondary">
|
||||
<span class="lcars-sidebar-btn-label">Operations</span>
|
||||
</a>
|
||||
<a href="#" class="lcars-sidebar-btn lcars-bg-accent">
|
||||
<span class="lcars-sidebar-btn-label">Sciences</span>
|
||||
</a>
|
||||
<a href="#" class="lcars-sidebar-btn lcars-bg-primary">
|
||||
<span class="lcars-sidebar-btn-label">Engineering</span>
|
||||
</a>
|
||||
<a href="#" class="lcars-sidebar-btn lcars-bg-secondary">
|
||||
<span class="lcars-sidebar-btn-label">Medical</span>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
60
embed.go
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),
|
||||
}
|
||||
}
|
||||
19
go.mod
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.15.2
|
||||
|
||||
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
|
||||
)
|
||||
56
go.sum
56
go.sum
@ -1,56 +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.15.2 h1:qTjJxa1pClxwehkdTxXiJDpHQLr5DoXBUCa40QRO0L8=
|
||||
git.dev.alexdunmow.com/block/core v0.15.2/go.mod h1:A+mgA9oeWoPhXruv4Mjpywwl2A9B5I3cKzlJjalBdlE=
|
||||
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=
|
||||
github.com/tetratelabs/wazero v1.12.0 h1:DuWcpNu/FzgEXgGBDp8J1Spc+CWOvvtvVyjKlaZopYU=
|
||||
github.com/tetratelabs/wazero v1.12.0/go.mod h1:LvKtzl2RqO4gyF27BiXU+nKAjcV8f38U+kP/q2vgxh0=
|
||||
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/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
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=
|
||||
17
main.go
17
main.go
@ -1,17 +0,0 @@
|
||||
//go:build wasip1
|
||||
|
||||
// Package main compiles the lcars theme to a wasip1 reactor-mode wasm guest
|
||||
// (WO-WZ-015). The .so build path is retired; `ninja plugin build` produces the
|
||||
// .bnp artifact the registry serves.
|
||||
//
|
||||
// Reactor mode: the host runs `_initialize` once per pooled instance, which
|
||||
// runs this init func (hence wasmguest.Serve) before any bn_invoke. Serve is
|
||||
// non-blocking; all theme work then arrives over the ABI hook calls. main is
|
||||
// never called.
|
||||
package main
|
||||
|
||||
import "git.dev.alexdunmow.com/block/core/plugin/wasmguest"
|
||||
|
||||
func init() { wasmguest.Serve(Registration) }
|
||||
|
||||
func main() {} // never called — reactor mode
|
||||
458
manifest.yaml
Normal file
458
manifest.yaml
Normal file
@ -0,0 +1,458 @@
|
||||
# LCARS theme — codeless .bnp manifest (WO-WZ-021 Phase B1).
|
||||
# Synthesized into manifest.pb by `ninja plugin build` (no plugin.wasm).
|
||||
# This theme was previously a pongo2 wasm guest (it shipped the pongo engine
|
||||
# inside the guest); the codeless path renders the SAME .ninjatpl sources
|
||||
# host-side via cms/backend/plugin/pongoengine, so no engine ships here.
|
||||
theme_presets: presets.json
|
||||
bundled_fonts: fonts.json
|
||||
master_pages: master_pages.json
|
||||
|
||||
system_templates:
|
||||
- key: lcars
|
||||
title: LCARS
|
||||
description: "Star Trek LCARS computer interface theme"
|
||||
|
||||
page_templates:
|
||||
- system: lcars
|
||||
key: default
|
||||
title: Standard Display
|
||||
description: "Classic LCARS layout with sidebar and header frame"
|
||||
slots: [header, sidebar, main, footer]
|
||||
- system: lcars
|
||||
key: full-display
|
||||
title: Full Display
|
||||
description: "Full-width LCARS display without sidebar"
|
||||
slots: [header, main, footer]
|
||||
|
||||
template_overrides:
|
||||
- { template: lcars, block: heading }
|
||||
- { template: lcars, block: text }
|
||||
|
||||
email_wrappers:
|
||||
- lcars
|
||||
|
||||
css:
|
||||
input_css_append: |
|
||||
/* ============================================================
|
||||
LCARS Theme — Star Trek Computer Interface
|
||||
============================================================ */
|
||||
|
||||
/* --- Typography --- */
|
||||
|
||||
.lcars-page {
|
||||
font-family: 'Antonio', sans-serif;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
/* --- Color Utilities --- */
|
||||
|
||||
.lcars-bg-primary { background-color: hsl(var(--primary)); }
|
||||
.lcars-bg-secondary { background-color: hsl(var(--secondary)); }
|
||||
.lcars-bg-accent { background-color: hsl(var(--accent)); }
|
||||
.lcars-bg-muted { background-color: hsl(var(--muted)); }
|
||||
.lcars-text-primary { color: hsl(var(--primary)); }
|
||||
.lcars-text-secondary { color: hsl(var(--secondary)); }
|
||||
.lcars-text-accent { color: hsl(var(--accent)); }
|
||||
|
||||
/* --- Frame Layout --- */
|
||||
|
||||
.lcars-frame {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100vh;
|
||||
padding: 0.5rem;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.lcars-body {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr;
|
||||
grid-template-rows: auto 1fr auto;
|
||||
flex: 1;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.lcars-content-area {
|
||||
display: grid;
|
||||
grid-template-columns: 12rem 0.25rem 1fr;
|
||||
gap: 0;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.lcars-frame-full .lcars-body-full {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
/* --- Elbow Brackets --- */
|
||||
|
||||
.lcars-elbow-top,
|
||||
.lcars-elbow-bottom {
|
||||
display: flex;
|
||||
align-items: stretch;
|
||||
height: 2rem;
|
||||
}
|
||||
|
||||
.lcars-elbow-corner {
|
||||
width: 12rem;
|
||||
min-width: 12rem;
|
||||
background-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.lcars-elbow-tl {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 2rem;
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 0;
|
||||
}
|
||||
|
||||
.lcars-elbow-bl {
|
||||
border-top-left-radius: 0;
|
||||
border-top-right-radius: 2rem;
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.lcars-bar {
|
||||
flex: 1;
|
||||
margin-left: 0.25rem;
|
||||
}
|
||||
|
||||
.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-bar-accent {
|
||||
height: 0.5rem;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(var(--primary)) 0%,
|
||||
hsl(var(--primary)) 30%,
|
||||
hsl(var(--secondary)) 30%,
|
||||
hsl(var(--secondary)) 60%,
|
||||
hsl(var(--accent)) 60%,
|
||||
hsl(var(--accent)) 100%
|
||||
);
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
/* --- Divider (between sidebar and main) --- */
|
||||
|
||||
.lcars-divider {
|
||||
background-color: hsl(var(--primary));
|
||||
width: 0.25rem;
|
||||
}
|
||||
|
||||
/* --- Sidebar --- */
|
||||
|
||||
.lcars-sidebar-area {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.lcars-sidebar {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
.lcars-sidebar-btn {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0 1.5rem 1.5rem 0;
|
||||
text-decoration: none;
|
||||
color: hsl(var(--background));
|
||||
font-weight: 700;
|
||||
font-size: 0.875rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
transition: filter 0.15s ease;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.lcars-sidebar-btn:hover {
|
||||
filter: brightness(1.2);
|
||||
}
|
||||
|
||||
.lcars-sidebar-btn-label {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* --- Main Content --- */
|
||||
|
||||
.lcars-main-area {
|
||||
padding: 1.5rem 2rem;
|
||||
}
|
||||
|
||||
.lcars-main-full {
|
||||
flex: 1;
|
||||
padding: 2rem;
|
||||
}
|
||||
|
||||
.lcars-empty-state {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 20rem;
|
||||
}
|
||||
|
||||
/* --- Header Block --- */
|
||||
|
||||
.lcars-header-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
.lcars-header-pill {
|
||||
width: 6rem;
|
||||
height: 2.5rem;
|
||||
border-radius: 1.25rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lcars-header-title-area {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.lcars-title {
|
||||
font-size: 2rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.15em;
|
||||
color: hsl(var(--primary));
|
||||
line-height: 1;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.lcars-subtitle {
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
color: hsl(var(--secondary));
|
||||
}
|
||||
|
||||
.lcars-header-indicators {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.lcars-stardate {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: hsl(var(--accent));
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.lcars-status {
|
||||
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));
|
||||
}
|
||||
|
||||
.lcars-editor-badge {
|
||||
position: absolute;
|
||||
top: 0.25rem;
|
||||
right: 0.5rem;
|
||||
font-size: 0.625rem;
|
||||
font-weight: 700;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.2em;
|
||||
color: hsl(var(--accent));
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
/* --- Panel Block --- */
|
||||
|
||||
.lcars-panel {
|
||||
border-left: 0.25rem solid hsl(var(--primary));
|
||||
margin: 1rem 0;
|
||||
}
|
||||
|
||||
.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.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.lcars-panel-header-pill {
|
||||
width: 3rem;
|
||||
height: 1.25rem;
|
||||
background-color: inherit;
|
||||
border-radius: 0.625rem;
|
||||
background-color: hsl(var(--primary));
|
||||
}
|
||||
|
||||
.lcars-panel-secondary .lcars-panel-header-pill { background-color: hsl(var(--secondary)); }
|
||||
.lcars-panel-accent .lcars-panel-header-pill { background-color: hsl(var(--accent)); }
|
||||
|
||||
.lcars-panel-title {
|
||||
font-size: 0.875rem;
|
||||
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 {
|
||||
padding: 0.75rem 1rem 0.75rem 1.25rem;
|
||||
}
|
||||
|
||||
/* --- Heading Override --- */
|
||||
|
||||
.lcars-heading {
|
||||
font-family: 'Antonio', sans-serif;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: hsl(var(--primary));
|
||||
border-bottom: 0.125rem solid hsl(var(--primary));
|
||||
padding-bottom: 0.25rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.lcars-heading-1 { font-size: 2.25rem; }
|
||||
.lcars-heading-2 { font-size: 1.75rem; }
|
||||
.lcars-heading-3 { font-size: 1.375rem; }
|
||||
.lcars-heading-4 { font-size: 1.125rem; }
|
||||
|
||||
/* --- Text Override --- */
|
||||
|
||||
.lcars-prose {
|
||||
color: hsl(var(--foreground));
|
||||
line-height: 1.7;
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
.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));
|
||||
}
|
||||
|
||||
.lcars-prose code {
|
||||
background-color: hsl(var(--muted));
|
||||
padding: 0.125rem 0.375rem;
|
||||
border-radius: 0.25rem;
|
||||
font-size: 0.875em;
|
||||
}
|
||||
|
||||
/* --- Footer --- */
|
||||
|
||||
.lcars-footer-area {
|
||||
padding: 0.5rem 0;
|
||||
}
|
||||
|
||||
/* --- Animations --- */
|
||||
|
||||
@keyframes lcars-pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.7; }
|
||||
}
|
||||
|
||||
@keyframes lcars-blink {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.3; }
|
||||
}
|
||||
|
||||
/* --- Responsive --- */
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.lcars-content-area {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.lcars-sidebar-area {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lcars-divider {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.lcars-elbow-corner {
|
||||
width: 4rem;
|
||||
min-width: 4rem;
|
||||
}
|
||||
|
||||
.lcars-sidebar-btn {
|
||||
border-radius: 1.5rem;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.lcars-header-pill {
|
||||
width: 3rem;
|
||||
}
|
||||
|
||||
.lcars-title {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
.lcars-main-area {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
21
master_pages.json
Normal file
21
master_pages.json
Normal file
@ -0,0 +1,21 @@
|
||||
[
|
||||
{
|
||||
"key": "lcars:standard-display",
|
||||
"title": "LCARS Standard Display",
|
||||
"page_templates": ["default"],
|
||||
"blocks": [
|
||||
{ "block_key": "lcars:lcars_header", "title": "LCARS Header", "content": { "title": "LCARS", "subtitle": "Library Computer Access/Retrieval System" }, "slot": "header", "sort_order": 0 },
|
||||
{ "block_key": "lcars:lcars_sidebar", "title": "LCARS Sidebar", "content": {}, "slot": "sidebar", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Main Content Slot", "content": { "slotName": "main", "placeholder": "Enter display data…" }, "slot": "main", "sort_order": 0 }
|
||||
]
|
||||
},
|
||||
{
|
||||
"key": "lcars:full-display",
|
||||
"title": "LCARS Full Display",
|
||||
"page_templates": ["full-display"],
|
||||
"blocks": [
|
||||
{ "block_key": "lcars:lcars_header", "title": "LCARS Header", "content": { "title": "LCARS", "subtitle": "Main Viewer" }, "slot": "header", "sort_order": 0 },
|
||||
{ "block_key": "slot", "title": "Main Content Slot", "content": { "slotName": "main", "placeholder": "Enter display data…" }, "slot": "main", "sort_order": 0 }
|
||||
]
|
||||
}
|
||||
]
|
||||
@ -2,11 +2,8 @@
|
||||
name = "lcars"
|
||||
display_name = "LCARS"
|
||||
scope = "@themes"
|
||||
version = "0.2.9"
|
||||
version = "0.3.0"
|
||||
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.15.0 <0.16.0"
|
||||
|
||||
50
register.go
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,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>
|
||||
Loading…
x
Reference in New Issue
Block a user