fba6c5b73ce5
stream real public save IDs gently behind the profile (v3.1.20)
- time
- 2026-10-08 17:35 UTC · 4 days ago
- machine
- Junes-MacBook-Pro.local
- agent
- gpt-6
- session
- gentle profile save stream
- parent
- e2328c2de1b6
↓ kepr-fba6c5b7.tar.gzatlas at this save
files changed5 files
package.json+1 −1public/css/kepr.css+35 −23public/js/kepr.js+12 −21src/pages/profile.ts+10 −5tests/front.test.ts+7 −1package.json+1 −1
@@ -1,6 +1,6 @@11 {22 "name": "kepr",3- "version": "3.1.19",3+ "version": "3.1.20",44 "private": true,55 "type": "module",66 "scripts": {public/css/kepr.css+35 −23
@@ -2023,7 +2023,7 @@20232023 display: flex;20242024 flex-direction: row-reverse;20252025 justify-content: center;2026- align-items: center;2026+ align-items: flex-start;20272027 gap: 13px;20282028 pointer-events: none;20292029 user-select: none;@@ -2032,44 +2032,56 @@20322032 line-height: 16px;20332033 color: var(--accent-dim);20342034 }2035-/* Characters top to bottom, columns right to left, each breathing on its own clock. */2035+/* Matching groups keep every column continuous across the loop boundary. */20362036 .hex-col {2037+ display: flex;2038+ flex-direction: column;2039+ flex-shrink: 0;2040+ opacity: 0.22;2041+ animation: hex-stream 120s linear infinite;2042+ animation-delay: -43s;2043+}2044+.hex-group {2045+ display: flex;2046+ flex-direction: column;2047+ gap: 36px;2048+ padding-bottom: 36px;2049+}2050+.hex-id {20372051 writing-mode: vertical-rl;20382052 text-orientation: upright;20392053 letter-spacing: 3px;2040- opacity: 0.05;2041- animation: hex-breathe 17s ease-in-out infinite;2042- transition:2043- color 1.6s ease,2044- text-shadow 1.6s ease;2054+ opacity: 0.45;20452055 }2056+.hex-id:first-child {2057+ color: var(--accent);2058+ opacity: 1;2059+}20462060 .hex-col:nth-child(3n) {20472061 color: var(--accent);2048- animation-duration: 23s;2049- animation-delay: -7s;2062+ animation-duration: 144s;2063+ animation-delay: -77s;20502064 }20512065 .hex-col:nth-child(4n + 1) {2052- animation-duration: 29s;2053- animation-delay: -13s;2066+ animation-duration: 160s;2067+ animation-delay: -119s;20542068 }20552069 .hex-col:nth-child(5n + 2) {2056- animation-duration: 19s;2057- animation-delay: -3s;2070+ animation-duration: 108s;2071+ animation-delay: -19s;20582072 }20592073 .hex-col:nth-child(7n) {2060- animation-delay: -21s;2074+ animation-delay: -91s;20612075 }2062-.hex-col.glow {2063- color: var(--text-heading);2064- text-shadow: 0 0 6px rgba(255, 173, 51, 0.2);2076+.hex-backdrop.paused .hex-col {2077+ animation-play-state: paused;20652078 }2066-@keyframes hex-breathe {2067- 0%,2068- 100% {2069- opacity: 0.05;2079+@keyframes hex-stream {2080+ from {2081+ transform: translateY(-50%);20702082 }2071- 50% {2072- opacity: 0.22;2083+ to {2084+ transform: translateY(0);20732085 }20742086 }20752087 .profile-cover-fade {public/js/kepr.js+12 −21
@@ -1,7 +1,7 @@11 // Kepr's browser islands (§18). Every page is complete without this file; it only adds the22 // ticker's pace, the drifting timeline, copy buttons, the inline panels, history's scrolling,33 // line-range highlighting, the Atlas panel, picker, filter and file viewer, the signed-in4-// editor, the phone nav menu, and the profile's glow.4+// editor, the phone nav menu, and the profile's visibility pause.55 66 // Kepr 2's save kinds (canvas.js), first match wins: each dot's colour and strength.77 const KINDS = [@@ -957,25 +957,16 @@957957 } catch {}958958 }959959 960-// The profile's hex backdrop already breathes in CSS; this lights a few columns at a time, as961-// Kepr 2's canvas did. Nothing moves under reduced motion, including when it is switched on later.962-function hexGlow() {963- const columns = document.querySelectorAll(".hex-col");964- if (columns.length === 0) return;965- const reduce = matchMedia("(prefers-reduced-motion: reduce)");966- let timer = null;967- const update = () => {968- clearInterval(timer);969- for (const column of columns) column.classList.remove("glow");970- if (reduce.matches) return;971- timer = setInterval(() => {972- const column = columns[Math.floor(Math.random() * columns.length)];973- if (document.querySelectorAll(".hex-col.glow").length >= 3) return;974- column.classList.add("glow");975- setTimeout(() => column.classList.remove("glow"), 3200);976- }, 1500);977- };978- reduce.addEventListener("change", update);960+function hexStream() {961+ const backdrop = document.querySelector(".hex-backdrop");962+ if (!backdrop) return;963+ let visible = true;964+ const update = () => backdrop.classList.toggle("paused", !visible || document.hidden);965+ new IntersectionObserver(([entry]) => {966+ visible = entry.isIntersecting;967+ update();968+ }).observe(backdrop);969+ document.addEventListener("visibilitychange", update);979970 update();980971 }981972 @@ -1001,4 +992,4 @@1001992 navMenu();1002993 lineRanges();1003994 atlas();1004-hexGlow();995+hexStream();src/pages/profile.ts+10 −5
@@ -58,11 +58,13 @@5858 function cover(ctx: PageContext, handle: string, recent: RecentSave[]): Html {5959 const since = setting(ctx.kepr, "building_since");6060 const signature = setting(ctx.kepr, "signature");61- // The backdrop repeats the newest IDs when there are fewer saves than columns.61+ // Duplicate each group so the loop joins without replacing or inventing save IDs.6262 const columns =6363 recent.length === 06464 ? []65- : Array.from({ length: HEX_COLUMNS }, (_, i) => recent[i % recent.length]?.save.id);65+ : Array.from({ length: HEX_COLUMNS }, (_, i) =>66+ Array.from({ length: 4 }, (_, j) => recent[(i + j) % recent.length]?.save.id),67+ );6668 // Without an uploaded signature, the handle in Sacramento stands in for it, as on Kepr 2.6769 const name =6870 signature && isSafePath(signature) && !signature.includes("/")@@ -71,9 +73,12 @@7173 )}" alt="${handle}">`7274 : handle;7375 return html`<section class="profile-cover">74-<div class="hex-backdrop" aria-hidden="true">${columns.map(75- (id) => html`<span class="hex-col">${id}</span>`,76- )}</div>76+<div class="hex-backdrop" aria-hidden="true">${columns.map((ids) => {77+ const group = html`<span class="hex-group">${ids.map(78+ (id) => html`<span class="hex-id">${id}</span>`,79+ )}</span>`;80+ return html`<span class="hex-col">${group}${group}</span>`;81+ })}</div>7782 <div class="profile-cover-fade"></div>7883 <div class="profile-identity">7984 <h1 class="profile-username">${name}</h1>tests/front.test.ts+7 −1
@@ -165,8 +165,14 @@165165 expect(html).toContain('class="profile-timeline" tabindex="0"');166166 expect(html).toContain('role="region" aria-label="recent activity"');167167 for (const save of realSaves()) {168- expect(html).toContain(`<span class="hex-col">${save.id}</span>`);168+ expect(html).toContain(`<span class="hex-id">${save.id}</span>`);169169 expect(html).toContain(`href="/alpha/saves/${save.id}">${save.message}</a>`);170+ }171+ const groups = [...html.matchAll(/<span class="hex-group">([\s\S]*?)<\/span><\/span>/g)];172+ expect(groups).toHaveLength(96);173+ for (let i = 0; i < groups.length; i += 2) {174+ expect(groups[i]?.[1]).toBe(groups[i + 1]?.[1]);175+ expect(groups[i]?.[1].match(/class="hex-id"/g)).toHaveLength(4);170176 }171177 for (const id of checkpoints()) expect(html).not.toContain(id);172178 expect(html).not.toContain(vaultSave);