hugo-theme-tui

TUI
Log | Files | Refs | README | LICENSE

commit ebdc7300fd19167036ecf52c9c59f10a4610e97c
Author: FedorVinog <fedor.vinogradov@student.howest.be>
Date:   Wed, 10 Jun 2026 21:01:33 +0200

initial release — hugo-theme-tui

Terminal-UI Hugo theme: box-drawn frame, lazygit-style row hovers, Nord
palette, monospace typography, dot-matrix glyph marker on the frame
border. Configurable per-page-kind marker words via params.marker.

- theme.toml with MIT license, Hugo theme-gallery fields
- LICENSE (MIT)
- README with install, demo, and configuration docs
- exampleSite/ with minimal demo content and full hugo.toml example
- images/ placeholder with screenshot requirements documented

Diffstat:
A.gitignore | 16++++++++++++++++
ALICENSE | 21+++++++++++++++++++++
AREADME.md | 135+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
AexampleSite/content/_index.md | 9+++++++++
AexampleSite/content/posts/_index.md | 4++++
AexampleSite/content/posts/hello-world.md | 37+++++++++++++++++++++++++++++++++++++
AexampleSite/content/projects.md | 23+++++++++++++++++++++++
AexampleSite/hugo.toml | 60++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Aimages/README.md | 17+++++++++++++++++
Alayouts/_default/baseof.html | 64++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alayouts/_default/list.html | 17+++++++++++++++++
Alayouts/_default/projects.html | 23+++++++++++++++++++++++
Alayouts/_default/single.html | 7+++++++
Alayouts/index.html | 15+++++++++++++++
Alayouts/partials/cyrillic-svg.html | 66++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Alayouts/partials/footer.html | 5+++++
Alayouts/partials/head.html | 21+++++++++++++++++++++
Alayouts/partials/header.html | 27+++++++++++++++++++++++++++
Alayouts/partials/icon.html | 22++++++++++++++++++++++
Alayouts/partials/socials.html | 18++++++++++++++++++
Alayouts/posts/list.html | 24++++++++++++++++++++++++
Alayouts/posts/single.html | 24++++++++++++++++++++++++
Astatic/css/custom.css | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Astatic/css/pattern.css | 54++++++++++++++++++++++++++++++++++++++++++++++++++++++
Astatic/css/shared.css | 49+++++++++++++++++++++++++++++++++++++++++++++++++
Astatic/css/tui.css | 512+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Atheme.toml | 14++++++++++++++
27 files changed, 1407 insertions(+), 0 deletions(-)

