commit e992d8247d186e0ff62dd58c73182cd13721da36 Author: Alex Dunmow Date: Tue Jun 2 23:11:11 2026 +0800 feat: LCARS theme plugin — Star Trek computer interface First-class pongo2 theme with 4 color presets (Federation, Red Alert, Sickbay, Engineering), 3 custom blocks (header, sidebar, panel), 2 page templates, heading/text overrides, email wrapper, bundled Antonio font, and full LCARS CSS with elbow brackets, pill buttons, and rounded bars. Co-Authored-By: Claude Opus 4.6 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..140f8cf --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.so diff --git a/assets/fonts/antonio-latin-ext.woff2 b/assets/fonts/antonio-latin-ext.woff2 new file mode 100644 index 0000000..2825535 Binary files /dev/null and b/assets/fonts/antonio-latin-ext.woff2 differ diff --git a/assets/fonts/antonio-latin.woff2 b/assets/fonts/antonio-latin.woff2 new file mode 100644 index 0000000..4fbb342 Binary files /dev/null and b/assets/fonts/antonio-latin.woff2 differ diff --git a/assets/style.css b/assets/style.css new file mode 100644 index 0000000..7860eb1 --- /dev/null +++ b/assets/style.css @@ -0,0 +1,423 @@ +/* ============================================================ + 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; + } +} diff --git a/blocks.go b/blocks.go new file mode 100644 index 0000000..9bb2043 --- /dev/null +++ b/blocks.go @@ -0,0 +1,53 @@ +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", +} diff --git a/embed.go b/embed.go new file mode 100644 index 0000000..0a6316f --- /dev/null +++ b/embed.go @@ -0,0 +1,60 @@ +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), + } +} diff --git a/fonts.json b/fonts.json new file mode 100644 index 0000000..1806db6 --- /dev/null +++ b/fonts.json @@ -0,0 +1,28 @@ +[ + { + "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" + } + ] + } +] diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..31184bc --- /dev/null +++ b/go.mod @@ -0,0 +1,19 @@ +module git.dev.alexdunmow.com/block/lcars + +go 1.26.2 + +require git.dev.alexdunmow.com/block/core v0.10.0 + +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 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..fb68555 --- /dev/null +++ b/go.sum @@ -0,0 +1,52 @@ +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.10.0 h1:dWfYVbGuJOnvE58GcGGd5c71dAAKcjxrSEfjNepu4ro= +git.dev.alexdunmow.com/block/core v0.10.0/go.mod h1:y1/Q9UMG29AplbExecnq9M7y16PZ7cYd24bjZO1SCBQ= +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= diff --git a/master_pages.go b/master_pages.go new file mode 100644 index 0000000..15451c5 --- /dev/null +++ b/master_pages.go @@ -0,0 +1,57 @@ +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, + }, + }, + }, + } +} diff --git a/plugin.mod b/plugin.mod new file mode 100644 index 0000000..2e87e71 --- /dev/null +++ b/plugin.mod @@ -0,0 +1,3 @@ +[plugin] +name = "lcars" +version = "0.1.0" diff --git a/presets.json b/presets.json new file mode 100644 index 0000000..0fba260 --- /dev/null +++ b/presets.json @@ -0,0 +1,202 @@ +[ + { + "id": "federation", + "name": "Federation", + "description": "Classic LCARS — gold, periwinkle, and coral on black", + "theme": { + "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%" + }, + "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" + } + }, + { + "id": "red-alert", + "name": "Red Alert", + "description": "Emergency operations — red and amber on black", + "theme": { + "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%" + }, + "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" + } + }, + { + "id": "sickbay", + "name": "Sickbay", + "description": "Medical operations — teal, cyan, and blue on black", + "theme": { + "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%" + }, + "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" + } + }, + { + "id": "engineering", + "name": "Engineering", + "description": "Engineering deck — orange, amber, and tan on black", + "theme": { + "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%" + }, + "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%" + }, + "mode": "dark" + } + } +] diff --git a/register.go b/register.go new file mode 100644 index 0000000..372cd8f --- /dev/null +++ b/register.go @@ -0,0 +1,50 @@ +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 +} diff --git a/registration.go b/registration.go new file mode 100644 index 0000000..127bf8b --- /dev/null +++ b/registration.go @@ -0,0 +1,24 @@ +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() }, +} diff --git a/schemas/lcars_header.schema.json b/schemas/lcars_header.schema.json new file mode 100644 index 0000000..70a5cf5 --- /dev/null +++ b/schemas/lcars_header.schema.json @@ -0,0 +1,41 @@ +{ + "$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"] +} diff --git a/schemas/lcars_panel.schema.json b/schemas/lcars_panel.schema.json new file mode 100644 index 0000000..a97fe4e --- /dev/null +++ b/schemas/lcars_panel.schema.json @@ -0,0 +1,29 @@ +{ + "$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"] +} diff --git a/schemas/lcars_sidebar.schema.json b/schemas/lcars_sidebar.schema.json new file mode 100644 index 0000000..42425e5 --- /dev/null +++ b/schemas/lcars_sidebar.schema.json @@ -0,0 +1,33 @@ +{ + "$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"] + } + } + } +} diff --git a/templates/blocks/header.html b/templates/blocks/header.html new file mode 100644 index 0000000..a5bb160 --- /dev/null +++ b/templates/blocks/header.html @@ -0,0 +1,23 @@ +
+
+
+
+

{{ title }}

+ {% if subtitle %} + {{ subtitle }} + {% endif %} +
+
+ {% if stardate %} + SD {{ stardate }} + {% endif %} + + {{ status|default:"online"|upper }} + +
+
+
+ {% if ctx.isEditor %} +
EDIT MODE
+ {% endif %} +
diff --git a/templates/blocks/heading_override.html b/templates/blocks/heading_override.html new file mode 100644 index 0000000..8d59fbd --- /dev/null +++ b/templates/blocks/heading_override.html @@ -0,0 +1 @@ +<{{ tag|default:"h2" }} class="lcars-heading lcars-heading-{{ level|default:2 }}">{{ text }} diff --git a/templates/blocks/panel.html b/templates/blocks/panel.html new file mode 100644 index 0000000..9d2e658 --- /dev/null +++ b/templates/blocks/panel.html @@ -0,0 +1,14 @@ +
+ {% if title %} +
+
+ {{ title }} +
+
+ {% endif %} +
+ {% if content %} + {{ content|safe }} + {% endif %} +
+
diff --git a/templates/blocks/sidebar.html b/templates/blocks/sidebar.html new file mode 100644 index 0000000..7e72262 --- /dev/null +++ b/templates/blocks/sidebar.html @@ -0,0 +1,11 @@ + diff --git a/templates/blocks/text_override.html b/templates/blocks/text_override.html new file mode 100644 index 0000000..71c0f22 --- /dev/null +++ b/templates/blocks/text_override.html @@ -0,0 +1 @@ +
{{ html|safe }}
diff --git a/templates/default.html b/templates/default.html new file mode 100644 index 0000000..8d9c694 --- /dev/null +++ b/templates/default.html @@ -0,0 +1,45 @@ +{% extends "base.html" %} + +{% block body_class %}lcars-page lcars-default bg-background text-foreground antialiased min-h-screen{% endblock %} + +{% block body %} +
+
+ {{ slots.header|safe }} +
+ +
+
+
+
+
+ +
+ + +
+ +
+ {% if slots.main %} + {{ slots.main|safe }} + {% else %} +
+

No data on file

+
+ {% endif %} +
+
+ +
+
+
+
+
+ +
+ {{ slots.footer|safe }} +
+
+{% endblock %} diff --git a/templates/email_wrapper.html b/templates/email_wrapper.html new file mode 100644 index 0000000..7fba0a6 --- /dev/null +++ b/templates/email_wrapper.html @@ -0,0 +1,43 @@ + + + + + + {% if preview_text %} + {{ preview_text }} + {% endif %} + + + +
+
+ + + +
{{ site_name }}
+
+ +
+ +
+ {{ body|safe }} +
+ + +
+ + diff --git a/templates/full_display.html b/templates/full_display.html new file mode 100644 index 0000000..4b7e4d0 --- /dev/null +++ b/templates/full_display.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} + +{% block body_class %}lcars-page lcars-full-display bg-background text-foreground antialiased min-h-screen{% endblock %} + +{% block body %} +
+
+ {{ slots.header|safe }} +
+ +
+
+ +
+ {% if slots.main %} + {{ slots.main|safe }} + {% else %} +
+

No data on file

+
+ {% endif %} +
+ +
+
+ + +
+{% endblock %}