fix(ui): purpose-built mobile layout for subscriptions hub
Vuetify's auto card-stack was too verbose (one subscription filled the whole phone screen) and the expanded sources still needed lateral scroll. Replace it below 600px (useDisplay) with a custom compact-card list: each subscription is a 2-line card (name + health + expand chevron, then platform chips + sources count + last activity) so several fit per screen. Expanding shows the action row + each source as a STACKED SourceCard (new) — platform/url/enabled/last/ next/errors/actions laid out vertically, no horizontal scroll. The mobile cards drive the same selected/expanded key arrays as the desktop data-table, so selection and bulk actions are unchanged. Desktop keeps the v-data-table. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
<template>
|
||||
<div class="fc-source-card">
|
||||
<div class="fc-source-card__top">
|
||||
<SourceHealthDot :source="source" :warning-threshold="warningThreshold" />
|
||||
<v-chip size="x-small" variant="tonal" label>{{ source.platform }}</v-chip>
|
||||
<v-spacer />
|
||||
<v-switch
|
||||
:model-value="source.enabled"
|
||||
density="compact" hide-details color="accent"
|
||||
@click.stop
|
||||
@update:model-value="onToggleEnabled"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<a
|
||||
:href="source.url" target="_blank" rel="noopener"
|
||||
class="fc-source-card__url" @click.stop
|
||||
>{{ source.url }}</a>
|
||||
|
||||
<div class="fc-source-card__meta">
|
||||
<span>Last {{ formatRelative(source.last_checked_at) }}</span>
|
||||
<span>Next {{ formatRelative(source.next_check_at, { future: true }) }}</span>
|
||||
<v-chip
|
||||
v-if="(source.consecutive_failures || 0) > 0"
|
||||
size="x-small" color="error" variant="tonal" label
|
||||
>{{ source.consecutive_failures }} err</v-chip>
|
||||
<v-chip
|
||||
v-else-if="(source.backfill_runs_remaining || 0) > 0"
|
||||
size="x-small" color="info" variant="tonal" label
|
||||
>backfill ({{ source.backfill_runs_remaining }}×)</v-chip>
|
||||
</div>
|
||||
|
||||
<div class="fc-source-card__actions">
|
||||
<v-btn
|
||||
size="x-small" variant="text" :loading="checking"
|
||||
@click.stop="$emit('check', source)"
|
||||
>
|
||||
<v-icon>mdi-play</v-icon>
|
||||
<v-tooltip activator="parent" location="top">Check now</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="x-small" variant="text"
|
||||
:disabled="(source.backfill_runs_remaining || 0) > 0"
|
||||
@click.stop="$emit('backfill', source)"
|
||||
>
|
||||
<v-icon>mdi-magnify-scan</v-icon>
|
||||
<v-tooltip activator="parent" location="top">Deep scan</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn size="x-small" variant="text" @click.stop="$emit('edit', source)">
|
||||
<v-icon>mdi-pencil</v-icon>
|
||||
<v-tooltip activator="parent" location="top">Edit</v-tooltip>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
size="x-small" variant="text" color="error"
|
||||
@click.stop="$emit('remove', source)"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
<v-tooltip activator="parent" location="top">Remove</v-tooltip>
|
||||
</v-btn>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import SourceHealthDot from './SourceHealthDot.vue'
|
||||
import { formatRelative } from '../../utils/date.js'
|
||||
|
||||
// Mobile-stacked equivalent of SourceRow (the desktop <tr>) — same data and
|
||||
// emits, but laid out vertically so the wide source columns never force the
|
||||
// lateral scroll the operator flagged on phones.
|
||||
const props = defineProps({
|
||||
source: { type: Object, required: true },
|
||||
checking: { type: Boolean, default: false },
|
||||
warningThreshold: { type: Number, default: 5 },
|
||||
})
|
||||
const emit = defineEmits(['edit', 'remove', 'toggle', 'check', 'backfill'])
|
||||
|
||||
function onToggleEnabled(value) {
|
||||
emit('toggle', { source: props.source, enabled: value })
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.fc-source-card {
|
||||
padding: 8px 10px;
|
||||
border: 1px solid rgb(var(--v-theme-surface-light));
|
||||
border-radius: 6px;
|
||||
background: rgb(var(--v-theme-surface));
|
||||
display: flex; flex-direction: column; gap: 6px;
|
||||
}
|
||||
.fc-source-card__top { display: flex; align-items: center; gap: 8px; }
|
||||
.fc-source-card__url {
|
||||
color: rgb(var(--v-theme-on-surface-variant));
|
||||
text-decoration: none;
|
||||
font-size: 0.8rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
.fc-source-card__url:hover { color: rgb(var(--v-theme-accent)); }
|
||||
.fc-source-card__meta {
|
||||
display: flex; flex-wrap: wrap; align-items: center; gap: 6px 12px;
|
||||
font-size: 0.78rem; color: rgb(var(--v-theme-on-surface-variant));
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
.fc-source-card__actions {
|
||||
display: flex; gap: 2px; justify-content: flex-end;
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user