design is now terminal alike
This commit is contained in:
@@ -1,31 +1,39 @@
|
||||
/* ============================================================
|
||||
* Gruvbox dark theme
|
||||
* Gruvbox terminal theme
|
||||
* ------------------------------------------------------------
|
||||
* Project-owned theme overrides. The vendored `app.css` is a
|
||||
* pre-compiled Tailwind + DaisyUI bundle and is NOT edited.
|
||||
* This file is loaded *after* it (see base.html / admin/base.html)
|
||||
* and only redefines DaisyUI's built-in `dark` theme.
|
||||
* Project-owned styling. The vendored `app.css` (a pre-compiled
|
||||
* Tailwind + DaisyUI bundle) is NOT edited. This file loads
|
||||
* after it (see base.html / admin/base.html) and provides:
|
||||
*
|
||||
* DaisyUI consumes each variable as `oklch(var(--x))`, so values
|
||||
* must be OKLch "L% C H" triplets (no function wrapper). The hex
|
||||
* next to each line is the source color it was converted from.
|
||||
* 1. the Gruvbox palette for DaisyUI's `dark` theme
|
||||
* 2. square corners (terminals have none)
|
||||
* 3. a terminal look & feel: monospace, window chrome,
|
||||
* vim-style statusline, CRT scanlines
|
||||
* 4. `.term-*` building blocks used by the templates
|
||||
*
|
||||
* Why CSS classes and not utility classes: `app.css` is frozen
|
||||
* and only contains the utilities the original project used, so
|
||||
* new Tailwind classes would not exist. The DaisyUI *components*
|
||||
* (card/btn/badge/menu/...) do exist and are reused; everything
|
||||
* else is defined here as real, themeable CSS.
|
||||
*
|
||||
* Palette: https://github.com/morhetz/gruvbox (dark, bright)
|
||||
* To retune, change the hex, reconvert to OKLch, update the line.
|
||||
* DaisyUI color vars are OKLch "L% C H" triplets; this file can
|
||||
* therefore tint anything with `oklch(var(--x) / <alpha>)`.
|
||||
* ============================================================ */
|
||||
|
||||
/* === 1. Gruvbox dark palette ================================ */
|
||||
/* Source hex noted per line. To retune: change hex, reconvert
|
||||
* to OKLch, update the value. */
|
||||
[data-theme="dark"] {
|
||||
/* --- surfaces ------------------------------------------------ */
|
||||
--b1: 27.69% 0 0; /* #282828 bg0 page background */
|
||||
--b2: 31.10% 0.003 49.7; /* #32302f bg0_s panels / elevated */
|
||||
--b3: 34.40% 0.0066 48.7; /* #3c3836 bg1 borders / dividers */
|
||||
--bc: 89.42% 0.0566 89.5; /* #ebdbb2 fg body text */
|
||||
--b1: 27.69% 0 0; /* #282828 bg0 screen background */
|
||||
--b2: 31.10% 0.003 49.7; /* #32302f bg0_s panels / chrome */
|
||||
--b3: 34.40% 0.0066 48.7; /* #3c3836 bg1 borders */
|
||||
--bc: 89.42% 0.0566 89.5; /* #ebdbb2 fg body text */
|
||||
|
||||
/* --- neutral ------------------------------------------------- */
|
||||
--n: 34.40% 0.0066 48.7; /* #3c3836 bg1 */
|
||||
--nc: 89.42% 0.0566 89.5; /* #ebdbb2 fg */
|
||||
|
||||
/* --- brand accents ------------------------------------------ */
|
||||
--p: 73.10% 0.182 51.7; /* #fe8019 bright orange primary */
|
||||
--pc: 27.69% 0 0; /* #282828 text on primary */
|
||||
--s: 70.54% 0.097 2.3; /* #d3869b bright purple secondary */
|
||||
@@ -33,13 +41,278 @@
|
||||
--a: 75.57% 0.108 137.6; /* #8ec07c bright aqua accent */
|
||||
--ac: 27.69% 0 0; /* #282828 text on accent */
|
||||
|
||||
/* --- status colors ------------------------------------------ */
|
||||
--in: 69.26% 0.042 169.8; /* #83a598 bright blue info */
|
||||
--su: 76.52% 0.158 110.8; /* #b8bb26 bright green success */
|
||||
--wa: 83.49% 0.160 83.6; /* #fabd2f bright yellow warning */
|
||||
--er: 65.97% 0.217 30.4; /* #fb4934 bright red error */
|
||||
--inc: 24.07% 0.005 220.9; /* #1d2021 bg0_h text on status */
|
||||
--suc: 24.07% 0.005 220.9; /* #1d2021 */
|
||||
--wac: 24.07% 0.005 220.9; /* #1d2021 */
|
||||
--erc: 24.07% 0.005 220.9; /* #1d2021 */
|
||||
--suc: 24.07% 0.005 220.9;
|
||||
--wac: 24.07% 0.005 220.9;
|
||||
--erc: 24.07% 0.005 220.9;
|
||||
}
|
||||
|
||||
/* === 2. Square corners ====================================== */
|
||||
/* `[data-theme]` matches the same <html> element as the vendored
|
||||
* `[data-theme=dark|light]` rules with equal specificity, and
|
||||
* wins by load order. Applies to both themes. */
|
||||
[data-theme] {
|
||||
--rounded-box: 0;
|
||||
--rounded-btn: 0;
|
||||
--rounded-badge: 0;
|
||||
--tab-radius: 0;
|
||||
--animation-btn: 0;
|
||||
--animation-input: 0;
|
||||
}
|
||||
|
||||
/* === 3. Terminal look & feel ================================ */
|
||||
body {
|
||||
font-family: "JetBrains Mono", "Cascadia Code", "Fira Code",
|
||||
ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* Gruvbox text selection + scrollbars (dark only) */
|
||||
[data-theme="dark"] ::selection { background: #fe8019; color: #282828; }
|
||||
[data-theme="dark"] { scrollbar-color: #504945 #282828; }
|
||||
[data-theme="dark"] ::-webkit-scrollbar { width: 12px; height: 12px; }
|
||||
[data-theme="dark"] ::-webkit-scrollbar-track { background: #282828; }
|
||||
[data-theme="dark"] ::-webkit-scrollbar-thumb {
|
||||
background: #504945; border: 3px solid #282828;
|
||||
}
|
||||
[data-theme="dark"] ::-webkit-scrollbar-thumb:hover { background: #665c54; }
|
||||
|
||||
/* Faint CRT scanlines — dark only. Remove this block to drop it. */
|
||||
[data-theme="dark"] body::before {
|
||||
content: "";
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 90;
|
||||
pointer-events: none;
|
||||
background: repeating-linear-gradient(
|
||||
0deg, rgba(0, 0, 0, 0.15), rgba(0, 0, 0, 0.15) 1px,
|
||||
transparent 1px, transparent 3px);
|
||||
opacity: 0.45;
|
||||
}
|
||||
|
||||
/* --- color helpers (theme-adaptive: gruvbox in dark) -------- */
|
||||
.t-orange { color: oklch(var(--p)); }
|
||||
.t-purple { color: oklch(var(--s)); }
|
||||
.t-aqua { color: oklch(var(--a)); }
|
||||
.t-blue { color: oklch(var(--in)); }
|
||||
.t-green { color: oklch(var(--su)); }
|
||||
.t-yellow { color: oklch(var(--wa)); }
|
||||
.t-red { color: oklch(var(--er)); }
|
||||
.t-dim { color: oklch(var(--bc) / 0.5); }
|
||||
|
||||
/* --- window titlebar (the header) -------------------------- */
|
||||
.term-titlebar {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 50;
|
||||
background: oklch(var(--b2));
|
||||
border-bottom: 1px solid oklch(var(--b3));
|
||||
}
|
||||
.term-nav {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.85rem;
|
||||
width: 100%;
|
||||
max-width: 72rem;
|
||||
margin: 0 auto;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
.term-dots { display: inline-flex; gap: 0.45rem; flex: none; }
|
||||
.term-dot {
|
||||
width: 0.72rem;
|
||||
height: 0.72rem;
|
||||
border-radius: 9999px;
|
||||
display: block;
|
||||
}
|
||||
.term-dot.r { background: oklch(var(--er)); }
|
||||
.term-dot.y { background: oklch(var(--wa)); }
|
||||
.term-dot.g { background: oklch(var(--su)); }
|
||||
.term-brand {
|
||||
font-size: 0.85rem;
|
||||
white-space: nowrap;
|
||||
text-decoration: none;
|
||||
}
|
||||
.term-brand:hover { text-decoration: none; }
|
||||
.term-nav-right { margin-left: auto; display: flex; align-items: center; gap: 0.25rem; }
|
||||
|
||||
/* horizontal nav links */
|
||||
.term-navlinks { padding: 0; gap: 0; }
|
||||
.term-navlinks li > a,
|
||||
.term-navlinks li > form > button {
|
||||
padding: 0.2rem 0.55rem;
|
||||
font-size: 0.85rem;
|
||||
border-radius: 0;
|
||||
}
|
||||
.term-navlinks li > a::before { content: ""; }
|
||||
.term-navlinks li > a:hover,
|
||||
.term-navlinks li > form > button:hover {
|
||||
background: transparent;
|
||||
color: oklch(var(--p));
|
||||
}
|
||||
.term-navlinks a.is-active {
|
||||
color: oklch(var(--p));
|
||||
background: oklch(var(--p) / 0.12);
|
||||
}
|
||||
.term-navlinks a.is-active::before {
|
||||
content: "▸ ";
|
||||
color: oklch(var(--p));
|
||||
}
|
||||
|
||||
/* --- page body layout -------------------------------------- */
|
||||
.term-main {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
max-width: 72rem;
|
||||
margin: 0 auto;
|
||||
padding: 2.25rem 1rem 3rem;
|
||||
}
|
||||
|
||||
/* --- command-prompt page heading --------------------------- */
|
||||
.term-cmd {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: flex-end;
|
||||
justify-content: space-between;
|
||||
gap: 1rem;
|
||||
margin-bottom: 1.75rem;
|
||||
padding-bottom: 0.85rem;
|
||||
border-bottom: 1px solid oklch(var(--b3));
|
||||
}
|
||||
.term-cmd-line { font-size: 0.8rem; color: oklch(var(--bc) / 0.85); }
|
||||
.term-title {
|
||||
margin-top: 0.4rem;
|
||||
font-size: 1.7rem;
|
||||
font-weight: 700;
|
||||
line-height: 1.15;
|
||||
color: oklch(var(--p));
|
||||
}
|
||||
.term-sub { margin-top: 0.2rem; font-size: 0.85rem; color: oklch(var(--bc) / 0.55); }
|
||||
.term-cmd-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||||
|
||||
/* --- responsive card grid ---------------------------------- */
|
||||
.term-grid {
|
||||
display: grid;
|
||||
gap: 1rem;
|
||||
grid-template-columns: repeat(auto-fill, minmax(16rem, 1fr));
|
||||
}
|
||||
.term-stack > * + * { margin-top: 1rem; }
|
||||
|
||||
/* --- cards as terminal windows ----------------------------- */
|
||||
.card {
|
||||
background: oklch(var(--b2));
|
||||
border: 1px solid oklch(var(--b3));
|
||||
box-shadow: none;
|
||||
}
|
||||
.card:hover { border-color: oklch(var(--p) / 0.5); }
|
||||
/* filename strip at the top of a card */
|
||||
.term-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
padding: 0.4rem 0.85rem;
|
||||
font-size: 0.74rem;
|
||||
color: oklch(var(--bc) / 0.6);
|
||||
background: oklch(var(--b1));
|
||||
border-bottom: 1px solid oklch(var(--b3));
|
||||
}
|
||||
.term-head .term-dots .term-dot { width: 0.55rem; height: 0.55rem; }
|
||||
.term-head-name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.term-head-meta { margin-left: auto; }
|
||||
.card-title a { color: oklch(var(--p)); text-decoration: none; }
|
||||
.card-title a:hover { text-decoration: underline; }
|
||||
|
||||
/* --- inline tags ------------------------------------------- */
|
||||
.term-tag {
|
||||
display: inline-block;
|
||||
padding: 0.02rem 0.45rem;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 0.03em;
|
||||
border: 1px solid oklch(var(--p) / 0.55);
|
||||
color: oklch(var(--p));
|
||||
}
|
||||
.term-tag.is-aqua { border-color: oklch(var(--a) / 0.55); color: oklch(var(--a)); }
|
||||
.term-tag.is-purple { border-color: oklch(var(--s) / 0.55); color: oklch(var(--s)); }
|
||||
.term-tag.is-blue { border-color: oklch(var(--in) / 0.55); color: oklch(var(--in)); }
|
||||
.term-tag.is-green { border-color: oklch(var(--su) / 0.55); color: oklch(var(--su)); }
|
||||
|
||||
/* --- empty / "no results" state ---------------------------- */
|
||||
.term-empty {
|
||||
padding: 2.25rem 1rem;
|
||||
text-align: center;
|
||||
color: oklch(var(--bc) / 0.6);
|
||||
border: 1px dashed oklch(var(--b3));
|
||||
}
|
||||
.term-empty-cmd { font-size: 0.8rem; color: oklch(var(--bc) / 0.45); }
|
||||
|
||||
/* --- terminal session block (mockup-code substitute) ------- */
|
||||
.term-screen {
|
||||
background: oklch(var(--b1));
|
||||
border: 1px solid oklch(var(--b3));
|
||||
padding: 0.85rem 1rem;
|
||||
font-size: 0.85rem;
|
||||
overflow-x: auto;
|
||||
}
|
||||
.term-screen .line { white-space: pre-wrap; }
|
||||
.term-screen .line::before {
|
||||
content: attr(data-p) " ";
|
||||
color: oklch(var(--su));
|
||||
}
|
||||
.term-screen .line.out::before { content: ""; }
|
||||
.term-screen .line.out { color: oklch(var(--bc) / 0.8); }
|
||||
|
||||
/* --- prose (article / about bodies) ------------------------ */
|
||||
.term-prose { line-height: 1.7; }
|
||||
.term-prose a { color: oklch(var(--in)); }
|
||||
|
||||
/* --- audio rows -------------------------------------------- */
|
||||
.term-track { padding: 0.6rem 0; border-top: 1px solid oklch(var(--b3)); }
|
||||
.term-track:first-child { border-top: 0; }
|
||||
.term-track-name { font-size: 0.9rem; }
|
||||
audio { width: 100%; margin-top: 0.45rem; }
|
||||
|
||||
/* --- vim-style statusline (the footer) --------------------- */
|
||||
.term-statusline {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: stretch;
|
||||
font-size: 0.72rem;
|
||||
border-top: 1px solid oklch(var(--b3));
|
||||
}
|
||||
.term-seg {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.25rem 0.8rem;
|
||||
background: oklch(var(--b3));
|
||||
color: oklch(var(--bc));
|
||||
white-space: nowrap;
|
||||
}
|
||||
.term-seg.is-mode {
|
||||
background: oklch(var(--p));
|
||||
color: oklch(var(--pc));
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.06em;
|
||||
}
|
||||
.term-seg.is-alt {
|
||||
background: oklch(var(--s));
|
||||
color: oklch(var(--sc));
|
||||
font-weight: 700;
|
||||
}
|
||||
.term-seg.is-fill {
|
||||
flex: 1 1 8rem;
|
||||
background: oklch(var(--b2));
|
||||
color: oklch(var(--bc) / 0.55);
|
||||
}
|
||||
|
||||
/* --- square the icon buttons ------------------------------- */
|
||||
.btn-circle { border-radius: 0; }
|
||||
|
||||
/* --- small screens ----------------------------------------- */
|
||||
@media (max-width: 767px) {
|
||||
.term-nav { gap: 0.5rem; }
|
||||
.term-title { font-size: 1.4rem; }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -23,19 +23,23 @@
|
||||
applyTheme(t);
|
||||
highlightTheme(t);
|
||||
}
|
||||
applyTheme(localStorage.getItem('theme') || 'system');
|
||||
applyTheme(localStorage.getItem('theme') || 'dark');
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
|
||||
if ((localStorage.getItem('theme') || 'system') === 'system') applyTheme('system');
|
||||
if ((localStorage.getItem('theme') || 'dark') === 'system') applyTheme('system');
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
highlightTheme(localStorage.getItem('theme') || 'system');
|
||||
highlightTheme(localStorage.getItem('theme') || 'dark');
|
||||
var path = location.pathname;
|
||||
document.querySelectorAll('.term-navlinks a[data-nav]').forEach(function (a) {
|
||||
var h = a.getAttribute('data-nav');
|
||||
if (h === path || (h !== '/' && path.indexOf(h) === 0)) a.classList.add('is-active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<link href="/static/css/app.css" rel="stylesheet" type="text/css">
|
||||
<link href="/static/css/theme.css" rel="stylesheet" type="text/css">
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
<style>
|
||||
.btn { --animation-btn: 0; --btn-focus-scale: 1; }
|
||||
@media (min-width: 768px) {
|
||||
.nav-menu { flex-direction: row; }
|
||||
}
|
||||
@@ -46,12 +50,12 @@
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.15s ease, visibility 0s linear 0.2s;
|
||||
}
|
||||
.navbar:has(.dropdown:focus-within) ~ #nav-backdrop {
|
||||
.term-titlebar:has(.dropdown:focus-within) ~ #nav-backdrop {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: opacity 0.15s ease, visibility 0s;
|
||||
@@ -59,24 +63,29 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen bg-base-200 font-sans text-base-content antialiased">
|
||||
<header class="navbar bg-base-100 shadow-sm">
|
||||
<nav class="mx-auto flex w-full max-w-6xl items-center justify-between gap-2 px-4">
|
||||
<a href="/admin/dashboard" class="min-w-0 truncate text-lg font-bold">Admin</a>
|
||||
<ul class="nav-menu menu menu-sm hidden items-center gap-1 md:flex">
|
||||
<li><a href="/admin/dashboard">Dashboard</a></li>
|
||||
<li><a href="/admin/blog/articles">Blog</a></li>
|
||||
<li><a href="/admin/audio/albums">Audio</a></li>
|
||||
<li><a href="/admin/images">Images</a></li>
|
||||
<li><a href="/admin/about">About</a></li>
|
||||
<li><a href="/">View site</a></li>
|
||||
<body class="flex min-h-screen flex-col bg-base-100 text-base-content antialiased">
|
||||
<header class="term-titlebar">
|
||||
<nav class="term-nav">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<a href="/admin/dashboard" class="term-brand">
|
||||
<span class="t-red">root</span><span class="t-dim">@universal-web</span><span class="t-dim">:</span><span class="t-yellow">/admin</span><span class="t-dim">#</span>
|
||||
</a>
|
||||
<ul class="nav-menu term-navlinks menu menu-sm hidden items-center md:flex">
|
||||
<li><a href="/admin/dashboard" data-nav="/admin/dashboard">dashboard</a></li>
|
||||
<li><a href="/admin/blog/articles" data-nav="/admin/blog">blog</a></li>
|
||||
<li><a href="/admin/audio/albums" data-nav="/admin/audio">audio</a></li>
|
||||
<li><a href="/admin/images" data-nav="/admin/images">images</a></li>
|
||||
<li><a href="/admin/about" data-nav="/admin/about">about</a></li>
|
||||
<li><a href="/" class="t-blue">exit</a></li>
|
||||
<li>
|
||||
<form method="post" action="/admin/logout">
|
||||
<button type="submit" class="w-full">Logout</button>
|
||||
<button type="submit" class="t-red w-full">logout</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="term-nav-right">
|
||||
<div class="dropdown dropdown-end md:hidden">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-sm btn-circle" aria-label="Menu">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
@@ -85,16 +94,16 @@
|
||||
</svg>
|
||||
</div>
|
||||
<ul tabindex="0"
|
||||
class="menu dropdown-content z-50 mt-3 w-52 rounded-box border border-base-300 bg-base-100 p-2 shadow-lg">
|
||||
<li><a href="/admin/dashboard">Dashboard</a></li>
|
||||
<li><a href="/admin/blog/articles">Blog</a></li>
|
||||
<li><a href="/admin/audio/albums">Audio</a></li>
|
||||
<li><a href="/admin/images">Images</a></li>
|
||||
<li><a href="/admin/about">About</a></li>
|
||||
<li><a href="/">View site</a></li>
|
||||
class="menu dropdown-content z-50 mt-3 w-52 border border-base-300 bg-base-200 p-2 shadow-lg">
|
||||
<li><a href="/admin/dashboard">dashboard</a></li>
|
||||
<li><a href="/admin/blog/articles">blog</a></li>
|
||||
<li><a href="/admin/audio/albums">audio</a></li>
|
||||
<li><a href="/admin/images">images</a></li>
|
||||
<li><a href="/admin/about">about</a></li>
|
||||
<li><a href="/" class="t-blue">exit</a></li>
|
||||
<li>
|
||||
<form method="post" action="/admin/logout">
|
||||
<button type="submit" class="w-full">Logout</button>
|
||||
<button type="submit" class="t-red w-full">logout</button>
|
||||
</form>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -108,19 +117,28 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul tabindex="0" class="menu dropdown-content z-50 mt-3 w-56 rounded-box border border-base-300 bg-base-100 p-2 shadow-lg">
|
||||
<li class="menu-title">Theme</li>
|
||||
<li><button type="button" data-theme-opt="system" onclick="setTheme('system')">System <span class="opt-check ml-auto hidden">✓</span></button></li>
|
||||
<li><button type="button" data-theme-opt="light" onclick="setTheme('light')">Light <span class="opt-check ml-auto hidden">✓</span></button></li>
|
||||
<li><button type="button" data-theme-opt="dark" onclick="setTheme('dark')">Dark <span class="opt-check ml-auto hidden">✓</span></button></li>
|
||||
<ul tabindex="0" class="menu dropdown-content z-50 mt-3 w-56 border border-base-300 bg-base-200 p-2 shadow-lg">
|
||||
<li class="menu-title">:set theme</li>
|
||||
<li><button type="button" data-theme-opt="system" onclick="setTheme('system')">system <span class="opt-check ml-auto hidden">✓</span></button></li>
|
||||
<li><button type="button" data-theme-opt="light" onclick="setTheme('light')">light <span class="opt-check ml-auto hidden">✓</span></button></li>
|
||||
<li><button type="button" data-theme-opt="dark" onclick="setTheme('dark')">dark <span class="opt-check ml-auto hidden">✓</span></button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div id="nav-backdrop" aria-hidden="true"></div>
|
||||
<main class="mx-auto max-w-6xl px-4 py-6">
|
||||
<main class="term-main">
|
||||
{% block content %}{% endblock content %}
|
||||
</main>
|
||||
<footer class="term-statusline">
|
||||
<span class="term-seg is-mode"> ADMIN </span>
|
||||
<span class="term-seg">universal-web</span>
|
||||
<span class="term-seg is-fill">/admin/{% block crumb %}{% endblock crumb %}</span>
|
||||
<span class="term-seg">utf-8</span>
|
||||
<span class="term-seg">root</span>
|
||||
<span class="term-seg is-alt">gruvbox-dark</span>
|
||||
<span class="term-seg is-mode">100%</span>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,69 +1,95 @@
|
||||
{% extends "admin/base.html" %}
|
||||
|
||||
{% block title %}Admin{% endblock title %}
|
||||
{% block crumb %}dashboard{% endblock crumb %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">Dashboard</h1>
|
||||
<p class="text-sm opacity-70">Logged in as {{ admin.email }}</p>
|
||||
</div>
|
||||
<a href="/" class="btn btn-ghost btn-sm">View site</a>
|
||||
<header class="term-cmd">
|
||||
<div>
|
||||
<p class="term-cmd-line">
|
||||
<span class="t-red">root@universal-web</span><span class="t-dim">:</span><span class="t-yellow">/admin</span><span class="t-dim">#</span>
|
||||
ls -la
|
||||
</p>
|
||||
<h1 class="term-title">dashboard</h1>
|
||||
<p class="term-sub">// session: {{ admin.email }}</p>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 pt-4">
|
||||
<div class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<h2 class="card-title text-base">Blog</h2>
|
||||
<span class="badge">Content</span>
|
||||
</div>
|
||||
<p class="text-sm opacity-70">Create and update blog articles.</p>
|
||||
<div class="pt-2">
|
||||
<a href="/admin/blog/articles" class="btn btn-neutral btn-sm">Manage blog</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<h2 class="card-title text-base">About page</h2>
|
||||
<span class="badge">Page</span>
|
||||
</div>
|
||||
<p class="text-sm opacity-70">Edit the public about page content.</p>
|
||||
<div class="pt-2">
|
||||
<a href="/admin/about" class="btn btn-neutral btn-sm">Edit about</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<h2 class="card-title text-base">Audio</h2>
|
||||
<span class="badge">Media</span>
|
||||
</div>
|
||||
<p class="text-sm opacity-70">Create albums and upload tracks.</p>
|
||||
<div class="pt-2">
|
||||
<a href="/admin/audio/albums" class="btn btn-neutral btn-sm">Manage audio</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="flex items-center justify-between gap-2">
|
||||
<h2 class="card-title text-base">Images</h2>
|
||||
<span class="badge">Uploads</span>
|
||||
</div>
|
||||
<p class="text-sm opacity-70">Upload images for covers and articles.</p>
|
||||
<div class="pt-2">
|
||||
<a href="/admin/images" class="btn btn-neutral btn-sm">Upload image</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="term-cmd-actions">
|
||||
<a href="/" class="btn btn-outline btn-sm">[ view site ]</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="term-screen mb-6">
|
||||
<p class="line" data-p="root@universal-web:/admin#">ls</p>
|
||||
<p class="line out">about/ audio/ blog/ images/</p>
|
||||
</div>
|
||||
|
||||
<div class="term-grid">
|
||||
<article class="card">
|
||||
<div class="term-head">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<span class="term-head-name">/admin/blog</span>
|
||||
<span class="term-head-meta term-tag">content</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-base">blog</h2>
|
||||
<p class="text-sm opacity-70">create and update blog articles.</p>
|
||||
<div class="pt-2">
|
||||
<a href="/admin/blog/articles" class="btn btn-primary btn-sm">[ manage → ]</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<div class="term-head">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<span class="term-head-name">/admin/about</span>
|
||||
<span class="term-head-meta term-tag is-blue">page</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-base">about page</h2>
|
||||
<p class="text-sm opacity-70">edit the public about page content.</p>
|
||||
<div class="pt-2">
|
||||
<a href="/admin/about" class="btn btn-primary btn-sm">[ edit → ]</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<div class="term-head">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<span class="term-head-name">/admin/audio</span>
|
||||
<span class="term-head-meta term-tag is-purple">media</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-base">audio</h2>
|
||||
<p class="text-sm opacity-70">create albums and upload tracks.</p>
|
||||
<div class="pt-2">
|
||||
<a href="/admin/audio/albums" class="btn btn-primary btn-sm">[ manage → ]</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
<article class="card">
|
||||
<div class="term-head">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<span class="term-head-name">/admin/images</span>
|
||||
<span class="term-head-meta term-tag is-green">uploads</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-base">images</h2>
|
||||
<p class="text-sm opacity-70">upload images for covers and articles.</p>
|
||||
<div class="pt-2">
|
||||
<a href="/admin/images" class="btn btn-primary btn-sm">[ upload → ]</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,27 +1,38 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Admin login{% endblock title %}
|
||||
{% block crumb %}admin/login{% endblock crumb %}
|
||||
|
||||
{% block content %}
|
||||
<div class="mx-auto mt-8 max-w-sm">
|
||||
<div class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card">
|
||||
<div class="term-head">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<span class="term-head-name">/bin/login — tty1</span>
|
||||
<span class="term-head-meta term-tag is-red">auth</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h1 class="card-title">Admin login</h1>
|
||||
<p class="term-cmd-line">
|
||||
<span class="t-dim">universal-web login:</span> <span class="t-red">root</span>
|
||||
</p>
|
||||
<h1 class="term-title">authenticate</h1>
|
||||
{% if error %}
|
||||
<div class="alert alert-error">
|
||||
<span>Invalid email or password.</span>
|
||||
<div class="alert alert-error mt-2">
|
||||
<span>✗ access denied — invalid email or password.</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form method="post" action="/admin/login" class="space-y-2">
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text">Email</span></label>
|
||||
<input type="email" name="email" required class="input input-bordered w-full">
|
||||
<label class="label"><span class="label-text t-green">email:</span></label>
|
||||
<input type="email" name="email" required autofocus class="input input-bordered w-full">
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label"><span class="label-text">Password</span></label>
|
||||
<label class="label"><span class="label-text t-green">password:</span></label>
|
||||
<input type="password" name="password" required class="input input-bordered w-full">
|
||||
</div>
|
||||
<button class="btn btn-neutral mt-2 w-full">Login</button>
|
||||
<button class="btn btn-primary mt-2 w-full">[ authenticate ]</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,48 +1,77 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ album.title }}{% endblock title %}
|
||||
{% block crumb %}audio/{{ album.slug }}{% endblock crumb %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">{{ album.title }}</h1>
|
||||
{% if album.artist %}
|
||||
<p class="text-sm opacity-70">{{ album.artist }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="/audio/albums" class="btn btn-ghost btn-sm">Back to albums</a>
|
||||
<header class="term-cmd">
|
||||
<div>
|
||||
<p class="term-cmd-line">
|
||||
<span class="t-green">visitor@universal-web</span><span class="t-dim">:</span><span class="t-blue">~/audio</span><span class="t-dim">$</span>
|
||||
cd {{ album.slug }}/ && ls
|
||||
</p>
|
||||
<h1 class="term-title">{{ album.title }}</h1>
|
||||
{% if album.artist %}
|
||||
<p class="term-sub">// by {{ album.artist }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{% if album.cover_image_id %}
|
||||
<img src="/images/{{ album.cover_image_id }}" alt="" class="mb-4 rounded">
|
||||
{% endif %}
|
||||
|
||||
{% if album.description %}
|
||||
<div class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<p class="whitespace-pre-line">{{ album.description }}</p>
|
||||
</div>
|
||||
<div class="term-cmd-actions">
|
||||
<a href="/audio/albums" class="btn btn-outline btn-sm">[ cd .. ]</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
<div class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
{% if tracks | length > 0 %}
|
||||
<div class="space-y-2">
|
||||
{% for track in tracks %}
|
||||
<div class="border-t border-base-300 pt-2">
|
||||
<p class="font-medium">{% if track.track_number %}{{ track.track_number }}. {% endif %}{{ track.title }}</p>
|
||||
<audio controls preload="metadata" class="mt-2 w-full">
|
||||
<source src="/audio/tracks/{{ track.id }}/stream">
|
||||
</audio>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center font-medium">No tracks yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% if album.cover_image_id %}
|
||||
<div class="card mb-6">
|
||||
<div class="term-head">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<span class="term-head-name">~/audio/{{ album.slug }}/cover.png</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<img src="/images/{{ album.cover_image_id }}" alt="">
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if album.description %}
|
||||
<div class="card mb-6">
|
||||
<div class="term-head">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<span class="term-head-name">~/audio/{{ album.slug }}/notes.txt</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p class="term-prose whitespace-pre-line">{{ album.description }}</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="card">
|
||||
<div class="term-head">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<span class="term-head-name">~/audio/{{ album.slug }}/tracklist</span>
|
||||
<span class="term-head-meta term-tag is-green">{{ tracks | length }} tracks</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if tracks | length > 0 %}
|
||||
{% for track in tracks %}
|
||||
<div class="term-track">
|
||||
<p class="term-track-name">
|
||||
<span class="t-dim">{% if track.track_number %}{{ track.track_number }}{% else %}-{% endif %}</span>
|
||||
<span class="t-green">▸</span> {{ track.title }}
|
||||
</p>
|
||||
<audio controls preload="metadata">
|
||||
<source src="/audio/tracks/{{ track.id }}/stream">
|
||||
</audio>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="term-empty-cmd">$ ls → no tracks yet</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,42 +1,56 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Audio{% endblock title %}
|
||||
{% block crumb %}audio{% endblock crumb %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-2">
|
||||
<header class="term-cmd">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">Audio</h1>
|
||||
<p class="text-sm opacity-70">Published albums.</p>
|
||||
<p class="term-cmd-line">
|
||||
<span class="t-green">visitor@universal-web</span><span class="t-dim">:</span><span class="t-blue">~/audio</span><span class="t-dim">$</span>
|
||||
ls -d */
|
||||
</p>
|
||||
<h1 class="term-title">audio</h1>
|
||||
<p class="term-sub">// {{ albums | length }} published album(s).</p>
|
||||
</div>
|
||||
<div class="term-cmd-actions">
|
||||
<a href="/audio/tracks" class="btn btn-outline btn-sm">[ all songs ]</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if albums | length > 0 %}
|
||||
<div class="grid grid-cols-2 gap-4 pt-4">
|
||||
{% for album in albums %}
|
||||
<article class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
{% if album.cover_image_id %}
|
||||
<img src="/images/{{ album.cover_image_id }}" alt="" class="mb-3 rounded">
|
||||
{% endif %}
|
||||
<h2 class="card-title text-base">{{ album.title }}</h2>
|
||||
{% if album.artist %}
|
||||
<p class="text-sm opacity-70">{{ album.artist }}</p>
|
||||
{% endif %}
|
||||
{% if album.description %}
|
||||
<p class="text-sm opacity-80">{{ album.description }}</p>
|
||||
{% endif %}
|
||||
<div class="pt-2">
|
||||
<a href="/audio/albums/{{ album.slug }}" class="btn btn-neutral btn-sm">Open album</a>
|
||||
</div>
|
||||
{% if albums | length > 0 %}
|
||||
<div class="term-grid">
|
||||
{% for album in albums %}
|
||||
<article class="card">
|
||||
<div class="term-head">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<span class="term-head-name">~/audio/{{ album.slug }}/</span>
|
||||
<span class="term-head-meta term-tag is-purple">album</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if album.cover_image_id %}
|
||||
<img src="/images/{{ album.cover_image_id }}" alt="" class="mb-3">
|
||||
{% endif %}
|
||||
<h2 class="card-title text-base">{{ album.title }}</h2>
|
||||
{% if album.artist %}
|
||||
<p class="text-sm t-aqua">{{ album.artist }}</p>
|
||||
{% endif %}
|
||||
{% if album.description %}
|
||||
<p class="term-prose text-sm opacity-80">{{ album.description }}</p>
|
||||
{% endif %}
|
||||
<div class="pt-2">
|
||||
<a href="/audio/albums/{{ album.slug }}" class="btn btn-primary btn-sm">[ open → ]</a>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<p class="font-medium">No published albums yet.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="term-empty">
|
||||
<p class="font-medium">no published albums yet</p>
|
||||
<p class="term-empty-cmd">$ ls ~/audio → 0 results</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,34 +1,44 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Songs{% endblock title %}
|
||||
{% block crumb %}audio/tracks{% endblock crumb %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">Songs</h1>
|
||||
<p class="text-sm opacity-70">Published songs from every album and ungrouped uploads.</p>
|
||||
</div>
|
||||
<a href="/audio/albums" class="btn btn-ghost btn-sm">Albums</a>
|
||||
<header class="term-cmd">
|
||||
<div>
|
||||
<p class="term-cmd-line">
|
||||
<span class="t-green">visitor@universal-web</span><span class="t-dim">:</span><span class="t-blue">~/audio</span><span class="t-dim">$</span>
|
||||
find . -name '*.mp3'
|
||||
</p>
|
||||
<h1 class="term-title">songs</h1>
|
||||
<p class="term-sub">// {{ tracks | length }} track(s) across every album.</p>
|
||||
</div>
|
||||
<div class="term-cmd-actions">
|
||||
<a href="/audio/albums" class="btn btn-outline btn-sm">[ albums ]</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
{% if tracks | length > 0 %}
|
||||
<div class="space-y-2">
|
||||
{% for track in tracks %}
|
||||
<div class="border-t border-base-300 pt-2">
|
||||
<p class="font-medium">{{ track.title }}</p>
|
||||
<audio controls preload="metadata" class="mt-2 w-full">
|
||||
<source src="/audio/tracks/{{ track.id }}/stream">
|
||||
</audio>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p class="text-center font-medium">No published songs yet.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="term-head">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<span class="term-head-name">~/audio/playlist.m3u</span>
|
||||
<span class="term-head-meta term-tag is-green">{{ tracks | length }} tracks</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if tracks | length > 0 %}
|
||||
{% for track in tracks %}
|
||||
<div class="term-track">
|
||||
<p class="term-track-name"><span class="t-green">▸</span> {{ track.title }}</p>
|
||||
<audio controls preload="metadata">
|
||||
<source src="/audio/tracks/{{ track.id }}/stream">
|
||||
</audio>
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="term-empty-cmd">$ find ~/audio -name '*.mp3' → 0 results</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-theme="light">
|
||||
<html lang="en" data-theme="dark">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
@@ -23,19 +23,23 @@
|
||||
applyTheme(t);
|
||||
highlightTheme(t);
|
||||
}
|
||||
applyTheme(localStorage.getItem('theme') || 'system');
|
||||
applyTheme(localStorage.getItem('theme') || 'dark');
|
||||
window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function () {
|
||||
if ((localStorage.getItem('theme') || 'system') === 'system') applyTheme('system');
|
||||
if ((localStorage.getItem('theme') || 'dark') === 'system') applyTheme('system');
|
||||
});
|
||||
document.addEventListener('DOMContentLoaded', function () {
|
||||
highlightTheme(localStorage.getItem('theme') || 'system');
|
||||
highlightTheme(localStorage.getItem('theme') || 'dark');
|
||||
var path = location.pathname;
|
||||
document.querySelectorAll('.term-navlinks a[data-nav]').forEach(function (a) {
|
||||
var h = a.getAttribute('data-nav');
|
||||
if (h === path || (h !== '/' && path.indexOf(h) === 0)) a.classList.add('is-active');
|
||||
});
|
||||
});
|
||||
</script>
|
||||
<link href="/static/css/app.css" rel="stylesheet" type="text/css">
|
||||
<link href="/static/css/theme.css" rel="stylesheet" type="text/css">
|
||||
<script src="https://unpkg.com/htmx.org@1.9.12"></script>
|
||||
<style>
|
||||
.btn { --animation-btn: 0; --btn-focus-scale: 1; }
|
||||
@media (min-width: 768px) {
|
||||
.nav-menu { flex-direction: row; }
|
||||
}
|
||||
@@ -46,12 +50,12 @@
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 40;
|
||||
background-color: rgba(0, 0, 0, 0.25);
|
||||
background-color: rgba(0, 0, 0, 0.5);
|
||||
opacity: 0;
|
||||
visibility: hidden;
|
||||
transition: opacity 0.15s ease, visibility 0s linear 0.2s;
|
||||
}
|
||||
.navbar:has(.dropdown:focus-within) ~ #nav-backdrop {
|
||||
.term-titlebar:has(.dropdown:focus-within) ~ #nav-backdrop {
|
||||
opacity: 1;
|
||||
visibility: visible;
|
||||
transition: opacity 0.15s ease, visibility 0s;
|
||||
@@ -59,28 +63,33 @@
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body class="min-h-screen bg-base-200 font-sans text-base-content antialiased">
|
||||
<header class="navbar bg-base-100 shadow-sm">
|
||||
<nav class="mx-auto flex w-full max-w-6xl items-center justify-between gap-2 px-4">
|
||||
<a href="/" class="min-w-0 truncate text-lg font-bold">Universal Web</a>
|
||||
<ul class="nav-menu menu menu-sm hidden items-center gap-1 md:flex">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
<li><a href="/blog">Blog</a></li>
|
||||
<li><a href="/audio/albums">Audio</a></li>
|
||||
<li><a href="/audio/tracks">Songs</a></li>
|
||||
<body class="flex min-h-screen flex-col bg-base-100 text-base-content antialiased">
|
||||
<header class="term-titlebar">
|
||||
<nav class="term-nav">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<a href="/" class="term-brand">
|
||||
<span class="t-green">{% if logged_in_admin %}root{% else %}visitor{% endif %}</span><span class="t-dim">@universal-web</span><span class="t-dim">:~$</span>
|
||||
</a>
|
||||
<ul class="nav-menu term-navlinks menu menu-sm hidden items-center md:flex">
|
||||
<li><a href="/" data-nav="/">home</a></li>
|
||||
<li><a href="/about" data-nav="/about">about</a></li>
|
||||
<li><a href="/blog" data-nav="/blog">blog</a></li>
|
||||
<li><a href="/audio/albums" data-nav="/audio/albums">audio</a></li>
|
||||
<li><a href="/audio/tracks" data-nav="/audio/tracks">songs</a></li>
|
||||
{% if logged_in_admin %}
|
||||
<li><a href="/admin/dashboard">Dashboard</a></li>
|
||||
<li><a href="/admin/dashboard" class="t-yellow" data-nav="/admin">admin</a></li>
|
||||
<li>
|
||||
<form method="post" action="/admin/logout">
|
||||
<button type="submit" class="w-full">Logout</button>
|
||||
<button type="submit" class="t-red w-full">logout</button>
|
||||
</form>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="/admin/login">Admin</a></li>
|
||||
<li><a href="/admin/login" data-nav="/admin/login">login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
<div class="flex items-center gap-1">
|
||||
<div class="term-nav-right">
|
||||
<div class="dropdown dropdown-end md:hidden">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-sm btn-circle" aria-label="Menu">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5"
|
||||
@@ -89,21 +98,21 @@
|
||||
</svg>
|
||||
</div>
|
||||
<ul tabindex="0"
|
||||
class="menu dropdown-content z-50 mt-3 w-52 rounded-box border border-base-300 bg-base-100 p-2 shadow-lg">
|
||||
<li><a href="/">Home</a></li>
|
||||
<li><a href="/about">About</a></li>
|
||||
<li><a href="/blog">Blog</a></li>
|
||||
<li><a href="/audio/albums">Audio</a></li>
|
||||
<li><a href="/audio/tracks">Songs</a></li>
|
||||
class="menu dropdown-content z-50 mt-3 w-52 border border-base-300 bg-base-200 p-2 shadow-lg">
|
||||
<li><a href="/">home</a></li>
|
||||
<li><a href="/about">about</a></li>
|
||||
<li><a href="/blog">blog</a></li>
|
||||
<li><a href="/audio/albums">audio</a></li>
|
||||
<li><a href="/audio/tracks">songs</a></li>
|
||||
{% if logged_in_admin %}
|
||||
<li><a href="/admin/dashboard">Dashboard</a></li>
|
||||
<li><a href="/admin/dashboard" class="t-yellow">admin</a></li>
|
||||
<li>
|
||||
<form method="post" action="/admin/logout">
|
||||
<button type="submit" class="w-full">Logout</button>
|
||||
<button type="submit" class="t-red w-full">logout</button>
|
||||
</form>
|
||||
</li>
|
||||
{% else %}
|
||||
<li><a href="/admin/login">Admin</a></li>
|
||||
<li><a href="/admin/login">login</a></li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
@@ -116,19 +125,28 @@
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z" />
|
||||
</svg>
|
||||
</div>
|
||||
<ul tabindex="0" class="menu dropdown-content z-50 mt-3 w-56 rounded-box border border-base-300 bg-base-100 p-2 shadow-lg">
|
||||
<li class="menu-title">Theme</li>
|
||||
<li><button type="button" data-theme-opt="system" onclick="setTheme('system')">System <span class="opt-check ml-auto hidden">✓</span></button></li>
|
||||
<li><button type="button" data-theme-opt="light" onclick="setTheme('light')">Light <span class="opt-check ml-auto hidden">✓</span></button></li>
|
||||
<li><button type="button" data-theme-opt="dark" onclick="setTheme('dark')">Dark <span class="opt-check ml-auto hidden">✓</span></button></li>
|
||||
<ul tabindex="0" class="menu dropdown-content z-50 mt-3 w-56 border border-base-300 bg-base-200 p-2 shadow-lg">
|
||||
<li class="menu-title">:set theme</li>
|
||||
<li><button type="button" data-theme-opt="system" onclick="setTheme('system')">system <span class="opt-check ml-auto hidden">✓</span></button></li>
|
||||
<li><button type="button" data-theme-opt="light" onclick="setTheme('light')">light <span class="opt-check ml-auto hidden">✓</span></button></li>
|
||||
<li><button type="button" data-theme-opt="dark" onclick="setTheme('dark')">dark <span class="opt-check ml-auto hidden">✓</span></button></li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
</header>
|
||||
<div id="nav-backdrop" aria-hidden="true"></div>
|
||||
<main class="mx-auto max-w-6xl px-4 py-6">
|
||||
<main class="term-main">
|
||||
{% block content %}{% endblock content %}
|
||||
</main>
|
||||
<footer class="term-statusline">
|
||||
<span class="term-seg is-mode">{% if logged_in_admin %} ADMIN {% else %} NORMAL {% endif %}</span>
|
||||
<span class="term-seg">universal-web</span>
|
||||
<span class="term-seg is-fill">~/{% block crumb %}{% endblock crumb %}</span>
|
||||
<span class="term-seg">utf-8</span>
|
||||
<span class="term-seg">EADGBE</span>
|
||||
<span class="term-seg is-alt">gruvbox-dark</span>
|
||||
<span class="term-seg is-mode">100%</span>
|
||||
</footer>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,47 +1,54 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Blog{% endblock title %}
|
||||
{% block crumb %}blog{% endblock crumb %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">Blog</h1>
|
||||
<p class="text-sm opacity-70">Published articles.</p>
|
||||
</div>
|
||||
{% if logged_in_admin %}
|
||||
<a href="/admin/blog/articles" class="btn btn-ghost btn-sm">Manage blog</a>
|
||||
{% endif %}
|
||||
<header class="term-cmd">
|
||||
<div>
|
||||
<p class="term-cmd-line">
|
||||
<span class="t-green">visitor@universal-web</span><span class="t-dim">:</span><span class="t-blue">~/blog</span><span class="t-dim">$</span>
|
||||
ls -la
|
||||
</p>
|
||||
<h1 class="term-title">blog</h1>
|
||||
<p class="term-sub">// {{ articles | length }} published article(s).</p>
|
||||
</div>
|
||||
{% if logged_in_admin %}
|
||||
<div class="term-cmd-actions">
|
||||
<a href="/admin/blog/articles" class="btn btn-outline btn-sm">[ manage ]</a>
|
||||
</div>
|
||||
|
||||
{% if articles | length > 0 %}
|
||||
<div class="grid gap-4 pt-4">
|
||||
{% for article in articles %}
|
||||
<article class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 class="card-title text-base">
|
||||
<a href="/blog/{{ article.slug }}">{{ article.title }}</a>
|
||||
</h2>
|
||||
<span class="badge">Post</span>
|
||||
</div>
|
||||
{% if article.excerpt %}
|
||||
<p class="text-sm leading-relaxed opacity-80">{{ article.excerpt }}</p>
|
||||
{% endif %}
|
||||
<div class="pt-2">
|
||||
<a href="/blog/{{ article.slug }}" class="btn btn-neutral btn-sm">Read</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<p class="font-medium">No published posts yet.</p>
|
||||
<p class="text-sm opacity-70">Published articles will appear here.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{% if articles | length > 0 %}
|
||||
<div class="term-stack">
|
||||
{% for article in articles %}
|
||||
<article class="card">
|
||||
<div class="term-head">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<span class="term-head-name">~/blog/{{ article.slug }}.txt</span>
|
||||
<span class="term-head-meta term-tag">post</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-base">
|
||||
<a href="/blog/{{ article.slug }}">{{ article.title }}</a>
|
||||
</h2>
|
||||
{% if article.excerpt %}
|
||||
<p class="term-prose text-sm opacity-80">{{ article.excerpt }}</p>
|
||||
{% endif %}
|
||||
<div class="pt-2">
|
||||
<a href="/blog/{{ article.slug }}" class="btn btn-primary btn-sm">[ cat → ]</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="term-empty">
|
||||
<p class="font-medium">no published posts yet</p>
|
||||
<p class="term-empty-cmd">$ ls ~/blog → 0 results</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,25 +1,37 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ article.title }}{% endblock title %}
|
||||
{% block crumb %}blog/{{ article.slug }}{% endblock crumb %}
|
||||
|
||||
{% block content %}
|
||||
<article class="space-y-2">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">{{ article.title }}</h1>
|
||||
<p class="text-sm opacity-70">Views: {{ article.view_count }}</p>
|
||||
</div>
|
||||
<a href="/blog" class="btn btn-ghost btn-sm">Back to blog</a>
|
||||
<header class="term-cmd">
|
||||
<div>
|
||||
<p class="term-cmd-line">
|
||||
<span class="t-green">visitor@universal-web</span><span class="t-dim">:</span><span class="t-blue">~/blog</span><span class="t-dim">$</span>
|
||||
cat {{ article.slug }}.txt
|
||||
</p>
|
||||
<h1 class="term-title">{{ article.title }}</h1>
|
||||
<p class="term-sub">// {{ article.view_count }} view(s) logged.</p>
|
||||
</div>
|
||||
<div class="term-cmd-actions">
|
||||
<a href="/blog" class="btn btn-outline btn-sm">[ cd .. ]</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<div class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
{% if article.excerpt %}
|
||||
<p class="text-base leading-relaxed opacity-80">{{ article.excerpt }}</p>
|
||||
<div class="border-t border-base-300 pt-4"></div>
|
||||
{% endif %}
|
||||
<div class="leading-relaxed whitespace-pre-line">{{ article.content }}</div>
|
||||
</div>
|
||||
<article class="card">
|
||||
<div class="term-head">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<span class="term-head-name">~/blog/{{ article.slug }}.txt</span>
|
||||
<span class="term-head-meta term-tag is-blue">readonly</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{% if article.excerpt %}
|
||||
<p class="term-prose t-yellow"># {{ article.excerpt }}</p>
|
||||
<div class="border-t border-base-300 pt-4"></div>
|
||||
{% endif %}
|
||||
<div class="term-prose whitespace-pre-line">{{ article.content }}</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,47 +1,62 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Home{% endblock title %}
|
||||
{% block crumb %}{% endblock crumb %}
|
||||
|
||||
{% block content %}
|
||||
<div class="space-y-2">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">Universal Web</h1>
|
||||
<p class="text-sm opacity-70">Latest updates from the site.</p>
|
||||
</div>
|
||||
<a href="/blog" class="btn btn-ghost btn-sm">All posts</a>
|
||||
<header class="term-cmd">
|
||||
<div>
|
||||
<p class="term-cmd-line">
|
||||
<span class="t-green">visitor@universal-web</span><span class="t-dim">:</span><span class="t-blue">~</span><span class="t-dim">$</span>
|
||||
ls -la
|
||||
</p>
|
||||
<h1 class="term-title">home</h1>
|
||||
<p class="term-sub">// latest news and updates.</p>
|
||||
</div>
|
||||
<div class="term-cmd-actions">
|
||||
<a href="/blog" class="btn btn-outline btn-sm">[ all posts ]</a>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<section class="pt-4">
|
||||
{% if articles | length > 0 %}
|
||||
<div class="grid gap-4">
|
||||
{% for article in articles %}
|
||||
<article class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<h2 class="card-title text-base">
|
||||
<a href="/blog/{{ article.slug }}">{{ article.title }}</a>
|
||||
</h2>
|
||||
<span class="badge">Post</span>
|
||||
</div>
|
||||
{% if article.excerpt %}
|
||||
<p class="text-sm leading-relaxed opacity-80">{{ article.excerpt }}</p>
|
||||
{% endif %}
|
||||
<div class="pt-2">
|
||||
<a href="/blog/{{ article.slug }}" class="btn btn-neutral btn-sm">Read</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body text-center">
|
||||
<p class="font-medium">No published posts yet.</p>
|
||||
<p class="text-sm opacity-70">Check back later.</p>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
<div class="term-screen mb-6">
|
||||
<p class="line" data-p="visitor@universal-web:~$">whoami</p>
|
||||
<p class="line out">→ guitar player - original songs, albums and notes</p>
|
||||
<p class="line" data-p="visitor@universal-web:~$">ls ~/sections</p>
|
||||
<p class="line out">about/ blog/ audio/ songs/</p>
|
||||
</div>
|
||||
|
||||
<section>
|
||||
<p class="term-cmd-line mb-6"><span class="t-dim"># </span>recent posts <span class="t-dim">({{ articles | length }})</span></p>
|
||||
{% if articles | length > 0 %}
|
||||
<div class="term-stack">
|
||||
{% for article in articles %}
|
||||
<article class="card">
|
||||
<div class="term-head">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<span class="term-head-name">~/blog/{{ article.slug }}.txt</span>
|
||||
<span class="term-head-meta term-tag">post</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h2 class="card-title text-base">
|
||||
<a href="/blog/{{ article.slug }}">{{ article.title }}</a>
|
||||
</h2>
|
||||
{% if article.excerpt %}
|
||||
<p class="term-prose text-sm opacity-80">{{ article.excerpt }}</p>
|
||||
{% endif %}
|
||||
<div class="pt-2">
|
||||
<a href="/blog/{{ article.slug }}" class="btn btn-primary btn-sm">[ cat → ]</a>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="term-empty">
|
||||
<p class="font-medium">no published posts yet</p>
|
||||
<p class="term-empty-cmd">$ ls ~/blog → 0 results</p>
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endblock content %}
|
||||
|
||||
@@ -1,23 +1,35 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}{{ page.title }}{% endblock title %}
|
||||
{% block crumb %}about{% endblock crumb %}
|
||||
|
||||
{% block content %}
|
||||
<article class="space-y-2">
|
||||
<div class="flex flex-wrap items-center justify-between gap-3">
|
||||
<div>
|
||||
<h1 class="text-2xl font-bold">{{ page.title }}</h1>
|
||||
<p class="text-sm opacity-70">About this site.</p>
|
||||
</div>
|
||||
{% if logged_in_admin %}
|
||||
<a href="/admin/about" class="btn btn-ghost btn-sm">Edit page</a>
|
||||
{% endif %}
|
||||
<header class="term-cmd">
|
||||
<div>
|
||||
<p class="term-cmd-line">
|
||||
<span class="t-green">visitor@universal-web</span><span class="t-dim">:</span><span class="t-blue">~</span><span class="t-dim">$</span>
|
||||
cat about.txt
|
||||
</p>
|
||||
<h1 class="term-title">{{ page.title }}</h1>
|
||||
<p class="term-sub">// about this site.</p>
|
||||
</div>
|
||||
{% if logged_in_admin %}
|
||||
<div class="term-cmd-actions">
|
||||
<a href="/admin/about" class="btn btn-outline btn-sm">[ edit ]</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
<div class="card border border-base-300 bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="leading-relaxed whitespace-pre-line">{{ page.content }}</div>
|
||||
</div>
|
||||
<article class="card">
|
||||
<div class="term-head">
|
||||
<span class="term-dots" aria-hidden="true">
|
||||
<span class="term-dot r"></span><span class="term-dot y"></span><span class="term-dot g"></span>
|
||||
</span>
|
||||
<span class="term-head-name">~/about.txt</span>
|
||||
<span class="term-head-meta term-tag is-blue">readonly</span>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div class="term-prose whitespace-pre-line">{{ page.content }}</div>
|
||||
</div>
|
||||
</article>
|
||||
{% endblock content %}
|
||||
|
||||
Reference in New Issue
Block a user