refactor(ui): consolidate 7 hand-rolled kebabs into one KebabMenu (DRY pattern sweep)
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:
@@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<!-- Canonical kebab (overflow ⋮) menu — the single primitive every kebab in
|
||||
the app uses (DRY pattern-consistency sweep 2026-06-09). It bakes in the
|
||||
modal-safe activator strategy that the image-modal kebabs needed (#711):
|
||||
a manual v-model toggled by @click.stop, `activator="parent"` for
|
||||
positioning only, `:open-on-click="false"`, and a z-index above the modal
|
||||
(2000) — so a kebab works identically inside a teleported modal and on a
|
||||
plain page. Menu items go in the default slot; close-on-content-click
|
||||
(Vuetify default) closes the menu when one is chosen. -->
|
||||
<span class="fc-kebab">
|
||||
<v-btn
|
||||
:icon="icon"
|
||||
:size="size"
|
||||
:variant="variant"
|
||||
:aria-label="label"
|
||||
@click.stop="open = !open"
|
||||
/>
|
||||
<v-menu
|
||||
v-model="open"
|
||||
activator="parent"
|
||||
:open-on-click="false"
|
||||
:location="location"
|
||||
:z-index="2400"
|
||||
>
|
||||
<v-list density="compact" :min-width="minWidth">
|
||||
<slot />
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
defineProps({
|
||||
icon: { type: String, default: 'mdi-dots-vertical' },
|
||||
size: { type: String, default: 'x-small' },
|
||||
variant: { type: String, default: 'text' },
|
||||
location: { type: String, default: 'bottom end' },
|
||||
// aria-label for the trigger (every kebab should name what it acts on).
|
||||
label: { type: String, default: 'More actions' },
|
||||
minWidth: { type: [String, Number], default: undefined },
|
||||
})
|
||||
|
||||
const open = ref(false)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-kebab { display: inline-flex; align-items: center; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user