Compare commits

...
Author SHA1 Message Date
Vito Sansevero 739c9fef5c fix(library): drop the CSS file name from a comment the consumer check reads (#524) 2026-10-30 04:43:00 -07:00
Vito Sansevero 715e381a78 fix(library): keep the prompt page heading on one row; no history subtitle on a deleted prompt's page (#524)
From 951px the heading actions keep one row and a long title clamps to two lines beside them (D164). A deleted prompt's page hides the Prompt history description.
2026-10-30 04:15:20 -07:00
4 changed files with 85 additions and 5 deletions
+8
View File
@@ -2928,6 +2928,14 @@ concept's danger "Archive…" maps to "Archive variant…" followed by a "Delete
Tests: `tests/browser/workspace-library-delete.spec.mjs` (#481 cases), and `workspace-library-editor.spec.mjs` (the
hand-off and the comparisons) and `library-editor-visual.spec.mjs` with the renamed button. JS only: no restart.
### Prompt page heading on one row (#524)
**D164** (beyond the concept, which never has four heading actions beside a long title): from 951px, the concept's
sidebar breakpoint, the prompt page's heading actions keep one row and the title takes the remaining width, clamped to
two lines with its full text in the DOM and in `title`. Below 951px the concept's wrapping and stacked heading is
unchanged. A short title lays out as the concept's, so the history concept comparison is unaffected. A deleted prompt's
page has no "Prompt history · …" description: it is emptied and hidden while the gone message shows.
### Delete review and prompt-page polish (#512)
Found in the live test of #505/#507/#509. **D163** (beyond the concept, maintainer ruling 2026-10-29: delete was hard to
@@ -529,6 +529,60 @@ test('a deleted prompt\'s page says it no longer exists, with the way back and n
await expect(page).toHaveURL(/#\/library$/);
});
// #524 (live test of #522): a long title clamps beside the heading actions, which stay on one row at desktop widths.
const LONG_TITLE='masterpiece, high quality, very_awa, newest, absurdres, highres, depth_of_field, detailed eyes, realistic eyes, fake eyelashes, black eyes, white_nails, 1girl, solo, black bob_cut, upper body';
async function openLongTitle(page,request,width) {
const dual=byText(await items(request),'Dual role portrait');
await page.route(new RegExp(`/prompt_manager/library/${dual.id}\\?`),async route=>{
const response=await route.fetch(),json=await response.json();
await route.fulfill({response,json:{...json,positive:{...json.positive,title:LONG_TITLE}}});
});
await page.setViewportSize({width,height:900});
await page.goto(`/prompt_manager/workspace#/library/${dual.id}`);
await expect(page.locator('#workspace-main h1')).toHaveText(LONG_TITLE);
expect(await page.evaluate(()=>document.documentElement.scrollWidth<=document.documentElement.clientWidth)).toBe(true);
return page.locator('#workspace-main .heading-actions > button, #workspace-main .heading-actions > .more-menu > button');
}
for(const width of [1440,951]) test(`a long title clamps and the heading actions stay on one row at ${width}px (#524)`,async({page,request})=>{
const actions=await openLongTitle(page,request,width);
await expect(actions).toHaveText(['← Prompt library','Edit metadata','Send to ComfyUI','⋯']);
const tops=await actions.evaluateAll(nodes=>nodes.map(node=>Math.round(node.getBoundingClientRect().top)));
expect(new Set(tops).size).toBe(1);
const h1=page.locator('#workspace-main h1');
const shape=await h1.evaluate(node=>({scroll:node.scrollHeight,client:node.clientHeight,line:parseFloat(getComputedStyle(node).lineHeight),title:node.title}));
expect(shape.scroll).toBeGreaterThan(shape.client);
expect(shape.client).toBeLessThanOrEqual(Math.ceil(shape.line*2));
// The clamp hides pixels only: the whole title stays the text and shows on hover.
expect(shape.title).toBe(LONG_TITLE);
// The buttons do not shrink: no label is cut or broken onto a second line.
const broken=await actions.evaluateAll(nodes=>nodes.filter(node=>node.scrollWidth>node.clientWidth||node.getBoundingClientRect().height>44).length);
expect(broken).toBe(0);
const [h1Box,firstBox]=[await h1.boundingBox(),await actions.first().boundingBox()];
expect(h1Box.x+h1Box.width).toBeLessThanOrEqual(firstBox.x);
});
test('a long title at 390px keeps the concept\'s stacked heading, not clamped, inside the viewport (#524)',async({page,request})=>{
const actions=await openLongTitle(page,request,390);
const h1=await page.locator('#workspace-main h1').evaluate(node=>({scroll:node.scrollHeight,client:node.clientHeight}));
expect(h1.scroll).toBeLessThanOrEqual(h1.client);
const right=await actions.evaluateAll(nodes=>Math.max(...nodes.map(node=>node.getBoundingClientRect().right)));
expect(right).toBeLessThanOrEqual(390);
});
test('a deleted prompt\'s page has no history subtitle, and a page that loads shows it (#524)',async({page,request})=>{
const list=await items(request),dual=byText(list,'Dual role portrait'),dog=byText(list,'Dog landscape');
const description=page.locator('#workspace-main > .heading p.muted');
await deleteFromPage(page,dual.id);
await choice(page).getByRole('radio',{name:'Delete permanently, with their saved pairs'}).check();
await review(page).getByRole('button',{name:'Delete 1 prompt',exact:true}).click();
await expect(page).toHaveURL(/#\/library$/);
await page.goto(`/prompt_manager/workspace#/library/${dual.id}`);
await expect(page.locator('#workspace-main .library-status')).toHaveText('This prompt no longer exists. It may have been deleted.');
await expect(description).toBeHidden();await expect(description).toHaveText('');
await page.goto(`/prompt_manager/workspace#/library/${dog.id}`);
await expect(description).toHaveText('Prompt history · one positive, one shared gallery');await expect(description).toBeVisible();
});
test('the archived shelf\'s heading count and its list agree under a remembered search (#512 item 5)',async({page,request})=>{
const all=await items(request);
const dual=byText(all,'Dual role portrait'),dog=byText(all,'Dog landscape');
+11 -5
View File
@@ -23,6 +23,7 @@ import {createMoreMenu} from './more-menu.mjs';
let shelfVisit=null;
// #512: what a prompt page says when its prompt no longer exists (404).
const PROMPT_GONE='This prompt no longer exists. It may have been deleted.';
const HISTORY_DESCRIPTION='Prompt history · one positive, one shared gallery';
// Every confirmed receipt advances this; a shelf mounted before it never restores its cached stream.
let writeGeneration=0, focusHeadingOnMount=false, savedParentId=null;
// The pair one reconciled save names, taken once by the history it opens (the concept selects it after a save, app.js 986).
@@ -62,8 +63,9 @@ function headingActions(main,fallback) {
}
/** The shell heading's description (concept `p.muted`); a view mounted without one shows it at the top of `fallback`. */
function headingDescription(main,fallback,text) {
const description=pageHeading(main)?.description;
if(description) description.textContent=text;else fallback.prepend(el('p',text,'muted'));
let description=pageHeading(main)?.description;
if(description) description.textContent=text;else {description=el('p',text,'muted');fallback.prepend(description);}
return description;
}
/** A confirmed receipt makes cached shelf items, cursor and measured heights stale; filters and anchor survive. */
@@ -488,7 +490,9 @@ export function mountLibrary(main,api,parentScope,{positiveId,savedState=null,on
const history=el('section',undefined,'library-history');history.setAttribute('aria-label','Prompt history');main.append(history);
const body=el('div');history.append(body);
// Concept heading (app.js a591–597): ← Prompt library, Edit metadata, then the selected pair's Send to ComfyUI.
headingDescription(main,history,'Prompt history · one positive, one shared gallery');
const description=headingDescription(main,history,HISTORY_DESCRIPTION);
// #524 D164: at desktop widths a long title clamps beside the actions.
pageHeading(main)?.heading.classList.add('prompt-page-heading');
const actions=headingActions(main,history);
const back=createButton('← Prompt library'),edit=createButton('Edit metadata'),send=createButton('','send');
send.append(createWorkspaceIcon('send'),el('span','Send to ComfyUI'));send.disabled=true;
@@ -584,7 +588,9 @@ export function mountLibrary(main,api,parentScope,{positiveId,savedState=null,on
// Edit metadata and ⋯ act on a loaded prompt only (review M2).
const hadFocus=[edit,moreMenu.button].includes(document.activeElement);
const loaded=!state.loading && !state.error;edit.disabled=!loaded;moreMenu.setDisabled(!loaded);
if(state.error?.status===404) {
// #524: a prompt that no longer exists has no history to describe.
const gone=state.error?.status===404;description.textContent=gone?'':HISTORY_DESCRIPTION;description.hidden=gone;
if(gone) {
const message=el('p',PROMPT_GONE,'library-status');message.setAttribute('role','status');body.append(message);
const open=createButton('Open the Prompt library');contentScope.listen(open,'click',()=>{window.location.hash='#/library';});body.append(open);
// A focused control that was just disabled hands focus to the way back (review M3).
@@ -741,7 +747,7 @@ export function mountLibrary(main,api,parentScope,{positiveId,savedState=null,on
const updateMeta=()=>{imagesMeta.textContent=countOf(detail.positive.image_count ?? 0,'image');variantsMeta.textContent=countOf(detail.positive.variant_count ?? 0,'negative variant');};
updateParent=()=>{
const name=displayTitle(detail.positive);
main.querySelector('h1').textContent=name;categoryChip.textContent=detail.positive.category || 'Uncategorized';updateMeta();
const h1=main.querySelector('h1');h1.textContent=name;h1.title=name;categoryChip.textContent=detail.positive.category || 'Uncategorized';updateMeta();
send.setAttribute('aria-label',`Send prompt pair to ComfyUI: ${name}`);blocks?.setTitle(name);
};
let activePairId;
+12
View File
@@ -493,3 +493,15 @@ pre.prompt-text { font: inherit; font-size: 12px; line-height: 1.75; margin-bloc
}
.more-menu-list[hidden] { display: none; }
.more-menu-list > button { justify-content: flex-start; text-align: left; width: 100%; white-space: nowrap; }
/* D164 (#524): the prompt page has four heading actions and titles of any length, which the concept never shows. From
the concept's sidebar breakpoint up the actions keep one row and the title takes what is left, clamped to two lines
(its full text stays in the DOM and in `title`). A short title lays out exactly as the concept's. */
@media (min-width: 951px) {
.heading.prompt-page-heading > div:first-child { min-width: 0; }
.heading.prompt-page-heading > .heading-actions { flex: 0 0 auto; }
.heading.prompt-page-heading h1 {
display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2;
overflow: hidden; overflow-wrap: anywhere;
}
}