This commit is contained in:
Priec
2026-06-16 12:44:31 +02:00
parent 4e1722ce35
commit 635cb34810
11 changed files with 3 additions and 1196 deletions

File diff suppressed because one or more lines are too long

View File

@@ -1,781 +0,0 @@
/* ============================================================
* Terminal 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:
*
* 1. Catppuccin Latte for DaisyUI's `light` theme
* and Gruvbox 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.
*
* Palettes:
* - https://github.com/catppuccin/catppuccin (Latte)
* - https://github.com/morhetz/gruvbox (dark, bright)
* DaisyUI color vars are OKLch "L% C H" triplets; this file can
* therefore tint anything with `oklch(var(--x) / <alpha>)`.
* ============================================================ */
/* === 1. Theme palettes ====================================== */
/* Catppuccin Latte. */
[data-theme="light"] {
--b1: 95.78% 0.006 264.5; /* #eff1f5 base */
--b2: 93.35% 0.009 264.5; /* #e6e9ef mantle */
--b3: 90.60% 0.012 264.5; /* #dce0e8 crust */
--bc: 43.55% 0.043 279.3; /* #4c4f69 text */
--n: 80.83% 0.017 271.2; /* #bcc0cc surface1 */
--nc: 43.55% 0.043 279.3; /* #4c4f69 text */
--p: 55.86% 0.226 262.1; /* #1e66f5 blue primary */
--pc: 95.78% 0.006 264.5; /* #eff1f5 text on primary */
--s: 55.47% 0.250 297.0; /* #8839ef mauve secondary */
--sc: 95.78% 0.006 264.5; /* #eff1f5 text on secondary */
--a: 60.23% 0.098 201.1; /* #179299 teal accent */
--ac: 95.78% 0.006 264.5; /* #eff1f5 text on accent */
--in: 68.20% 0.145 235.4; /* #04a5e5 sky info */
--su: 62.50% 0.177 140.4; /* #40a02b green success */
--wa: 71.40% 0.149 67.8; /* #df8e1d yellow warning */
--er: 55.05% 0.216 19.8; /* #d20f39 red error */
--inc: 43.55% 0.043 279.3; /* #4c4f69 text on status */
--suc: 95.78% 0.006 264.5;
--wac: 95.78% 0.006 264.5;
--erc: 95.78% 0.006 264.5;
}
/* Source hex noted per line. To retune: change hex, reconvert
* to OKLch, update the value. */
[data-theme="dark"] {
--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 */
--n: 34.40% 0.0066 48.7; /* #3c3836 bg1 */
--nc: 89.42% 0.0566 89.5; /* #ebdbb2 fg */
--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 */
--sc: 27.69% 0 0; /* #282828 text on secondary */
--a: 75.57% 0.108 137.6; /* #8ec07c bright aqua accent */
--ac: 27.69% 0 0; /* #282828 text on accent */
--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;
--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 ================================ */
/* Root font-size drives every rem in this file and in app.css.
* Bump this to scale the whole UI; drop to shrink. */
html { font-size: 19px; }
body {
font-family: "JetBrains Mono", "Cascadia Code", "Fira Code",
ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
font-size: 1rem;
line-height: 1.6;
}
/* Text selection + scrollbars */
[data-theme="light"] ::selection { background: #acb0be; color: #4c4f69; }
[data-theme="light"] { scrollbar-color: #bcc0cc #eff1f5; }
[data-theme="light"] ::-webkit-scrollbar { width: 12px; height: 12px; }
[data-theme="light"] ::-webkit-scrollbar-track { background: #eff1f5; }
[data-theme="light"] ::-webkit-scrollbar-thumb {
background: #bcc0cc; border: 3px solid #eff1f5;
}
[data-theme="light"] ::-webkit-scrollbar-thumb:hover { background: #acb0be; }
[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.75); }
/* --- 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.8); }
.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); }
/* --- how-it-works note + form helpers (admin) -------------- */
.term-note {
margin-bottom: 1.5rem;
padding: 0.9rem 1.1rem;
background: oklch(var(--b2));
border: 1px solid oklch(var(--b3));
border-left: 3px solid oklch(var(--a));
}
.term-note-title { margin-bottom: 0.55rem; font-size: 0.8rem; color: oklch(var(--a)); }
.term-step { display: flex; gap: 0.55rem; font-size: 0.88rem; }
.term-step + .term-step { margin-top: 0.3rem; }
.term-step-n { flex: none; color: oklch(var(--p)); }
.term-note-foot { margin-top: 0.6rem; font-size: 0.8rem; color: oklch(var(--bc) / 0.6); }
.term-help { margin-top: 0.2rem; font-size: 0.76rem; color: oklch(var(--bc) / 0.55); }
.term-picklist {
border: 1px solid oklch(var(--b3));
background: oklch(var(--b1));
max-height: 18rem;
overflow-y: auto;
}
.term-pick {
display: flex;
align-items: center;
gap: 0.65rem;
padding: 0.5rem 0.7rem;
border-top: 1px solid oklch(var(--b3));
cursor: pointer;
}
.term-pick:first-child { border-top: 0; }
.term-pick:hover { background: oklch(var(--b2)); }
.term-formdiv { margin: 1.25rem 0; border-top: 1px dashed oklch(var(--b3)); }
/* --- 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 {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.5rem 0;
border-top: 1px solid oklch(var(--b3));
}
.term-track:first-child { border-top: 0; }
.term-track .btn { flex: none; }
.term-track-name { font-size: 0.9rem; }
/* "play album" row sitting above the per-track list */
.term-track-bar {
display: flex;
align-items: center;
gap: 0.7rem;
padding-bottom: 0.65rem;
margin-bottom: 0.15rem;
border-bottom: 1px solid oklch(var(--b3));
}
.term-track-bar .btn { flex: none; }
/* --- persistent audio player bar --------------------------- */
/* Hidden until the first song plays; shown by adding `uw-playing`
* to <html>. The bar itself carries `hx-preserve` so the <audio>
* keeps playing while htmx swaps the page around it. */
#uw-player { display: none; }
.uw-playing #uw-player {
display: block;
position: fixed;
left: 0;
right: 0;
bottom: 0;
z-index: 80;
background: oklch(var(--b2));
border-top: 3px solid oklch(var(--p));
box-shadow: 0 -12px 32px rgba(0, 0, 0, 0.6);
}
.uw-playing body { padding-bottom: 6.75rem; }
.uw-player-inner {
display: flex;
align-items: center;
gap: 1.15rem;
width: 100%;
max-width: 72rem;
margin: 0 auto;
padding: 1rem 1.5rem;
}
.uw-player-tag {
flex: none;
font-size: 0.98rem;
font-weight: 700;
letter-spacing: 0.02em;
color: oklch(var(--p));
white-space: nowrap;
}
.uw-player-title {
flex: none;
max-width: 24rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 1.1rem;
color: oklch(var(--bc));
}
#uw-audio {
flex: 1;
min-width: 9rem;
width: auto;
height: 3.25rem;
margin: 0;
}
.uw-player-close {
flex: none;
width: 2.85rem;
height: 2.85rem;
font-size: 1.05rem;
background: transparent;
border: 1px solid oklch(var(--b3));
color: oklch(var(--bc) / 0.7);
cursor: pointer;
line-height: 1;
}
.uw-player-close:hover { color: oklch(var(--er)); border-color: oklch(var(--er)); }
/* transport + playlist toggle buttons in the player bar */
.uw-player-btn {
flex: none;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.3rem;
min-width: 2.85rem;
height: 2.85rem;
padding: 0 0.6rem;
font-size: 1.05rem;
background: transparent;
border: 1px solid oklch(var(--b3));
color: oklch(var(--bc) / 0.78);
cursor: pointer;
line-height: 1;
}
.uw-player-btn:hover { color: oklch(var(--p)); border-color: oklch(var(--p)); }
.uw-queue-badge {
font-size: 0.72rem;
font-weight: 700;
min-width: 1.25rem;
padding: 0.05rem 0.3rem;
background: oklch(var(--p));
color: oklch(var(--pc));
}
#uw-player:not(.uw-has-queue) .uw-queue-badge { display: none; }
/* --- the SoundCloud-style playlist panel ------------------- */
.uw-queue {
width: 100%;
max-width: 72rem;
margin: 0 auto;
border-bottom: 1px solid oklch(var(--b3));
}
.uw-queue[hidden] { display: none; }
.uw-queue-head {
display: flex;
align-items: center;
gap: 0.6rem;
padding: 0.6rem 1.5rem;
border-bottom: 1px solid oklch(var(--b3));
}
.uw-queue-title { font-weight: 700; font-size: 0.95rem; color: oklch(var(--p)); }
.uw-queue-meta { font-size: 0.8rem; color: oklch(var(--bc) / 0.6); }
.uw-queue-clear {
margin-left: auto;
padding: 0.25rem 0.6rem;
font-size: 0.8rem;
background: transparent;
border: 1px solid oklch(var(--b3));
color: oklch(var(--bc) / 0.7);
cursor: pointer;
}
.uw-queue-clear:hover { color: oklch(var(--er)); border-color: oklch(var(--er)); }
.uw-queue-list {
list-style: none;
margin: 0;
padding: 0.35rem 0;
max-height: 15rem;
overflow-y: auto;
}
.uw-queue-item {
display: flex;
align-items: center;
gap: 0.7rem;
padding: 0.4rem 1.5rem;
}
.uw-queue-item:hover { background: oklch(var(--b3) / 0.5); }
.uw-queue-item.is-current { background: oklch(var(--p) / 0.12); }
.uw-queue-jump {
flex: none;
width: 1.85rem;
height: 1.85rem;
font-size: 0.8rem;
background: transparent;
border: 1px solid oklch(var(--b3));
color: oklch(var(--bc) / 0.7);
cursor: pointer;
line-height: 1;
}
.uw-queue-item.is-current .uw-queue-jump { color: oklch(var(--p)); border-color: oklch(var(--p)); }
.uw-queue-name {
flex: 1;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
font-size: 0.9rem;
cursor: pointer;
}
.uw-queue-item.is-current .uw-queue-name { color: oklch(var(--p)); font-weight: 600; }
.uw-queue-remove {
flex: none;
width: 1.85rem;
height: 1.85rem;
font-size: 0.8rem;
background: transparent;
border: 1px solid transparent;
color: oklch(var(--bc) / 0.5);
cursor: pointer;
line-height: 1;
}
.uw-queue-remove:hover { color: oklch(var(--er)); border-color: oklch(var(--er)); }
@media (max-width: 640px) {
/* Two-row layout: [title][prev][next][queue][close] on top,
* full-width <audio> scrubber underneath. */
.uw-player-tag { display: none; }
.uw-player-inner {
flex-wrap: wrap;
padding: 0.6rem 0.75rem;
gap: 0.4rem;
row-gap: 0.5rem;
}
.uw-player-title {
flex: 1 1 auto;
min-width: 0;
max-width: none;
font-size: 0.95rem;
}
.uw-player-btn {
min-width: 2.2rem;
height: 2.2rem;
padding: 0 0.35rem;
font-size: 0.9rem;
}
.uw-player-close { width: 2.2rem; height: 2.2rem; font-size: 0.95rem; }
#uw-audio {
order: 99;
flex: 1 1 100%;
width: 100%;
min-width: 0;
height: 2.4rem;
}
.uw-playing body { padding-bottom: 8.25rem; }
.uw-queue-head, .uw-queue-item { padding-left: 0.95rem; padding-right: 0.95rem; }
}
/* --- 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; }
/* --- blog editor ------------------------------------------- */
.blog-editor {
min-height: 24rem;
background: oklch(var(--b1));
}
.blog-editor .ql-editor {
min-height: 24rem;
font-size: 1rem;
line-height: 1.7;
}
.ql-toolbar.ql-snow,
.ql-container.ql-snow {
border-color: oklch(var(--b3));
}
.ql-toolbar.ql-snow {
background: oklch(var(--b2));
}
.ql-snow .ql-stroke,
.ql-snow .ql-stroke-miter {
stroke: oklch(var(--bc));
}
.ql-snow .ql-fill,
.ql-snow .ql-stroke.ql-fill {
fill: oklch(var(--bc));
}
.ql-snow .ql-picker,
.ql-snow .ql-picker-options {
color: oklch(var(--bc));
}
.ql-snow .ql-picker-options {
background: oklch(var(--b1));
border-color: oklch(var(--b3));
}
.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label,
.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options {
border-color: oklch(var(--b3));
}
/* active / hover toolbar state -> gruvbox accent */
.ql-snow.ql-toolbar button:hover,
.ql-snow.ql-toolbar button:focus,
.ql-snow.ql-toolbar button.ql-active,
.ql-snow.ql-toolbar .ql-picker-label:hover,
.ql-snow.ql-toolbar .ql-picker-label.ql-active,
.ql-snow.ql-toolbar .ql-picker-item:hover,
.ql-snow.ql-toolbar .ql-picker-item.ql-selected,
.ql-snow .ql-picker.ql-expanded .ql-picker-label {
color: oklch(var(--p));
}
.ql-snow.ql-toolbar button:hover .ql-stroke,
.ql-snow.ql-toolbar button:focus .ql-stroke,
.ql-snow.ql-toolbar button.ql-active .ql-stroke,
.ql-snow.ql-toolbar button:hover .ql-stroke-miter,
.ql-snow.ql-toolbar button.ql-active .ql-stroke-miter,
.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke,
.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke,
.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke,
.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke,
.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke {
stroke: oklch(var(--p));
}
.ql-snow.ql-toolbar button:hover .ql-fill,
.ql-snow.ql-toolbar button:focus .ql-fill,
.ql-snow.ql-toolbar button.ql-active .ql-fill,
.ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill,
.ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill,
.ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill,
.ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill,
.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill,
.ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill,
.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill {
fill: oklch(var(--p));
}
/* link tooltip popup */
.ql-snow .ql-tooltip {
background-color: oklch(var(--b1));
border-color: oklch(var(--b3));
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.45);
color: oklch(var(--bc));
}
.ql-snow .ql-tooltip input[type=text] {
background: oklch(var(--b2));
border-color: oklch(var(--b3));
color: oklch(var(--bc));
}
.ql-snow .ql-tooltip a {
color: oklch(var(--p));
}
.ql-snow .ql-tooltip a.ql-action::after {
border-color: oklch(var(--b3));
}
.ql-snow .ql-editor a {
color: oklch(var(--p));
}
.blog-image-size-controls {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 0.5rem;
margin-top: 0.5rem;
font-size: 0.875rem;
}
.blog-image-size-controls.hidden {
display: none;
}
.blog-image-size-controls button {
border: 1px solid oklch(var(--b3));
background: oklch(var(--b2));
padding: 0.3rem 0.65rem;
line-height: 1;
}
.blog-image-size-controls button:hover {
border-color: oklch(var(--p));
color: oklch(var(--p));
}
.blog-image-size-controls label {
display: inline-flex;
align-items: center;
gap: 0.4rem;
}
.blog-image-size-controls input {
width: 5rem;
}
.blog-editor img,
.blog-content img {
display: block;
max-width: 100%;
height: auto;
margin: 1rem auto;
border-radius: 0.25rem;
}
.blog-editor img {
cursor: pointer;
}
.blog-image-small {
width: min(100%, 18rem);
}
.blog-image-medium {
width: min(100%, 34rem);
}
.blog-image-full {
width: 100%;
}
.blog-content {
line-height: 1.75;
}
.blog-content h2 {
margin: 1.5rem 0 0.75rem;
font-size: 1.35rem;
font-weight: 700;
}
.blog-content h3 {
margin: 1.25rem 0 0.5rem;
font-size: 1.15rem;
font-weight: 700;
}
.blog-content p,
.blog-content ul,
.blog-content ol {
margin: 0.75rem 0;
}
.blog-content ul {
list-style: disc;
padding-left: 1.4rem;
}
.blog-content ol {
list-style: decimal;
padding-left: 1.4rem;
}
.blog-content a {
color: oklch(var(--p));
text-decoration: underline;
}
/* --- small screens ----------------------------------------- */
@media (max-width: 767px) {
.term-nav { gap: 0.5rem; }
.term-title { font-size: 1.4rem; }
}

