feat(artist-view): ArtistHeader — sticky frosted bar (top:64px) matching TopNav recipe, hosts name + image/post stats + tab strip

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-26 15:59:46 -04:00
parent 2e8d7c960c
commit a36f72b383
@@ -0,0 +1,116 @@
<template>
<header class="fc-artist-header">
<div class="fc-artist-header__left">
<h1 class="fc-artist-header__name">{{ name }}</h1>
<span v-if="stats" class="fc-artist-header__stats">{{ stats }}</span>
</div>
<v-tabs
:model-value="modelValue"
color="accent"
density="compact"
class="fc-artist-header__tabs"
@update:model-value="$emit('update:modelValue', $event)"
>
<v-tab value="posts">
Posts
<span v-if="postCount != null" class="fc-artist-header__tab-count">
({{ postCount }})
</span>
</v-tab>
<v-tab value="gallery">
Gallery
<span v-if="imageCount != null" class="fc-artist-header__tab-count">
({{ imageCount }})
</span>
</v-tab>
<v-tab value="management">Management</v-tab>
</v-tabs>
</header>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
name: { type: String, required: true },
imageCount: { type: Number, default: null },
postCount: { type: Number, default: null },
lastAdded: { type: String, default: null },
modelValue: { type: String, required: true },
})
defineEmits(['update:modelValue'])
const stats = computed(() => {
const parts = []
if (props.imageCount != null) {
parts.push(`${props.imageCount} image${props.imageCount === 1 ? '' : 's'}`)
}
if (props.lastAdded) {
parts.push(`last added ${props.lastAdded.slice(0, 10)}`)
}
return parts.join(' · ')
})
</script>
<style scoped>
/* Matches TopNav.vue's frosted recipe exactly — top:64px parks it under
the 64px-tall TopNav with no visible seam. */
.fc-artist-header {
position: sticky;
top: 64px;
z-index: 4;
display: flex;
align-items: center;
gap: 1rem;
padding: 0.5rem 1rem;
background: linear-gradient(
to bottom,
rgba(20, 23, 26, 0.92) 0%,
rgba(20, 23, 26, 0.65) 60%,
rgba(20, 23, 26, 0) 100%
);
backdrop-filter: blur(2px);
-webkit-backdrop-filter: blur(2px);
}
.fc-artist-header__left {
flex: 1 1 0;
min-width: 0;
display: flex;
align-items: baseline;
gap: 12px;
overflow: hidden;
}
.fc-artist-header__name {
font-family: 'Fraunces', Georgia, serif;
font-size: 24px;
font-weight: 500;
margin: 0;
color: rgb(var(--v-theme-on-surface));
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fc-artist-header__stats {
font-size: 13px;
color: rgb(var(--v-theme-on-surface-variant));
font-variant-numeric: tabular-nums;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.fc-artist-header__tabs {
flex: 0 0 auto;
}
.fc-artist-header__tab-count {
margin-left: 4px;
font-size: 12px;
color: rgb(var(--v-theme-on-surface-variant));
font-variant-numeric: tabular-nums;
}
</style>