refactor(ui): consolidate 7 hand-rolled kebabs into one KebabMenu (DRY pattern sweep)
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 22s
CI / backend-lint-and-test (push) Successful in 40s
CI / integration (push) Successful in 3m18s

First pattern-consistency DRY pass (process #594). The overflow kebab was
hand-rolled 7 ways in two divergent activator strategies — Pattern A
(#activator + v-bind) which silently breaks inside the teleported image modal
(#711), and Pattern B (manual v-model + activator=parent + open-on-click=false +
z-index 2400) the modal kebabs needed as a workaround.

New <KebabMenu> (components/common) bakes in the modal-safe strategy
UNIVERSALLY, so every kebab works in modal and non-modal contexts — folding the
latent #711-class bug fix into all five Pattern-A sites. Menu items go in the
default slot; variations (size/variant/location/label/min-width) are props.

Adopted across all 7: TagChip, SuggestionItem, TagCard, SeriesView card,
SeriesManageView, BackupRunsTable, SourceActions. Exhaustiveness (§8b):
mdi-dots-vertical now lives only in KebabMenu. Labeled dropdowns / nav menus /
filter popovers are a different concept and left alone. Seeded the pattern
catalog so new code reuses the primitive. Test: KebabMenu renders slot items +
trigger label/glyph + presentational props.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 22:25:11 -04:00
co-authored by Claude Opus 4.8
parent d5d23a92f2
commit c774042a85
9 changed files with 211 additions and 196 deletions
+20 -28
View File
@@ -46,34 +46,25 @@
<v-chip size="x-small" label>{{ card.kind }}</v-chip>
<div class="fc-tagcard__meta-right">
<span class="fc-tagcard__count">{{ card.image_count }}</span>
<v-menu>
<template #activator="{ props: act }">
<v-btn
class="fc-tagcard__menu"
icon="mdi-dots-vertical" size="x-small" variant="text"
v-bind="act" @click.stop
/>
</template>
<v-list density="compact">
<v-list-item
v-if="card.kind === 'character'"
title="Set fandom…"
prepend-icon="mdi-book-open-page-variant"
@click="$emit('set-fandom', card)"
/>
<v-list-item
title="Merge with…"
prepend-icon="mdi-call-merge"
@click="$emit('merge-with', card)"
/>
<v-list-item
title="Delete tag"
prepend-icon="mdi-delete"
base-color="error"
@click="$emit('delete', card)"
/>
</v-list>
</v-menu>
<KebabMenu class="fc-tagcard__menu" :label="`Actions for ${card.name}`">
<v-list-item
v-if="card.kind === 'character'"
title="Set fandom…"
prepend-icon="mdi-book-open-page-variant"
@click="$emit('set-fandom', card)"
/>
<v-list-item
title="Merge with…"
prepend-icon="mdi-call-merge"
@click="$emit('merge-with', card)"
/>
<v-list-item
title="Delete tag"
prepend-icon="mdi-delete"
base-color="error"
@click="$emit('delete', card)"
/>
</KebabMenu>
</div>
</div>
</v-card-text>
@@ -82,6 +73,7 @@
<script setup>
import { ref } from 'vue'
import KebabMenu from '../common/KebabMenu.vue'
const props = defineProps({ card: { type: Object, required: true } })
const emit = defineEmits([