View File

@@ -1,149 +0,0 @@
(function () {
function setImageSize(image, size) {
image.classList.remove('blog-image-small', 'blog-image-medium', 'blog-image-full');
image.style.removeProperty('width');
image.style.removeProperty('height');
image.classList.add('blog-image-' + size);
}
function setImageWidth(image, width) {
var px = parseInt(width, 10);
if (!Number.isFinite(px) || px < 40) return;
image.classList.remove('blog-image-small', 'blog-image-medium', 'blog-image-full');
image.style.width = Math.min(px, 1200) + 'px';
image.style.height = 'auto';
}
function normalizeEditorImages(root) {
root.querySelectorAll('img').forEach(function (image) {
if (
!image.classList.contains('blog-image-small')
&& !image.classList.contains('blog-image-medium')
&& !image.classList.contains('blog-image-full')
) {
image.classList.add('blog-image-full');
}
});
}
function initEditor(form) {
var editorEl = form.querySelector('[data-rich-editor]');
var contentInput = form.querySelector('[data-rich-content]');
var status = form.querySelector('[data-rich-status]');
var imageControls = form.querySelector('[data-image-size-controls]');
var imageWidthInput = form.querySelector('[data-image-width]');
if (!editorEl || !contentInput || !window.Quill) return;
var selectedImage = null;
var toolbar = [
[{ header: [2, 3, false] }],
['bold', 'italic'],
[{ list: 'ordered' }, { list: 'bullet' }],
['link', 'image'],
['clean']
];
var editor = new Quill(editorEl, {
modules: { toolbar: toolbar },
placeholder: '',
theme: 'snow'
});
var initialContent = contentInput.value.trim();
if (initialContent) {
if (initialContent.indexOf('<') >= 0) editor.clipboard.dangerouslyPasteHTML(initialContent);
else editor.setText(initialContent);
normalizeEditorImages(editor.root);
}
function syncContent() {
normalizeEditorImages(editor.root);
contentInput.value = editor.root.innerHTML;
}
function setStatus(message) {
if (status) status.textContent = message || '';
}
function chooseImageFile() {
var input = document.createElement('input');
input.type = 'file';
input.accept = 'image/jpeg,image/png,image/webp,image/gif';
input.addEventListener('change', function () {
var file = input.files && input.files[0];
if (!file) return;
uploadImage(file);
});
input.click();
}
async function uploadImage(file) {
var formData = new FormData();
formData.append('file', file);
setStatus(status ? status.dataset.uploading : '');
try {
var response = await fetch('/images/upload', {
method: 'POST',
body: formData,
credentials: 'same-origin'
});
if (!response.ok) throw new Error('upload failed');
var result = await response.json();
var range = editor.getSelection(true);
editor.insertEmbed(range.index, 'image', result.url, 'user');
editor.setSelection(range.index + 1, 0, 'silent');
window.setTimeout(function () {
var images = editor.root.querySelectorAll('img');
var image = images[images.length - 1];
if (image) {
setImageSize(image, 'full');
selectedImage = image;
if (imageControls) imageControls.classList.remove('hidden');
}
syncContent();
}, 0);
setStatus(status ? status.dataset.uploaded : '');
} catch (_error) {
setStatus(status ? status.dataset.error : '');
}
}
editor.getModule('toolbar').addHandler('image', chooseImageFile);
editor.root.addEventListener('click', function (event) {
if (event.target && event.target.tagName === 'IMG') {
selectedImage = event.target;
if (imageWidthInput) imageWidthInput.value = parseInt(selectedImage.style.width, 10) || '';
if (imageControls) imageControls.classList.remove('hidden');
}
});
if (imageControls) {
imageControls.addEventListener('click', function (event) {
var button = event.target.closest('[data-image-size]');
if (button && selectedImage) {
setImageSize(selectedImage, button.dataset.imageSize);
if (imageWidthInput) imageWidthInput.value = '';
syncContent();
}
});
}
if (imageWidthInput) {
imageWidthInput.addEventListener('change', function () {
if (!selectedImage) return;
setImageWidth(selectedImage, imageWidthInput.value);
syncContent();
});
}
editor.on('text-change', syncContent);
form.addEventListener('submit', syncContent);
syncContent();
}
document.addEventListener('DOMContentLoaded', function () {
document.querySelectorAll('[data-rich-editor]').forEach(function (editorEl) {
var form = editorEl.closest('form');
if (form) initEditor(form);
});
});
})();

