Compare commits

..
3 Commits
8 changed files with 21 additions and 33 deletions
+2 -2
View File
@@ -8,7 +8,7 @@
*/
import { computed, onBeforeUnmount, onMounted, shallowRef, watch } from 'vue'
import { DAY } from './analytics/time.js'
import { formatCount } from './analytics/format.js'
import { formatCount, formatReadTime } from './analytics/format.js'
import {
TNODE_W,
TNODE_H,
@@ -151,7 +151,7 @@ onBeforeUnmount(() => cancelAnimationFrame(rafId))
const fitsPill = (label, fontPx = 19) => label.length * 0.52 * fontPx <= TNODE_W - 16
const countLabel = (n) =>
n.readMin ? `${formatCount(n.views)}×${n.readMin}m` : formatCount(n.views)
n.readSec ? `${formatCount(n.views)}×${formatReadTime(n.readSec)}` : formatCount(n.views)
</script>
<template>
+9 -9
View File
@@ -259,8 +259,8 @@ function sortByNav(root, navOrder) {
walk(root)
}
/** Compute median reading time per article in whole minutes. */
function buildReadMinutes(visits) {
/** Compute median reading time per article in seconds. */
function buildReadSeconds(visits) {
const times = {}
for (const v of visits || []) {
for (const [path, sec] of Object.entries(v.read || {})) {
@@ -269,19 +269,19 @@ function buildReadMinutes(visits) {
}
}
}
const minutes = {}
const seconds = {}
for (const [path, arr] of Object.entries(times)) {
arr.sort((a, b) => a - b)
const mid = Math.floor(arr.length / 2)
const median =
arr.length % 2 ? arr[mid] : (arr[mid - 1] + arr[mid]) / 2
minutes[path] = Math.max(1, Math.round(median / 60))
seconds[path] = Math.round(median)
}
return minutes
return seconds
}
/** Compute view counts, labels and hidden flags for each node. */
function annotateNodes(nodes, viewsData, titles, readMinutes) {
function annotateNodes(nodes, viewsData, titles, readSeconds) {
const viewCount = (p) => {
let n = 0
for (const c of Object.values(viewsData?.[p] || {})) n += c
@@ -290,7 +290,7 @@ function annotateNodes(nodes, viewsData, titles, readMinutes) {
for (const n of nodes) {
n.views = viewCount(n.path)
n.readMin = readMinutes[n.path] || 0
n.readSec = readSeconds[n.path] || 0
// Article title inside the pill (clipped at the pill border on
// render), slug as fallback for pages missing from the site tree.
n.label = titles.get(n.path) || (n.path === '/' ? '🏠︎' : n.path.split('/').pop())
@@ -895,13 +895,13 @@ export function buildTransitionGraph(data, pageTree, visits = [], dayScale = 1)
const exits = collectExitPairs(data?.transitions)
const navOrder = buildNavigationOrder(pageTree)
const titles = buildTitleMap(pageTree)
const readMinutes = buildReadMinutes(visits)
const readSeconds = buildReadSeconds(visits)
if (!internal.length && !navOrder.size) return null
const { nodes, byPath, root } = buildNodeTree(internal, navOrder)
sortByNav(root, navOrder)
annotateNodes(nodes, data?.views, titles, readMinutes)
annotateNodes(nodes, data?.views, titles, readSeconds)
const { arcs, arcLeft } = layoutGroups(root)
const placed = nodes.filter((n) => !n.hidden)
const pairs = aggregatePairs(internal)
+5
View File
@@ -65,6 +65,11 @@
box-sizing: border-box;
}
::selection {
background: color-mix(var(--accent) 30%, transparent);
color: inherit;
}
/* Links never underline — including SVG link text, which the UA stylesheet
underlines by default. */
a {
-5
View File
@@ -32,11 +32,6 @@
}
}
::selection {
background: var(--accent);
color: #fff;
}
/* Genuinely large solid brand with a soft blue shadow overlapping the
artwork — conservative, but unmissable. */
#brand {
-6
View File
@@ -2,12 +2,6 @@
(inlined by the backend into #page-banner), in neutral dark greys that
follow the page's color scheme. */
/* Bezier-swept banner with wide orange stripes (inlined SVG), separated
from the page by a straight orange blade. */
#banner {
border-bottom: 4px solid var(--accent);
}
/* Banner artwork dark tones: neutral greys in light mode (retinted to the
page's violet family by the dark-scheme block below). */
.nb-base {
+5 -1
View File
@@ -63,7 +63,11 @@
::selection {
background: var(--accent);
color: var(--ink);
}
/* Any banner used is separated from page by a thick orange line */
#banner {
border-bottom: 4px solid var(--accent);
}
/* Oversized outlined brand, spilling off the banner edge: orange stroke,
-5
View File
@@ -19,11 +19,6 @@
--font-heading: var(--font-fraunces);
}
::selection {
background: var(--accent2);
color: #fff;
}
/* Oversized tilted brand in the sky→violet gradient. */
#brand {
font-size: clamp(3.2rem, 9vw, 7.5rem);
-5
View File
@@ -57,11 +57,6 @@ body {
color: var(--text);
}
::selection {
background: var(--sun);
color: #3d5223;
}
/* No separation between banner and page: the frame loses its background,
border and shadow, and the artwork overflows into the document below,
masked by a transparency gradient so it cross-fades into the body's fixed