refactor(ui): CardHeading primitive for icon+title card/dialog headings (DRY pattern sweep)
CI / lint (push) Successful in 3s
CI / frontend-build (push) Successful in 21s
CI / backend-lint-and-test (push) Successful in 32s
CI / integration (push) Successful in 3m10s

The icon+title v-card-title heading (d-flex align-center + gap + <v-icon size=small> +
<span>) was hand-rolled identically in 13 cards/dialogs (15 heading instances).
Consolidate to <CardHeading icon title> (components/common) with an iconColor
prop (error headings) and a default slot for trailing content (spacer+actions,
inline status chip). Adopted everywhere the pattern appears — all-or-nothing per
the hardened DRY process.

Over-DRY guard: plain text-only <v-card-title> one-liners are NOT this pattern
and stay; DownloadDetailModal leads with a status CHIP (not an icon), a different
concept, left alone. §8b: the only remaining d-flex align-center v-card-title is
that intentional variant. Catalog updated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 23:22:01 -04:00
parent 4854d74c5a
commit 9deebfa133
14 changed files with 66 additions and 59 deletions
@@ -0,0 +1,25 @@
<template>
<!-- Canonical card/dialog heading: an optional leading icon + the title, with
a default slot for trailing content (a v-spacer + actions, or an inline
status chip). The icon+title v-card-title row was hand-rolled identically
in 14 cards/dialogs (DRY pattern sweep 2026-06-09). Plain text-only
v-card-titles are NOT this pattern and keep using <v-card-title> directly. -->
<v-card-title class="d-flex align-center fc-card-heading">
<v-icon v-if="icon" :icon="icon" :color="iconColor || undefined" size="small" />
<span>{{ title }}</span>
<slot />
</v-card-title>
</template>
<script setup>
defineProps({
icon: { type: String, default: '' },
title: { type: String, default: '' },
// e.g. 'error' for danger/error headings; '' keeps the default text color.
iconColor: { type: String, default: '' },
})
</script>
<style scoped>
.fc-card-heading { gap: 10px; }
</style>
@@ -2,14 +2,15 @@
<v-dialog :model-value="modelValue" max-width="900"
@update:model-value="$emit('update:modelValue', $event)">
<v-card>
<v-card-title class="d-flex align-center" style="gap: 12px;">
<v-icon icon="mdi-alert-circle-outline" color="error" />
<span>{{ displayTitle }}</span>
<CardHeading
icon="mdi-alert-circle-outline" icon-color="error"
:title="displayTitle"
>
<v-spacer />
<v-btn icon variant="text" size="small" @click="close">
<v-icon>mdi-close</v-icon>
</v-btn>
</v-card-title>
</CardHeading>
<v-card-text>
<dl v-if="contextRows.length" class="fc-err-context">
<template v-for="(row, idx) in contextRows" :key="idx">
@@ -37,6 +38,7 @@ import { toast } from '../../utils/toast.js'
import { computed, ref, watch } from 'vue'
import { copyText } from '../../utils/clipboard.js'
import CardHeading from './CardHeading.vue'
const props = defineProps({
modelValue: { type: Boolean, default: false },