From 2be3b32586d2a44abbbcadf5265612a58237bfda Mon Sep 17 00:00:00 2001 From: Leo Vasanko Date: Fri, 18 Sep 2026 18:22:08 +0000 Subject: [PATCH] Referer badge: translucent theme-aware pill, compact UTM summary with click-to-copy --- frontend/src/AnalyticsView.vue | 13 +++- frontend/src/RefererBadge.vue | 105 ++++++++++++++++++++++--------- frontend/src/analytics/format.js | 35 +++++++++-- frontend/src/assets/pagerite.css | 13 ++-- 4 files changed, 122 insertions(+), 44 deletions(-) diff --git a/frontend/src/AnalyticsView.vue b/frontend/src/AnalyticsView.vue index c8ac619..3ae5772 100644 --- a/frontend/src/AnalyticsView.vue +++ b/frontend/src/AnalyticsView.vue @@ -482,10 +482,19 @@ const abuseRows = computed(() => formatAbuseRows(rangeData.value?.abuse || [], c } /* The referer badge outgrows the 8rem trail-link cap (it carries the UTM - summary too); keep the inline-flex layout from the component. */ -.visit-table .trail a.referer-badge { + summary too); keep the inline-flex layout from the component. The + generic trail-link rule above would otherwise clip the badge (overflow: + hidden, hiding the absolutely positioned favicon) and cap its inner + link — the link is display: contents, so its parts lay out as badge + flex items. */ +.visit-table .trail .referer-badge { display: inline-flex; max-width: 100%; + overflow: visible; +} + +.visit-table .trail .referer-badge a.badge-link { + display: contents; } /* Same flag chip as the visitor cells (VisitorCell.vue); the flags here diff --git a/frontend/src/RefererBadge.vue b/frontend/src/RefererBadge.vue index 4221f0d..e3af123 100644 --- a/frontend/src/RefererBadge.vue +++ b/frontend/src/RefererBadge.vue @@ -1,11 +1,15 @@ diff --git a/frontend/src/analytics/format.js b/frontend/src/analytics/format.js index 2a7050a..b762332 100644 --- a/frontend/src/analytics/format.js +++ b/frontend/src/analytics/format.js @@ -180,22 +180,45 @@ function stepOf(path, titles) { /** * Badge data combining a visit's/crawler's external referer origin with the * visit's UTM tags: the origin as the badge link/label (the favicon is - * looked up by origin in the component), the known UTM values as a short - * inline summary, and a one-fact-per-line tooltip — the full origin URL on - * the first line, then every ``utm_*=value`` pair. Null when there is no - * external referer and no UTM tag (a plain direct visit). + * looked up by origin in the component), a compact UTM summary (the few + * most informative values) as ``utm`` with the full ``utm_*=value`` list + * as ``utmCopy`` for click-to-copy, and a one-fact-per-line tooltip — the + * full origin URL on the first line, then every ``utm_*=value`` pair. + * Null when there is no external referer and no UTM tag (a plain direct + * visit). */ function refererBadgeOf(referer, titles, utmTags = {}) { const step = stepOf(referer, titles) const external = step?.external ? step : null - const known = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content'] - const utm = known.map((k) => utmTags[k]).filter(Boolean).join(' · ') + // Compact UTM summary, in display order source, campaign, content, term: + // source only when no referer is known (it just repeats where the visitor + // came from), content only as a stand-in when there is no term. The + // remaining tags (medium and any nonstandard utm_*) fill in only when + // fewer than three of these more useful items exist — and never when a + // term is present (the term alone says enough). The tooltip keeps + // every tag, one pair per line. + const useful = [] + if (utmTags.utm_source && !referer) useful.push(utmTags.utm_source) + if (utmTags.utm_campaign) useful.push(utmTags.utm_campaign) + if (utmTags.utm_content && !utmTags.utm_term) useful.push(utmTags.utm_content) + if (utmTags.utm_term) useful.push(utmTags.utm_term) + const rest = useful.length < 3 && !utmTags.utm_term + ? Object.keys(utmTags) + .filter((k) => !['utm_source', 'utm_campaign', 'utm_content', 'utm_term'].includes(k)) + .map((k) => utmTags[k]) + .filter(Boolean) + : [] + const utm = [...useful, ...rest].join(' · ') if (!external && !utm) return null + const utmCopy = Object.entries(utmTags) + .map(([k, value]) => `${k}=${value}`) + .join('\n') return { href: external?.origin || '', label: external?.slug || '', origin: external?.origin || '', utm, + utmCopy, title: [ ...(external ? [external.origin] : []), ...Object.entries(utmTags).map(([k, value]) => `${k}=${value}`), diff --git a/frontend/src/assets/pagerite.css b/frontend/src/assets/pagerite.css index 4b4f4e1..1f06f46 100644 --- a/frontend/src/assets/pagerite.css +++ b/frontend/src/assets/pagerite.css @@ -26,14 +26,15 @@ /* Selection fill for page text and the CodeMirror editors; themes override when the accent tint clashes with accent-colored text. */ --selection-bg: color-mix(var(--accent) 30%, transparent); - /* Referer-badge chip in the analytics viewer: a fixed neutral palette, + /* Referer-badge chip in the analytics viewer: a translucent neutral wash, deliberately NOT themed — the chip sits behind transparent favicons, so black-on-transparent and white-on-transparent glyphs must both stay - legible on every theme (dark greys kill black glyphs, pure white kills - white ones); its text likewise stays dark on any theme. */ - --badge-bg: #c9d1d9; - --badge-text: #1f2328; - --badge-muted: #59636e; + legible on every theme (a slight whitening keeps black glyphs readable + on dark bars without a glaring solid-white chip). Being translucent, it + takes the page's tone, so its text follows the theme's colors. */ + --badge-bg: #aaaaaa44; + --badge-text: var(--text); + --badge-muted: var(--muted); /* Code highlighting palette, consumed by pygments.css: complete light and dark sets (background included), resolved by light-dark() from the used color-scheme. A theme picks a set simply by declaring