diff --git a/css/style.css b/css/style.css index ee6e9a1..2317b46 100644 --- a/css/style.css +++ b/css/style.css @@ -3,6 +3,38 @@ Monochrome palette with light & dark modes ======================================== */ +/* ---------------------------------------- + INTER — self-hosted (no third-party font request) + ---------------------------------------- */ +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 400; + font-display: swap; + src: url('../fonts/inter-400.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 500; + font-display: swap; + src: url('../fonts/inter-500.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 600; + font-display: swap; + src: url('../fonts/inter-600.woff2') format('woff2'); +} +@font-face { + font-family: 'Inter'; + font-style: normal; + font-weight: 700; + font-display: swap; + src: url('../fonts/inter-700.woff2') format('woff2'); +} + /* ---------------------------------------- THEME TOKENS — Light Mode (default) ---------------------------------------- */ @@ -2212,6 +2244,164 @@ button:focus:not(:focus-visible) { .toc-drawer { display: none !important; } } +/* ======================================== + SEARCH OVERLAY + ======================================== */ +.search-overlay { + position: fixed; + inset: 0; + z-index: 2500; + display: flex; + justify-content: center; + align-items: flex-start; + padding: 12vh 1rem 1rem; + background: rgba(0, 0, 0, 0.5); + opacity: 0; + visibility: hidden; + transition: opacity 0.15s ease, visibility 0s linear 0.15s; +} + +.search-overlay.is-open { + opacity: 1; + visibility: visible; + transition: opacity 0.15s ease; +} + +.search-box { + width: 100%; + max-width: 560px; + overflow: hidden; + background: var(--bg-surface); + border: 1px solid var(--border-color); + border-radius: var(--radius-md); + box-shadow: var(--shadow-lg); +} + +.search-input { + width: 100%; + padding: 1rem 1.2rem; + border: none; + border-bottom: 1px solid var(--border-color); + background: transparent; + color: var(--text-primary); + font-family: var(--font-sans); + font-size: 1.05rem; + outline: none; +} + +.search-input::placeholder { color: var(--text-muted); } + +.search-results { + list-style: none; + margin: 0; + padding: 0.4rem; + max-height: 50vh; + overflow-y: auto; +} + +.search-result a { + display: flex; + align-items: baseline; + gap: 0.6rem; + padding: 0.55rem 0.8rem; + border-radius: var(--radius-sm); + color: var(--text-primary); +} + +.search-result a:hover, +.search-result.is-active a { + background: var(--bg-hover); + text-decoration: none; +} + +.search-result-title { font-weight: 600; } +.search-result-meta { + margin-left: auto; + font-size: var(--font-size-xs); + color: var(--text-muted); + white-space: nowrap; +} + +.search-empty { padding: 1rem 1.2rem; color: var(--text-muted); } + +/* ======================================== + SERIES BOX + ======================================== */ +.series-box { + margin: 0 0 var(--space-lg); + padding: var(--space-sm) var(--space-md); + border: 1px solid var(--border-color); + border-left: 4px solid var(--accent); + border-radius: var(--radius-sm); + background: var(--bg-subtle); +} + +.series-box-title { + display: flex; + align-items: center; + gap: 0.45rem; + margin-bottom: 0.4rem; + font-weight: 700; + color: var(--text-primary); +} + +.series-box-title .bd-icon { color: var(--accent); flex: 0 0 auto; } +.series-list { margin: 0; padding-left: 1.5rem; } +.series-item { padding: 0.12rem 0; color: var(--text-secondary); } +.series-item a { color: var(--text-secondary); } +.series-item a:hover { color: var(--accent); } +.series-item.is-current span { color: var(--accent); font-weight: 600; } + +/* ======================================== + CONTENT EXTRAS (kbd, mark, task lists, footnotes, external links) + ======================================== */ +.content kbd { + display: inline-block; + padding: 0.1em 0.45em; + font-family: var(--font-sans); + font-size: 0.8em; + line-height: 1.4; + color: var(--text-primary); + background: var(--bg-subtle); + border: 1px solid var(--border-color); + border-bottom-width: 2px; + border-radius: 5px; + white-space: nowrap; +} + +.content mark { + padding: 0.05em 0.2em; + border-radius: 3px; + background: rgba(245, 200, 66, 0.35); + color: inherit; +} + +/* Small ↗ after outbound links (JS adds target=_blank to external links) */ +.content a[target="_blank"]::after { + content: "↗"; + margin-left: 0.15em; + font-size: 0.8em; + color: var(--text-muted); +} + +/* Task lists (- [ ] / - [x]) — rendered by JS into real checkboxes */ +.content ul.task-list { list-style: none; padding-left: 0.2rem; } +.content .task-list-item { display: flex; align-items: flex-start; gap: 0.5rem; } +.content .task-list-item input[type="checkbox"] { margin-top: 0.35em; flex: 0 0 auto; } + +/* Footnotes (when the parser emits them) */ +.content .footnotes { + margin-top: var(--space-lg); + padding-top: var(--space-md); + border-top: 1px solid var(--border-subtle); + font-size: var(--font-size-sm); + color: var(--text-secondary); +} + +/* Definition lists */ +.content dt { font-weight: 700; } +.content dd { margin: 0 0 0.6rem; color: var(--text-secondary); } + /* ======================================== RESPONSIVE ======================================== */ diff --git a/fonts/inter-400.woff2 b/fonts/inter-400.woff2 new file mode 100644 index 0000000..f15b025 Binary files /dev/null and b/fonts/inter-400.woff2 differ diff --git a/fonts/inter-500.woff2 b/fonts/inter-500.woff2 new file mode 100644 index 0000000..54f0a59 Binary files /dev/null and b/fonts/inter-500.woff2 differ diff --git a/fonts/inter-600.woff2 b/fonts/inter-600.woff2 new file mode 100644 index 0000000..d189794 Binary files /dev/null and b/fonts/inter-600.woff2 differ diff --git a/fonts/inter-700.woff2 b/fonts/inter-700.woff2 new file mode 100644 index 0000000..a68fb10 Binary files /dev/null and b/fonts/inter-700.woff2 differ diff --git a/index.php b/index.php index 9eae6aa..84f6ea6 100644 --- a/index.php +++ b/index.php @@ -191,6 +191,7 @@ // Theme directory URL, so the deferred bundle can load self-hosted assets // (highlight.js + its theme CSS) relative to the theme. window.BLOWDIT_THEME_URL = ; + window.BLOWDIT_HOME = ; // Localised UI strings for the deferred bundle. $L->get() falls back to // the key itself when a translation is missing, so this is safe in any language. window.BLOWDIT_I18N = { @@ -203,7 +204,11 @@ goToSlide: get('Go to slide')); ?>, prevImage: get('Previous image')); ?>, nextImage: get('Next image')); ?>, - imageViewer: get('Image viewer')); ?> + imageViewer: get('Image viewer')); ?>, + onThisPage: get('On this page')); ?>, + search: get('Search')); ?>, + searchPlaceholder: get('Search posts…')); ?>, + noResults: get('No results')); ?> }; ' + + '' + + '' + + '' + + ''; + document.body.appendChild(overlay); + + var input = overlay.querySelector('.search-input'); + var results = overlay.querySelector('.search-results'); + var empty = overlay.querySelector('.search-empty'); + var active = -1; + var current = []; + + function open() { + overlay.classList.add('is-open'); + document.body.style.overflow = 'hidden'; + input.value = ''; + render([]); + setTimeout(function () { input.focus(); }, 30); + } + function close() { + overlay.classList.remove('is-open'); + document.body.style.overflow = ''; + } + function isOpen() { return overlay.classList.contains('is-open'); } + + function score(item, q) { + var hay = (item.t + ' ' + (item.c || '') + ' ' + (item.g || []).join(' ') + ' ' + (item.d || '')).toLowerCase(); + return hay.indexOf(q) !== -1; + } + + function render(list) { + current = list; + active = list.length ? 0 : -1; + results.innerHTML = ''; + empty.hidden = !(input.value.trim() && list.length === 0); + list.forEach(function (item, i) { + var li = document.createElement('li'); + li.className = 'search-result' + (i === 0 ? ' is-active' : ''); + li.innerHTML = '' + + (item.c ? '' : '') + ''; + li.querySelector('.search-result-title').textContent = item.t; + if (item.c) li.querySelector('.search-result-meta').textContent = item.c; + results.appendChild(li); + }); + } + + function update() { + var q = input.value.trim().toLowerCase(); + if (!q) { render([]); return; } + render(data.filter(function (item) { return score(item, q); }).slice(0, 8)); + } + + function move(delta) { + var items = results.querySelectorAll('.search-result'); + if (!items.length) return; + items[active] && items[active].classList.remove('is-active'); + active = (active + delta + items.length) % items.length; + items[active].classList.add('is-active'); + items[active].scrollIntoView({ block: 'nearest' }); + } + + function go() { + var items = results.querySelectorAll('.search-result'); + if (items[active]) { var a = items[active].querySelector('a'); if (a) window.location.href = a.href; } + } + + input.addEventListener('input', update); + input.addEventListener('keydown', function (e) { + if (e.key === 'ArrowDown') { e.preventDefault(); move(1); } + else if (e.key === 'ArrowUp') { e.preventDefault(); move(-1); } + else if (e.key === 'Enter') { e.preventDefault(); go(); } + }); + overlay.addEventListener('click', function (e) { if (e.target === overlay) close(); }); + + Array.prototype.forEach.call(document.querySelectorAll('.js-search-open'), function (b) { + b.addEventListener('click', function (e) { e.preventDefault(); open(); }); + }); + + // Expose for the keyboard-shortcuts handler. + window.BLOWDIT_SEARCH = { open: open, close: close, isOpen: isOpen }; + })(); + + /* -------------------------------------------------------- + Keyboard shortcuts: / search · g h home · t top · Esc close. + -------------------------------------------------------- */ + (function () { + var lastG = 0; + function typing(t) { + return t && (t.tagName === 'INPUT' || t.tagName === 'TEXTAREA' || t.isContentEditable); + } + document.addEventListener('keydown', function (e) { + var s = window.BLOWDIT_SEARCH; + if (e.key === 'Escape' || e.key === 'Esc') { + if (s && s.isOpen()) s.close(); + return; + } + if (typing(e.target) || e.metaKey || e.ctrlKey || e.altKey) return; + + if (e.key === '/') { e.preventDefault(); if (s) s.open(); return; } + if (e.key === 't' || e.key === 'T') { + var reduce = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches; + window.scrollTo({ top: 0, behavior: reduce ? 'auto' : 'smooth' }); + return; + } + // "g" then "h" -> home + if (e.key === 'g' || e.key === 'G') { lastG = Date.now(); return; } + if ((e.key === 'h' || e.key === 'H') && Date.now() - lastG < 600) { + window.location.href = (window.BLOWDIT_HOME || '/'); + } + }); + })(); + })(); diff --git a/languages/en.json b/languages/en.json index 845050e..513ec11 100644 --- a/languages/en.json +++ b/languages/en.json @@ -18,6 +18,10 @@ "Copy link": "Copy link", "Link copied": "Link copied", "Related posts": "Related posts", + "Search": "Search", + "Search posts…": "Search posts…", + "No results": "No results", + "Part of a series": "Part of a series", "Home": "Home", "On this page": "On this page", "Page not found": "Page not found", diff --git a/metadata.json b/metadata.json index f1c864d..c637f43 100644 --- a/metadata.json +++ b/metadata.json @@ -2,9 +2,9 @@ "author": "spleenftw", "email": "", "website": "https://github.com/Spleenftw/blowdit", - "version": "1.5.0", - "releaseDate": "2026-06-02", + "version": "1.6.0", + "releaseDate": "2026-06-03", "license": "MIT", "compatible": "3.22.0", - "notes": "A Bludit theme inspired by the Blowfish Hugo theme. Five-theme swatch picker (Light/Dark/Nord/Dracula/Catppuccin), sticky ToC sidebar on articles, image lightbox with zoom, image carousel (```carousel fence), tabbed code blocks (```tabs fence), smart sidebar per page type, and clean responsive typography. v1.3.0: SEO (Open Graph, Twitter Card, JSON-LD, theme-color, cover preload), per-theme syntax highlighting (self-hosted highlight.js), copy-to-clipboard on code blocks, numbered pagination, inline-SVG icons (dropped the Bootstrap Icons webfont), removed jQuery/Bootstrap JS, deferred cached JS bundle, reading progress bar, back-to-top, randomised sidebar selection, lazy-loaded images, accessibility (skip link, ARIA, :focus-visible) and a print stylesheet. v1.4.0: canonical URL, WebSite + BreadcrumbList + author sameAs JSON-LD, og:image:alt / twitter:site+creator, cover-image aspect-ratio (CLS), heading anchor links, prev/next article nav, related posts, share buttons (X/LinkedIn/copy-link), mailto social icon, PWA web app manifest, 'updated on' date, and outbound-link hardening (noopener noreferrer). v1.5.0: callout/admonition boxes (> [!NOTE]/[!TIP]/[!WARNING]/...), code-block header bar with language label + copy button, visual breadcrumbs, custom 404 page, image figure/figcaption from the markdown title, a mobile table-of-contents drawer, and third-party resource hints." + "notes": "A Bludit theme inspired by the Blowfish Hugo theme. Five-theme swatch picker (Light/Dark/Nord/Dracula/Catppuccin), sticky ToC sidebar on articles, image lightbox with zoom, image carousel (```carousel fence), tabbed code blocks (```tabs fence), smart sidebar per page type, and clean responsive typography. v1.3.0: SEO (Open Graph, Twitter Card, JSON-LD, theme-color, cover preload), per-theme syntax highlighting (self-hosted highlight.js), copy-to-clipboard on code blocks, numbered pagination, inline-SVG icons (dropped the Bootstrap Icons webfont), removed jQuery/Bootstrap JS, deferred cached JS bundle, reading progress bar, back-to-top, randomised sidebar selection, lazy-loaded images, accessibility (skip link, ARIA, :focus-visible) and a print stylesheet. v1.4.0: canonical URL, WebSite + BreadcrumbList + author sameAs JSON-LD, og:image:alt / twitter:site+creator, cover-image aspect-ratio (CLS), heading anchor links, prev/next article nav, related posts, share buttons (X/LinkedIn/copy-link), mailto social icon, PWA web app manifest, 'updated on' date, and outbound-link hardening (noopener noreferrer). v1.5.0: callout/admonition boxes (> [!NOTE]/[!TIP]/[!WARNING]/...), code-block header bar with language label + copy button, visual breadcrumbs, custom 404 page, image figure/figcaption from the markdown title, a mobile table-of-contents drawer, and third-party resource hints. v1.6.0: self-hosted Inter (no Google Fonts), client-side search overlay (\"/\" key + navbar button), keyboard shortcuts (/ , g h, t, Esc), a tabbed-code copy button, series navigation (series-* tag), and content typography (keycaps, task-list checkboxes, , external-link icon, footnote/definition-list styling)." } diff --git a/php/footer.php b/php/footer.php index 6f9a9b8..07be646 100644 --- a/php/footer.php +++ b/php/footer.php @@ -18,3 +18,28 @@ + +getList(1, 1000, true) as $bdSk) { + $bdSp = new Page($bdSk); + if ($bdSp->isStatic()) { continue; } // posts only + $bdSearchIndex[] = array( + 't' => blowdit_plain($bdSp->title()), + 'u' => $bdSp->permalink(), + 'd' => blowdit_plain($bdSp->description()), + 'c' => blowdit_plain($bdSp->category()), + 'g' => array_values(array_map('blowdit_plain', $bdSp->tags(true))), + ); + } + } + echo ''; + } catch (\Throwable $e) {} +?> diff --git a/php/head.php b/php/head.php index cf4ded3..9a4adfa 100644 --- a/php/head.php +++ b/php/head.php @@ -244,12 +244,9 @@ - - - - - + + diff --git a/php/icons.php b/php/icons.php index cbbb55e..f18cbb3 100644 --- a/php/icons.php +++ b/php/icons.php @@ -21,6 +21,7 @@ if (!function_exists('blowdit_icon')) { 'chevron-right' => '', 'house' => '', 'link' => '', + 'search' => '', 'share' => '', // --- Theme picker (mirror in blowdit.js) --- 'sun' => '', diff --git a/php/navbar.php b/php/navbar.php index f2717ca..5c1c315 100644 --- a/php/navbar.php +++ b/php/navbar.php @@ -59,6 +59,13 @@ + + +