diff options
| author | Danilo M. <danix@danix.xyz> | 2026-05-11 10:17:53 +0200 |
|---|---|---|
| committer | Danilo M. <danix@danix.xyz> | 2026-05-11 10:17:53 +0200 |
| commit | 884df671a1bd744d3bc004cd8a6f2b5838d24b7b (patch) | |
| tree | 729a073e24f69e234aa514927c8a76d3af2c8df5 | |
| download | pkgs-html-structure-884df671a1bd744d3bc004cd8a6f2b5838d24b7b.tar.gz pkgs-html-structure-884df671a1bd744d3bc004cd8a6f2b5838d24b7b.zip | |
feat: initial commit — Apache autoindex theme with matrix rain
- gen_web_hook.sh: generates _header.html/_footer.html per repo/category/package dir
- .assets/matrix-rain.js: danix2-engine matrix rain, header-scoped (65% width, right-aligned)
- Category headers include gradient accent pill with category name
- htaccess: autoindex config, MIME types, cache headers, IndexIgnore .assets
- vhost.conf: Apache VirtualHost template (values masked for public repo)
- CLAUDE.md: repo architecture docs
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| -rw-r--r-- | .assets/matrix-rain.js | 136 | ||||
| -rw-r--r-- | CLAUDE.md | 49 | ||||
| -rw-r--r-- | docs/superpowers/plans/2026-05-11-matrix-rain.md | 373 | ||||
| -rw-r--r-- | docs/superpowers/specs/2026-05-11-matrix-rain-design.md | 71 | ||||
| -rw-r--r-- | gen_web_hook.sh | 342 | ||||
| -rw-r--r-- | htaccess | 43 | ||||
| -rw-r--r-- | vhost.conf | 33 |
7 files changed, 1047 insertions, 0 deletions
diff --git a/.assets/matrix-rain.js b/.assets/matrix-rain.js new file mode 100644 index 0000000..e22ada2 --- /dev/null +++ b/.assets/matrix-rain.js @@ -0,0 +1,136 @@ +// Matrix rain background effect — header-scoped build for packages.danix.xyz +// Derived from danix2-hugo-theme/assets/js/matrix-rain.js +(function() { + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + + var header = document.querySelector('.site-header'); + if (!header) return; + + // Canvas + var canvas = document.createElement('canvas'); + canvas.id = 'matrix-rain'; + canvas.style.cssText = 'position:absolute;top:0;right:0;height:100%;pointer-events:none;z-index:0;'; + header.appendChild(canvas); + + // Gradient fade div — covers left portion of canvas area, left→transparent + var fade = document.createElement('div'); + fade.style.cssText = 'position:absolute;top:0;right:0;bottom:0;width:65%;background:linear-gradient(to right,var(--bg-card) 0%,transparent 60%);pointer-events:none;z-index:1;'; + header.appendChild(fade); + + var ctx = canvas.getContext('2d'); + + var columns = []; + var frameCount = 0; + var colors = { accent: '#5c9cf5', accent2: '#4ec97b', bg: '#161b25', head: '#ffffff' }; + + var ASCII = Array.from({ length: 94 }, function(_, i) { return String.fromCharCode(33 + i); }); + var KATA = Array.from({ length: 96 }, function(_, i) { return String.fromCodePoint(0x30a0 + i); }); + var CHARS = shuffle([].concat(ASCII, KATA, KATA, KATA)); + + function shuffle(arr) { + for (var i = arr.length - 1; i > 0; i--) { + var j = Math.floor(Math.random() * (i + 1)); + var tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; + } + return arr; + } + + function hexToRgba(color, alpha) { + var rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); + if (rgbMatch) return 'rgba(' + rgbMatch[1] + ',' + rgbMatch[2] + ',' + rgbMatch[3] + ',' + alpha + ')'; + var hex = color.replace('#', ''); + var r = parseInt(hex.substring(0, 2), 16); + var g = parseInt(hex.substring(2, 4), 16); + var b = parseInt(hex.substring(4, 6), 16); + return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')'; + } + + function sampleColors() { + var style = getComputedStyle(document.documentElement); + var isDark = !document.documentElement.classList.contains('theme-light'); + colors.accent = style.getPropertyValue('--accent').trim() || '#5c9cf5'; + colors.accent2 = style.getPropertyValue('--accent2').trim() || '#4ec97b'; + colors.bg = style.getPropertyValue('--bg-card').trim() || '#161b25'; + colors.head = isDark ? '#ffffff' : '#1a0533'; + } + + function resizeCanvas() { + canvas.width = Math.floor(header.offsetWidth * 0.65); + canvas.height = header.offsetHeight; + ctx.font = '14px "IBM Plex Mono", monospace'; + ctx.textBaseline = 'top'; + initColumns(); + } + + function initColumns() { + columns = []; + var columnWidth = 14; + var columnCount = Math.floor(canvas.width / columnWidth); + for (var i = 0; i < columnCount; i++) { + columns.push({ + x: i * columnWidth, + y: -Math.floor(Math.random() * 40), + speed: 2 + Math.floor(Math.random() * 3), + color: Math.random() < 0.6 ? 'accent2' : 'accent', + charIndex: Math.floor(Math.random() * CHARS.length), + length: 8 + Math.floor(Math.random() * 13) + }); + } + } + + function setupThemeObserver() { + var observer = new MutationObserver(function(mutations) { + for (var i = 0; i < mutations.length; i++) { + if (mutations[i].attributeName === 'class') { sampleColors(); break; } + } + }); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); + } + + function drawFrame() { + frameCount++; + ctx.fillStyle = hexToRgba(colors.bg, 0.085); + ctx.fillRect(0, 0, canvas.width, canvas.height); + + for (var ci = 0; ci < columns.length; ci++) { + var col = columns[ci]; + if (frameCount % col.speed !== 0) continue; + + ctx.fillStyle = colors[col.color]; + for (var i = 1; i <= col.length; i++) { + var trailY = (col.y - i) * 14; + if (trailY < 0) continue; + var trailCharIndex = (col.charIndex - i + CHARS.length) % CHARS.length; + ctx.fillText(CHARS[trailCharIndex], col.x, trailY); + } + + ctx.fillStyle = colors.head; + ctx.fillText(CHARS[col.charIndex % CHARS.length], col.x, col.y * 14); + + col.y++; + col.charIndex = (col.charIndex + 1) % CHARS.length; + + if (col.y * 14 > canvas.height + col.length * 14) { + col.y = -Math.floor(Math.random() * 20); + col.charIndex = Math.floor(Math.random() * CHARS.length); + col.color = Math.random() < 0.6 ? 'accent2' : 'accent'; + } + } + + requestAnimationFrame(drawFrame); + } + + sampleColors(); + resizeCanvas(); + setupThemeObserver(); + + var resizeTimer; + window.addEventListener('resize', function() { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(resizeCanvas, 150); + }); + + document.fonts.ready.then(function() { + requestAnimationFrame(drawFrame); + }); +})(); diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..2e61988 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,49 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this repo is + +Web frontend for `packages.danix.xyz` — an Apache autoindex-based Slackware package repository. Apache serves directory listings; this repo provides the HTML chrome injected around them. + +## How header/footer injection works + +Apache's `HeaderName` / `ReadmeName` directives (in `.htaccess`) point to `_header.html` and `_footer.html`. These static files are generated by `gen_web_hook.sh`, which walks `$PKGREPO/{category}/{package}/` and writes a pair at each level. Run standalone or as a slackrepo `HOOK_FINISH` hook. + +## Repository directory structure (on server) + +``` +/var/www/pkgs/ ← REPO_ROOT (depth 0) + category/ ← depth 1 + pkgname/ ← depth 2 + pkgname-ver.txz + pkgname-ver.txt ← parsed for title/description/Homepage + pkgname-ver.meta ← parsed for compressed/uncompressed sizes +``` + +## CSS design system + +CSS variables defined on `:root`: + +- Colors: `--bg`, `--bg-card`, `--bg-hover`, `--border`, `--accent`, `--accent-dim`, `--accent2`, `--green`, `--text`, `--text-dim`, `--text-head` +- Fonts: `--mono` (IBM Plex Mono), `--sans` (IBM Plex Sans) — loaded from Google Fonts + +## Static assets + +`.assets/matrix-rain.js` — matrix rain canvas animation. Served from `$PKGREPO/.assets/`. Loaded via `<script defer>` in every `_header.html`. Hidden from Apache autoindex via `IndexIgnore .assets` in `.htaccess`. When updating, copy the adapted file to `$PKGREPO/.assets/matrix-rain.js` on the server — it is not regenerated by `gen_web_hook.sh`. + +## Apache configuration + +- `vhost.conf`: VirtualHost for `packages.danix.xyz:443`, DocumentRoot `/var/www/pkgs`, delegates all index config to `.htaccess` +- `htaccess`: `IndexOptions HTMLTable SuppressHTMLPreamble ...` — `SuppressHTMLPreamble` is critical; it tells Apache not to emit its own `<html>` so our header file controls the full document + +## Running the hook + +```bash +# Standalone against a local repo path +bash gen_web_hook.sh /path/to/pkgrepo + +# Via slackrepo (picks up SR_PKGREPO automatically) +HOOK_FINISH=/path/to/gen_web_hook.sh slackrepo build pkgname +``` + diff --git a/docs/superpowers/plans/2026-05-11-matrix-rain.md b/docs/superpowers/plans/2026-05-11-matrix-rain.md new file mode 100644 index 0000000..e7b38e1 --- /dev/null +++ b/docs/superpowers/plans/2026-05-11-matrix-rain.md @@ -0,0 +1,373 @@ +# Matrix Rain Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add matrix rain canvas animation to `packages.danix.xyz` headers, using the `danix2-hugo-theme` engine adapted for the header-scoped layout. + +**Architecture:** A single JS file at `.assets/matrix-rain.js` is served from the repo root and loaded via `<script defer>` in every generated `_header.html`. The script appends a canvas + gradient fade div to `.site-header` at runtime. CSS vars `--accent`, `--accent2`, `--bg` drive colors — no hardcoded values. + +**Tech Stack:** Vanilla JS (ES5-compatible IIFE), HTML Canvas API, Apache `.htaccess`, bash (`gen_web_hook.sh`) + +--- + +### Task 1: Add `--accent2` CSS variable and `position:relative` to `.site-header` + +**Files:** +- Modify: `gen_web_hook.sh` (CSS heredoc and `.site-header` rule) + +- [ ] **Step 1: Add `--accent2` to the `:root` block in the `CSS=` heredoc** + +In `gen_web_hook.sh`, find the `:root {` block inside the `CSS='<style>` heredoc (around line 37). Add `--accent2` after `--green`: + +```bash +# Before: + --green: #4ec97b; + +# After: + --green: #4ec97b; + --accent2: #4ec97b; +``` + +- [ ] **Step 2: Add `position: relative` to `.site-header`** + +In the same `CSS=` heredoc, find the `.site-header {` rule (around line 56). Add `position: relative;`: + +```css +.site-header { border-bottom: 1px solid var(--border); padding: 2rem 2.5rem 1.5rem; background: var(--bg-card); position: relative; overflow: hidden; } +``` + +(`overflow: hidden` clips the canvas to the header bounds.) + +- [ ] **Step 3: Verify the heredoc is syntactically intact** + +```bash +bash -n gen_web_hook.sh +``` + +Expected: no output (no syntax errors). + +- [ ] **Step 4: Commit** + +```bash +git add gen_web_hook.sh +git commit -m "style: add --accent2 CSS var and position:relative to .site-header" +``` + +--- + +### Task 2: Create `.assets/matrix-rain.js` + +**Files:** +- Create: `.assets/matrix-rain.js` + +This is the danix2 engine with canvas targeting changed from `#matrix-rain` (full-screen) to a dynamically created canvas appended to `.site-header` (65% width, right-aligned, with gradient fade div). + +- [ ] **Step 1: Create the `.assets/` directory and write the file** + +```bash +mkdir -p .assets +``` + +Write `.assets/matrix-rain.js` with the following content (danix2 engine, header-scoped): + +```javascript +// Matrix rain background effect — header-scoped build for packages.danix.xyz +// Derived from danix2-hugo-theme/assets/js/matrix-rain.js +(function() { + if (window.matchMedia('(prefers-reduced-motion: reduce)').matches) return; + + var header = document.querySelector('.site-header'); + if (!header) return; + + // Canvas + var canvas = document.createElement('canvas'); + canvas.id = 'matrix-rain'; + canvas.style.cssText = 'position:absolute;top:0;right:0;height:100%;pointer-events:none;z-index:0;'; + header.appendChild(canvas); + + // Gradient fade div — covers left 75% of canvas area, left→transparent + var fade = document.createElement('div'); + fade.style.cssText = 'position:absolute;top:0;right:0;bottom:0;width:' + Math.floor(0.65 * 100) + '%;background:linear-gradient(to right,var(--bg-card) 0%,transparent 60%);pointer-events:none;z-index:1;'; + header.appendChild(fade); + + var ctx = canvas.getContext('2d'); + + var columns = []; + var frameCount = 0; + var colors = { accent: '#5c9cf5', accent2: '#4ec97b', bg: '#161b25', head: '#ffffff' }; + + var ASCII = Array.from({ length: 94 }, function(_, i) { return String.fromCharCode(33 + i); }); + var KATA = Array.from({ length: 96 }, function(_, i) { return String.fromCodePoint(0x30a0 + i); }); + var CHARS = shuffle([].concat(ASCII, KATA, KATA, KATA)); + + function shuffle(arr) { + for (var i = arr.length - 1; i > 0; i--) { + var j = Math.floor(Math.random() * (i + 1)); + var tmp = arr[i]; arr[i] = arr[j]; arr[j] = tmp; + } + return arr; + } + + function hexToRgba(color, alpha) { + var rgbMatch = color.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/); + if (rgbMatch) return 'rgba(' + rgbMatch[1] + ',' + rgbMatch[2] + ',' + rgbMatch[3] + ',' + alpha + ')'; + var hex = color.replace('#', ''); + var r = parseInt(hex.substring(0, 2), 16); + var g = parseInt(hex.substring(2, 4), 16); + var b = parseInt(hex.substring(4, 6), 16); + return 'rgba(' + r + ',' + g + ',' + b + ',' + alpha + ')'; + } + + function sampleColors() { + var style = getComputedStyle(document.documentElement); + var isDark = !document.documentElement.classList.contains('theme-light'); + colors.accent = style.getPropertyValue('--accent').trim() || '#5c9cf5'; + colors.accent2 = style.getPropertyValue('--accent2').trim() || '#4ec97b'; + colors.bg = style.getPropertyValue('--bg-card').trim() || '#161b25'; + colors.head = isDark ? '#ffffff' : '#1a0533'; + } + + function resizeCanvas() { + canvas.width = Math.floor(header.offsetWidth * 0.65); + canvas.height = header.offsetHeight; + ctx.font = '14px "IBM Plex Mono", monospace'; + ctx.textBaseline = 'top'; + initColumns(); + } + + function initColumns() { + columns = []; + var columnWidth = 14; + var columnCount = Math.floor(canvas.width / columnWidth); + for (var i = 0; i < columnCount; i++) { + columns.push({ + x: i * columnWidth, + y: -Math.floor(Math.random() * 40), + speed: 2 + Math.floor(Math.random() * 3), + color: Math.random() < 0.6 ? 'accent2' : 'accent', + charIndex: Math.floor(Math.random() * CHARS.length), + length: 8 + Math.floor(Math.random() * 13) + }); + } + } + + function setupThemeObserver() { + var observer = new MutationObserver(function(mutations) { + for (var i = 0; i < mutations.length; i++) { + if (mutations[i].attributeName === 'class') { sampleColors(); break; } + } + }); + observer.observe(document.documentElement, { attributes: true, attributeFilter: ['class'] }); + } + + function drawFrame() { + frameCount++; + ctx.fillStyle = hexToRgba(colors.bg, 0.085); + ctx.fillRect(0, 0, canvas.width, canvas.height); + + for (var ci = 0; ci < columns.length; ci++) { + var col = columns[ci]; + if (frameCount % col.speed !== 0) continue; + + ctx.fillStyle = colors[col.color]; + for (var i = 1; i <= col.length; i++) { + var trailY = (col.y - i) * 14; + if (trailY < 0) continue; + var trailCharIndex = (col.charIndex - i + CHARS.length) % CHARS.length; + ctx.fillText(CHARS[trailCharIndex], col.x, trailY); + } + + ctx.fillStyle = colors.head; + ctx.fillText(CHARS[col.charIndex % CHARS.length], col.x, col.y * 14); + + col.y++; + col.charIndex = (col.charIndex + 1) % CHARS.length; + + if (col.y * 14 > canvas.height + col.length * 14) { + col.y = -Math.floor(Math.random() * 20); + col.charIndex = Math.floor(Math.random() * CHARS.length); + col.color = Math.random() < 0.6 ? 'accent2' : 'accent'; + } + } + + requestAnimationFrame(drawFrame); + } + + sampleColors(); + resizeCanvas(); + setupThemeObserver(); + + var resizeTimer; + window.addEventListener('resize', function() { + clearTimeout(resizeTimer); + resizeTimer = setTimeout(resizeCanvas, 150); + }); + + document.fonts.ready.then(function() { + requestAnimationFrame(drawFrame); + }); +})(); +``` + +- [ ] **Step 2: Commit** + +```bash +git add .assets/matrix-rain.js +git commit -m "feat: add matrix-rain.js (danix2 engine, header-scoped)" +``` + +--- + +### Task 3: Wire `<script>` tag into `write_header()` in `gen_web_hook.sh` + +**Files:** +- Modify: `gen_web_hook.sh` (`write_header()` function) + +- [ ] **Step 1: Add the `<script>` tag to `write_header()`** + +In `gen_web_hook.sh`, find `write_header()` (around line 241). Inside the `cat > "$dir/_header.html" << EOF` block, add the script tag in `<head>`, after the Google Fonts `<link>` and before `${CSS}`: + +```bash + <script src="/.assets/matrix-rain.js" defer></script> + ${CSS} +``` + +Full `<head>` block after change: + +```bash +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>danix Slackware Repository</title> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@300;400;600&display=swap" rel="stylesheet"> + <script src="/.assets/matrix-rain.js" defer></script> + ${CSS} +</head> +``` + +- [ ] **Step 2: Verify syntax** + +```bash +bash -n gen_web_hook.sh +``` + +Expected: no output. + +- [ ] **Step 3: Commit** + +```bash +git add gen_web_hook.sh +git commit -m "feat: load matrix-rain.js in generated _header.html" +``` + +--- + +### Task 4: Hide `.assets/` from Apache directory listing + +**Files:** +- Modify: `htaccess` + +- [ ] **Step 1: Add `IndexIgnore .assets` to `htaccess`** + +In `htaccess`, find the `IndexIgnore` line (around line 15). Append `.assets` to it: + +```apache +IndexIgnore _header.html _footer.html .htaccess .htpasswd \ + CHECKSUMS.md5 CHECKSUMS.md5.asc CHECKSUMS.md5.gz CHECKSUMS.md5.gz.asc \ + FILELIST.TXT MANIFEST.bz2 \ + PACKAGES.TXT PACKAGES.TXT.gz \ + ChangeLog.txt.gz GPG-KEY \ + .assets +``` + +- [ ] **Step 2: Commit** + +```bash +git add htaccess +git commit -m "chore: hide .assets dir from Apache autoindex" +``` + +--- + +### Task 5: Update CLAUDE.md + +**Files:** +- Modify: `CLAUDE.md` + +- [ ] **Step 1: Add `.assets/` and `--accent2` notes to CLAUDE.md** + +In the **CSS design system** section, update the CSS variables list to include `--accent2`: + +```markdown +- Colors: `--bg`, `--bg-card`, `--bg-hover`, `--border`, `--accent`, `--accent-dim`, `--accent2`, `--green`, `--text`, `--text-dim`, `--text-head` +``` + +Add a new **Static assets** section after the CSS section: + +```markdown +## Static assets + +`.assets/matrix-rain.js` — matrix rain canvas animation. Served from `$PKGREPO/.assets/`. Loaded via `<script defer>` in every `_header.html`. Hidden from Apache autoindex via `IndexIgnore .assets` in `.htaccess`. When updating, copy the adapted file to `$PKGREPO/.assets/matrix-rain.js` on the server — it is not regenerated by `gen_web_hook.sh`. +``` + +- [ ] **Step 2: Commit** + +```bash +git add CLAUDE.md +git commit -m "docs: document .assets/ dir and --accent2 var in CLAUDE.md" +``` + +--- + +### Task 6: Smoke-test the generated output + +**Files:** none (verification only) + +- [ ] **Step 1: Run `gen_web_hook.sh` against a test directory** + +```bash +mkdir -p /tmp/test-pkgrepo/audio/ffmpeg +echo "ffmpeg: FFmpeg multimedia framework" > /tmp/test-pkgrepo/audio/ffmpeg/ffmpeg-6.1-x86_64-1.txt +bash gen_web_hook.sh /tmp/test-pkgrepo +``` + +Expected output: +``` +gen_web_hook: Generating static web files in /tmp/test-pkgrepo ... +gen_web_hook: Written: /tmp/test-pkgrepo/_header.html +gen_web_hook: Written: /tmp/test-pkgrepo/_footer.html +gen_web_hook: Written: /tmp/test-pkgrepo/audio/_header.html +gen_web_hook: Written: /tmp/test-pkgrepo/audio/_footer.html +gen_web_hook: Written: /tmp/test-pkgrepo/audio/ffmpeg/_header.html +gen_web_hook: Written: /tmp/test-pkgrepo/audio/ffmpeg/_footer.html +gen_web_hook: Done. +``` + +- [ ] **Step 2: Verify `_header.html` contains expected elements** + +```bash +grep -c 'matrix-rain.js' /tmp/test-pkgrepo/_header.html +grep -c '\-\-accent2' /tmp/test-pkgrepo/_header.html +grep -c 'position: relative' /tmp/test-pkgrepo/_header.html +grep -c 'overflow: hidden' /tmp/test-pkgrepo/_header.html +``` + +Expected: each command prints `1`. + +- [ ] **Step 3: Validate HTML structure** + +```bash +grep -A2 'matrix-rain' /tmp/test-pkgrepo/_header.html +``` + +Expected output contains: +```html +<script src="/.assets/matrix-rain.js" defer></script> +``` + +- [ ] **Step 4: Clean up** + +```bash +rm -rf /tmp/test-pkgrepo +``` diff --git a/docs/superpowers/specs/2026-05-11-matrix-rain-design.md b/docs/superpowers/specs/2026-05-11-matrix-rain-design.md new file mode 100644 index 0000000..17d8401 --- /dev/null +++ b/docs/superpowers/specs/2026-05-11-matrix-rain-design.md @@ -0,0 +1,71 @@ +# Matrix Rain — Design Spec +**Date:** 2026-05-11 +**Repo:** repo-html-structure (`packages.danix.xyz`) + +## Goal + +Add the matrix rain canvas animation to the `_header.html` of the Slackware package repository, matching the implementation in `danix2-hugo-theme` as closely as possible for brand consistency. + +## Architecture + +### New file: `.assets/matrix-rain.js` + +Served from `$PKGREPO/.assets/matrix-rain.js` (i.e. `https://packages.danix.xyz/.assets/matrix-rain.js`). + +Source: `danix2-hugo-theme/assets/js/matrix-rain.js` with minimal adaptations (see below). No logic changes — rendering engine, trail model, CSS var integration, font-ready wait, debounced resize, MutationObserver for theme switching all carried over verbatim. + +**Adaptations from danix2 version:** + +| danix2 | This repo | +|--------|-----------| +| Canvas = `#matrix-rain`, full window width/height | Canvas appended to `.site-header`, width = 65% of header width, height = header height | +| No fade overlay | Gradient fade `<div>` over left 75% of canvas (matches cgit approach) | +| `canvas.width = window.innerWidth` | `canvas.width = Math.floor(header.offsetWidth * 0.65)` | +| `canvas.height = window.innerHeight` | `canvas.height = header.offsetHeight` | +| Resize: `resizeCanvas()` uses `window.innerWidth/Height` | Resize: recalculate from `header.offsetWidth/Height` | +| Canvas positioned full-screen fixed | Canvas `position:absolute; top:0; right:0; height:100%; pointer-events:none; z-index:0` | + +Everything else (character set, trail rendering, color sampling, `hexToRgba`, `sampleColors`, `MutationObserver`, `document.fonts.ready`, debounced resize) is identical. + +### CSS variable addition: `--accent2` + +Add to `:root` in the `CSS=` heredoc in `gen_web_hook.sh`: + +```css +--accent2: #4ec97b; +``` + +This matches the existing `--green` value and aligns the var name with danix2-hugo-theme. + +The script references `--accent2` (green rain) and `--accent` (purple rain) — both already present in this repo's palette. + +### Header structure change (in `gen_web_hook.sh` → `write_header()`) + +Add `position: relative` to `.site-header` so the absolutely-positioned canvas is contained within it. + +Add `<script src="/.assets/matrix-rain.js" defer></script>` to the `<head>` of every generated `_header.html`. + +The canvas and fade `<div>` are injected by the JS itself (matching cgit pattern) — no HTML changes needed beyond the `<script>` tag. + +### Apache: hide `.assets/` from directory listing + +Add to `.htaccess`: + +```apache +IndexIgnore .assets +``` + +## File changes summary + +| File | Change | +|------|--------| +| `gen_web_hook.sh` | Add `--accent2` CSS var; add `position:relative` to `.site-header`; add `<script>` tag in `write_header()` | +| `.assets/matrix-rain.js` | New file — danix2 engine adapted for header-scoped canvas | +| `.htaccess` | Add `IndexIgnore .assets` | +| `CLAUDE.md` | Note `.assets/` directory and CSS var addition | + +## Out of scope + +- Light theme CSS (no light theme exists yet; MutationObserver in the script already handles it when added) +- Any changes to `_footer.html` generation +- Feather icons or any other JS (cgit-specific, not needed here) diff --git a/gen_web_hook.sh b/gen_web_hook.sh new file mode 100644 index 0000000..050b89d --- /dev/null +++ b/gen_web_hook.sh @@ -0,0 +1,342 @@ +#!/bin/bash +# gen_web_hook.sh — generates static HTML header/footer files for the +# Apache autoindex listing of the danix Slackware package repository. +# +# Can be called standalone or from slackrepo's HOOK_FINISH. +# Usage: gen_web_hook.sh [/path/to/pkgrepo] +# +# If no argument is given, uses $SR_PKGREPO if set, otherwise /repo. + +set -euo pipefail + +PKGREPO="${1:-${SR_PKGREPO:-/repo}}" + +log() { echo "gen_web_hook: $*"; } +warn() { echo "gen_web_hook: WARNING: $*" >&2; } + +if [ ! -d "$PKGREPO" ]; then + warn "PKGREPO '$PKGREPO' not found." + exit 1 +fi + +log "Generating static web files in $PKGREPO ..." + +# ── HTML escape ─────────────────────────────────────────────────────────────── +html_escape() { + local s="$1" + s="${s//&/&}" + s="${s//</<}" + s="${s//>/>}" + s="${s//\"/"}" + printf '%s' "$s" +} + +# ── Shared CSS ──────────────────────────────────────────────────────────────── +CSS='<style> +:root { + --bg: #0e1117; + --bg-card: #161b25; + --bg-hover: #1e2535; + --border: #2a3147; + --accent: #5c9cf5; + --accent-dim:#3a5f99; + --green: #4ec97b; + --accent2: #4ec97b; + --text: #c9d1e0; + --text-dim: #6b7a99; + --text-head: #e8edf7; + --mono: "IBM Plex Mono", monospace; + --sans: "IBM Plex Sans", sans-serif; +} +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +body { + background: var(--bg); color: var(--text); font-family: var(--sans); + font-size: 15px; line-height: 1.6; min-height: 100vh; + display: flex; flex-direction: column; +} +.site-header { border-bottom: 1px solid var(--border); padding: 2rem 2.5rem 1.5rem; background: var(--bg-card); position: relative; overflow: hidden; } +.header-top { display: flex; align-items: baseline; gap: 1rem; flex-wrap: wrap; margin-bottom: 0.5rem; } +.site-title { font-family: var(--mono); font-size: 1.35rem; font-weight: 600; color: var(--text-head); } +.site-title span { color: var(--accent); } +.site-subtitle { font-size: 0.8rem; color: var(--text-dim); font-family: var(--mono); } +.header-desc { font-size: 0.9rem; color: var(--text-dim); max-width: 65ch; margin-bottom: 1rem; } +.header-links { display: flex; gap: 1.25rem; align-items: center; flex-wrap: wrap; } +.header-links a { + font-family: var(--mono); font-size: 0.8rem; color: var(--accent); text-decoration: none; + border: 1px solid var(--accent-dim); padding: 0.2rem 0.65rem; border-radius: 3px; +} +.header-links a:hover { background: var(--accent); color: var(--bg); } +pre { font-family: var(--mono) !important; color: var(--text) !important; background: transparent !important; padding: 1.5rem 2.5rem !important; flex: 1; } +table { width: calc(100% - 5rem); margin: 1.5rem 2.5rem; border-collapse: collapse; font-family: var(--mono); font-size: 0.85rem; } +th { text-align: left; color: var(--text-dim); font-weight: 600; font-size: 0.72rem; text-transform: uppercase; letter-spacing: 0.08em; padding: 0.5rem 0.75rem; border-bottom: 1px solid var(--border); } +td { padding: 0.4rem 0.75rem; border-bottom: 1px solid rgba(42,49,71,0.5); vertical-align: middle; } +tr:hover td { background: var(--bg-hover); } +td a, td a:visited { color: var(--text-head); text-decoration: none; } +td a[href$="/"]:not([href="../"]) { color: var(--accent); font-weight: 600; } +td a:hover { color: var(--accent); text-decoration: underline; } +td:nth-child(3), td:nth-child(4) { color: var(--text-dim); font-size: 0.8rem; } +td a[href$=".txz"] { color: var(--green); font-weight: 600; } +.autoindex-wrapper { flex: 1; display: flex; flex-direction: column; } +hr { display: none; } +address { display: none; } +.pkg-info { margin: 0 2.5rem 1.5rem; border: 1px solid var(--border); border-radius: 6px; overflow: hidden; background: var(--bg-card); } +.pkg-info-header { display: flex; align-items: baseline; gap: 0.75rem; padding: 0.6rem 1rem; background: rgba(92,156,245,0.06); border-bottom: 1px solid var(--border); } +.pkg-info-label { font-family: var(--mono); font-size: 0.7rem; text-transform: uppercase; letter-spacing: 0.1em; color: var(--text-dim); } +.pkg-info-name { font-family: var(--mono); font-size: 0.85rem; font-weight: 600; color: var(--accent); } +.pkg-info-body { padding: 0.75rem 1rem; font-size: 0.875rem; line-height: 1.65; color: var(--text); } +.pkg-info-body p { margin-bottom: 0.2rem; } +.pkg-title { font-weight: 600; color: var(--text-head); margin-bottom: 0.5rem !important; } +.pkg-meta { display: flex; gap: 1.5rem; flex-wrap: wrap; padding: 0.5rem 1rem; border-top: 1px solid var(--border); font-family: var(--mono); font-size: 0.75rem; } +.pkg-meta em { font-style: normal; margin-right: 0.3rem; color: var(--text-dim); } +.pkg-meta span { color: var(--text); } +.pkg-homepage { padding: 0.4rem 1rem 0.6rem; font-family: var(--mono); font-size: 0.78rem; border-top: 1px solid var(--border); } +.pkg-homepage a { color: var(--accent); text-decoration: none; } +.pkg-homepage a:hover { text-decoration: underline; } +.site-footer { border-top: 1px solid var(--border); background: var(--bg-card); padding: 1rem 2.5rem; margin-top: auto; } +.footer-inner { display: flex; flex-direction: column; gap: 0.35rem; } +.footer-meta { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.2rem; } +.footer-updated { font-family: var(--mono); font-size: 0.8rem; color: var(--text-dim); } +.footer-date { color: var(--green); font-weight: 600; } +.footer-rss { display: inline-flex; align-items: center; gap: 0.3rem; font-family: var(--mono); font-size: 0.75rem; color: #e8923a; text-decoration: none; border: 1px solid rgba(232,146,58,0.3); padding: 0.15rem 0.5rem; border-radius: 3px; } +.footer-rss:hover { background: rgba(232,146,58,0.1); } +.footer-sig { font-family: var(--mono); font-size: 0.75rem; color: var(--text-dim); display: flex; gap: 0.5rem; align-items: center; flex-wrap: wrap; } +.footer-sig a { color: var(--accent); text-decoration: none; } +.footer-sep { opacity: 0.3; } +.category-pill { position: absolute; top: 50%; right: 2.5rem; transform: translateY(-50%); z-index: 2; background: var(--bg-card); border: 1px solid var(--border); border-radius: 999px; padding: 0.4rem 1.25rem; font-family: var(--mono); font-size: 1.4rem; font-weight: 700; letter-spacing: 0.02em; text-transform: capitalize; } +.category-pill span { background: linear-gradient(135deg, var(--accent) 0%, var(--accent2) 100%); -webkit-background-clip: text; background-clip: text; color: transparent; } +</style>' + +# ── Shared footer signature ─────────────────────────────────────────────────── +footer_sig() { + cat << 'EOF' +<footer class="site-footer"> + <div class="footer-inner"> + <div class="footer-sig"> + <span>danix packages · Slackware64-current</span> + <span class="footer-sep">·</span> + <a href="https://danix.xyz/is/here/">Contact danix</a> + <span class="footer-sep">·</span> + <span>GPG signed · use at your own risk</span> + </div> + </div> +</footer> +</body> +</html> +EOF +} + +# ── Generate root _footer.html (last-updated + signature) ──────────────────── +generate_root_footer() { + local last_updated='unknown' + if [ -f "$PKGREPO/ChangeLog.txt" ]; then + last_updated=$(grep -m1 -E '^(Mon|Tue|Wed|Thu|Fri|Sat|Sun)' "$PKGREPO/ChangeLog.txt" || echo 'unknown') + fi + + { + cat << EOF +</div><!-- .autoindex-wrapper --> +<footer class="site-footer"> + <div class="footer-inner"> + <div class="footer-meta"> + <span class="footer-updated">Last updated: + <span class="footer-date">$(html_escape "$last_updated")</span> + </span> + <a class="footer-rss" href="/ChangeLog.rss"> + <svg width="12" height="12" viewBox="0 0 24 24" fill="currentColor"> + <circle cx="5" cy="19" r="3"/> + <path d="M4 4a16 16 0 0 1 16 16h-3A13 13 0 0 0 4 7z"/> + <path d="M4 11a9 9 0 0 1 9 9H10a6 6 0 0 0-6-6z"/> + </svg> + RSS feed + </a> + </div> + <div class="footer-sig"> + <span>danix packages · Slackware64-current</span> + <span class="footer-sep">·</span> + <a href="https://danix.xyz/is/here/">Contact danix</a> + <span class="footer-sep">·</span> + <span>GPG signed · use at your own risk</span> + </div> + </div> +</footer> +</body> +</html> +EOF + } > "$PKGREPO/_footer.html" + log "Written: $PKGREPO/_footer.html" +} + +# ── Generate per-package _footer.html ──────────────────────────────────────── +generate_package_footer() { + local pkg_dir="$1" + local pkg_name + pkg_name=$(basename "$pkg_dir") + + local txt_file + txt_file=$(find "$pkg_dir" -maxdepth 1 -name '*.txt' | head -1) + + if [ -z "$txt_file" ]; then + # No .txt file — minimal footer + { echo '</div><!-- .autoindex-wrapper -->'; footer_sig; } > "$pkg_dir/_footer.html" + return + fi + + # Parse .txt + local title='' homepage='' body_lines=() + while IFS= read -r line; do + # Strip "pkgname:" or "pkgname: " prefix + local text + text=$(echo "$line" | sed 's/^[^:]*: \?//') + # Skip lines that were just "pkgname:" with nothing after + [[ "$line" == *: ]] && text='' + if [[ "$text" == Homepage:* ]]; then + homepage="${text#Homepage: }" + continue + fi + if [ -z "$title" ]; then + [ -n "$text" ] && title="$text" + else + [ -n "$text" ] && body_lines+=("$text") + fi + done < "$txt_file" + + # Parse .meta for sizes + local meta_file size_c='' size_u='' + meta_file=$(find "$pkg_dir" -maxdepth 1 -name '*.meta' | head -1) + if [ -n "$meta_file" ]; then + size_c=$(grep 'PACKAGE SIZE (compressed):' "$meta_file" | sed 's/.*: *//') + size_u=$(grep 'PACKAGE SIZE (uncompressed):' "$meta_file" | sed 's/.*: *//') + fi + + { + echo '</div><!-- .autoindex-wrapper -->' + echo '<section class="pkg-info">' + echo ' <div class="pkg-info-header">' + echo ' <span class="pkg-info-label">Package description</span>' + echo " <span class=\"pkg-info-name\">$(html_escape "$pkg_name")</span>" + echo ' </div>' + echo ' <div class="pkg-info-body">' + echo " <p class=\"pkg-title\">$(html_escape "$title")</p>" + for bline in "${body_lines[@]}"; do + echo " <p>$(html_escape "$bline")</p>" + done + echo ' </div>' + + if [ -n "$size_c" ] || [ -n "$size_u" ]; then + echo ' <div class="pkg-meta">' + [ -n "$size_c" ] && echo " <span><em>Size (compressed):</em> $(html_escape "$size_c")</span>" + [ -n "$size_u" ] && echo " <span><em>Size (uncompressed):</em> $(html_escape "$size_u")</span>" + echo ' </div>' + fi + + if [ -n "$homepage" ]; then + echo " <div class=\"pkg-homepage\"><a href=\"$(html_escape "$homepage")\" rel=\"noopener noreferrer\">$(html_escape "$homepage")</a></div>" + fi + + echo '</section>' + footer_sig + } > "$pkg_dir/_footer.html" + + log "Written: $pkg_dir/_footer.html" +} + +# ── Write _header.html to a given directory ─────────────────────────────────── +write_header() { + local dir="$1" + cat > "$dir/_header.html" << EOF +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>danix Slackware Repository</title> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@300;400;600&display=swap" rel="stylesheet"> + <script src="/.assets/matrix-rain.js" defer></script> + ${CSS} +</head> +<body> +<header class="site-header"> + <div class="header-top"> + <div class="site-title"><span>//</span> <a href="https://packages.danix.xyz" style="color:var(--accent)">danix packages</a></div> + <div class="site-subtitle">Slackware64-current · unofficial repository</div> + </div> + <p class="header-desc"> + Third-party Slackware packages built with + <a href="https://github.com/aclemons/slackrepo" style="color:var(--accent)">slackrepo</a> + on Slackware64-current. Use at your own risk. All packages are signed with my GPG key. + </p> + <div class="header-links"> + <a href="https://danix.xyz/is/here/">Contact me</a> + <a href="/ChangeLog.txt">ChangeLog</a> + <a href="/ChangeLog.rss">RSS</a> + </div> +</header> +<div class="autoindex-wrapper"> +EOF + log "Written: $dir/_header.html" +} + +# ── Write _header.html for a category directory (includes pill) ─────────────── +write_category_header() { + local dir="$1" + local cat_name + cat_name=$(basename "$dir") + cat > "$dir/_header.html" << EOF +<!DOCTYPE html> +<html lang="en"> +<head> + <meta charset="UTF-8"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title>danix Slackware Repository</title> + <link rel="preconnect" href="https://fonts.googleapis.com"> + <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;600&family=IBM+Plex+Sans:wght@300;400;600&display=swap" rel="stylesheet"> + <script src="/.assets/matrix-rain.js" defer></script> + ${CSS} +</head> +<body> +<header class="site-header"> + <div class="header-top"> + <div class="site-title"><span>//</span> <a href="https://packages.danix.xyz" style="color:var(--accent)">danix packages</a></div> + <div class="site-subtitle">Slackware64-current · unofficial repository</div> + </div> + <p class="header-desc"> + Third-party Slackware packages built with + <a href="https://github.com/aclemons/slackrepo" style="color:var(--accent)">slackrepo</a> + on Slackware64-current. Use at your own risk. All packages are signed with my GPG key. + </p> + <div class="header-links"> + <a href="https://danix.xyz/is/here/">Contact me</a> + <a href="/ChangeLog.txt">ChangeLog</a> + <a href="/ChangeLog.rss">RSS</a> + </div> + <div class="category-pill"><span>$(html_escape "$cat_name")</span></div> +</header> +<div class="autoindex-wrapper"> +EOF + log "Written: $dir/_header.html" +} + +# ── Generate category _footer.html (just the signature) ────────────────────── +generate_category_footer() { + local cat_dir="$1" + { echo '</div><!-- .autoindex-wrapper -->'; footer_sig; } > "$cat_dir/_footer.html" + log "Written: $cat_dir/_footer.html" +} + +# ── Main ────────────────────────────────────────────────────────────────────── +write_header "$PKGREPO" +generate_root_footer + +for category in "$PKGREPO"/*/; do + [ -d "$category" ] || continue + write_category_header "$category" + generate_category_footer "$category" + for pkg in "$category"*/; do + [ -d "$pkg" ] || continue + write_header "$pkg" + generate_package_footer "$pkg" + done +done + +log "Done." diff --git a/htaccess b/htaccess new file mode 100644 index 0000000..bb92f15 --- /dev/null +++ b/htaccess @@ -0,0 +1,43 @@ +# danix Slackware package repository + +Options +Indexes + +# ── Autoindex options ───────────────────────────────────────────────────────── +IndexOptions FancyIndexing HTMLTable IgnoreCase SuppressDescription SuppressHTMLPreamble NameWidth=* FoldersFirst ScanHTMLTitles + +IndexOrderDefault Ascending Name + +# ── Header / Footer (static HTML, generated by gen_web_hook) ───────────────── +HeaderName _header.html +ReadmeName _footer.html + +# ── Hide internal files from the listing ───────────────────────────────────── +IndexIgnore _header.html _footer.html .htaccess .htpasswd \ + CHECKSUMS.md5 CHECKSUMS.md5.asc CHECKSUMS.md5.gz CHECKSUMS.md5.gz.asc \ + FILELIST.TXT MANIFEST.bz2 \ + PACKAGES.TXT PACKAGES.TXT.gz \ + ChangeLog.txt.gz GPG-KEY \ + .assets + +# ── Icons ───────────────────────────────────────────────────────────────────── +AddIconByType (DIR,/icons-pkg/dir.svg) httpd/unix-directory +AddIcon /icons-pkg/dir.svg ^^DIRECTORY^^ +AddIcon /icons-pkg/package.svg .txz +AddIcon /icons-pkg/signature.svg .asc +AddIcon /icons-pkg/checksum.svg .md5 .sha256 +AddIcon /icons-pkg/compressed.svg .gz .bz2 +AddIcon /icons-pkg/rss.svg .rss +AddIcon /icons-pkg/file.svg .txt .lst .meta .dep +DefaultIcon /icons-pkg/file.svg + +# ── MIME types for Slackware-specific extensions ───────────────────────────── +AddType application/octet-stream .txz .dep +AddType text/plain .txt .asc .lst .meta .md5 .sha256 + +# ── Cache control ───────────────────────────────────────────────────────────── +<FilesMatch "\.(txz|dep)$"> + Header set Cache-Control "public, max-age=86400" +</FilesMatch> +<FilesMatch "\.(txt|asc|lst|meta|md5|sha256)$"> + Header set Cache-Control "public, max-age=3600" +</FilesMatch> diff --git a/vhost.conf b/vhost.conf new file mode 100644 index 0000000..03fef31 --- /dev/null +++ b/vhost.conf @@ -0,0 +1,33 @@ +<VirtualHost YOUR_SERVER_IP:443> + ServerName YOUR_DOMAIN + DocumentRoot /path/to/pkgrepo + + SSLEngine On + + ErrorLog "/var/log/apache2/packages_error_log" + CustomLog "/var/log/apache2/packages_access_log" common + + <Directory /path/to/pkgrepo> + Require all granted + Options +Indexes + AllowOverride All + </Directory> + + # All IndexOptions, HeaderName, ReadmeName are managed in .htaccess + + Include /etc/letsencrypt/options-ssl-apache.conf + SSLCertificateFile /etc/letsencrypt/live/YOUR_DOMAIN/fullchain.pem + SSLCertificateKeyFile /etc/letsencrypt/live/YOUR_DOMAIN/privkey.pem +</VirtualHost> + +# ── Required Apache modules ─────────────────────────────────────────────────── +# Check what's active: apache2ctl -M | grep -E 'autoindex|headers|php|proxy' +# +# a2enmod autoindex <- directory listings +# a2enmod headers <- Cache-Control headers in .htaccess +# +# PHP -- pick one: +# mod_php: a2enmod php8.4 +# php-fpm: a2enmod proxy_fcgi setenvif && a2enconf php8.4-fpm +# +# systemctl reload apache2
\ No newline at end of file |
