fix(modal): pin Related to rail bottom, floppy-disk download, drop suggestions cap
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>
This commit is contained in:
@@ -1,7 +1,8 @@
|
|||||||
<template>
|
<template>
|
||||||
<section v-if="img" class="fc-meta" aria-label="Image details">
|
<section v-if="img" class="fc-meta" aria-label="Image details">
|
||||||
<!-- #4a: dimensions / size / type — already on the payload, shown nowhere
|
<!-- #4a: dimensions / size / type sit as a compact top block alongside a
|
||||||
until now. -->
|
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">
|
<dl class="fc-meta__grid">
|
||||||
<div v-if="img.width && img.height" class="fc-meta__item">
|
<div v-if="img.width && img.height" class="fc-meta__item">
|
||||||
<dt>Dimensions</dt><dd>{{ img.width }} × {{ img.height }}</dd>
|
<dt>Dimensions</dt><dd>{{ img.width }} × {{ img.height }}</dd>
|
||||||
@@ -14,21 +15,27 @@
|
|||||||
</div>
|
</div>
|
||||||
</dl>
|
</dl>
|
||||||
|
|
||||||
<!-- #4b: split Download — default Download, chevron → Copy link. Image-to-
|
<!-- #4b: floppy-disk save icon = download; the kebab menu keeps Copy link.
|
||||||
clipboard is OFF the table on the plain-HTTP origin (rule 95), so the
|
Image-to-clipboard is OFF the table on the plain-HTTP origin (rule 95),
|
||||||
secondary action copies the link TEXT, which works everywhere. -->
|
so the secondary action copies the link TEXT, which works everywhere. -->
|
||||||
<v-btn-group divided density="comfortable" variant="tonal" color="accent" rounded="pill">
|
<div class="fc-meta__actions">
|
||||||
<v-btn prepend-icon="mdi-download" @click="download">Download</v-btn>
|
<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">
|
<v-menu location="bottom end">
|
||||||
<template #activator="{ props }">
|
<template #activator="{ props }">
|
||||||
<v-btn v-bind="props" icon="mdi-chevron-down" aria-label="More download options" />
|
<v-btn
|
||||||
|
v-bind="props" icon="mdi-dots-vertical" size="small" variant="text"
|
||||||
|
aria-label="More download options"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
<v-list density="compact">
|
<v-list density="compact">
|
||||||
<v-list-item prepend-icon="mdi-link-variant" title="Copy link" @click="copyLink" />
|
<v-list-item prepend-icon="mdi-link-variant" title="Copy link" @click="copyLink" />
|
||||||
<v-list-item prepend-icon="mdi-download" title="Download" @click="download" />
|
<v-list-item prepend-icon="mdi-content-save" title="Download" @click="download" />
|
||||||
</v-list>
|
</v-list>
|
||||||
</v-menu>
|
</v-menu>
|
||||||
</v-btn-group>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -83,11 +90,16 @@ async function copyLink () {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.fc-meta {
|
.fc-meta {
|
||||||
padding: 14px 16px 0;
|
padding: 14px 16px 0;
|
||||||
display: flex; flex-direction: column; gap: 10px;
|
display: flex; align-items: flex-start; gap: 12px;
|
||||||
}
|
}
|
||||||
.fc-meta__grid {
|
.fc-meta__grid {
|
||||||
|
flex: 1 1 auto; min-width: 0;
|
||||||
display: flex; flex-wrap: wrap; gap: 4px 18px; margin: 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 { display: flex; flex-direction: column; }
|
||||||
.fc-meta__item dt {
|
.fc-meta__item dt {
|
||||||
font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;
|
font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;
|
||||||
|
|||||||
@@ -73,12 +73,17 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside v-if="modal.current" class="fc-viewer__side">
|
<aside v-if="modal.current" class="fc-viewer__side">
|
||||||
<ImageMetaBar />
|
<!-- Meta + provenance + tags + suggestions scroll together here… -->
|
||||||
<ProvenancePanel />
|
<div class="fc-viewer__side-main">
|
||||||
<TagPanel />
|
<ImageMetaBar />
|
||||||
<!-- Non-blocking: fetches its own similar set after the modal is up;
|
<ProvenancePanel />
|
||||||
collapses silently if empty/slow/failed (see RelatedStrip). -->
|
<TagPanel />
|
||||||
<RelatedStrip />
|
</div>
|
||||||
|
<!-- …while Related is PINNED to the bottom of the rail so it stays
|
||||||
|
reachable no matter how long Tags/Suggestions run (operator-asked
|
||||||
|
2026-06-26). Non-blocking: fetches its own similar set; collapses
|
||||||
|
silently (and takes no footer space) if empty/slow/failed. -->
|
||||||
|
<RelatedStrip class="fc-viewer__related" />
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -309,6 +314,18 @@ function nextFrame() {
|
|||||||
width: var(--fc-side-w); flex-shrink: 0;
|
width: var(--fc-side-w); flex-shrink: 0;
|
||||||
background: rgb(var(--v-theme-surface));
|
background: rgb(var(--v-theme-surface));
|
||||||
border-left: 1px solid rgb(var(--v-theme-surface-light));
|
border-left: 1px solid rgb(var(--v-theme-surface-light));
|
||||||
|
/* Flex column: a scrolling main area + a pinned Related footer. */
|
||||||
|
display: flex; flex-direction: column; min-height: 0;
|
||||||
|
}
|
||||||
|
.fc-viewer__side-main {
|
||||||
|
flex: 1 1 auto; min-height: 0; overflow-y: auto;
|
||||||
|
}
|
||||||
|
.fc-viewer__related {
|
||||||
|
/* Pinned to the bottom; capped so a tall strip can't swallow the rail —
|
||||||
|
it scrolls internally past the cap. RelatedStrip's own border-top draws
|
||||||
|
the divider; it self-collapses (no space) when there's nothing to show. */
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-height: 45%;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -339,6 +356,10 @@ function nextFrame() {
|
|||||||
border-left: none;
|
border-left: none;
|
||||||
border-top: 1px solid rgb(var(--v-theme-surface-light));
|
border-top: 1px solid rgb(var(--v-theme-surface-light));
|
||||||
}
|
}
|
||||||
|
/* The whole body scrolls on mobile — don't nest scrolls or pin Related; let
|
||||||
|
the rail flow naturally (Related lands at the end). */
|
||||||
|
.fc-viewer__side-main { flex: none; overflow: visible; }
|
||||||
|
.fc-viewer__related { max-height: none; overflow: visible; }
|
||||||
/* Re-center the prev/next arrows over the 55vh image band (their base
|
/* Re-center the prev/next arrows over the 55vh image band (their base
|
||||||
top:50% would land on the scrolling panel); next uses the full width
|
top:50% would land on the scrolling panel); next uses the full width
|
||||||
now that the panel is below, not beside. Close + integrity badge keep
|
now that the panel is below, not beside. Close + integrity badge keep
|
||||||
|
|||||||
@@ -12,10 +12,9 @@
|
|||||||
No suggestions above threshold.
|
No suggestions above threshold.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Scroll-capped so a long suggestion set (the General bucket can run to
|
<!-- Flows in the rail's main scroll area; Related is pinned to the bottom
|
||||||
dozens) scrolls WITHIN this box instead of stretching the right rail
|
of the rail (ImageViewer side layout), so a long suggestion set no
|
||||||
and shoving the Related strip below the fold. Mirrors the
|
longer needs an internal scroll cap to keep Related reachable. -->
|
||||||
ProvenancePanel cards/attachments treatment. -->
|
|
||||||
<div v-else class="fc-suggestions__list">
|
<div v-else class="fc-suggestions__list">
|
||||||
<SuggestionsCategoryGroup
|
<SuggestionsCategoryGroup
|
||||||
v-for="cat in peopleCats" :key="cat"
|
v-for="cat in peopleCats" :key="cat"
|
||||||
@@ -137,13 +136,9 @@ async function onRemoveAlias(s) {
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
.fc-suggestions__list {
|
.fc-suggestions__list {
|
||||||
/* Cap the suggestion run so it scrolls internally rather than pushing the
|
/* No internal scroll cap: the rail now scrolls suggestions in its single
|
||||||
Related strip off the bottom of the rail. Hairline scrollbar matching
|
main scroll area while Related is pinned to the bottom (ImageViewer side
|
||||||
ProvenancePanel's capped regions. */
|
layout), so suggestions flow naturally without a nested scrollbar. */
|
||||||
max-height: 320px;
|
|
||||||
overflow-y: auto;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: rgb(var(--v-theme-surface-light)) transparent;
|
|
||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
.fc-suggestions__skeleton { display: flex; flex-direction: column; gap: 8px; }
|
.fc-suggestions__skeleton { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
|||||||
Reference in New Issue
Block a user