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:
@@ -67,37 +67,29 @@
|
||||
@click="openPicker(ch.id)"
|
||||
>Add pages</v-btn>
|
||||
|
||||
<v-menu location="bottom end">
|
||||
<template #activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props" size="small" variant="text"
|
||||
icon="mdi-dots-vertical" title="Part actions"
|
||||
/>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item
|
||||
prepend-icon="mdi-chevron-up" title="Move up"
|
||||
:disabled="ci === 0"
|
||||
@click="store.moveChapter(ch.id, -1)"
|
||||
/>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-chevron-down" title="Move down"
|
||||
:disabled="ci === store.chapters.length - 1"
|
||||
@click="store.moveChapter(ch.id, 1)"
|
||||
/>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-arrow-collapse-up" title="Merge into previous"
|
||||
:disabled="ci === 0"
|
||||
@click="store.mergeChapter(ch.id, store.chapters[ci - 1].id)"
|
||||
/>
|
||||
<v-divider />
|
||||
<v-list-item
|
||||
prepend-icon="mdi-delete-outline" title="Delete part"
|
||||
base-color="error"
|
||||
@click="confirmDelete(ch)"
|
||||
/>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<KebabMenu size="small" label="Part actions">
|
||||
<v-list-item
|
||||
prepend-icon="mdi-chevron-up" title="Move up"
|
||||
:disabled="ci === 0"
|
||||
@click="store.moveChapter(ch.id, -1)"
|
||||
/>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-chevron-down" title="Move down"
|
||||
:disabled="ci === store.chapters.length - 1"
|
||||
@click="store.moveChapter(ch.id, 1)"
|
||||
/>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-arrow-collapse-up" title="Merge into previous"
|
||||
:disabled="ci === 0"
|
||||
@click="store.mergeChapter(ch.id, store.chapters[ci - 1].id)"
|
||||
/>
|
||||
<v-divider />
|
||||
<v-list-item
|
||||
prepend-icon="mdi-delete-outline" title="Delete part"
|
||||
base-color="error"
|
||||
@click="confirmDelete(ch)"
|
||||
/>
|
||||
</KebabMenu>
|
||||
</header>
|
||||
|
||||
<div class="fc-part__statedrow">
|
||||
@@ -230,6 +222,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { useSeriesManageStore, moveItem } from '../stores/seriesManage.js'
|
||||
import { useInfiniteScroll } from '../composables/useInfiniteScroll.js'
|
||||
import KebabMenu from '../components/common/KebabMenu.vue'
|
||||
|
||||
const route = useRoute()
|
||||
const store = useSeriesManageStore()
|
||||
|
||||
@@ -60,27 +60,20 @@
|
||||
<span v-if="s.has_gap" class="fc-sbcard__gap" title="Has a missing-page gap">
|
||||
<v-icon size="x-small">mdi-alert-outline</v-icon> gap
|
||||
</span>
|
||||
<v-menu>
|
||||
<template #activator="{ props: menuProps }">
|
||||
<v-btn
|
||||
v-bind="menuProps"
|
||||
icon="mdi-dots-vertical" size="x-small" variant="flat"
|
||||
class="fc-sbcard__kebab" :aria-label="`Actions for ${s.name}`"
|
||||
@click.stop
|
||||
/>
|
||||
</template>
|
||||
<v-list density="compact">
|
||||
<v-list-item prepend-icon="mdi-pencil" @click.stop="openRename(s)">
|
||||
<v-list-item-title>Rename</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-delete" base-color="error"
|
||||
@click.stop="openDelete(s)"
|
||||
>
|
||||
<v-list-item-title>Delete</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<KebabMenu
|
||||
class="fc-sbcard__kebab" variant="flat"
|
||||
:label="`Actions for ${s.name}`"
|
||||
>
|
||||
<v-list-item prepend-icon="mdi-pencil" @click.stop="openRename(s)">
|
||||
<v-list-item-title>Rename</v-list-item-title>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
prepend-icon="mdi-delete" base-color="error"
|
||||
@click.stop="openDelete(s)"
|
||||
>
|
||||
<v-list-item-title>Delete</v-list-item-title>
|
||||
</v-list-item>
|
||||
</KebabMenu>
|
||||
</div>
|
||||
<div class="fc-sbcard__body">
|
||||
<h3 class="fc-sbcard__name">{{ s.name }}</h3>
|
||||
@@ -210,6 +203,7 @@ import { useRouter } from 'vue-router'
|
||||
import { useSeriesBrowseStore } from '../stores/seriesBrowse.js'
|
||||
import { useSeriesSuggestionsStore } from '../stores/seriesSuggestions.js'
|
||||
import TagRenameDialog from '../components/modal/TagRenameDialog.vue'
|
||||
import KebabMenu from '../components/common/KebabMenu.vue'
|
||||
|
||||
const router = useRouter()
|
||||
const store = useSeriesBrowseStore()
|
||||
|
||||
Reference in New Issue
Block a user