fix(inspector): coarse viewer-detail targets and Prompt history on the shown route
- Prompt history closes the viewer and, when its target is the route already shown, re-renders it as the concept's go() does (navigateTo plus a router refresh event), instead of doing nothing. - The viewer details' disclosure summaries and "Download original" keep 40px targets on coarse pointers (#249 D34), as TESTING.md promises. - Drop the dead `.prompt-positive` rules and merge the two `pre.prompt-text` rules. - TESTING.md records the side panel's D20/D21 deviations.
This commit is contained in:
+11
-1
@@ -576,7 +576,17 @@ Recorded deviations on this page (spec §8):
|
||||
D21 badge's words in lower case, left out when there is no badge. Raw records keep
|
||||
their names, representations and the two downloads with their unavailable reasons;
|
||||
the viewer's pair send and history buttons may wrap at 200% zoom. Tag chips in the
|
||||
viewer are plain (the concept's go to the Library).
|
||||
viewer are plain (the concept's go to the Library). The details' disclosure
|
||||
summaries and "Download original" keep 40px coarse targets (D34). Prompt history
|
||||
closes the viewer and, when its target is the route already shown, re-renders it
|
||||
(the concept's `go()`).
|
||||
- **D20/D21 side panel**: the sticky panel scrolls inside the window
|
||||
(`max-block-size` with `overflow: auto`, not the concept's `overflow: hidden`), so
|
||||
long real prompts keep the footer reachable; an empty badge or tag `.tags` row is
|
||||
left out (the concept always renders both); the footer, its pair send and Prompt
|
||||
history appear only with resolved prompt roles or a saved link; a video file keeps
|
||||
`video.detail-cover`; and only the linked positive's own tags are `button.chip`
|
||||
filters, while recipe, recorded-run and embedded tags stay plain `span.chip`.
|
||||
- **D22**: an empty positive reads "No positive prompt (explicitly empty)" (the concept
|
||||
applies its negative wording to both roles), and a confirmed `p.prompt-text` is
|
||||
focusable (`tabindex=0`, marked focus ring) so long prompts stay keyboard-readable.
|
||||
|
||||
@@ -369,6 +369,20 @@ test('embedded gallery uses the same full inspector',async({page,request})=>{
|
||||
await page.goto(`/prompt_manager/workspace#/library/${id}`);await selectFile(page,'complete.png');const panel=inspector(page);await expect(panel.getByRole('heading',{name:POSITIVE_TITLE,exact:true})).toBeVisible();await expect(panel.locator('.meta-grid dt')).toHaveText(['Seed','Steps','CFG','Sampler','Scheduler','Model']);await expect(panel.locator('.detail-inner > .tags').last().locator('span.chip')).toHaveText(['fixture','geometric']);await expect(panel).not.toHaveAttribute('id','image-inspector');await openSelected(page);await expect(page.locator('#viewer-dialog .file-inspector')).toHaveCount(1);
|
||||
});
|
||||
|
||||
test('Prompt history to the shown route closes the viewer and re-renders it, as the concept go() does',async({page,request})=>{
|
||||
await expect(await request.post('/__test__/reset',{data:{full_inspector:true}})).toBeOK();const list=await(await request.get('/prompt_manager/library')).json();const id=list.items.find(item=>item.text.startsWith('Inspector portrait')).id;
|
||||
await page.goto(`/prompt_manager/workspace#/library/${id}`);await selectFile(page,'complete.png');await expect(inspector(page).getByRole('heading',{name:POSITIVE_TITLE,exact:true})).toBeVisible();
|
||||
await openSelected(page);const viewer=page.locator('dialog#viewer-dialog');await expect(viewer).toBeVisible();
|
||||
const heading=page.locator('#workspace-main h1');await heading.evaluate(node=>{node.dataset.beforeHistory='true';});
|
||||
await viewer.locator('#viewer-details').getByRole('button',{name:'Prompt history',exact:true}).click();
|
||||
await expect(page.locator('dialog#viewer-dialog[open]')).toHaveCount(0);await expect(page).toHaveURL(new RegExp(`#/library/${id}$`));
|
||||
await expect(page.locator('#workspace-main h1[data-before-history]')).toHaveCount(0);await expect(heading).toBeFocused();
|
||||
// The side panel's Prompt history does the same.
|
||||
await selectFile(page,'complete.png');await heading.evaluate(node=>{node.dataset.beforeHistory='true';});
|
||||
await inspector(page).getByRole('button',{name:'Prompt history',exact:true}).click();
|
||||
await expect(page.locator('#workspace-main h1[data-before-history]')).toHaveCount(0);await expect(heading).toBeFocused();
|
||||
});
|
||||
|
||||
test('full inspector labels container representations and releases action URLs',async({page,request})=>{
|
||||
await fullFixture(page,request);
|
||||
await page.evaluate(async()=>{
|
||||
@@ -440,6 +454,11 @@ test.describe('full inspector coarse pointer',()=>{
|
||||
expect(box.width,box.name).toBeGreaterThanOrEqual(80);expect(box.height,box.name).toBeGreaterThanOrEqual(80);
|
||||
}
|
||||
const raw=viewer.getByText('Workflow & raw metadata',{exact:true});await raw.scrollIntoViewIfNeeded();await raw.focus();await expect(raw).toBeInViewport();
|
||||
// #249 D34: the viewer details' disclosure summaries and "Download original" keep 40px coarse targets as well.
|
||||
const rail=viewer.locator('#viewer-details');await rail.getByText('File & provenance',{exact:true}).click();await expect(rail.getByRole('link',{name:'Download original',exact:true})).toBeVisible();
|
||||
const details=await rail.locator(':is(summary,a)').evaluateAll(nodes=>nodes.map(node=>{const {width,height}=node.getBoundingClientRect();return {name:node.textContent,width,height};}));
|
||||
expect(details.map(target=>target.name)).toEqual(['File & provenance','Download original','Workflow & raw metadata']);
|
||||
for(const target of details){expect(target.width,target.name).toBeGreaterThanOrEqual(40);expect(target.height,target.name).toBeGreaterThanOrEqual(40);}
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -147,3 +147,16 @@ test('a Library or vocabulary write re-reads only the Library and Tags counts',
|
||||
assert.deepEqual(settled.sort(), [['library', 4], ['tags', 9]]);
|
||||
assert.deepEqual([...counts], [['library', 4], ['tags', 9]]);
|
||||
});
|
||||
|
||||
test('navigating to the current route re-renders it, as the concept go() does', async () => {
|
||||
const {mountRouter,navigateTo}=await source;
|
||||
const win=new EventTarget();win.location={hash:'#/library/12'};
|
||||
win.history={state:null,replaceState(state,unused,hash){win.location.hash=hash;}};
|
||||
const seen=[];const dispose=mountRouter(win,route=>seen.push([route.id,route.positiveId]));
|
||||
navigateTo('#/library/12',win);
|
||||
assert.deepEqual(seen,[['library',12],['library',12]]);
|
||||
// A different route only sets the hash; its hashchange renders it once.
|
||||
navigateTo('#/library/13',win);assert.equal(win.location.hash,'#/library/13');assert.equal(seen.length,2);
|
||||
win.dispatchEvent(new Event('hashchange'));assert.deepEqual(seen.at(-1),['library',13]);
|
||||
dispose();navigateTo('#/library/13',win);assert.equal(seen.length,3);
|
||||
});
|
||||
|
||||
@@ -3,6 +3,7 @@ import { mountPromptBlocks, promptRoles } from './prompt-blocks.mjs';
|
||||
import { createInspectionController } from './inspection-state.mjs';
|
||||
import { createSendButton } from './prompt-sender.mjs';
|
||||
import { createWorkspaceIcon } from './icons.mjs';
|
||||
import { navigateTo } from './state.mjs';
|
||||
|
||||
const RAW_KEYS = ['prompt', 'workflow', 'parameters', 'comfyassets_prompt_pairs'];
|
||||
const RAW_LIMIT = 'Raw metadata exceeds the response limit. Download the original file to inspect it externally.';
|
||||
@@ -207,7 +208,12 @@ export function mountFileInspector(container, {sender, parentScope, variant, inl
|
||||
if (record.historyTarget) {
|
||||
const history = createButton('');
|
||||
history.append(createWorkspaceIcon('branch'), ' Prompt history');
|
||||
contentScope.listen(history, 'click', () => {window.location.hash = record.historyTarget;}); footer.append(history);
|
||||
// The concept's go(): the viewer closes, and a link to the route already shown re-renders it.
|
||||
contentScope.listen(history, 'click', () => {
|
||||
document.querySelector('dialog#viewer-dialog[open]')?.close();
|
||||
navigateTo(record.historyTarget);
|
||||
});
|
||||
footer.append(history);
|
||||
}
|
||||
if (!full && footer.childNodes.length) container.append(footer);
|
||||
}
|
||||
|
||||
@@ -77,8 +77,21 @@ export async function loadNavCounts(api, signal, onCount = () => {}, ids = NAV_C
|
||||
return new Map([...summarizeNavCounts(pages)].filter(([id]) => ids.includes(id)));
|
||||
}
|
||||
|
||||
/** Fired on the window to render the current route again, where setting the same hash fires no `hashchange`. */
|
||||
const ROUTE_REFRESH_EVENT = 'prompt-manager:route-refresh';
|
||||
|
||||
/** The concept's `go()` (app.js 292–297): a link to the route already shown re-renders it instead of doing nothing. */
|
||||
export function navigateTo(hash, win = window) {
|
||||
if (win.location.hash === hash) win.dispatchEvent(new Event(ROUTE_REFRESH_EVENT));
|
||||
else win.location.hash = hash;
|
||||
}
|
||||
|
||||
export function mountRouter(win, onRoute) {
|
||||
let current;
|
||||
const refresh = () => {
|
||||
current = undefined;
|
||||
update();
|
||||
};
|
||||
const update = () => {
|
||||
const route = resolveRoute(win.location.hash);
|
||||
const hash = route.hash || `#/${route.id}`;
|
||||
@@ -88,6 +101,10 @@ export function mountRouter(win, onRoute) {
|
||||
onRoute(route);
|
||||
};
|
||||
win.addEventListener('hashchange', update);
|
||||
win.addEventListener(ROUTE_REFRESH_EVENT, refresh);
|
||||
update();
|
||||
return () => win.removeEventListener('hashchange', update);
|
||||
return () => {
|
||||
win.removeEventListener('hashchange', update);
|
||||
win.removeEventListener(ROUTE_REFRESH_EVENT, refresh);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -128,6 +128,10 @@ main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; }
|
||||
@media (pointer: coarse) {
|
||||
/* #249 D34: coarse pointers keep 40px square viewer targets (the concept's "−" and "+" are narrower). */
|
||||
#viewer-dialog :is(.dialog-header > button, #viewer-toolbar > button, #viewer-toolbar > a) { min-width: 40px; min-height: 40px; /* D34 */ }
|
||||
/* #249 D34: the details' disclosure summaries and "Download original" keep 40px targets too (the concept's coarse
|
||||
rule covers buttons only); the summary keeps its disclosure marker. */
|
||||
#viewer-details summary { box-sizing: border-box; min-block-size: 40px; padding-block: 10px; /* D34 */ }
|
||||
#viewer-details a { display: inline-flex; align-items: center; min-block-size: 40px; /* D34 */ }
|
||||
}
|
||||
@media (max-width: 700px) {
|
||||
#viewer-toolbar > .media-download { font-size: 10px; /* D34: as the concept's `.viewer-toolbar button` */ }
|
||||
@@ -138,7 +142,6 @@ main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; }
|
||||
.ca-tools dialog.prompt-sender { width: min(760px, calc(100% - 32px)); max-width: none; max-height: calc(100dvh - 32px); padding: 22px; border-color: var(--ca-line); border-radius: 12px; } /* PR D */
|
||||
.ca-tools dialog.prompt-sender::backdrop { background: #000b; } /* PR D */
|
||||
.ca-tools dialog.recorded-run-dialog { width: min(1100px, calc(100% - 32px)); max-width: calc(100% - 32px); max-height: calc(100dvh - 32px); padding: 22px; border-radius: 7px; } /* PR F */
|
||||
pre.prompt-text { background: none; padding: 0; border: 0; border-radius: 0; max-height: none; overflow: visible; } /* Library and history rebuilds: the Archive dialog and frozen field provenance keep `pre.prompt-text` */
|
||||
.prompt-text[tabindex]:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 3px; } /* production-only: a confirmed prompt stays keyboard-reachable (#249 D22) */
|
||||
@media (pointer: coarse) {
|
||||
.ca-tools .recorded-history .history-toolbar button { min-height: 44px; } /* PR F: concept coarse buttons are 40px */
|
||||
@@ -177,14 +180,13 @@ pre.prompt-text { background: none; padding: 0; border: 0; border-radius: 0; max
|
||||
dialog and send button. */
|
||||
.prompt-inspector { margin-block-start: 22px; min-inline-size: 0; }
|
||||
.prompt-block { border-block-start: 1px solid var(--ca-line); padding-block: 13px; min-inline-size: 0; }
|
||||
.prompt-positive { --prompt-role: #73dda9; }
|
||||
[data-ca-theme="light"] .prompt-positive { --prompt-role: #176b46; }
|
||||
.prompt-negative { --prompt-role: #ff8c98; }
|
||||
[data-ca-theme="light"] .prompt-negative { --prompt-role: #b71931; }
|
||||
.prompt-block-heading { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 8px; }
|
||||
.prompt-block h3 { font-size: 10px; text-transform: uppercase; letter-spacing: .5px; margin: 0; color: var(--prompt-role); }
|
||||
.prompt-empty, .prompt-block > .muted { font-size: 10px; }
|
||||
pre.prompt-text { font: inherit; font-size: 12px; line-height: 1.75; white-space: pre-wrap; overflow-wrap: anywhere; margin-block: 10px; }
|
||||
/* PR E/F: the Archive dialog and the frozen field provenance keep `pre.prompt-text`, guarded against the concept `pre` rule. */
|
||||
pre.prompt-text { font: inherit; font-size: 12px; line-height: 1.75; white-space: pre-wrap; overflow-wrap: anywhere; margin-block: 10px; background: none; padding: 0; border: 0; border-radius: 0; max-height: none; overflow: visible; }
|
||||
.prompt-send-button { display: inline-flex; align-items: center; justify-content: center; gap: 7px; color: var(--ca-focus); }
|
||||
.prompt-send-button svg { inline-size: 16px; block-size: 16px; flex-shrink: 0; }
|
||||
.prompt-sender { background: var(--ca-surface); color: var(--ca-text); border: 1px solid var(--ca-line); border-radius: 12px; padding: 22px; inline-size: min(760px, calc(100% - 32px)); max-inline-size: none; max-block-size: calc(100dvh - 32px); overflow: auto; }
|
||||
|
||||
Reference in New Issue
Block a user