Right-rail layout fixes (operator-flagged 2026-06-26 — the prior change wasn't
the intended improvement):
- Pin the Related strip to the BOTTOM of the rail: the side becomes a flex
column with a scrolling main area (meta + provenance + tags + suggestions)
and a pinned Related footer (capped at 45% of the rail, scrolls past that).
Related now stays reachable no matter how long Tags/Suggestions run, and
self-collapses (no footer space) when there's nothing to show.
- Remove the 320px suggestions scroll cap (3fcc4ae) — it was a workaround "so
Related stays reachable"; pinning Related is the proper fix, so suggestions
flow in the single main scroll instead of a nested scrollbar.
- Shrink the Download button to a floppy-disk save icon (mdi-content-save); the
meta (dimensions/size/type) + save action now sit as a compact top block
(meta left, icon right). Copy link moves into the adjacent kebab menu.
ImageMetaBar is shared with the Explore center pane, so the compact save
control applies there too (parity). Mobile (<=900px) keeps the single body
scroll — no nested scroll, Related flows at the end.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
113 lines
3.9 KiB
Vue
113 lines
3.9 KiB
Vue
<template>
|
||
<section v-if="img" class="fc-meta" aria-label="Image details">
|
||
<!-- #4a: dimensions / size / type sit as a compact top block alongside a
|
||
small save action (operator-asked 2026-06-26) — meta on the left, the
|
||
download control shrunk to a floppy-disk icon on the right. -->
|
||
<dl class="fc-meta__grid">
|
||
<div v-if="img.width && img.height" class="fc-meta__item">
|
||
<dt>Dimensions</dt><dd>{{ img.width }} × {{ img.height }}</dd>
|
||
</div>
|
||
<div v-if="img.size_bytes" class="fc-meta__item">
|
||
<dt>Size</dt><dd>{{ humanSize(img.size_bytes) }}</dd>
|
||
</div>
|
||
<div v-if="img.mime" class="fc-meta__item">
|
||
<dt>Type</dt><dd>{{ shortType(img.mime) }}</dd>
|
||
</div>
|
||
</dl>
|
||
|
||
<!-- #4b: floppy-disk save icon = download; the kebab menu keeps Copy link.
|
||
Image-to-clipboard is OFF the table on the plain-HTTP origin (rule 95),
|
||
so the secondary action copies the link TEXT, which works everywhere. -->
|
||
<div class="fc-meta__actions">
|
||
<v-btn
|
||
icon="mdi-content-save" size="small" variant="tonal" color="accent"
|
||
aria-label="Download image" title="Download" @click="download"
|
||
/>
|
||
<v-menu location="bottom end">
|
||
<template #activator="{ props }">
|
||
<v-btn
|
||
v-bind="props" icon="mdi-dots-vertical" size="small" variant="text"
|
||
aria-label="More download options"
|
||
/>
|
||
</template>
|
||
<v-list density="compact">
|
||
<v-list-item prepend-icon="mdi-link-variant" title="Copy link" @click="copyLink" />
|
||
<v-list-item prepend-icon="mdi-content-save" title="Download" @click="download" />
|
||
</v-list>
|
||
</v-menu>
|
||
</div>
|
||
</section>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed } from 'vue'
|
||
import { useModalStore } from '../../stores/modal.js'
|
||
import { copyText } from '../../utils/clipboard.js'
|
||
import { toast } from '../../utils/toast.js'
|
||
|
||
// `image` lets a non-modal surface (the Explore workspace) render the same
|
||
// meta + download block for its anchor. Defaults to the modal store's current
|
||
// image so the image modal is unchanged.
|
||
const props = defineProps({ image: { type: Object, default: null } })
|
||
const modal = useModalStore()
|
||
const img = computed(() => props.image ?? modal.current)
|
||
|
||
function humanSize (bytes) {
|
||
const units = ['B', 'KB', 'MB', 'GB']
|
||
let n = bytes
|
||
let i = 0
|
||
while (n >= 1024 && i < units.length - 1) { n /= 1024; i++ }
|
||
return `${n < 10 && i > 0 ? n.toFixed(1) : Math.round(n)} ${units[i]}`
|
||
}
|
||
function shortType (mime) { return mime.split('/')[1]?.toUpperCase() || mime }
|
||
|
||
function absoluteUrl () {
|
||
const origin = typeof window !== 'undefined' ? window.location.origin : ''
|
||
return origin + img.value.image_url
|
||
}
|
||
|
||
function download () {
|
||
// Same-origin /images/* link — the download attribute lets the browser save
|
||
// the original (filename derived from the path) instead of navigating to it.
|
||
const a = document.createElement('a')
|
||
a.href = img.value.image_url
|
||
a.setAttribute('download', '')
|
||
document.body.appendChild(a)
|
||
a.click()
|
||
a.remove()
|
||
}
|
||
|
||
async function copyLink () {
|
||
try {
|
||
await copyText(absoluteUrl())
|
||
toast({ text: 'Link copied to clipboard', type: 'success' })
|
||
} catch {
|
||
toast({ text: 'Could not copy the link', type: 'error' })
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<style scoped>
|
||
.fc-meta {
|
||
padding: 14px 16px 0;
|
||
display: flex; align-items: flex-start; gap: 12px;
|
||
}
|
||
.fc-meta__grid {
|
||
flex: 1 1 auto; min-width: 0;
|
||
display: flex; flex-wrap: wrap; gap: 4px 18px; margin: 0;
|
||
}
|
||
.fc-meta__actions {
|
||
flex: 0 0 auto;
|
||
display: flex; align-items: center; gap: 2px;
|
||
}
|
||
.fc-meta__item { display: flex; flex-direction: column; }
|
||
.fc-meta__item dt {
|
||
font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;
|
||
color: rgb(var(--v-theme-on-surface-variant));
|
||
}
|
||
.fc-meta__item dd {
|
||
margin: 0; font-size: 13px; font-variant-numeric: tabular-nums;
|
||
color: rgb(var(--v-theme-on-surface));
|
||
}
|
||
</style>
|