Closes #531
This commit is contained in:
@@ -35,7 +35,7 @@ export function collectionTargets(images, { allMatching = false, total = images.
|
||||
* the dialog seam. A stale source (409) calls `onSourceChanged()` after its toast, so the page reloads the listing.
|
||||
*/
|
||||
export function openAddToCollection({ scope, collections, images, trigger = null, allMatching = false, total = images.length,
|
||||
notify = showWorkspaceToast, openInput = openInputDialog, onSourceChanged = null }) {
|
||||
notify = showWorkspaceToast, openInput = openInputDialog, onSourceChanged = null, onAdded = null }) {
|
||||
const { targets, refusal } = collectionTargets(images, { allMatching, total });
|
||||
if (refusal) { notify(refusal); return; }
|
||||
openInput({ scope, trigger, title: ADD_DIALOG_TITLE, label: ADD_DIALOG_LABEL, action: ADD_DIALOG_ACTION,
|
||||
@@ -45,7 +45,7 @@ export function openAddToCollection({ scope, collections, images, trigger = null
|
||||
if (name === null) { notify(ADD_FAILED_TOAST); return; }
|
||||
try {
|
||||
const result = await collections.add({ name }, targets);
|
||||
if (!scope.signal.aborted) notify(addedMessage(result));
|
||||
if (!scope.signal.aborted) { notify(addedMessage(result)); onAdded?.(); }
|
||||
} catch (error) {
|
||||
if (scope.signal.aborted) return;
|
||||
notify(writeFailure(error, ADD_FAILED_TOAST));
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { keepingPlace, publishBarHeight } from './sticky-chrome.mjs';
|
||||
import { createButton } from '../shared/toolkit/core.js';
|
||||
import { imageKey, toggleSelection, selectionLabel } from './images-state.mjs';
|
||||
import { inspectPath, toInspectionRecord } from './inspection-state.mjs';
|
||||
@@ -51,7 +52,7 @@ function el(tag, text, className) {
|
||||
*/
|
||||
export function mountSelectionBar({ scope, api, grid, getItems, focusFallback, onCategorySet, activity = null, onTagged,
|
||||
getListing, getFilter, getCategories, checked: initial = Object.freeze(new Set()), collections = null,
|
||||
collection = null, onCollectionsChanged = null, onSourceChanged = null, onDeleted = null, removedView = null,
|
||||
collection = null, onCollectionsChanged = null, onSourceChanged = null, onCollectionAdded = null, onDeleted = null, removedView = null,
|
||||
onReturn = null }) {
|
||||
let checked = initial;
|
||||
let exporting = false, categorizing = false;
|
||||
@@ -59,6 +60,7 @@ export function mountSelectionBar({ scope, api, grid, getItems, focusFallback, o
|
||||
let anchor = null, allMatching = false, touchMode = false, lastPointer = 'mouse', shiftBox = null;
|
||||
const element = el('div');
|
||||
element.id = 'bulkbar';
|
||||
publishBarHeight(scope, element);
|
||||
const bulk = el('div', undefined, 'bulkbar'), count = el('strong');
|
||||
const selectMatching = createButton('');
|
||||
scope.listen(selectMatching, 'click', () => {
|
||||
@@ -87,7 +89,7 @@ export function mountSelectionBar({ scope, api, grid, getItems, focusFallback, o
|
||||
if (collections) {
|
||||
const addToCollection = createButton('Add to collection');
|
||||
scope.listen(addToCollection, 'click', () => openAddToCollection({ scope, collections, images: checkedImages(),
|
||||
trigger: addToCollection, ...collectionSelection(), onSourceChanged }));
|
||||
trigger: addToCollection, ...collectionSelection(), onSourceChanged, onAdded: onCollectionAdded }));
|
||||
bulk.append(addToCollection);
|
||||
}
|
||||
if (collections && collection) {
|
||||
@@ -124,7 +126,8 @@ export function mountSelectionBar({ scope, api, grid, getItems, focusFallback, o
|
||||
}
|
||||
|
||||
/** The count updates in place: the same bar and `strong`, its text written only when it changes. */
|
||||
function render() {
|
||||
function render() { keepingPlace(element, paintBar); }
|
||||
function paintBar() {
|
||||
if (!checked.size && !allMatching) { touchMode = false; bulk.remove(); return; }
|
||||
paintMode();
|
||||
const listing = getListing();
|
||||
|
||||
@@ -5,6 +5,7 @@ import { layoutImages, visibleImages, gridMetrics, stacksCardInfo } from './imag
|
||||
import { mountFileInspector } from './file-inspector.mjs';
|
||||
import { createInspectionController } from './inspection-state.mjs';
|
||||
import { mountSelectionBar } from './images-selection.mjs';
|
||||
import { scrollToPageTop } from './sticky-chrome.mjs';
|
||||
import { createPromptSender } from './prompt-sender.mjs';
|
||||
import { promptRoles } from './prompt-blocks.mjs';
|
||||
import { createWorkspaceIcon } from './icons.mjs';
|
||||
@@ -301,6 +302,7 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage
|
||||
collection.append(continuation);
|
||||
// After a Load more or Retry press, the index of the first image that batch appends (concept: focus it).
|
||||
let focusAfterLoad = null;
|
||||
let panelHold = false;
|
||||
// The concept's side panel (app.js a426, D20); the Library-embedded gallery has its own page around it.
|
||||
const details = el('aside', undefined, 'detail-panel');
|
||||
if (!embedded) details.id = 'image-inspector';
|
||||
@@ -310,11 +312,13 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage
|
||||
// The checked cards and the concept's selection bar (#249 D8), between the filter row and the gallery layout. The
|
||||
// selection is kept across appended batches and cleared with the collection; the Library-embedded gallery has none.
|
||||
const bar = embedded ? null : mountSelectionBar({scope, api, grid, getItems:() => state?.items ?? [], focusFallback,
|
||||
checked:visit?.checked, onCategorySet:() => { loadCategories(); reset(); }, activity, onTagged:keys => refreshTagged(keys),
|
||||
onDeleted:deleted,
|
||||
checked:visit?.checked, onCategorySet:() => { loadCategories(); reset(); scrollToPageTop(); }, activity,
|
||||
// #531: a finished bulk action shows its result from the top. Tagging and collections keep the selection (D85, D138).
|
||||
onTagged:keys => { refreshTagged(keys); bulkRefreshed(); },
|
||||
onDeleted:() => { deleted(); scrollToPageTop(); },
|
||||
// #498: the removed view's bar holds only Return to library and Clear.
|
||||
removedView:() => filters.removed,
|
||||
onReturn:(images, extra, trigger) => returnSome({images, ...extra, trigger}),
|
||||
onReturn:(images, extra, trigger) => returnSome({images, ...extra, trigger, onDone:scrollToPageTop}),
|
||||
// The Set category field suggests the toolbar's categories (#332 MC13).
|
||||
getCategories:() => categoryNames,
|
||||
// #392 "Select all N matching": the listing's total and its filter, as the server lists them.
|
||||
@@ -325,7 +329,7 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage
|
||||
return filter.root !== undefined && typeof scope === 'string' ? {...filter, root_scope:scope} : filter;
|
||||
},
|
||||
// #331 D138: Add to collection, and Remove from collection on an open collection, which then lists it again.
|
||||
collections, collection:filters.collection || null, onCollectionsChanged:collectionChanged,
|
||||
collections, collection:filters.collection || null, onCollectionsChanged:() => { collectionChanged(); scrollToPageTop(); }, onCollectionAdded:bulkRefreshed,
|
||||
onSourceChanged:refreshInspectionCollection});
|
||||
if (bar) main.append(bar.element);
|
||||
main.append(body);
|
||||
@@ -402,7 +406,10 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage
|
||||
if (event.target.closest('.star-button')) toggleFavorite(api, image);
|
||||
else if (event.target.closest('.image-open')) previewImage(image);
|
||||
else if (action && !openCardTagging({api, scope, image, action, embedded, onSourceChanged:recovery.recover,
|
||||
onRefresh:refreshInspectionCollection})) inspect(image);
|
||||
onRefresh:refreshInspectionCollection})) {
|
||||
inspect(image);
|
||||
bringPanelIntoView();
|
||||
}
|
||||
});
|
||||
|
||||
function renderVisible() {
|
||||
@@ -637,7 +644,19 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage
|
||||
}
|
||||
scope.listen(more, 'click', pressLoad);
|
||||
scope.listen(retry, 'click', () => (state?.error?.code === 'cursor-stale' ? refreshInspectionCollection() : pressLoad()));
|
||||
/**
|
||||
* D189: at one column the panel follows the whole list, so Info goes to it. That passes the continuation, so loading
|
||||
* waits for the reader's next move: a batch appended now would push the panel away.
|
||||
*/
|
||||
function bringPanelIntoView() {
|
||||
if (getComputedStyle(details).position !== 'static') return;
|
||||
panelHold = true;
|
||||
details.scrollIntoView({ block: 'start' });
|
||||
}
|
||||
const releasePanel = () => { if (panelHold) { panelHold = false; scheduleLoad(); } };
|
||||
for (const name of ['wheel', 'touchmove', 'keydown']) scope.listen(window, name, releasePanel, { passive: true });
|
||||
function maybeLoad() {
|
||||
if (panelHold) return;
|
||||
if (!observer || recovering || scope.signal.aborted || !state?.hasMore || state.loading || state.error || main.querySelector('#viewer-dialog[open]')) return;
|
||||
const bounds = continuation.getBoundingClientRect();
|
||||
if (bounds.top <= window.innerHeight + 250 && bounds.bottom >= 0) stream.loadMore();
|
||||
@@ -702,8 +721,8 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage
|
||||
else if (!showRemoved.isConnected) heading.actions.prepend(showRemoved);
|
||||
}
|
||||
/** #498: Return to library for the checked images, "all N matching" (`filter`) or the inspector's one. */
|
||||
function returnSome({images = [], filter = null, count = images.length, trigger = null}) {
|
||||
return returnImages({api, scope, store:activity, images, filter, count, trigger, onReturned:() => { if (!scope.signal.aborted) reset(); }});
|
||||
function returnSome({images = [], filter = null, count = images.length, trigger = null, onDone = null}) {
|
||||
return returnImages({api, scope, store:activity, images, filter, count, trigger, onReturned:() => { if (!scope.signal.aborted) { reset(); onDone?.(); } }});
|
||||
}
|
||||
/** #480: Delete… for one file, from the inspector or the viewer (`after` closes the viewer first). */
|
||||
function deleteOne(target, trigger, after = null) {
|
||||
@@ -819,6 +838,12 @@ export function mountImages(main, api, parentScope, {embedded = false, fetchPage
|
||||
* is read again in place, keeping the scroll and the checked files still listed. A failed re-read lists the page
|
||||
* again from its start, whose failure shows the usual Retry (or the deleted-collection page).
|
||||
*/
|
||||
/** #531: after a bulk action that keeps the selection: the listing re-read, shown from the top. */
|
||||
function bulkRefreshed() {
|
||||
if (scope.signal.aborted) return;
|
||||
scrollToPageTop();
|
||||
if (!recovering) stream.refresh().catch(() => { if (!scope.signal.aborted) reset(); });
|
||||
}
|
||||
async function collectionChanged() {
|
||||
if (scope.signal.aborted || !filters.collection || recovering) return;
|
||||
try {
|
||||
|
||||
@@ -4,6 +4,7 @@ import { routes, mountRouter, summarizeStats, routeTitle, loadNavCounts, navigat
|
||||
LIBRARY_WRITE_COUNTS, NAV_COUNT_ROUTES, FAVORITE_WRITE_COUNTS, FAVORITE_COUNT_DELAY_MS, REVIEW_WRITE_COUNTS,
|
||||
JOB_END_COUNTS } from './state.mjs';
|
||||
import { mountWorkspaceTheme } from './theme.mjs';
|
||||
import { mountBackToTop } from './sticky-chrome.mjs';
|
||||
import { forgetImagesVisit, mountImages, onFavoriteChange } from './images.mjs';
|
||||
import { forgetLibraryVisit, mountLibrary, mountNewPromptAction, onLibraryWrite } from './library.mjs';
|
||||
import { mountVocabulary } from './vocabulary.mjs';
|
||||
@@ -83,6 +84,7 @@ function mountWorkspace() {
|
||||
// row's count and the Activity page, on every route.
|
||||
const activity = createActivityStore({ api, scope, connect: url => new EventSource(resolveUrl(url)) });
|
||||
mountActivityTopbar({ store: activity, scope });
|
||||
mountBackToTop({ scope, toast: document.getElementById('workspace-toast') });
|
||||
activity.load();
|
||||
// D129: a node-folder library is offered a move (or "Keep it here") above every page.
|
||||
mountLibraryBanner(document.getElementById('library-banner'), { api, activity, scope });
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { keepingPlace, publishBarHeight } from './sticky-chrome.mjs';
|
||||
import { createButton } from '../shared/toolkit/core.js';
|
||||
import { selectionLabel, toggleSelection } from './images-state.mjs';
|
||||
import { applyKeys, rangeKeys } from './images-multiselect.mjs';
|
||||
@@ -129,10 +130,11 @@ export function categoryProblem(value) {
|
||||
* to `focusFallback` when it was on the bar the reload hides.
|
||||
*/
|
||||
export function mountLibrarySelection({ scope, api, grid, getItems, getEpoch, getCategories, focusFallback, onCommitted,
|
||||
onReload, getShelf = () => 'active', nameOf = () => null }) {
|
||||
onReload, getShelf = () => 'active', nameOf = () => null, onPlaceKept = () => {} }) {
|
||||
let checked = Object.freeze(new Set()), anchor = null, shiftBox = null, writing = false;
|
||||
const element = el('div');
|
||||
element.id = 'library-bulkbar';
|
||||
publishBarHeight(scope, element);
|
||||
const bulk = el('div', undefined, 'bulkbar'), count = el('strong');
|
||||
const addTags = createButton('Add tags');
|
||||
const setCategory = createButton('Set category');
|
||||
@@ -164,6 +166,10 @@ export function mountLibrarySelection({ scope, api, grid, getItems, getEpoch, ge
|
||||
|
||||
const ids = () => getItems().map(item => item.id);
|
||||
function render() {
|
||||
const shift = keepingPlace(element, paintBar);
|
||||
if (shift) onPlaceKept(shift);
|
||||
}
|
||||
function paintBar() {
|
||||
if (!checked.size) { bulk.remove(); return; }
|
||||
const shelf = getShelf() === 'archived' ? 'archived' : 'active';
|
||||
if (shelf !== shownShelf) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import {mountToolbar} from './toolbar.mjs';
|
||||
import {libraryFilters,libraryViewState} from './saved-views.mjs';
|
||||
import {openRefineDialog,openContentDialog} from './workspace-dialogs.mjs';
|
||||
import {mountLibrarySelection} from './library-selection.mjs';
|
||||
import {scrollToPageTop} from './sticky-chrome.mjs';
|
||||
import {fallBackToOriginal,replaceOnError,showsThumbnail,thumbnailSource} from './thumbnail-support.mjs';
|
||||
import {COPY as DELETE_COPY,RESTORE_URL,archivedNoticeText,restoreOutcome,reviewPromptDelete} from './library-delete.mjs';
|
||||
import {createMoreMenu} from './more-menu.mjs';
|
||||
@@ -190,10 +191,11 @@ export function mountLibrary(main,api,parentScope,{positiveId,savedState=null,on
|
||||
const grid=el('div',undefined,'library-grid');grid.setAttribute('role','list');grid.setAttribute('aria-label','Positive prompts');
|
||||
// #437 D148: select prompts and set their category (the legacy admin's Set Category); a write reloads the shelf.
|
||||
let categoryNames=[];
|
||||
const selection=mountLibrarySelection({scope,api,grid,getItems:()=>state?.items ?? [],getEpoch:()=>state?.epoch ?? null,
|
||||
// #531: the bar's compensating scroll is not the reader moving, so a pending anchor keeps its offset.
|
||||
const selection=mountLibrarySelection({scope,api,grid,onPlaceKept:shift=>{observedScrollY+=shift;},getItems:()=>state?.items ?? [],getEpoch:()=>state?.epoch ?? null,
|
||||
getCategories:()=>categoryNames,focusFallback:{focus:()=>focusHeading(main)},getShelf:()=>filters.shelf,
|
||||
nameOf:id=>{const item=state?.items.find(entry=>entry.id===id);return item?displayTitle(item):null;},
|
||||
onCommitted:(kind,receipt)=>invalidateLibraryAfterWrite({kind:`bulk-${kind}`,receipt}),onReload:()=>{loadCategories();reset();}});
|
||||
onCommitted:(kind,receipt)=>invalidateLibraryAfterWrite({kind:`bulk-${kind}`,receipt}),onReload:()=>{loadCategories();reset();scrollToPageTop();}});
|
||||
const message=el('p','','library-status');message.setAttribute('role','status');
|
||||
const continuation=el('div',undefined,'library-continuation');
|
||||
const more=createButton('Load more prompts');const retry=createButton('Retry prompt library');retry.hidden=true;
|
||||
|
||||
@@ -314,6 +314,8 @@ export function mountReview(main, { api, scope, onDecision = () => {}, onTotal,
|
||||
}
|
||||
// Info selects the item, as the concept's `inspect` does on this page (app.js a1244–1248).
|
||||
controller.select(item.id);
|
||||
// D189: at one column the panel cannot stay beside the queue, so Info brings it into view.
|
||||
if (getComputedStyle(panel).position === 'static') panel.scrollIntoView({ block: 'nearest' });
|
||||
});
|
||||
|
||||
let shownTotal = null;
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
/**
|
||||
* The chrome that stays with you as a page scrolls (#531, beyond the concept; TESTING.md D186 and D187).
|
||||
*
|
||||
* - `keepingPlace`: repaint a sticky selection bar without moving the cards under the pointer.
|
||||
* - `publishBarHeight`: tell the root how much of the viewport's top a shown bar takes, for the Images panel's sticky
|
||||
* offset and for `scroll-margin` (a focused card is not left under the bar).
|
||||
* - `mountBackToTop`: the shell's chip, which rests above the toast's spot and rises only for a taller toast.
|
||||
*/
|
||||
|
||||
/** How far off the top of the page the chip appears, in CSS pixels. */
|
||||
export const BACK_TO_TOP_AFTER = 200;
|
||||
/** The gap between the stuck bar and whatever it clears (its `inset-block-start` above, and as much below). */
|
||||
const BAR_GAP = 16;
|
||||
const TOAST_GAP = 8;
|
||||
|
||||
/**
|
||||
* Run `paint`, which may change the height of `element`. While the element's place in the page's flow is above the
|
||||
* viewport (the bar is stuck), scroll by the difference, so the content below stays where it was. On screen, the bar
|
||||
* pushes the content as the concept's does. Native scroll anchoring would add its own correction in some browsers, so
|
||||
* it is off until the next frame. Returns the distance scrolled.
|
||||
*/
|
||||
export function keepingPlace(element, paint) {
|
||||
const next = element.nextElementSibling;
|
||||
if (!next || !element.isConnected) { paint(); return 0; }
|
||||
const before = next.getBoundingClientRect().top;
|
||||
const root = document.documentElement;
|
||||
const anchoring = root.style.overflowAnchor;
|
||||
root.style.overflowAnchor = 'none';
|
||||
paint();
|
||||
const shift = next.getBoundingClientRect().top - before;
|
||||
requestAnimationFrame(() => { root.style.overflowAnchor = anchoring; });
|
||||
if (!shift || before >= Math.max(0, element.getBoundingClientRect().bottom)) return 0;
|
||||
const from = window.scrollY;
|
||||
window.scrollBy({ top: shift, behavior: 'instant' });
|
||||
return window.scrollY - from;
|
||||
}
|
||||
|
||||
/** Publish the height of the bar inside `element` as `--selection-bar-block` while it shows; cleared with `scope`. */
|
||||
export function publishBarHeight(scope, element) {
|
||||
const root = document.documentElement;
|
||||
const clear = () => root.style.removeProperty('--selection-bar-block');
|
||||
const update = () => {
|
||||
const height = element.getBoundingClientRect().height;
|
||||
if (height > 0) root.style.setProperty('--selection-bar-block', `${height + BAR_GAP}px`);
|
||||
else clear();
|
||||
};
|
||||
const observer = new ResizeObserver(update);
|
||||
observer.observe(element);
|
||||
scope.cleanup(() => { observer.disconnect(); clear(); });
|
||||
}
|
||||
|
||||
/** Go to the top of the page at once: the pages' own height corrections would cancel a smooth scroll part way. */
|
||||
export function scrollToPageTop() {
|
||||
window.scrollTo({ top: 0, behavior: 'instant' });
|
||||
}
|
||||
|
||||
/** One chip for the whole shell: shown off the top of any page; pressing it goes to the top at once. */
|
||||
export function mountBackToTop({ scope, toast }) {
|
||||
const button = document.createElement('button');
|
||||
button.type = 'button';
|
||||
button.className = 'back-to-top';
|
||||
button.hidden = true;
|
||||
const arrow = document.createElement('span');
|
||||
arrow.setAttribute('aria-hidden', 'true');
|
||||
arrow.textContent = '↑';
|
||||
button.append(arrow, ' Back to top');
|
||||
toast.after(button);
|
||||
|
||||
// Scroll events already come once a frame, and this only reads `scrollY`.
|
||||
const update = () => { button.hidden = window.scrollY <= BACK_TO_TOP_AFTER; };
|
||||
scope.listen(window, 'scroll', update, { passive: true });
|
||||
scope.listen(button, 'click', () => {
|
||||
// Never animated, so reduced motion holds. The chip hides under the focus, so focus moves to the page.
|
||||
scrollToPageTop();
|
||||
document.getElementById('workspace-main')?.focus({ preventScroll: true });
|
||||
update();
|
||||
});
|
||||
|
||||
const root = document.documentElement;
|
||||
const followToast = () => {
|
||||
const height = toast.getBoundingClientRect().height;
|
||||
if (height > 0) root.style.setProperty('--toast-block', `${height + TOAST_GAP}px`);
|
||||
else root.style.removeProperty('--toast-block');
|
||||
};
|
||||
const toastObserver = new MutationObserver(followToast);
|
||||
toastObserver.observe(toast, { childList: true, characterData: true, subtree: true });
|
||||
scope.listen(window, 'resize', followToast);
|
||||
scope.cleanup(() => { toastObserver.disconnect(); root.style.removeProperty('--toast-block'); button.remove(); });
|
||||
update();
|
||||
return button;
|
||||
}
|
||||
@@ -290,7 +290,7 @@ main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; }
|
||||
}
|
||||
/* #249 D42: a discovery alert a rescan leaves scrolls clear of the concept's fixed toast (22px inset, 12px at 700px and
|
||||
below, two 12px lines at 390px), never under it; the toast itself keeps the concept's placement. */
|
||||
.options-status { font-size: 12px; padding-inline: 4px; scroll-margin-block-end: 96px; /* D42 */ }
|
||||
.options-status { font-size: 12px; padding-inline: 4px; scroll-margin-block-end: 140px; /* D42, D187: clear of the toast and the chip above it */ }
|
||||
|
||||
/* Prompt library cards (#249 PR E): the concept's `article.prompt-card` (s653–708) with real prompts, titles and media. */
|
||||
/* #249 D23: real prompts are far longer than the concept's samples, so the card clamps the positive to two lines; copy
|
||||
@@ -542,3 +542,34 @@ pre.prompt-text { font: inherit; font-size: 12px; line-height: 1.75; margin-bloc
|
||||
overflow: hidden; overflow-wrap: anywhere;
|
||||
}
|
||||
}
|
||||
|
||||
/* D186 (#531, beyond the concept): the selection bars stay in view while you select. This one rule is their position. `--selection-bar-block`
|
||||
(sticky-chrome.mjs) is the room a shown bar takes; a focused card control and the Images panel keep clear of it
|
||||
(scroll-margin on them, not scroll-padding on the root, which would scroll the page to reveal the bar's own buttons). At one
|
||||
column the bar is one row that scrolls sideways, so it does not take a fifth of a phone screen. */
|
||||
#bulkbar, #library-bulkbar { position: sticky; inset-block-start: 8px; z-index: 5; }
|
||||
:is(#image-grid, .library-grid) :is(article, a, button, input, select, textarea, [tabindex]) { scroll-margin-block-start: var(--selection-bar-block, 0px); }
|
||||
.detail-panel { inset-block-start: max(20px, var(--selection-bar-block, 0px)); max-block-size: calc(100dvh - 20px - max(20px, var(--selection-bar-block, 0px))); /* D186 */ }
|
||||
@media (max-width: 700px) {
|
||||
.detail-panel { max-block-size: none; /* D186 */ }
|
||||
.bulkbar { flex-wrap: nowrap; overflow-x: auto; scrollbar-width: thin; /* D186 */ }
|
||||
.bulkbar > * { flex: none; /* D186 */ }
|
||||
}
|
||||
@container images-page (inline-size < 600px) { .detail-panel { max-block-size: none; /* D186 */ } }
|
||||
|
||||
/* D187 (#531, beyond the concept): Back to top, in the toast's column. It rests above a two-line toast, so the usual
|
||||
toast does not move it under a finger, and rises only for a taller one. */
|
||||
.ca-tools .back-to-top {
|
||||
--toast-rest: 70px;
|
||||
position: fixed; inset-inline-start: 50%; transform: translateX(-50%); z-index: 10;
|
||||
inset-block-end: calc(22px + max(var(--toast-rest), var(--toast-block, 0px)));
|
||||
padding: 6px 12px; border: 1px solid var(--ca-line); border-radius: 999px; background: var(--ca-surface);
|
||||
box-shadow: 0 8px 35px #0004; font-size: 12px; white-space: nowrap;
|
||||
}
|
||||
@media (max-width: 700px) { .ca-tools .back-to-top { inset-block-end: calc(12px + max(var(--toast-rest), var(--toast-block, 0px))); } }
|
||||
@media (pointer: coarse) { .ca-tools .back-to-top { min-block-size: 40px; } }
|
||||
|
||||
/* D188 (#531, beyond the concept): the Resolve panel stays with you as the Images panel does. */
|
||||
.review-panel { position: sticky; inset-block-start: 20px; }
|
||||
@media (max-width: 700px) { .review-panel { position: static; } }
|
||||
@media (min-width: 701px) and (max-height: 720px) { .review-panel { max-block-size: calc(100dvh - 40px); overflow: auto; } }
|
||||
|
||||
Reference in New Issue
Block a user