3d6201734c
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
133 lines
3.8 KiB
Vue
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>
|