Referer badge: translucent theme-aware pill, compact UTM summary with click-to-copy
This commit is contained in:
@@ -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
|
/* The referer badge outgrows the 8rem trail-link cap (it carries the UTM
|
||||||
summary too); keep the inline-flex layout from the component. */
|
summary too); keep the inline-flex layout from the component. The
|
||||||
.visit-table .trail a.referer-badge {
|
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;
|
display: inline-flex;
|
||||||
max-width: 100%;
|
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
|
/* Same flag chip as the visitor cells (VisitorCell.vue); the flags here
|
||||||
|
|||||||
@@ -1,11 +1,15 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
// Referer + UTM as one badge in the analytics visit/crawler tables: the
|
// Referer + UTM as one badge in the analytics visit/crawler tables: the
|
||||||
// referer's favicon flush on the left, its host as the link text, then the
|
// referer's favicon flush on the left, its host as the link text, then the
|
||||||
// UTM summary smaller/muted inside the same badge. The whole badge links to
|
// UTM summary smaller/muted inside the same badge. Only the favicon and
|
||||||
// the referer origin (external, new tab) and carries a single one-fact-
|
// host are the link (external, new tab) — everything else, padding and
|
||||||
// per-line tooltip (badge.title: origin, then each utm pair) — no titles on
|
// UTM text included, copies the full utm tag list to the clipboard. The
|
||||||
// the inner elements. Referers are external, so there is no close event.
|
// link is display: contents so its parts lay out as badge flex items.
|
||||||
|
// The badge carries a single one-fact-per-line tooltip (badge.title:
|
||||||
|
// origin, then each utm pair) — no titles on the inner elements.
|
||||||
|
// Referers are external, so there is no close event.
|
||||||
import { computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
|
import { copyList } from './analytics/format.js'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
badge: { type: Object, required: true },
|
badge: { type: Object, required: true },
|
||||||
@@ -16,53 +20,94 @@ const favicon = computed(() => (props.badge.origin ? props.favicons?.[props.badg
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<a class="referer-badge"
|
<span class="referer-badge"
|
||||||
:href="badge.href || undefined"
|
:class="{ 'with-icon': favicon, copyable: badge.utm }"
|
||||||
:title="badge.title"
|
:title="badge.title"
|
||||||
:target="badge.href ? '_blank' : undefined"
|
@click="badge.utm && copyList(badge.utmCopy, $event)">
|
||||||
:rel="badge.href ? 'noopener' : undefined">
|
<a v-if="badge.href" class="badge-link" :href="badge.href"
|
||||||
<img v-if="favicon" class="badge-favicon" :src="favicon" alt="" />
|
target="_blank" rel="noopener" @click.stop>
|
||||||
<span v-if="badge.label">{{ badge.label }}</span>
|
<img v-if="favicon" class="badge-favicon" :src="favicon" alt="" />
|
||||||
|
<span v-if="badge.label">{{ badge.label }}</span>
|
||||||
|
</a>
|
||||||
|
<template v-else>
|
||||||
|
<img v-if="favicon" class="badge-favicon" :src="favicon" alt="" />
|
||||||
|
<span v-if="badge.label">{{ badge.label }}</span>
|
||||||
|
</template>
|
||||||
<small v-if="badge.utm" class="small">{{ badge.utm }}</small>
|
<small v-if="badge.utm" class="small">{{ badge.utm }}</small>
|
||||||
</a>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
/* Browser-chrome chip on a fixed neutral palette (--badge-* in
|
/* Browser-chrome chip on a translucent neutral wash (--badge-* in
|
||||||
pagerite.css, deliberately unthemed): black-on-transparent and
|
pagerite.css, deliberately unthemed): black-on-transparent and
|
||||||
white-on-transparent favicons both stay legible on it, and the text is
|
white-on-transparent favicons both stay legible on it. Colors go on the
|
||||||
always dark regardless of the theme's link/text colors. Colors go on the
|
inner elements, so the theme's link color rules cannot cascade in. The
|
||||||
inner elements, so the theme's a / a:hover color rules (which target the
|
padding is matched by negative margins so the chip's content stays
|
||||||
anchor) cannot cascade in. */
|
exactly where the bare text would sit without the badge — except on the
|
||||||
|
right, which keeps a small positive margin so the next trail item does
|
||||||
|
not abut the chip. */
|
||||||
.referer-badge {
|
.referer-badge {
|
||||||
|
position: relative;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 0.35em;
|
gap: 0.35em;
|
||||||
padding-right: 0.45em;
|
/* Fixed line-height: the bar height is then exactly 1.2em + padding =
|
||||||
border-radius: 0.25rem;
|
1.6em, so the icon below can be sized to match precisely (an
|
||||||
|
absolutely positioned replaced element cannot derive its height from
|
||||||
|
top/bottom offsets — its aspect ratio wins and bottom is dropped). */
|
||||||
|
line-height: 1.2;
|
||||||
|
padding: 0.2em 0.5em;
|
||||||
|
margin: -0.2em 0.25em -0.2em -0.2em;
|
||||||
|
/* Fully rounded: the bar is exactly 1.6em tall, so a 0.8em radius makes
|
||||||
|
both ends semicircles — a pill, with a full circle around the favicon
|
||||||
|
on the left. */
|
||||||
|
border-radius: 0.8em;
|
||||||
background: var(--badge-bg);
|
background: var(--badge-bg);
|
||||||
color: var(--badge-text);
|
color: var(--badge-text);
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Flush with the badge's top/left/bottom edges: a full-height square (the
|
/* Room for the absolutely positioned icon (its 1.6em width plus the gap). */
|
||||||
badge has no padding on those sides), corners clipped by the badge's
|
.referer-badge.with-icon {
|
||||||
overflow: hidden border-radius. */
|
padding-left: 1.95em;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Exactly the bar's height (1.6em, see line-height above), flush to the
|
||||||
|
top/left/bottom borders. The badge does not clip it (no overflow:
|
||||||
|
hidden): the icon may stick out past the bar's rounded corners.
|
||||||
|
Absolute on purpose: an in-flow image is the flex
|
||||||
|
container's first item and would supply the badge's baseline (an image's
|
||||||
|
baseline is its bottom edge), pushing the badge text above the baseline
|
||||||
|
of the trail items that follow. Out of flow, the badge's baseline comes
|
||||||
|
from its text, so baselines match. */
|
||||||
.badge-favicon {
|
.badge-favicon {
|
||||||
width: 1.5em;
|
position: absolute;
|
||||||
height: 1.5em;
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 1.6em;
|
||||||
|
height: 1.6em;
|
||||||
object-fit: cover;
|
object-fit: cover;
|
||||||
flex: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.referer-badge > span,
|
/* The link is display: contents: the favicon and label lay out as flex
|
||||||
.referer-badge > small {
|
items of the badge itself, and only their actual boxes are clickable. */
|
||||||
|
.badge-link {
|
||||||
|
display: contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Click-to-copy affordance on everything outside the link (the UTM text
|
||||||
|
and the surrounding padding). */
|
||||||
|
.referer-badge.copyable {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.referer-badge span,
|
||||||
|
.referer-badge small {
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.referer-badge > span { color: var(--badge-text); }
|
.referer-badge span { color: var(--badge-text); }
|
||||||
.referer-badge > small { color: var(--badge-muted); }
|
.referer-badge small { color: var(--badge-muted); }
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -180,22 +180,45 @@ function stepOf(path, titles) {
|
|||||||
/**
|
/**
|
||||||
* Badge data combining a visit's/crawler's external referer origin with the
|
* 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
|
* 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
|
* looked up by origin in the component), a compact UTM summary (the few
|
||||||
* inline summary, and a one-fact-per-line tooltip — the full origin URL on
|
* most informative values) as ``utm`` with the full ``utm_*=value`` list
|
||||||
* the first line, then every ``utm_*=value`` pair. Null when there is no
|
* as ``utmCopy`` for click-to-copy, and a one-fact-per-line tooltip — the
|
||||||
* external referer and no UTM tag (a plain direct visit).
|
* 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 = {}) {
|
function refererBadgeOf(referer, titles, utmTags = {}) {
|
||||||
const step = stepOf(referer, titles)
|
const step = stepOf(referer, titles)
|
||||||
const external = step?.external ? step : null
|
const external = step?.external ? step : null
|
||||||
const known = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content']
|
// Compact UTM summary, in display order source, campaign, content, term:
|
||||||
const utm = known.map((k) => utmTags[k]).filter(Boolean).join(' · ')
|
// 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
|
if (!external && !utm) return null
|
||||||
|
const utmCopy = Object.entries(utmTags)
|
||||||
|
.map(([k, value]) => `${k}=${value}`)
|
||||||
|
.join('\n')
|
||||||
return {
|
return {
|
||||||
href: external?.origin || '',
|
href: external?.origin || '',
|
||||||
label: external?.slug || '',
|
label: external?.slug || '',
|
||||||
origin: external?.origin || '',
|
origin: external?.origin || '',
|
||||||
utm,
|
utm,
|
||||||
|
utmCopy,
|
||||||
title: [
|
title: [
|
||||||
...(external ? [external.origin] : []),
|
...(external ? [external.origin] : []),
|
||||||
...Object.entries(utmTags).map(([k, value]) => `${k}=${value}`),
|
...Object.entries(utmTags).map(([k, value]) => `${k}=${value}`),
|
||||||
|
|||||||
@@ -26,14 +26,15 @@
|
|||||||
/* Selection fill for page text and the CodeMirror editors; themes
|
/* Selection fill for page text and the CodeMirror editors; themes
|
||||||
override when the accent tint clashes with accent-colored text. */
|
override when the accent tint clashes with accent-colored text. */
|
||||||
--selection-bg: color-mix(var(--accent) 30%, transparent);
|
--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
|
deliberately NOT themed — the chip sits behind transparent favicons, so
|
||||||
black-on-transparent and white-on-transparent glyphs must both stay
|
black-on-transparent and white-on-transparent glyphs must both stay
|
||||||
legible on every theme (dark greys kill black glyphs, pure white kills
|
legible on every theme (a slight whitening keeps black glyphs readable
|
||||||
white ones); its text likewise stays dark on any theme. */
|
on dark bars without a glaring solid-white chip). Being translucent, it
|
||||||
--badge-bg: #c9d1d9;
|
takes the page's tone, so its text follows the theme's colors. */
|
||||||
--badge-text: #1f2328;
|
--badge-bg: #aaaaaa44;
|
||||||
--badge-muted: #59636e;
|
--badge-text: var(--text);
|
||||||
|
--badge-muted: var(--muted);
|
||||||
/* Code highlighting palette, consumed by pygments.css: complete light and
|
/* Code highlighting palette, consumed by pygments.css: complete light and
|
||||||
dark sets (background included), resolved by light-dark() from the
|
dark sets (background included), resolved by light-dark() from the
|
||||||
used color-scheme. A theme picks a set simply by declaring
|
used color-scheme. A theme picks a set simply by declaring
|
||||||
|
|||||||
Reference in New Issue
Block a user