Files
ComfyAssets-ComfyUI_PromptM…/web/js/workspace/library-layout.mjs
T
Vito Sansevero ae88f9cd6e feat(library): concept prompt cards with real runs and last use
The Prompt library shelf renders the concept's article.prompt-card:
.card-art (up to two previews), .card-title with a title button and the
category chip, the positive prompt block, .card-meta ("− N negative
variants", "N runs", "N images"), .tags, and .card-footer with the rating
and last-use line and the positive send plus Explore history buttons.

D23: "N runs" is the parent's run_count; the footer reads Used today /
yesterday / N days ago / <Mon D> in the viewer's local calendar days, or
"Not used yet"; stars carry a visually hidden "Rated N out of 5" and an
unrated parent shows no stars. The positive is clamped to two lines while
copy and send use the full text. Long titles wrap inside the title row
and the category chip never breaks mid-word. Card actions are named after
the prompt ("Copy positive prompt: <title>") through a new title option on
the shared prompt blocks. The production .library-card CSS is removed and
the row estimates follow the clamped card height.

The shelf error now reads "Prompt library could not be loaded. Retry."
(Refresh is gone). TESTING.md records D23, the production-only shelf copy
(D40) and the concept search placeholder kept on the Library (D41).
2026-10-20 21:04:22 -07:00

67 lines
3.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
export function layoutLibraryRows(heights, columns, {gap = 18, estimate = 468} = {}) {
if (!Number.isSafeInteger(columns) || columns < 1) throw new TypeError('Invalid Library columns');
if (!Number.isFinite(gap) || gap < 0 || !Number.isFinite(estimate) || estimate <= 0) throw new TypeError('Invalid Library geometry');
const rows = [];
let top = 0;
for (let first = 0; first < heights.length; first += columns) {
const last = Math.min(first + columns, heights.length) - 1;
let height = 0;
for (let index = first; index <= last; index++) {
const measured = heights[index];
height = Math.max(height, Number.isFinite(measured) && measured > 0 ? measured : estimate);
}
rows.push({first, last, top, height});
top += height + gap;
}
return {rows, height: rows.length ? top - gap : 0};
}
function lowerBound(rows, after) {
let low = 0, high = rows.length;
while (low < high) {
const middle = low + Math.floor((high - low) / 2);
if (after(rows[middle])) low = middle + 1;
else high = middle;
}
return low;
}
export function visibleLibraryRows(rows, top, viewportHeight, focusedIndex = -1) {
if (!rows.length) return [];
top = Number.isFinite(top) ? Math.max(0, top) : 0;
viewportHeight = Number.isFinite(viewportHeight) ? Math.max(0, viewportHeight) : 0;
const first = Math.min(rows.length - 1, lowerBound(rows, row => row.top + row.height <= top));
const end = Math.max(first + 1, lowerBound(rows, row => row.top < top + viewportHeight));
const visible = new Set();
for (let index = Math.max(0, first - 2); index < Math.min(rows.length, end + 2); index++) visible.add(index);
if (Number.isSafeInteger(focusedIndex) && focusedIndex >= 0) {
const focus = lowerBound(rows, row => row.last < focusedIndex);
if (focus < rows.length && rows[focus].first <= focusedIndex) visible.add(focus);
}
return [...visible].sort((a, b) => a - b);
}
// #249 D23: the card footer's words. English month names as the concept's copy, independent of the browser locale.
const MONTHS = Object.freeze(['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']);
const DAY_MS = 86400000;
/** The local calendar day as a UTC day number, so a DST change never makes a day 23 or 25 hours long. */
const localDay = date => Date.UTC(date.getFullYear(), date.getMonth(), date.getDate()) / DAY_MS;
/** "Used today", "Used yesterday", "Used N days ago" (under a week), "Used <Mon D>", or "Not used yet" without a valid time. */
export function usedLabel(lastUsedAt, now = new Date()) {
const used = typeof lastUsedAt === 'string' && lastUsedAt ? new Date(lastUsedAt) : null;
if (!used || Number.isNaN(used.getTime())) return 'Not used yet';
// A server clock ahead of this browser is not a future use.
const days = Math.max(0, localDay(now) - localDay(used));
if (days === 0) return 'Used today';
if (days === 1) return 'Used yesterday';
if (days < 7) return `Used ${days} days ago`;
return `Used ${MONTHS[used.getMonth()]} ${used.getDate()}`;
}
/** The concept's stars (`★` × rating) with their visually hidden reading; an unrated parent shows neither. */
export function ratingLabel(rating) {
if (!Number.isInteger(rating) || rating < 1 || rating > 5) return {stars: '', hidden: ''};
return {stars: '★'.repeat(rating), hidden: `Rated ${rating} out of 5`};
}