diff --git a/.gitignore b/.gitignore @@ -0,0 +1,16 @@ +# Hugo build artifacts (the example site is for demo only) +exampleSite/public/ +exampleSite/resources/ +exampleSite/.hugo_build.lock + +# Hugo build artifacts at any level +public/ +resources/ +.hugo_build.lock + +# Editor / OS noise +.DS_Store +.idea/ +.vscode/ +*.swp +*.swo diff --git a/LICENSE b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Fedor Vinogradov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md @@ -0,0 +1,135 @@ +# hugo-theme-tui + +A terminal-UI Hugo theme: a box-drawn frame around the page, lazygit-style +row hovers on lists, a restrained Nord palette, a monospace font ladder, +and a dot-matrix glyph marker on the frame border. + +Built for personal sites — bio + projects + a blog. + +``` +╭── whoami.md ──────────────────────────────────── • • • • ▏──╮ +│ │ +│ user@example.com ~/ [whoami] projects … │ +│ ──────────────────────────────────────────────────────── │ +│ Fedor Vinogradov │ +│ one-line description here │ +│ │ +│ ── about ────────────────────────────────────────────── │ +│ · recently graduated … │ +│ · working on … │ +│ · passionate about open source │ +│ │ +╰──────────────────────────────────────────────────────────────╯ +``` + +## Install + +```bash +# from your Hugo site root +git submodule add https://github.com/tddra/hugo-theme-tui themes/hugo-theme-tui +``` + +Then in your `hugo.toml`: + +```toml +theme = "hugo-theme-tui" +``` + +Or clone directly without submodules: + +```bash +git clone https://github.com/tddra/hugo-theme-tui themes/hugo-theme-tui +``` + +## Try the demo site + +```bash +git clone https://github.com/tddra/hugo-theme-tui +cd hugo-theme-tui +hugo server --source exampleSite --themesDir ../.. +``` + +Then open `http://localhost:1313`. + +## Configure + +All configuration lives in `hugo.toml`. See `exampleSite/hugo.toml` for a +complete working example. Minimum to look right: + +```toml +[params] +brandUser = "fedor" # left side of the user@host brand line +brandHost = "fedorvin.com" # right side + +[params.about] +title = "Your Name" +description = "one-line tagline shown under the title" +``` + +### The dot-matrix marker + +The bracketed dot-matrix glyphs sitting on the frame's top border are the +theme's signature element. The word changes per page kind, and is +configurable: + +```toml +[params.marker] +home = ["Д", "О", "М"] +projects = ["К", "О", "Д"] +posts_list = ["П", "О", "С", "Т"] +posts_single = ["С", "Т", "А", "Т"] +fallback = ["С", "Т", "А", "Т"] +``` + +Each entry is a list of single characters. The bitmap font ships with +**eight Cyrillic glyphs**: `А Д К М О П С Т`. To use different +characters, add 5×5 bitmap entries to the `$font` dict in +`layouts/partials/cyrillic-svg.html`. (Each glyph is a list of five +strings of five `1`/`0` digits — see the existing entries for examples.) + +### Socials + +The home page renders a socials grid driven by `params.socialLinks`: + +```toml +[[params.socialLinks]] +key = "email" +value = "hello@example.com" +url = "mailto:hello@example.com" +icon = "email" +``` + +Built-in icons: `email`, `linkedin`, `github`, `git`, `rss`, `pgp`, +`coffee`. Add more by extending `layouts/partials/icon.html`. + +### Projects list + +A "projects" page renders a `proj-list` from page params, not from child +pages. See `exampleSite/content/projects.md` for the structure: + +```toml +[[projects]] +title = "my-project" +url = "https://github.com/…" +lang = "rust" +desc = "one-line description" +``` + +## Customizing + +The whole theme is four CSS files under `static/css/`: + +| File | Purpose | +| ------------ | --------------------------------------------------------- | +| `shared.css` | Nord palette variables, base typography, body reset | +| `tui.css` | All component styles (frame, header, sections, rows, etc.) | +| `pattern.css`| Marker positioning (border-anchored) | +| `custom.css` | Hugo-rendered-markdown adjustments (TOC, images, etc.) | + +Override anything by creating a same-named partial or stylesheet in your +site's own `layouts/` or `static/` directory — Hugo's lookup order picks +your site's files over the theme's. + +## License + +[MIT](./LICENSE) — Fedor Vinogradov, 2026. diff --git a/exampleSite/content/_index.md b/exampleSite/content/_index.md @@ -0,0 +1,9 @@ ++++ +title = "whoami" ++++ + +* You are looking at the `hugo-theme-tui` demo site +* Edit `content/_index.md` to write your own bio +* The brand line in the header is set via `params.brandUser` and `params.brandHost` +* The dot-matrix marker word is set per page kind via `params.marker` +* Socials below come from `params.socialLinks` in `hugo.toml` diff --git a/exampleSite/content/posts/_index.md b/exampleSite/content/posts/_index.md @@ -0,0 +1,4 @@ ++++ +title = "posts" +description = "thoughts, notes, write-ups" ++++ diff --git a/exampleSite/content/posts/hello-world.md b/exampleSite/content/posts/hello-world.md @@ -0,0 +1,37 @@ ++++ +title = "hello, world" +date = 2026-06-10 +description = "first post in the demo site" +tags = ["meta", "intro"] ++++ + +This is a demo post that ships with `hugo-theme-tui`. It shows how a single +post is rendered: the frame title shows the file basename, the dot-matrix +marker shows the `posts_single` word, and a table of contents appears above +the article if the post has any `##` or `###` headings. + +## A section heading + +Body copy uses a monospace font (Atkinson Hyperlegible Mono) on a Nord +palette. Inline `code` like this gets a subtle panel background and a 2px +border-radius. + +```bash +# fenced code blocks get a left rule in the Nord green +echo "hello" +``` + +### A subsection + +Links look like [this](https://example.com) — solid underline in the link +color at 25% opacity, switching to a brighter color on hover. + +> Blockquotes get a left rule in the Nord purple. Use them for asides. + +## Another section + +Lists use a thin `·` bullet: + +- item one +- item two +- item three diff --git a/exampleSite/content/projects.md b/exampleSite/content/projects.md @@ -0,0 +1,23 @@ ++++ +title = "projects" +description = "things I'm building" +type = "projects" +layout = "projects" + +[[projects]] +title = "hugo-theme-tui" +url = "https://github.com/tddra/hugo-theme-tui" +lang = "hugo" +desc = "this very theme" + +[[projects]] +title = "example-cli" +url = "https://example.com" +lang = "rust" +desc = "a one-line description of what this project does" + +[[projects]] +title = "another-thing" +lang = "go" +desc = "no link, just a name + description" ++++ diff --git a/exampleSite/hugo.toml b/exampleSite/hugo.toml @@ -0,0 +1,60 @@ +baseURL = "https://example.com/" +languageCode = "en-us" +title = "hugo-theme-tui demo" +theme = "hugo-theme-tui" + +defaultContentLanguage = "en" + +[params] +# Brand string rendered in the header: <user>@<host> ~/path +brandUser = "user" +brandHost = "example.com" + +[params.about] +title = "hugo-theme-tui" +description = "A terminal-UI Hugo theme. Edit `content/_index.md` to make this your own." + +# Dot-matrix marker words per page kind. Each is a list of single characters. +# Only characters present in the bitmap font (see partials/cyrillic-svg.html) +# will render — by default that's the Cyrillic set: А Д К М О П С Т. +# Override any of these to use your own word. +[params.marker] +home = ["Д", "О", "М"] +projects = ["К", "О", "Д"] +posts_list = ["П", "О", "С", "Т"] +posts_single = ["С", "Т", "А", "Т"] +fallback = ["С", "Т", "А", "Т"] + +# Socials grid (home page). icon must be one of: +# email | linkedin | github | git | rss | pgp | coffee +[[params.socialLinks]] +key = "email" +value = "hello@example.com" +url = "mailto:hello@example.com" +icon = "email" + +[[params.socialLinks]] +key = "github" +value = "github.com/example" +url = "https://github.com/example" +icon = "github" + +[[params.socialLinks]] +key = "rss" +value = "/index.xml" +url = "/index.xml" +icon = "rss" + +[menu] + + [[menu.main]] + identifier = "projects" + name = "projects" + url = "/projects/" + weight = 10 + + [[menu.main]] + identifier = "posts" + name = "posts" + url = "/posts/" + weight = 20 diff --git a/images/README.md b/images/README.md @@ -0,0 +1,17 @@ +# Theme gallery images + +Drop two PNGs into this directory before submitting to the Hugo themes +gallery (https://themes.gohugo.io): + +- `screenshot.png` — 1500 × 1000 px, full-page screenshot +- `tn.png` — 900 × 600 px, thumbnail + +Generate them by running the example site and screenshotting: + +```bash +hugo server --source exampleSite --themesDir ../.. +# open http://localhost:1313 and capture +``` + +This is a Hugo gallery requirement, not a theme requirement — the theme +works fine without them. diff --git a/layouts/_default/baseof.html b/layouts/_default/baseof.html @@ -0,0 +1,64 @@ +<!DOCTYPE html> +<html lang="{{ site.Language.Lang }}"> +<head> +{{ partial "head.html" . }} +</head> +<body> +{{- /* + Derive per-page-kind variables: frame title, dot-matrix marker word, + brand path, active nav state. + + The marker word is a slice of single characters (one bitmap glyph per + char). Site users can override the per-page-kind words via: + [params.marker] + home = ["H","O","M","E"] + projects = ["C","O","D","E"] + posts_list = ["P","O","S","T"] + posts_single = ["R","E","A","D"] + fallback = ["P","A","G","E"] + Only characters present in the bitmap font (see partials/cyrillic-svg.html + $font dict) will render. Defaults below use the original Cyrillic motif. +*/ -}} +{{- $frameTitle := "" -}} +{{- $cyrWord := slice -}} +{{- $brandPath := "" -}} +{{- $navActive := "home" -}} +{{- $marker := site.Params.marker | default dict -}} + +{{- if .IsHome -}} + {{- $frameTitle = "── whoami.md ──" -}} + {{- $cyrWord = index $marker "home" | default (slice "Д" "О" "М") -}} + {{- $navActive = "home" -}} +{{- else if eq .Type "projects" -}} + {{- $frameTitle = "── projects/ ──" -}} + {{- $cyrWord = index $marker "projects" | default (slice "К" "О" "Д") -}} + {{- $brandPath = "projects" -}} + {{- $navActive = "projects" -}} +{{- else if eq .Section "posts" -}} + {{- $brandPath = "posts" -}} + {{- $navActive = "posts" -}} + {{- if .IsPage -}} + {{- $cyrWord = index $marker "posts_single" | default (slice "С" "Т" "А" "Т") -}} + {{- $slug := .File.BaseFileName -}} + {{- $frameTitle = printf "── %s.md ──" $slug -}} + {{- else -}} + {{- $cyrWord = index $marker "posts_list" | default (slice "П" "О" "С" "Т") -}} + {{- $frameTitle = "── posts/ ──" -}} + {{- end -}} +{{- else -}} + {{- $frameTitle = printf "── %s ──" (.Title | default "page") -}} + {{- $cyrWord = index $marker "fallback" | default (slice "С" "Т" "А" "Т") -}} +{{- end -}} + +<div class="tui"> + <div class="frame" data-title="{{ $frameTitle }}"> + {{ partial "cyrillic-svg.html" (dict "word" $cyrWord) }} + <div class="wrap"> + {{ partial "header.html" (dict "active" $navActive "path" $brandPath "mobileWord" $cyrWord) }} + {{ block "main" . }}{{ end }} + </div> + {{ partial "footer.html" . }} + </div> +</div> +</body> +</html> diff --git a/layouts/_default/list.html b/layouts/_default/list.html @@ -0,0 +1,17 @@ +{{ define "main" }} +<h1 class="page-title">{{ .Title }}</h1> +{{- with .Description }}<p class="page-sub">{{ . | markdownify }}</p>{{ end }} +{{- with .Content }}<div class="bullets-wrap">{{ . }}</div>{{ end }} + +<ul class="proj-list"> + {{- range .Pages }} + <li> + <div class="lang">{{ .Section }}</div> + <div class="body"> + <a href="{{ .Permalink }}">{{ .Title }}</a> + {{- with .Description }}<div class="desc">{{ . }}</div>{{ end }} + </div> + </li> + {{- end }} +</ul> +{{ end }} diff --git a/layouts/_default/projects.html b/layouts/_default/projects.html @@ -0,0 +1,23 @@ +{{ define "main" }} +<h1 class="page-title">{{ .Title | default "projects" | lower }}</h1> +{{- with .Description }}<p class="page-sub">{{ . | markdownify }}</p>{{ end }} + +{{- with .Content }}<div class="bullets-wrap">{{ . }}</div>{{ end }} + +{{- $projects := .Params.projects -}} +{{- with $projects }} +<h2 class="section">repos<span class="count">{{ len . }} entries</span></h2> +<ul class="proj-list"> + {{- range . }} + <li> + <div class="lang">{{ .lang }}</div> + <div class="body"> + {{- if .url }}<a href="{{ .url }}">{{ .title }}</a> + {{- else }}<span>{{ .title }}</span>{{ end }} + {{- with .desc }}<div class="desc">{{ . }}</div>{{ end }} + </div> + </li> + {{- end }} +</ul> +{{- end }} +{{ end }} diff --git a/layouts/_default/single.html b/layouts/_default/single.html @@ -0,0 +1,7 @@ +{{ define "main" }} +<h1 class="page-title">{{ .Title }}</h1> +{{- with .Description }}<p class="page-sub">{{ . | markdownify }}</p>{{ end }} +<article class="post"> + {{ .Content }} +</article> +{{ end }} diff --git a/layouts/index.html b/layouts/index.html @@ -0,0 +1,15 @@ +{{ define "main" }} +{{- $about := site.Params.about -}} +<h1 class="page-title">{{ with $about.title }}{{ . }}{{ else }}{{ site.Title }}{{ end }}</h1> +{{- with $about.description }}<p class="page-sub">{{ . | markdownify }}</p>{{ end }} + +<h2 class="section">about</h2> +<div class="bullets-wrap"> + {{ .Content }} +</div> + +{{ if site.Params.socialLinks }} +<h2 class="section">socials<span class="count">contact</span></h2> +{{ partial "socials.html" . }} +{{ end }} +{{ end }} diff --git a/layouts/partials/cyrillic-svg.html b/layouts/partials/cyrillic-svg.html @@ -0,0 +1,66 @@ +{{- /* + Renders a 5×5 dot-matrix page marker as an inline <svg>. The bitmap font + ($font dict below) ships with eight Cyrillic glyphs; add more entries to + support other characters. + + Context: + .word — a slice of single-character strings, in reading order + (e.g. (slice "Д" "О" "М") for "ДОМ") + .W — optional width (default 320) + .H — optional height (default 32) + .class — optional class for the <svg> (default "frame-marker") + + Geometry: + step=4.5 r=1.45 cy=H/2 rightX = W - step*1.2 + For each char (iterated right-to-left as col=0, -6, -12, ...) and + each bitmap cell (rr,cc) where the bit is "1": + x = rightX + (col + cc - 4) * step + y = cy + (rr - 2 ) * step +*/ -}} + +{{- $word := .word -}} +{{- $W := (.W | default 320) -}} +{{- $H := (.H | default 32) -}} +{{- $class := (.class | default "frame-marker") -}} + +{{- /* Scale step / r proportionally to height: + step = (H/32)*4.5, r = (H/32)*1.45. */ -}} +{{- $hRatio := div (float $H) 32.0 -}} +{{- $step := mul $hRatio 4.5 -}} +{{- $r := mul $hRatio 1.45 -}} +{{- $cy := div (float $H) 2.0 -}} +{{- $rightX := sub (float $W) (mul $step 1.2) -}} + +{{- $font := dict + "А" (slice "01110" "10001" "11111" "10001" "10001") + "Д" (slice "01110" "01010" "01010" "11111" "10001") + "К" (slice "10010" "10100" "11000" "10100" "10010") + "М" (slice "10001" "11011" "10101" "10001" "10001") + "О" (slice "01110" "10001" "10001" "10001" "01110") + "П" (slice "11111" "10001" "10001" "10001" "10001") + "С" (slice "01111" "10000" "10000" "10000" "01111") + "Т" (slice "11111" "00100" "00100" "00100" "00100") +-}} + +{{- /* Reverse the word so the rightmost char gets col=0. */ -}} +{{- $reversed := collections.Reverse $word -}} + +<svg xmlns="http://www.w3.org/2000/svg" class="{{ $class }}" viewBox="0 0 {{ $W }} {{ $H }}" preserveAspectRatio="none" aria-hidden="true"> + <line x1="0" y1="{{ $cy }}" x2="{{ $W }}" y2="{{ $cy }}" stroke="#434c5e" stroke-width="1" shape-rendering="crispEdges"/> + {{- range $idx, $ch := $reversed -}} + {{- $col := mul $idx -6 -}} + {{- with index $font $ch -}} + {{- range $rr, $row := . -}} + {{- range $cc := seq 0 4 -}} + {{- if eq (substr $row $cc 1) "1" -}} + {{- $i := add $col (sub $cc 4) -}} + {{- $j := sub $rr 2 -}} + {{- $x := add $rightX (mul (float $i) $step) -}} + {{- $y := add $cy (mul (float $j) $step) -}} + <circle cx="{{ printf "%.2f" $x }}" cy="{{ printf "%.2f" $y }}" r="{{ $r }}" fill="#88c0d0"/> + {{- end -}} + {{- end -}} + {{- end -}} + {{- end -}} + {{- end }} +</svg> diff --git a/layouts/partials/footer.html b/layouts/partials/footer.html @@ -0,0 +1,5 @@ +{{- with site.Copyright -}} +<footer class="site-footer"> + <p class="copyright">{{ . | markdownify }}</p> +</footer> +{{- end -}} diff --git a/layouts/partials/head.html b/layouts/partials/head.html @@ -0,0 +1,21 @@ +<meta charset="utf-8"> +<meta name="viewport" content="width=device-width, initial-scale=1"> +<title>{{ with .Title }}{{ . }} &ndash; {{ end }}{{ .Site.Title }}</title> +{{ with .Site.Params.about.description }}<meta name="description" content="{{ . }}">{{ end }} +{{ if .Site.Params.noindex }}<meta name="robots" content="noindex">{{ end }} + +<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> +<link rel="stylesheet" href="{{ "css/shared.css" | absURL }}"> +<link rel="stylesheet" href="{{ "css/tui.css" | absURL }}"> +<link rel="stylesheet" href="{{ "css/pattern.css" | absURL }}"> +<link rel="stylesheet" href="{{ "css/custom.css" | absURL }}"> + +{{ if os.FileExists "static/favicon.ico" }}<link rel="icon" href="{{ "favicon.ico" | absURL }}">{{ end }} +{{ if os.FileExists "static/favicon-32x32.png" }}<link rel="icon" type="image/png" sizes="32x32" href="{{ "favicon-32x32.png" | absURL }}">{{ end }} +{{ if os.FileExists "static/favicon-16x16.png" }}<link rel="icon" type="image/png" sizes="16x16" href="{{ "favicon-16x16.png" | absURL }}">{{ end }} +{{ if os.FileExists "static/apple-touch-icon.png" }}<link rel="apple-touch-icon" href="{{ "apple-touch-icon.png" | absURL }}">{{ end }} +{{ if os.FileExists "static/site.webmanifest" }}<link rel="manifest" href="{{ "site.webmanifest" | absURL }}">{{ end }} + +{{ with .OutputFormats.Get "rss" -}} + {{ printf `<link rel=%q type=%q href=%q title=%q>` .Rel .MediaType.Type .Permalink site.Title | safeHTML }} +{{ end }} diff --git a/layouts/partials/header.html b/layouts/partials/header.html @@ -0,0 +1,27 @@ +{{- /* + Site header — brand (user@host ~/path) + nav (whoami / projects / posts). + The brand prefix `user@host` links back to the home page; the trailing + ` ~/path` reflects current section and stays as text. + + Context: + .active — one of "home" | "projects" | "posts" + .path — section path shown after `~/` in the brand line + .mobileWord — slice of single chars for the mobile dot-matrix marker +*/ -}} +{{- $active := .active -}} +{{- $path := .path -}} +{{- $user := site.Params.brandUser | default "user" -}} +{{- $host := site.Params.brandHost | default "example.com" -}} +<header class="site"> + <div class="brand"> + <a class="brand-link" href="{{ "/" | relURL }}" aria-label="whoami"><span class="user">{{ $user }}</span><span class="at">@</span><span class="host">{{ $host }}</span></a><span class="path"> ~/{{ $path }}</span> + </div> + <nav class="site"> + <a href="{{ "/" | relURL }}"{{ if eq $active "home" }} class="is-active"{{ end }}>whoami</a> + <a href="{{ "/projects/" | relURL }}"{{ if eq $active "projects" }} class="is-active"{{ end }}>projects</a> + <a href="{{ "/posts/" | relURL }}"{{ if eq $active "posts" }} class="is-active"{{ end }}>posts</a> + </nav> + {{- with .mobileWord -}} + {{ partial "cyrillic-svg.html" (dict "word" . "W" 320 "H" 26 "class" "frame-marker frame-marker-mobile") }} + {{- end -}} +</header> diff --git a/layouts/partials/icon.html b/layouts/partials/icon.html @@ -0,0 +1,22 @@ +{{- /* + Inline SVG icons for socials, keyed by name. + Context: a plain string — the icon name ("email" | "linkedin" | "github" | + "git" | "rss" | "pgp" | "coffee"). Unknown names render nothing. +*/ -}} +{{- $name := . -}} +{{- $common := `viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round"` -}} +{{- if eq $name "email" -}} +<svg {{ $common | safeHTMLAttr }}><rect x="3" y="5" width="18" height="14" rx="1"/><path d="M3 7l9 6 9-6"/></svg> +{{- else if eq $name "linkedin" -}} +<svg {{ $common | safeHTMLAttr }}><rect x="3" y="3" width="18" height="18" rx="1"/><path d="M8 10v8M8 7v.01M12 18v-5M16 18v-3a2 2 0 0 0-4 0"/></svg> +{{- else if eq $name "github" -}} +<svg {{ $common | safeHTMLAttr }}><path d="M9 19c-4 1.5-4-2-6-2.5M15 22v-4c0-1 .1-1.4-.5-2 3-.3 6-1.5 6-6.5a4.7 4.7 0 0 0-1.3-3.3 4.4 4.4 0 0 0-.1-3.3s-1-.3-3.4 1.3a11.6 11.6 0 0 0-6.2 0C6.9 1.6 5.9 1.9 5.9 1.9a4.4 4.4 0 0 0-.1 3.3A4.7 4.7 0 0 0 4.5 8.5c0 5 3 6.2 6 6.5-.6.6-.6 1.2-.5 2v4"/></svg> +{{- else if eq $name "git" -}} +<svg {{ $common | safeHTMLAttr }}><path d="M12 2v6M12 22v-6M4.93 4.93l4.24 4.24M14.83 14.83l4.24 4.24M2 12h6M22 12h-6M4.93 19.07l4.24-4.24M14.83 9.17l4.24-4.24"/></svg> +{{- else if eq $name "rss" -}} +<svg {{ $common | safeHTMLAttr }}><path d="M4 11a9 9 0 0 1 9 9M4 4a16 16 0 0 1 16 16"/><circle cx="5" cy="19" r="1"/></svg> +{{- else if eq $name "pgp" -}} +<svg {{ $common | safeHTMLAttr }}><rect x="4" y="11" width="16" height="10" rx="1"/><path d="M8 11V7a4 4 0 0 1 8 0v4"/></svg> +{{- else if eq $name "coffee" -}} +<svg {{ $common | safeHTMLAttr }}><path d="M18 8h1a4 4 0 0 1 0 8h-1M2 8h16v9a4 4 0 0 1-4 4H6a4 4 0 0 1-4-4V8z"/><line x1="6" y1="2" x2="6" y2="4"/><line x1="10" y1="2" x2="10" y2="4"/><line x1="14" y1="2" x2="14" y2="4"/></svg> +{{- end -}} diff --git a/layouts/partials/socials.html b/layouts/partials/socials.html @@ -0,0 +1,18 @@ +{{- /* + Socials grid for the home page. Driven by site.Params.socialLinks. + Each entry uses: + key — label shown in the tile's small caps line (e.g., "email") + value — value shown below the key (e.g., "hello@example.com") + url — href + icon — name passed to partials/icon.html (see that partial for keys) +*/ -}} +{{- with site.Params.socialLinks -}} +<div class="socials"> + {{- range . }} + <a href="{{ .url }}"{{ if hasPrefix .url "http" }} rel="me"{{ end }} aria-label="{{ .key }}" title="{{ .key }}"> + <span class="ico">{{ partial "icon.html" .icon }}</span> + <span class="lbl"><span class="k">{{ .key }}</span><span class="v">{{ .value }}</span></span> + </a> + {{- end }} +</div> +{{- end -}} diff --git a/layouts/posts/list.html b/layouts/posts/list.html @@ -0,0 +1,24 @@ +{{ define "main" }} +<h1 class="page-title">{{ .Title | default "posts" | lower }}</h1> +{{- with .Description }}<p class="page-sub">{{ . | markdownify }}</p> +{{- else }}{{ with .Content }}<div class="page-sub">{{ . }}</div>{{ end }}{{ end }} + +{{- $pages := where .Pages "Section" "posts" -}} +{{- range $pages.GroupByDate "2006" }} +<h2 class="section">{{ .Key }}<span class="count">{{ len .Pages }} entries</span></h2> +<div class="qf"> + {{- range .Pages }} + <div class="qf-row"> + <div class="date">{{ .Date.Format "2006-01-02" }}</div> + <div class="title-cell"> + <a href="{{ .Permalink }}">{{ .Title }}</a> + {{- with .Description }}<div class="desc">{{ . }}</div>{{ end }} + </div> + <div class="tags"> + {{- range .Params.tags }}<span class="tag">#{{ . }}</span>{{ end -}} + </div> + </div> + {{- end }} +</div> +{{- end }} +{{ end }} diff --git a/layouts/posts/single.html b/layouts/posts/single.html @@ -0,0 +1,24 @@ +{{ define "main" }} +<h1 class="page-title">{{ .Title }}</h1> +<p class="page-sub"> + {{- if not .Date.IsZero }}<span style="color:var(--base0D)">{{ .Date.Format "2006-01-02" }}</span>{{ end -}} + {{- with .ReadingTime }}<span style="color:var(--base04);margin:0 8px">·</span><span>~{{ . }} min read</span>{{ end -}} + {{- with .Params.tags }} + <span style="color:var(--base04);margin:0 8px">·</span> + {{- range . }}<span style="color:var(--base0C)">[#{{ . }}]</span> {{ end -}} + {{- end -}} +</p> + +<div class="post-layout"> + {{- $toc := .TableOfContents -}} + {{- if $toc }} + <aside class="toc toc-vertical" aria-label="table of contents"> + <div class="toc-head">── on this page ──</div> + {{ $toc }} + </aside> + {{- end }} + <article class="post"> + {{ .Content }} + </article> +</div> +{{ end }} diff --git a/static/css/custom.css b/static/css/custom.css @@ -0,0 +1,123 @@ +/* Hugo-output tweaks: things the static design source doesn't cover + because it was hand-authored HTML rather than markdown-rendered. */ + +/* ----- Home: scope the .bullets style to markdown-rendered <ul> ----- */ +.tui .bullets-wrap ul { + list-style: none; + padding: 0; + margin: 0 0 26px; + display: flex; + flex-direction: column; +} +.tui .bullets-wrap ul li { + padding: 4px 10px 4px 22px; + position: relative; + color: var(--fg); + font-size: 14px; + border-left: 2px solid transparent; +} +.tui .bullets-wrap ul li::before { + content: "·"; + position: absolute; + left: 10px; top: 4px; + color: var(--muted); + font-weight: 700; +} +.tui .bullets-wrap a { color: var(--link); text-decoration: none; } +.tui .bullets-wrap a:hover { color: var(--hl); text-decoration: underline; text-decoration-style: dotted; } +.tui .bullets-wrap strong { color: var(--fg-bright); } +.tui .bullets-wrap p { margin: 0 0 18px; color: var(--fg); font-size: 14px; } + +/* ----- Single-post TOC: style Hugo's <nav id="TableOfContents"> to + approximate the design's tree-branch TOC ----- */ +.tui #TableOfContents > ul { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 4px; + counter-reset: tocnum; +} +.tui #TableOfContents > ul > li { + counter-increment: tocnum; +} +.tui #TableOfContents > ul > li > a { + color: var(--fg); + text-decoration: none; + font-size: 12.5px; + border-bottom: 1px dotted transparent; + line-height: 1.5; +} +.tui #TableOfContents > ul > li > a::before { + content: counter(tocnum, decimal-leading-zero) " "; + color: var(--accent); + font-size: 11px; +} +.tui #TableOfContents > ul > li > a:hover { + color: var(--hl); + border-bottom-color: var(--accent); +} +.tui #TableOfContents > ul > li > ul { + list-style: none; + margin: 2px 0 6px; + padding: 0 0 0 26px; + display: flex; + flex-direction: column; + gap: 1px; +} +.tui #TableOfContents > ul > li > ul > li { + display: flex; + align-items: baseline; + line-height: 1.5; +} +.tui #TableOfContents > ul > li > ul > li::before { + content: "├── "; + color: var(--rule-bright); + white-space: pre; + user-select: none; + font-size: 12px; +} +.tui #TableOfContents > ul > li > ul > li:last-child::before { + content: "└── "; +} +.tui #TableOfContents > ul > li > ul > li > a { + color: var(--muted); + text-decoration: none; + font-size: 12px; + border-bottom: 1px dotted transparent; +} +.tui #TableOfContents > ul > li > ul > li:hover > a { + color: var(--hl); + border-bottom-color: var(--accent); +} + +/* The TOC container "── on this page ──" head when wrapped in our aside */ +.tui .toc { margin: 0 0 22px; padding: 12px 14px; border: 1px solid var(--rule); background: transparent; font-size: 12px; color: var(--muted); } +.tui .toc-head { color: var(--accent); font-weight: 700; font-size: 10.5px; letter-spacing: 0.06em; margin-bottom: 8px; text-transform: lowercase; } + +/* ----- Make Hugo-rendered post bodies pick up article.post styles + even if the markdown renderer adds an extra wrapper. The actual + heading/paragraph styles live in tui.css. ----- */ +.tui article.post > :first-child { margin-top: 0; } + +/* Hugo renders code fences as <pre><code class="language-xxx">. Keep the + tui.css pre styling, just make sure inline class doesn't break it. */ +.tui article.post pre code[class*="language-"] { background: none; padding: 0; color: inherit; } + +/* Constrain markdown-rendered images to the article width. */ +.tui article.post img { + display: block; + max-width: 100%; + height: auto; + margin: 16px auto; + border: 1px solid var(--rule); +} +.tui article.post figure { margin: 16px 0; } +.tui article.post figure img { margin: 0 auto; } +.tui article.post figure figcaption { + font-size: 11.5px; + color: var(--muted); + text-align: center; + margin-top: 6px; +} diff --git a/static/css/pattern.css b/static/css/pattern.css @@ -0,0 +1,54 @@ +/* ===== Cyrillic dot-matrix page-marker ===== + Desktop: marker sits ON the frame's top border (right side, ~320px), + reading as part of the border itself. + Mobile: marker sits ON the header's bottom border. */ + +.tui .frame { position: relative; } + +/* Hide V5's native right-meta strip (data-meta on .frame::after) — the + marker replaces it on this theme. */ +.tui .frame::after { display: none; } + +/* Desktop marker. + The SVG itself contains a full-width centerline at y=H/2 stroked in the + same color as the frame's border-top, so the two lines pixel-align and + read as one continuous rule. */ +.tui .frame > .frame-marker { + position: absolute; + /* -16.5px (not -16px) so the SVG's 1px centerline pixel-aligns with the + frame's 1px border-top — otherwise the two lines sit half a pixel apart + and read as two slightly-shifted rules. */ + top: -16.5px; + right: 8px; + width: 320px; + height: 32px; + background: var(--bg); + pointer-events: none; + display: block; +} + +/* Mobile marker — hidden on desktop. */ +.tui .frame-marker-mobile { display: none; } + +/* Real-viewport mobile (≤560px) — desktop marker off, mobile marker on + the header's bottom-border. */ +@media (max-width: 560px) { + .tui .frame > .frame-marker { display: none; } + + .tui header.site { + position: relative; + padding-bottom: 12px; + } + + .tui header.site .frame-marker-mobile { + display: block; + position: absolute; + /* Keep visually centered on the header's bottom border: bottom = -height/2 */ + bottom: -13px; + right: 6px; + width: 320px; + height: 26px; + background: var(--bg); + pointer-events: none; + } +} diff --git a/static/css/shared.css b/static/css/shared.css @@ -0,0 +1,49 @@ +/* Nord palette — exact values from the V8 design source. */ +:root { + /* Polar Night */ + --base00: #242933; + --base01: #2e3440; + --base02: #3b4252; + --base03: #434c5e; + --base04: #4c566a; + + /* Snow Storm */ + --base05: #d8dee9; + --base06: #e5e9f0; + --base07: #eceff4; + + /* Frost */ + --base08: #8fbcbb; + --base09: #88c0d0; + --base0A: #81a1c1; + --base0B: #5e81ac; + + /* Aurora */ + --base0C: #b48ead; + --base0D: #ebcb8b; + --base0E: #a3be8c; + --base0F: #d08770; + + /* Soft hover surface — panel at ~60% so row-selection feels eased + instead of slamming to a fully different background. */ + --panel-soft: rgba(46, 52, 64, 0.6); +} + +@import url("https://fonts.googleapis.com/css2?family=Atkinson+Hyperlegible+Mono:wght@400;500;700&display=swap"); + +* { box-sizing: border-box; } + +html, body { + height: 100%; + margin: 0; + padding: 0; + background: var(--base00); + font-family: "Atkinson Hyperlegible Mono", "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace; + font-feature-settings: "ss01", "ss02"; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +a { color: inherit; } + +::selection { background: var(--base09); color: var(--base01); } diff --git a/static/css/tui.css b/static/css/tui.css @@ -0,0 +1,512 @@ +/* ===== hugo-theme-tui — base styles ===== + Box-drawn frame around the page, lazygit-style row selection, + restrained Nord palette. */ + +.tui { + --bg: var(--base00); + --panel: var(--base01); + --panel-2: var(--base02); + --fg: var(--base05); + --fg-bright: var(--base07); + --muted: var(--base04); + --rule: var(--base02); + --rule-bright: var(--base03); + --link: var(--base09); + --accent: var(--base08); + --hl: var(--base0D); + --tag: var(--base0C); + --green: var(--base0E); + + background: var(--bg); + color: var(--fg); + font-size: 14px; + line-height: 1.65; + min-height: 100%; + padding: 28px 28px; + display: flex; + flex-direction: column; + + /* Cap width on large displays so the frame doesn't stretch edge-to-edge. + Expressed in rem so it scales with the user's root font size. */ + max-width: 64rem; + margin: 0 auto; +} + +/* Hover decorations ease in over ~90ms — fast enough to feel native, slow + enough to not slam like a static screenshot flipping frames. Scoped to + the things that actually change on hover (color/background/border). + Animations stay off (no spinners etc). */ +.tui nav.site a, +.tui ul.bullets li, +.tui ul.bullets a, +.tui .bullets-wrap a, +.tui .qf .qf-row, +.tui .qf .qf-row .title-cell a, +.tui .proj-list li, +.tui .proj-list .body a, +.tui .socials a, +.tui .socials a .ico, +.tui .socials a .lbl .k, +.tui article.post a, +.tui .toc-vertical .toc-row a, +.tui .toc-vertical .toc-subitem a, +.tui #TableOfContents a, +.tui footer.plain a, +.tui .brand .brand-link, +.tui .brand .brand-link .user, +.tui .brand .brand-link .host { + transition: color 90ms ease-out, background-color 90ms ease-out, border-color 90ms ease-out; +} + +.tui .frame { + border: 1px solid var(--rule-bright); + position: relative; + padding: 30px 36px 18px; + display: flex; + flex-direction: column; +} +/* corner-anchored title strip — looks like ╭─ whoami.md ─╮ */ +.tui .frame::before { + content: attr(data-title); + position: absolute; + top: -0.7em; + left: 18px; + background: var(--bg); + padding: 0 10px; + color: var(--accent); + font-size: 11px; + font-weight: 600; + letter-spacing: 0.04em; +} +/* right-side meta strip */ +.tui .frame::after { + content: attr(data-meta); + position: absolute; + top: -0.7em; + right: 18px; + background: var(--bg); + padding: 0 10px; + color: var(--muted); + font-size: 11px; + letter-spacing: 0.04em; +} + +.tui .wrap { + max-width: 50rem; + margin: 0 auto; + width: 100%; + flex: 1; +} + +/* Header — bracketed tabs (kept from V4) */ +.tui header.site { + display: flex; + align-items: baseline; + justify-content: space-between; + margin-bottom: 22px; + gap: 24px; + font-size: 13px; + padding-bottom: 12px; + border-bottom: 1px solid var(--rule); +} +.tui .brand { + font-weight: 700; + color: var(--fg); +} +.tui .brand .user { color: var(--hl); } +.tui .brand .at { color: var(--muted); } +.tui .brand .host { color: var(--green); } +.tui .brand .path { color: var(--muted); } + +/* The `user@host` brand string is a link back to the home page. Keep the + per-span colors at rest; on hover, the whole link turns bright (white). */ +.tui .brand .brand-link { + color: inherit; + text-decoration: none; + cursor: pointer; +} +.tui .brand .brand-link:hover .user, +.tui .brand .brand-link:hover .host { color: var(--fg-bright); } + +.tui nav.site { display: flex; gap: 14px; font-size: 13px; } +.tui nav.site a { + text-decoration: none; + color: var(--muted); + padding: 0 2px; +} +.tui nav.site a:hover { color: var(--fg-bright); } +.tui nav.site a.is-active { color: var(--hl); font-weight: 700; } +.tui nav.site a.is-active::before { content: "["; color: var(--accent); margin-right: 1px; font-weight: 400; } +.tui nav.site a.is-active::after { content: "]"; color: var(--accent); margin-left: 1px; font-weight: 400; } + +/* Page title — minimal, no big # */ +.tui h1.page-title { + font-size: 17px; + font-weight: 700; + color: var(--fg-bright); + margin: 0 0 4px; +} +.tui .page-sub { + color: var(--muted); + margin: 0 0 22px; + font-size: 12px; +} + +/* Section divider — `── label ──` rule */ +.tui h2.section { + font-size: 11px; + text-transform: lowercase; + letter-spacing: 0.1em; + color: var(--accent); + font-weight: 700; + margin: 32px 0 16px; + display: flex; + align-items: center; + gap: 12px; +} +.tui h2.section::before { content: "──"; color: var(--rule-bright); font-weight: 400; } +.tui h2.section::after { content: ""; flex: 1; height: 1px; background: var(--rule); } +.tui h2.section .count { color: var(--muted); font-weight: 400; font-size: 11px; letter-spacing: 0; } + +/* Key/value bio rows */ +.tui .kv { + display: grid; + grid-template-columns: 100px 1fr; + row-gap: 3px; + column-gap: 14px; + font-size: 14px; + margin: 0 0 30px; +} +.tui .kv .k { color: var(--accent); font-weight: 700; } +.tui .kv .v { color: var(--fg); } +.tui .kv .v .name { color: var(--hl); font-weight: 700; } +.tui .kv .v .at { color: var(--muted); } +.tui .kv .v .host { color: var(--green); font-weight: 700; } +.tui .kv .v a { color: var(--link); text-decoration: none; } +.tui .kv .v a:hover { color: var(--hl); text-decoration: underline; text-decoration-style: dotted; } +.tui .kv .v .sep { color: var(--muted); margin: 0 8px; } + +/* Bullets */ +.tui ul.bullets { + list-style: none; + padding: 0; + margin: 0 0 30px; + display: flex; + flex-direction: column; +} +.tui ul.bullets li { + padding: 4px 10px 4px 22px; + position: relative; + color: var(--fg); + font-size: 14px; + border-left: 2px solid transparent; +} +.tui ul.bullets li::before { + content: "·"; + position: absolute; + left: 10px; top: 4px; + color: var(--muted); + font-weight: 700; +} +.tui ul.bullets li:hover { + background: var(--panel-soft); + border-left-color: var(--accent); +} +.tui ul.bullets a { color: var(--link); text-decoration: none; } +.tui ul.bullets a:hover { color: var(--hl); text-decoration: underline; text-decoration-style: dotted; } +.tui ul.bullets strong { color: var(--fg-bright); } + +/* Quickfix-style posts list — lazygit row selection on hover */ +.tui .qf { + font-size: 13px; + font-variant-numeric: tabular-nums; + margin: 0; +} +.tui .qf .qf-row { + display: grid; + grid-template-columns: 92px 1fr auto; + gap: 14px; + padding: 9px 14px 9px 12px; + align-items: baseline; + border-left: 2px solid transparent; + cursor: pointer; +} +.tui .qf .qf-row:hover { + background: var(--panel-soft); + border-left-color: var(--accent); +} +.tui .qf .qf-row .date { color: var(--muted); font-size: 12px; } +.tui .qf .qf-row .title-cell { display: flex; flex-direction: column; gap: 1px; min-width: 0; } +.tui .qf .qf-row .title-cell a { color: var(--link); text-decoration: none; font-weight: 700; } +.tui .qf .qf-row:hover .title-cell a { color: var(--hl); } +.tui .qf .qf-row .title-cell .desc { color: var(--muted); font-size: 12px; line-height: 1.5; } +.tui .qf .qf-row .tags { font-size: 10.5px; color: var(--tag); white-space: nowrap; } +.tui .qf .qf-row .tags .tag { color: var(--tag); margin-left: 6px; } +.tui .qf .qf-row .tags .tag::before { content: "["; color: var(--rule-bright); } +.tui .qf .qf-row .tags .tag::after { content: "]"; color: var(--rule-bright); } + +/* Project rows */ +.tui .proj-list { + margin: 0; padding: 0; list-style: none; + display: flex; flex-direction: column; +} +.tui .proj-list li { + display: grid; + grid-template-columns: 56px 1fr; + gap: 14px; + padding: 9px 14px 9px 12px; + align-items: baseline; + font-size: 13px; + border-left: 2px solid transparent; +} +.tui .proj-list li:hover { + background: var(--panel-soft); + border-left-color: var(--accent); +} +.tui .proj-list .lang { color: var(--muted); font-size: 10.5px; text-align: left; } +.tui .proj-list .lang::before { content: "["; color: var(--rule-bright); } +.tui .proj-list .lang::after { content: "]"; color: var(--rule-bright); } +.tui .proj-list .body a { color: var(--link); text-decoration: none; font-weight: 700; } +.tui .proj-list li:hover .body a { color: var(--hl); } +.tui .proj-list .body .desc { color: var(--muted); font-size: 12px; line-height: 1.5; } + +/* Post article */ +.tui article.post { + font-size: 14px; + line-height: 1.72; +} +.tui article.post p { margin: 0 0 16px; } +.tui article.post h2 { + font-size: 14.5px; + color: var(--fg-bright); + margin: 26px 0 12px; + font-weight: 700; + display: flex; + align-items: center; + gap: 10px; +} +.tui article.post h2::before { content: "▌"; color: var(--accent); font-size: 14px; } +.tui article.post h3 { + font-size: 13px; + color: var(--fg-bright); + margin: 22px 0 10px; + font-weight: 700; + padding-left: 12px; + border-left: 2px solid var(--accent); + letter-spacing: 0.02em; +} +.tui article.post a { + color: var(--link); + text-decoration: none; + border-bottom: 1px solid rgba(136, 192, 208, 0.25); +} +.tui article.post a:hover { color: var(--hl); border-bottom-color: var(--hl); } +.tui article.post code { + background: var(--panel-2); + padding: 2px 6px; + color: var(--accent); + font-size: 12.5px; + border-radius: 2px; +} +.tui article.post pre { + background: var(--panel-2); + padding: 14px 16px; + margin: 16px 0; + overflow: auto; + font-size: 12.5px; + border-left: 2px solid var(--green); + color: var(--fg); + line-height: 1.6; +} +.tui article.post pre code { background: none; padding: 0; color: inherit; border-radius: 0; } +.tui article.post blockquote { + margin: 16px 0; + padding: 8px 18px; + border-left: 2px solid var(--tag); + color: var(--muted); +} + +/* Socials — icon tiles, lazygit/btop style row of compact panels */ +.tui .socials { + display: grid; + grid-template-columns: repeat(3, 1fr); + grid-auto-rows: 1fr; + align-items: stretch; + gap: 8px; + margin: 0 0 30px; +} +.tui .socials a { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 12px; + border: 1px solid var(--rule); + border-left: 2px solid var(--rule); + background: var(--bg); + text-decoration: none; + color: var(--fg); + font-size: 13px; +} +.tui .socials a:hover { + background: var(--panel-soft); + border-left-color: var(--accent); + color: var(--hl); +} +.tui .socials a .ico { + flex: 0 0 16px; + width: 16px; height: 16px; + color: var(--accent); + display: flex; + align-items: center; + justify-content: center; +} +.tui .socials a:hover .ico { color: var(--hl); } +.tui .socials a .ico svg { width: 16px; height: 16px; stroke: currentColor; fill: none; stroke-width: 1.6; stroke-linecap: round; stroke-linejoin: round; } +.tui .socials a .lbl { display: flex; flex-direction: column; line-height: 1.3; min-width: 0; overflow: hidden; } +.tui .socials a .lbl .k { color: var(--accent); font-size: 10.5px; text-transform: lowercase; letter-spacing: 0.06em; font-weight: 700; } +.tui .socials a:hover .lbl .k { color: var(--hl); } +.tui .socials a .lbl .v { color: var(--fg); font-size: 12px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +/* Post layout — TOC sits above the article as a compact box */ +.tui .post-layout { display: block; } + +.tui .toc { + margin: 0 0 22px; + padding: 12px 14px; + border: 1px solid var(--rule); + background: transparent; + font-size: 12px; + color: var(--muted); +} +.tui .toc-head { + color: var(--accent); + font-weight: 700; + font-size: 10.5px; + letter-spacing: 0.06em; + margin-bottom: 8px; + text-transform: lowercase; +} + +/* Vertical — stacked numbered list, with h3 children branching as a tree */ +.tui .toc-vertical .toc-list { + list-style: none; + padding: 0; + margin: 0; + display: flex; + flex-direction: column; + gap: 4px; +} +.tui .toc-vertical .toc-item { display: block; } +.tui .toc-vertical .toc-row { + display: flex; + align-items: baseline; + gap: 10px; + line-height: 1.5; +} +.tui .toc-vertical .num { + color: var(--accent); + font-size: 11px; + font-variant-numeric: tabular-nums; + flex-shrink: 0; +} +.tui .toc-vertical .toc-row a { + color: var(--fg); + text-decoration: none; + font-size: 12.5px; + border-bottom: 1px dotted transparent; +} +.tui .toc-vertical .toc-row:hover a { + color: var(--hl); + border-bottom-color: var(--accent); +} + +/* h3 sub-items: tree branches indented under each h2 */ +.tui .toc-vertical .toc-sub { + list-style: none; + margin: 2px 0 6px; + padding: 0 0 0 26px; + display: flex; + flex-direction: column; + gap: 1px; +} +.tui .toc-vertical .toc-subitem { + display: flex; + align-items: baseline; + line-height: 1.5; +} +.tui .toc-vertical .branch { + color: var(--rule-bright); + white-space: pre; + user-select: none; + font-size: 12px; +} +.tui .toc-vertical .toc-subitem a { + color: var(--muted); + text-decoration: none; + font-size: 12px; + border-bottom: 1px dotted transparent; +} +.tui .toc-vertical .toc-subitem:hover a { + color: var(--hl); + border-bottom-color: var(--accent); +} + +/* Mobile — tighter padding */ +.tui-mobile .tui .toc { margin-bottom: 16px; padding: 8px 10px; } +.tui-mobile .tui .toc-vertical .toc-row a { font-size: 11.5px; } +.tui-mobile .tui .toc-vertical .toc-subitem a, +.tui-mobile .tui .toc-vertical .branch { font-size: 11px; } +.tui footer.plain { + margin-top: 32px; + font-size: 11.5px; + color: var(--muted); + line-height: 1.7; + letter-spacing: 0.02em; +} +.tui footer.plain .row { display: block; } +.tui footer.plain .sep { color: var(--rule-bright); margin: 0 6px; } +.tui footer.plain .hash { color: var(--green); } +.tui footer.plain a { color: var(--muted); text-decoration: none; border-bottom: 1px dotted var(--rule-bright); } +.tui footer.plain a:hover { color: var(--fg); border-bottom-color: var(--fg); } + +@media (max-width: 560px) { + .tui footer.plain { font-size: 11px; margin-top: 24px; } +} + +/* ===== Real-viewport mobile fallback (when viewing the actual site on a phone) ===== */ +@media (max-width: 560px) { + .tui { padding: 10px 8px; font-size: 13.5px; } + .tui .frame { padding: 18px 12px 10px; border: none; } + .tui .frame::before, + .tui .frame::after { display: none; } + .tui .wrap { max-width: 100%; } + .tui header.site { + flex-direction: column; align-items: stretch; gap: 8px; + margin-bottom: 16px; padding-bottom: 8px; + } + .tui .brand { font-size: 12px; word-break: break-all; } + .tui nav.site { + gap: 14px; justify-content: flex-start; font-size: 12px; + border-top: 1px dashed var(--rule); padding-top: 8px; + } + .tui h1.page-title { font-size: 15.5px; } + .tui .page-sub { font-size: 11.5px; margin-bottom: 16px; word-wrap: break-word; } + .tui h2.section { font-size: 11px; margin: 20px 0 10px; } + .tui ul.bullets li { font-size: 12.5px; padding: 5px 4px 5px 18px; line-height: 1.55; } + .tui .socials { grid-template-columns: 1fr; gap: 0; } + .tui .socials a { + padding: 12px 10px; font-size: 12.5px; border: none; + border-bottom: 1px solid var(--rule); border-left: 2px solid transparent; + } + .tui .socials a:first-child { border-top: 1px solid var(--rule); } + .tui .qf .qf-row { grid-template-columns: 1fr; gap: 4px; padding: 10px 8px; } + .tui .qf .qf-row .tags { margin-top: 4px; white-space: normal; display: flex; flex-wrap: wrap; gap: 4px; } + .tui .qf .qf-row .tags .tag { margin-left: 0; } + .tui .proj-list li { grid-template-columns: 44px 1fr; gap: 8px; padding: 10px 8px; font-size: 12.5px; } + .tui article.post { font-size: 13px; line-height: 1.7; } + .tui article.post pre { padding: 10px; font-size: 11.5px; } + .tui footer.site { + flex-direction: column; align-items: flex-start; gap: 6px; + font-size: 10.5px; padding-top: 10px; margin-top: 18px; + } + .tui footer.site .kb { padding: 0; border-right: none; } +} diff --git a/theme.toml b/theme.toml @@ -0,0 +1,14 @@ +name = "hugo-theme-tui" +license = "MIT" +licenselink = "https://github.com/tddra/hugo-theme-tui/blob/main/LICENSE" +description = "Terminal-UI personal-site theme: box-drawn frame, lazygit-style row hovers, Nord palette, dot-matrix page markers." +homepage = "https://github.com/tddra/hugo-theme-tui" +demosite = "https://github.com/tddra/hugo-theme-tui" +min_version = "0.110.0" + +tags = ["minimal", "terminal", "tui", "blog", "personal", "nord", "dark", "monospace"] +features = ["blog", "projects", "posts", "rss", "table-of-contents", "responsive", "dark-mode"] + +[author] + name = "Fedor Vinogradov" + homepage = "https://fedorvin.com"