Files
FabledCurator/frontend/src/components/common/MaintenanceTile.vue
T
bvandeusen 3d6201734c
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 17s
CI / backend-lint-and-test (push) Successful in 26s
CI / integration (push) Successful in 3m25s
fix(settings): maintenance tiles start collapsed; remember manual open state
GpuAgentCard was hardcoded :open=true, HeadsCard opened whenever any head
existed, TagEvalCard whenever a persisted run existed — so a fresh Settings
load greeted the operator with several tiles already expanded. All three now
force-open only while their task is actually running (the #877 resurface
behavior on the busy-driven tiles is untouched).

MaintenanceTile additionally persists MANUAL expand/collapse per tile in
localStorage, so the section reloads the way the operator left it; a forced
open while a task runs stays transient and is never saved as a preference.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CDgx8bQS5YrGRK76v8HUnM
2026-07-01 22:28:52 -04:00

133 lines
3.8 KiB
Vue

<!--
Compact, expandable maintenance/cleanup action tile. Collapsed it shows just an
icon + short title + one-line purpose, so a section of these tiles in a grid is
scannable at a glance; clicking the header expands the full controls / preview /
result UI inline (the operator opted for "compact tiles in a grid, detail on
expand", 2026-06-18, replacing the old long stack of full-width cards).
Usage: wrap a card's action body in the default slot; pass icon/title/blurb.
`destructive` tints the icon error-red for delete actions. `open` can be forced
(e.g. keep a running task's tile expanded). Manual expand/collapse persists per
tile in localStorage, so the page reloads the way the operator left it.
Keyboard accessible: the header is a real <button> with aria-expanded + focus
ring.
-->
<template>
<v-card class="fc-tile" :class="{ 'fc-tile--open': isOpen }">
<button
type="button"
class="fc-tile__head"
:aria-expanded="isOpen"
@click="toggle"
>
<v-icon
:icon="icon"
:color="destructive ? 'error' : 'accent'"
class="fc-tile__icon"
/>
<span class="fc-tile__text">
<span class="fc-tile__title">{{ title }}</span>
<span class="fc-tile__blurb">{{ blurb }}</span>
</span>
<v-icon
:icon="isOpen ? 'mdi-chevron-up' : 'mdi-chevron-down'"
class="fc-tile__chev"
/>
</button>
<v-expand-transition>
<div v-show="isOpen" class="fc-tile__body">
<slot />
</div>
</v-expand-transition>
</v-card>
</template>
<script setup>
import { computed, ref, watch } from 'vue'
const props = defineProps({
icon: { type: String, required: true },
title: { type: String, required: true },
blurb: { type: String, default: '' },
destructive: { type: Boolean, default: false },
// Force-open (e.g. a tile whose task is mid-run). When set, the operator can
// still collapse it locally, but a change to `open` re-applies.
open: { type: Boolean, default: false },
})
// Only MANUAL toggles are saved (keyed by tile title): a forced `open` while a
// task is mid-run is transient state, not a preference — persisting it would
// resurrect the "several tiles open by default" bug this replaces. When the
// force clears, the tile falls back to the operator's saved choice.
const storeKey = `fc.tile.${props.title}`
function savedOpen() {
try { return localStorage.getItem(storeKey) === '1' } catch { return false }
}
const local = ref(props.open || savedOpen())
watch(() => props.open, (v) => { local.value = v || savedOpen() })
const isOpen = computed(() => local.value)
function toggle() {
local.value = !local.value
try { localStorage.setItem(storeKey, local.value ? '1' : '0') } catch { /* non-fatal */ }
}
</script>
<style scoped>
.fc-tile {
border-radius: 8px;
overflow: hidden;
}
.fc-tile__head {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 12px 14px;
background: transparent;
border: none;
text-align: left;
cursor: pointer;
color: inherit;
}
.fc-tile__head:hover {
background: rgba(var(--v-theme-on-surface), 0.04);
}
.fc-tile__head:focus-visible {
outline: 2px solid rgb(var(--v-theme-accent));
outline-offset: -2px;
}
.fc-tile__icon {
flex: 0 0 auto;
}
.fc-tile__text {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1 1 auto;
}
.fc-tile__title {
font-weight: 600;
font-size: 0.95rem;
line-height: 1.2;
}
.fc-tile__blurb {
font-size: 0.8rem;
line-height: 1.25;
color: rgb(var(--v-theme-on-surface-variant));
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.fc-tile--open .fc-tile__blurb {
white-space: normal;
}
.fc-tile__chev {
flex: 0 0 auto;
color: rgb(var(--v-theme-on-surface-variant));
}
.fc-tile__body {
padding: 0 14px 14px;
}
</style>