View File

@@ -1,31 +0,0 @@
Copyright (c) 2017-2024, Slab
Copyright (c) 2014, Jason Chen
Copyright (c) 2013, salesforce.com
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions
are met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A
PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

File diff suppressed because one or more lines are too long

View File

@@ -1,7 +0,0 @@
/*!
* Quill Editor v2.0.3
* https://quilljs.com
* Copyright (c) 2017-2024, Slab
* Copyright (c) 2014, Jason Chen
* Copyright (c) 2013, salesforce.com
*/

File diff suppressed because one or more lines are too long

View File

@@ -42,9 +42,7 @@
});
});
</script>
<link href="/static/css/app.css?v=2026-05-20b" rel="stylesheet" type="text/css">
{% block head %}{% endblock head %}
<link href="/static/css/theme.css?v=2026-05-20b" rel="stylesheet" type="text/css">
<script src="/static/vendor/htmx/htmx-1.9.12.min.js"></script>
<style>
@media (min-width: 768px) {

View File

@@ -1,9 +1,7 @@
{% extends "admin/base.html" %}
{% block title %}{{ t(key="edit-article", lang=lang | default(value='sk')) }}{% endblock title %}
{% block head %}
<link href="/static/vendor/quill/quill.snow.css" rel="stylesheet" type="text/css">
{% endblock head %}
{% block head %}{% endblock head %}
{% block content %}
<div class="space-y-2">
@@ -58,6 +56,4 @@
</div>
</div>
</div>
<script src="/static/vendor/quill/quill.js"></script>
<script src="/static/js/blog-editor.js"></script>
{% endblock content %}

View File

@@ -1,9 +1,7 @@
{% extends "admin/base.html" %}
{% block title %}{{ t(key="new-article", lang=lang | default(value='sk')) }}{% endblock title %}
{% block head %}
<link href="/static/vendor/quill/quill.snow.css" rel="stylesheet" type="text/css">
{% endblock head %}
{% block head %}{% endblock head %}
{% block content %}
<div class="space-y-2">
@@ -59,6 +57,4 @@
</div>
</div>
</div>
<script src="/static/vendor/quill/quill.js"></script>
<script src="/static/js/blog-editor.js"></script>
{% endblock content %}

View File

@@ -44,190 +44,9 @@
highlightTheme(localStorage.getItem('theme') || 'dark');
markActiveNav();
}
// --- persistent audio player with playlist queue ----------
// Survives htmx-boosted navigation: window state persists and
// #uw-player carries hx-preserve so <audio> keeps playing.
var uwQueue = []; // [{ src, title }]
var uwIndex = -1; // index of the current track, -1 when empty
function uwSave() {
try {
sessionStorage.setItem('uwQueue', JSON.stringify({ q: uwQueue, i: uwIndex }));
} catch (e) {}
}
function uwRestore() {
try {
var d = JSON.parse(sessionStorage.getItem('uwQueue') || 'null');
if (d && d.q) { uwQueue = d.q; uwIndex = (typeof d.i === 'number' ? d.i : -1); }
} catch (e) {}
}
function uwRenderQueue() {
var player = document.getElementById('uw-player');
if (player) player.classList.toggle('uw-has-queue', uwQueue.length > 0);
var badge = document.getElementById('uw-queue-badge');
if (badge) badge.textContent = uwQueue.length;
var count = document.getElementById('uw-queue-count');
if (count) count.textContent = uwQueue.length + (uwQueue.length === 1 ? ' track' : ' tracks');
var list = document.getElementById('uw-queue-list');
if (!list) return;
list.innerHTML = '';
uwQueue.forEach(function (t, idx) {
var li = document.createElement('li');
li.className = 'uw-queue-item' + (idx === uwIndex ? ' is-current' : '');
var jump = document.createElement('button');
jump.type = 'button';
jump.className = 'uw-queue-jump';
jump.setAttribute('data-uw-jump', idx);
jump.textContent = (idx === uwIndex ? '▸' : (idx + 1));
var name = document.createElement('span');
name.className = 'uw-queue-name';
name.setAttribute('data-uw-jump', idx);
name.textContent = t.title || 'unknown track';
var rm = document.createElement('button');
rm.type = 'button';
rm.className = 'uw-queue-remove';
rm.setAttribute('data-uw-remove', idx);
rm.setAttribute('aria-label', 'Remove from playlist');
rm.textContent = '✕';
li.appendChild(jump);
li.appendChild(name);
li.appendChild(rm);
list.appendChild(li);
});
}
// Point <audio> at the current queue entry; play it when asked.
function uwLoad(autoplay) {
var audio = document.getElementById('uw-audio');
var now = document.getElementById('uw-now');
if (!audio) return;
var t = uwQueue[uwIndex];
if (!t) {
if (now) now.textContent = '—';
audio.pause();
audio.removeAttribute('src');
document.documentElement.classList.remove('uw-playing');
uwRenderQueue();
uwSave();
return;
}
document.documentElement.classList.add('uw-playing');
if (now) now.textContent = t.title || 'unknown track';
if (audio.getAttribute('src') !== t.src) {
audio.setAttribute('src', t.src);
audio.load();
}
if (autoplay) {
var p = audio.play();
if (p && p.catch) p.catch(function () {});
}
uwRenderQueue();
uwSave();
}
// Replace the whole queue with a fresh set of tracks and play.
function uwPlayList(tracks) {
if (!tracks || !tracks.length) return;
uwQueue = tracks.slice();
uwIndex = 0;
uwLoad(true);
}
// Add one track: play it now if idle, otherwise queue it.
function uwAdd(src, title) {
var audio = document.getElementById('uw-audio');
var idle = uwIndex < 0 || !audio || audio.ended || !audio.getAttribute('src');
uwQueue.push({ src: src, title: title });
if (idle) { uwIndex = uwQueue.length - 1; uwLoad(true); }
else { uwRenderQueue(); uwSave(); }
}
function uwNext() {
if (uwIndex >= 0 && uwIndex < uwQueue.length - 1) { uwIndex++; uwLoad(true); }
}
function uwPrev() {
var audio = document.getElementById('uw-audio');
if (audio && audio.currentTime > 3) { audio.currentTime = 0; return; }
if (uwIndex > 0) { uwIndex--; uwLoad(true); }
else if (audio) audio.currentTime = 0;
}
function uwJump(idx) {
if (idx >= 0 && idx < uwQueue.length) { uwIndex = idx; uwLoad(true); }
}
function uwRemove(idx) {
if (idx < 0 || idx >= uwQueue.length) return;
var playing = document.documentElement.classList.contains('uw-playing');
uwQueue.splice(idx, 1);
if (idx < uwIndex) { uwIndex--; uwRenderQueue(); uwSave(); }
else if (idx > uwIndex) { uwRenderQueue(); uwSave(); }
else {
if (uwIndex >= uwQueue.length) uwIndex = uwQueue.length - 1;
uwLoad(playing);
}
}
function uwClear() {
uwQueue = [];
uwIndex = -1;
uwLoad(false);
var panel = document.getElementById('uw-queue');
if (panel) panel.hidden = true;
}
function uwInit() {
var audio = document.getElementById('uw-audio');
if (!audio || audio.dataset.uwBound) return;
audio.dataset.uwBound = '1';
uwRestore();
audio.addEventListener('ended', uwNext);
uwRenderQueue();
if (uwIndex >= 0 && uwQueue[uwIndex]) uwLoad(false);
}
document.addEventListener('DOMContentLoaded', function () { initPage(); uwInit(); });
document.addEventListener('DOMContentLoaded', initPage);
document.addEventListener('htmx:afterSwap', initPage);
document.addEventListener('click', function (e) {
if (!e.target.closest) return;
var albumBtn = e.target.closest('.uw-play-album');
if (albumBtn) {
var sel = albumBtn.getAttribute('data-tracks-from');
var scope = (sel && document.querySelector(sel)) || document;
var tracks = [];
scope.querySelectorAll('.uw-play').forEach(function (b) {
tracks.push({ src: b.getAttribute('data-src'), title: b.getAttribute('data-title') });
});
uwPlayList(tracks);
return;
}
// Play an album straight from the listing: fetch its tracks first.
var remoteBtn = e.target.closest('.uw-play-album-remote');
if (remoteBtn) {
var rurl = remoteBtn.getAttribute('data-album-tracks-url');
if (rurl) {
remoteBtn.disabled = true;
fetch(rurl, { headers: { 'Accept': 'application/json' } })
.then(function (r) { return r.json(); })
.then(function (d) { if (d && d.tracks) uwPlayList(d.tracks); })
.catch(function () {})
.then(function () { remoteBtn.disabled = false; });
}
return;
}
var playBtn = e.target.closest('.uw-play');
if (playBtn) {
uwAdd(playBtn.getAttribute('data-src'), playBtn.getAttribute('data-title'));
return;
}
var jumpEl = e.target.closest('[data-uw-jump]');
if (jumpEl) { uwJump(parseInt(jumpEl.getAttribute('data-uw-jump'), 10)); return; }
var rmEl = e.target.closest('[data-uw-remove]');
if (rmEl) { uwRemove(parseInt(rmEl.getAttribute('data-uw-remove'), 10)); return; }
if (e.target.closest('#uw-next')) { uwNext(); return; }
if (e.target.closest('#uw-prev')) { uwPrev(); return; }
if (e.target.closest('#uw-queue-clear')) { uwClear(); return; }
if (e.target.closest('#uw-queue-toggle')) {
var panel = document.getElementById('uw-queue');
if (panel) panel.hidden = !panel.hidden;
return;
}
if (e.target.closest('#uw-close')) { uwClear(); return; }
});
</script>
<link href="/static/css/app.css?v=2026-05-20b" rel="stylesheet" type="text/css">
<link href="/static/css/theme.css?v=2026-05-20b" rel="stylesheet" type="text/css">
<script src="/static/vendor/htmx/htmx-1.9.12.min.js"></script>
<style>
@media (min-width: 768px) {
@@ -343,24 +162,5 @@
<main class="term-main">
{% block content %}{% endblock content %}
</main>
<div id="uw-player" hx-preserve="true">
<div id="uw-queue" class="uw-queue" hidden>
<div class="uw-queue-head">
<span class="uw-queue-title">&#9776; playlist</span>
<span id="uw-queue-count" class="uw-queue-meta">0 tracks</span>
<button type="button" id="uw-queue-clear" class="uw-queue-clear">clear</button>
</div>
<ol id="uw-queue-list" class="uw-queue-list"></ol>
</div>
<div class="uw-player-inner">
<span class="uw-player-tag">&#9654; now playing</span>
<span id="uw-now" class="uw-player-title">&mdash;</span>
<button type="button" id="uw-prev" class="uw-player-btn" aria-label="Previous track" title="Previous">&#9198;</button>
<audio id="uw-audio" controls preload="none"></audio>
<button type="button" id="uw-next" class="uw-player-btn" aria-label="Next track" title="Next">&#9197;</button>
<button type="button" id="uw-queue-toggle" class="uw-player-btn" aria-label="Toggle playlist" title="Playlist">&#9776;<span id="uw-queue-badge" class="uw-queue-badge">0</span></button>
<button type="button" id="uw-close" class="uw-player-close" aria-label="Stop playback" title="Stop">&#10005;</button>
</div>
</div>
</body>
</html>