From a6d6129ec64544a916d1ef41ad85ac2f4e836380 Mon Sep 17 00:00:00 2001 From: Vito Date: Fri, 30 Oct 2026 12:16:55 +0000 Subject: [PATCH] fix(library): keep the prompt page heading on one row; no history subtitle on a deleted prompt's page (#524) (#526) * 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. * fix(library): drop the CSS file name from a comment the consumer check reads (#524) --- TESTING.md | 8 +++ .../browser/workspace-library-delete.spec.mjs | 54 +++++++++++++++++++ web/js/workspace/library.mjs | 16 ++++-- web/js/workspace/workspace.css | 12 +++++ 4 files changed, 85 insertions(+), 5 deletions(-) diff --git a/TESTING.md b/TESTING.md index 3f9f3b36..22ed40bd 100644 --- a/TESTING.md +++ b/TESTING.md @@ -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 diff --git a/tests/browser/workspace-library-delete.spec.mjs b/tests/browser/workspace-library-delete.spec.mjs index d18d8ac8..66b7e968 100644 --- a/tests/browser/workspace-library-delete.spec.mjs +++ b/tests/browser/workspace-library-delete.spec.mjs @@ -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'); diff --git a/web/js/workspace/library.mjs b/web/js/workspace/library.mjs index 4b221ca1..a7238296 100644 --- a/web/js/workspace/library.mjs +++ b/web/js/workspace/library.mjs @@ -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; diff --git a/web/js/workspace/workspace.css b/web/js/workspace/workspace.css index c602f8b5..05df5256 100644 --- a/web/js/workspace/workspace.css +++ b/web/js/workspace/workspace.css @@ -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; + } +}