Files
FabledCurator/frontend/src/components/settings/SystemStatsCards.vue
T
bvandeusen 6104452d2e
CI / lint (push) Successful in 2s
CI / frontend-build (push) Successful in 25s
CI / backend-lint-and-test (push) Successful in 35s
CI / integration (push) Successful in 3m49s
feat(deps): vuetify 3→4 (batch 4 phase B, #1449)
Vuetify 4 is a Material-Design-3 styling refresh with revert snippets, not a
component-API overhaul. FC's exposure was small:

- Bump vuetify ^4, vite-plugin-vuetify ^2.1.0, vue ^3.5, engines node>=24.
- Restore the dropped global CSS reset (minimal reset from the upgrade guide) in
  Vuetify's own low-precedence reset layer, so FC's margin-zeroing assumptions hold.
- v-row prop→utility: 'dense' → density=compact (×3), align=center → class=align-center.
- v-snackbar: multi-line removed → min-height=68.
- v-autocomplete #item slot: item→internalItem (item now aliases raw) in TagPicker
  + GalleryFilterBar (item.raw.* → internalItem.raw.*).

ACCEPTED (cosmetic, operator reviews live per plan #158): MD3 typography
(text-body-2 ×73), non-uppercase buttons (v4 dropped the uppercase default),
MD3 elevation. CI verifies BUILD only — the LOOK is the live-review pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-13 13:35:01 -04:00

66 lines
1.9 KiB
Vue

<template>
<v-row density="compact">
<v-col v-for="card in cards" :key="card.label" cols="12" sm="6" md="4" lg="3" xl="2">
<v-card class="fc-stat">
<v-card-text>
<div class="fc-stat__label text-caption">{{ card.label }}</div>
<div class="fc-stat__value">{{ card.value }}</div>
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
stats: { type: Object, default: null }
})
function formatBytes(bytes) {
if (!bytes) return '0 B'
const units = ['B', 'KiB', 'MiB', 'GiB', 'TiB']
let i = 0
let value = bytes
while (value >= 1024 && i < units.length - 1) { value /= 1024; i++ }
return `${value.toFixed(value >= 100 || i === 0 ? 0 : 1)} ${units[i]}`
}
const cards = computed(() => {
const s = props.stats
if (!s) return [
{ label: 'Total images', value: '—' },
{ label: 'Total tags', value: '—' },
{ label: 'Storage used', value: '—' },
{ label: 'Subscriptions', value: '—' },
{ label: 'Pending tasks', value: '—' },
{ label: 'Failed tasks', value: '—' }
]
return [
{ label: 'Total images', value: s.total_images.toLocaleString() },
{ label: 'Total tags', value: s.total_tags.toLocaleString() },
{ label: 'Storage used', value: formatBytes(s.storage_bytes) },
{ label: 'Subscriptions', value: (s.subscription_count ?? 0).toLocaleString() },
{ label: 'Pending', value: (s.tasks.pending + s.tasks.queued).toLocaleString() },
{ label: 'Failed', value: s.tasks.failed.toLocaleString() }
]
})
</script>
<style scoped>
.fc-stat__label {
color: rgb(var(--v-theme-on-surface-variant, var(--v-theme-on-surface)));
text-transform: uppercase;
letter-spacing: 0.05em;
font-weight: 500;
}
.fc-stat__value {
font-family: 'Fraunces', Georgia, serif;
font-size: 28px;
font-weight: 500;
color: rgb(var(--v-theme-on-surface));
margin-top: 4px;
}
</style>