Stack branch labels when a row is tight; fan them out on hover or selection

This commit is contained in:
2026-10-14 22:01:35 +02:00
parent ad4d087aaf
commit 04cf5c5dae
5 changed files with 190 additions and 36 deletions
+109
View File
@@ -0,0 +1,109 @@
<script setup lang="ts">
import { computed } from "vue";
import { shouldStack, stackWidth, PEEK } from "../lib/labels";
import { textWidth } from "../lib/measure";
import type { RefKind } from "../lib/types";
import RefIcon from "./RefIcon.vue";
export interface Label { name: string; short: string; kind: RefKind; isHead: boolean }
const props = defineProps<{
labels: Label[];
color: string;
/** Width of the message text in this row, in pixels. */
messageWidth: number;
/** Width the message and the labels share, in pixels. */
space: number;
/** A selected row shows its stack fanned out, as if hovered. */
selected: boolean;
}>();
const FONT = '11.5px "Inter", "Noto Sans", system-ui, sans-serif';
const MAX = 230;
// Padding, border, icon and the gap after the icon.
const CHROME = 14 + 2 + 12 + 4;
const widths = computed(() =>
props.labels.map((l) =>
Math.min(MAX, Math.ceil(textWidth(l.short, (l.isHead ? "600 " : "") + FONT) + CHROME)),
),
);
const stacked = computed(() => shouldStack(widths.value, props.messageWidth, props.space));
</script>
<template>
<span
class="labels"
:class="{ stacked, open: selected }"
:style="
stacked
? { width: stackWidth(widths) + 'px', '--w': widths[0] + 'px', '--peek': PEEK + 'px', '--space': space + 'px' }
: {}
"
>
<span class="stack" :style="{ '--c': color }">
<span
v-for="(l, i) in labels"
:key="l.name"
class="label"
:class="{ head: l.isHead }"
:style="{ zIndex: labels.length - i }"
:title="l.name"
>
<RefIcon :kind="l.kind" /><span class="label-text">{{ l.short }}</span>
</span>
</span>
</span>
</template>
<style scoped>
.labels { margin-left: auto; flex: 0 1 auto; min-width: 0; position: relative; height: 20px; display: flex; }
.stack { display: flex; gap: 5px; align-items: center; min-width: 0; }
.label {
display: inline-flex;
align-items: center;
gap: 4px;
height: 20px;
padding: 0 7px;
border-radius: 999px;
font-size: 11.5px;
color: var(--c);
border: 1px solid color-mix(in srgb, var(--c) 55%, var(--bg));
/* Solid, so stacked labels do not show through each other. */
background: color-mix(in srgb, var(--c) 16%, var(--bg));
white-space: nowrap;
max-width: 230px;
min-width: 4.5em;
flex: 0 1 auto;
position: relative;
}
.label-text { min-width: 0; overflow: hidden; text-overflow: ellipsis; }
.label.head { flex: none; color: #10121c; background: var(--c); font-weight: 600; }
/* Stacked: every label has the first one's width and hides behind it, all but a sliver. */
.labels.stacked { flex: none; }
.labels.stacked .stack { gap: 0; }
.labels.stacked .label { flex: none; width: var(--w); min-width: 0; transition: margin-left 0.15s; }
.labels.stacked .label + .label { margin-left: calc(var(--peek) - var(--w)); }
/* Hovered or selected: fan the stack out over the message. */
.labels.stacked:hover .stack,
.labels.stacked.open .stack {
position: absolute;
right: 0;
top: 0;
gap: 5px;
padding-left: 10px;
z-index: 50;
max-width: var(--space);
background: linear-gradient(90deg, transparent, var(--row-bg, var(--bg)) 10px);
}
.labels.stacked:hover .label,
.labels.stacked.open .label {
width: auto;
max-width: 230px;
min-width: 4.5em;
flex: 0 1 auto;
margin-left: 0;
}
</style>