feat(modal): post title is the primary click (artist-scoped) + suggestion rows look like buttons
Two operator-asked modal UX changes (Scribe plan #514): 1. **Post title click → artist-scoped posts feed** - The bold post title in ProvenancePanel is now the primary click target. Click navigates to /posts?post_id=N&artist_id=A; the PostsView already filters on `artist_id`, so the user lands in that creator's stream, not the global one. - The redundant "View post" link is removed. "Show description" stays as the only action link below the meta line. - Title is styled as a button-link: accent color, hover underline, focus ring for keyboard nav (family rule 24 — UI quality bar). 2. **Suggestion rows look like buttons** - SuggestionItem becomes a chip-card: visible border, hover/focus background, unified container for name + score + actions (operator's "nothing visual to unify the buttons to their object" complaint). - Accept is an explicit tonal pill button labeled "Accept" (operator's pick over whole-row-clickable). 3-dot menu retains alias/dismiss, now `variant="outlined"` so it reads as a button. - Score is a fixed-width monospace pill on the right. - "+ new" badge upgraded to a pill chip with accent border so it's visibly an annotation, not part of the tag name.
This commit is contained in:
@@ -18,9 +18,13 @@
|
|||||||
<span class="fc-prov__platform">{{ e.source.platform }}</span>
|
<span class="fc-prov__platform">{{ e.source.platform }}</span>
|
||||||
<span v-if="postDate(e)" class="fc-prov__date">{{ postDate(e) }}</span>
|
<span v-if="postDate(e)" class="fc-prov__date">{{ postDate(e) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="fc-prov__post">
|
<button
|
||||||
|
type="button" class="fc-prov__post"
|
||||||
|
:title="`Open ${postTitle(e)} in the posts feed for ${e.artist.name}`"
|
||||||
|
@click="openPost(e.post.id, e.artist.id)"
|
||||||
|
>
|
||||||
{{ postTitle(e) }}
|
{{ postTitle(e) }}
|
||||||
</div>
|
</button>
|
||||||
<div class="fc-prov__meta">
|
<div class="fc-prov__meta">
|
||||||
<RouterLink :to="`/artist/${e.artist.slug}`">
|
<RouterLink :to="`/artist/${e.artist.slug}`">
|
||||||
by {{ e.artist.name }}
|
by {{ e.artist.name }}
|
||||||
@@ -29,12 +33,8 @@
|
|||||||
· {{ e.post.attachment_count }} files
|
· {{ e.post.attachment_count }} files
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="fc-prov__actions">
|
<div v-if="e.post.description_html" class="fc-prov__actions">
|
||||||
<a href="#" @click.prevent="openPost(e.post.id)">
|
|
||||||
View post
|
|
||||||
</a>
|
|
||||||
<a
|
<a
|
||||||
v-if="e.post.description_html"
|
|
||||||
href="#" @click.prevent="toggleDesc(e.provenance_id)"
|
href="#" @click.prevent="toggleDesc(e.provenance_id)"
|
||||||
>{{ expanded[e.provenance_id] ? 'Hide description ▴' : 'Show description ▾' }}</a>
|
>{{ expanded[e.provenance_id] ? 'Hide description ▴' : 'Show description ▾' }}</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -137,10 +137,16 @@ function postTitle(e) {
|
|||||||
return toPlainText(e.post.title) || `Post ${e.post.external_post_id}`
|
return toPlainText(e.post.title) || `Post ${e.post.external_post_id}`
|
||||||
}
|
}
|
||||||
|
|
||||||
function openPost(postId) {
|
function openPost(postId, artistId) {
|
||||||
// Land on the post in the posts feed (in context), not the gallery
|
// Land on the post in the posts feed (in context), not the gallery
|
||||||
// image grid. Operator-flagged 2026-05-28.
|
// image grid. Scope the feed to this artist so the user lands in
|
||||||
router.push({ path: '/posts', query: { post_id: postId } })
|
// that creator's stream, not the global one — operator-flagged
|
||||||
|
// 2026-06-01. PostsView reads `artist_id` from the query string
|
||||||
|
// (PostsView.vue line ~92) and filters via post_feed_service.
|
||||||
|
router.push({
|
||||||
|
path: '/posts',
|
||||||
|
query: { post_id: postId, artist_id: artistId },
|
||||||
|
})
|
||||||
modal.close()
|
modal.close()
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -163,8 +169,21 @@ function openPost(postId) {
|
|||||||
text-transform: lowercase;
|
text-transform: lowercase;
|
||||||
}
|
}
|
||||||
.fc-prov__post {
|
.fc-prov__post {
|
||||||
font-weight: 700; margin: 4px 0;
|
/* Clickable title — opens the post in the artist-scoped feed
|
||||||
color: rgb(var(--v-theme-on-surface));
|
(operator-flagged 2026-06-01: title IS the primary action, the
|
||||||
|
prior "View post" link was redundant). Styled as a button-link:
|
||||||
|
accent color, underline on hover, focus ring for keyboard nav. */
|
||||||
|
display: block; width: 100%; text-align: left;
|
||||||
|
background: none; border: none; padding: 0;
|
||||||
|
font: inherit; font-weight: 700;
|
||||||
|
margin: 4px 0;
|
||||||
|
color: rgb(var(--v-theme-accent));
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.fc-prov__post:hover { text-decoration: underline; }
|
||||||
|
.fc-prov__post:focus-visible {
|
||||||
|
outline: 2px solid rgb(var(--v-theme-accent));
|
||||||
|
outline-offset: 2px; border-radius: 3px;
|
||||||
}
|
}
|
||||||
.fc-prov__meta {
|
.fc-prov__meta {
|
||||||
font-size: 13px; color: rgb(var(--v-theme-on-surface-variant));
|
font-size: 13px; color: rgb(var(--v-theme-on-surface-variant));
|
||||||
|
|||||||
@@ -1,19 +1,33 @@
|
|||||||
<template>
|
<template>
|
||||||
|
<!-- Chip-card row: visible border + hover/focus state unifies the
|
||||||
|
name, score, and action buttons as one "object" (operator-asked
|
||||||
|
2026-06-01). The row itself is informational; the explicit
|
||||||
|
Accept button + 3-dot menu are the action affordances. -->
|
||||||
<div class="fc-suggestion">
|
<div class="fc-suggestion">
|
||||||
<span class="fc-suggestion__name">
|
<span class="fc-suggestion__name">
|
||||||
{{ suggestion.display_name }}
|
{{ suggestion.display_name }}
|
||||||
<span v-if="suggestion.creates_new_tag" class="fc-suggestion__new"
|
<span v-if="suggestion.creates_new_tag" class="fc-suggestion__new"
|
||||||
title="No matching tag yet — accepting creates it">+new</span>
|
title="No matching tag yet — accepting creates it">+ new</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="fc-suggestion__score">{{ scorePct }}</span>
|
<span class="fc-suggestion__score">{{ scorePct }}</span>
|
||||||
<v-btn
|
<v-btn
|
||||||
icon="mdi-plus" size="x-small" variant="text" color="accent"
|
class="fc-suggestion__accept"
|
||||||
|
size="small" variant="tonal" color="accent"
|
||||||
|
density="compact" rounded="pill"
|
||||||
:aria-label="`Accept ${suggestion.display_name}`"
|
:aria-label="`Accept ${suggestion.display_name}`"
|
||||||
@click="$emit('accept', suggestion)"
|
@click="$emit('accept', suggestion)"
|
||||||
/>
|
>
|
||||||
|
Accept
|
||||||
|
</v-btn>
|
||||||
<v-menu>
|
<v-menu>
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<v-btn icon="mdi-dots-vertical" size="x-small" variant="text" v-bind="props" />
|
<v-btn
|
||||||
|
class="fc-suggestion__menu"
|
||||||
|
icon="mdi-dots-vertical" size="small"
|
||||||
|
variant="outlined" density="compact"
|
||||||
|
:aria-label="`More actions for ${suggestion.display_name}`"
|
||||||
|
v-bind="props"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<v-list density="compact">
|
<v-list density="compact">
|
||||||
<v-list-item @click="$emit('alias', suggestion)">
|
<v-list-item @click="$emit('alias', suggestion)">
|
||||||
@@ -38,17 +52,45 @@ const scorePct = computed(() => `${Math.round(props.suggestion.score * 100)}%`)
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.fc-suggestion {
|
.fc-suggestion {
|
||||||
display: flex; align-items: center; gap: 6px;
|
display: flex; align-items: center; gap: 8px;
|
||||||
padding: 2px 0;
|
padding: 6px 10px; margin-bottom: 4px;
|
||||||
|
background: rgb(var(--v-theme-surface));
|
||||||
|
border: 1px solid rgb(var(--v-theme-surface-light));
|
||||||
|
border-radius: 6px;
|
||||||
|
transition: background 120ms ease, border-color 120ms ease;
|
||||||
|
}
|
||||||
|
.fc-suggestion:hover {
|
||||||
|
background: rgb(var(--v-theme-surface-light));
|
||||||
|
border-color: rgb(var(--v-theme-accent), 0.4);
|
||||||
|
}
|
||||||
|
.fc-suggestion__name {
|
||||||
|
flex: 1; min-width: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
color: rgb(var(--v-theme-on-surface));
|
||||||
|
overflow: hidden; text-overflow: ellipsis; white-space: nowrap;
|
||||||
}
|
}
|
||||||
.fc-suggestion__name { flex: 1; min-width: 0; }
|
|
||||||
.fc-suggestion__new {
|
.fc-suggestion__new {
|
||||||
font-size: 10px; color: rgb(var(--v-theme-accent));
|
display: inline-block;
|
||||||
margin-left: 4px;
|
font-size: 10px; font-weight: 600;
|
||||||
|
color: rgb(var(--v-theme-accent));
|
||||||
|
background: rgba(var(--v-theme-accent), 0.12);
|
||||||
|
border: 1px solid rgb(var(--v-theme-accent), 0.4);
|
||||||
|
padding: 1px 6px; border-radius: 999px;
|
||||||
|
margin-left: 6px;
|
||||||
|
text-transform: uppercase; letter-spacing: 0.04em;
|
||||||
}
|
}
|
||||||
.fc-suggestion__score {
|
.fc-suggestion__score {
|
||||||
|
flex: 0 0 auto; min-width: 38px; text-align: right;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
color: rgb(var(--v-theme-on-surface-variant, var(--v-theme-on-surface)));
|
color: rgb(var(--v-theme-on-surface-variant, var(--v-theme-on-surface)));
|
||||||
font-family: 'JetBrains Mono', monospace;
|
font-family: 'JetBrains Mono', monospace;
|
||||||
}
|
}
|
||||||
|
/* Vuetify's compact density doesn't shrink the tonal button enough
|
||||||
|
for a tight row; clamp the min-width so Accept stays compact. */
|
||||||
|
.fc-suggestion__accept :deep(.v-btn__content) {
|
||||||
|
font-size: 12px; letter-spacing: 0.02em;
|
||||||
|
}
|
||||||
|
.fc-suggestion__menu {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user