fix(inspector): give image tags their own labelled section (#449) (#454)

The inspector drew a file's tags as a bare chip row right under the negative
prompt, so they read as part of it. The side panel, the viewer details and
History's current file now draw them in labelled sections in the prompt
sections' frame: "Image tags · N" for the file's own tags, then "Prompt tags
· N" or "Recipe tags · N" for the linked tags. Each count is true: repeats are
removed within a source only. Recorded as D154.
This commit is contained in:
Vito
2026-10-29 02:44:42 +00:00
committed by GitHub
parent d03d0ed1c5
commit aad3a37bcf
10 changed files with 334 additions and 83 deletions
+36 -15
View File
@@ -176,24 +176,32 @@ function extensionOf(record) {
return (record.file?.extension || record.identity.path.split('.').at(-1)).replace(/^\./, '').toUpperCase();
}
/**
* The chip row after the prompt blocks (D88): the file's current image tags first, then D21's linked tags not already
* shown (case-insensitive). With `filterable` (a Tags filter exists, as in the Images panel) every image tag sets it, and
* a linked tag only when it is the positive's own (D21); otherwise, as in the viewer, every chip is plain.
* The tag chips after the prompt blocks (D88, D154): the file's current image tags (`source: 'image'`), then D21's linked
* tags (`source: 'linked'`), each without case-insensitive repeats within its own source (D154: a linked tag that is also
* an image tag stays in both labelled sections). With `filterable` (a Tags filter exists, as in the Images panel) every
* image tag sets it, and a linked tag only when it is the positive's own (D21); otherwise, as in the viewer, every chip
* is plain.
*/
export function inspectorTagChips(record, {filterable}) {
const shown = new Set(), chips = [];
const add = (names, filter) => {
const chips = [];
const add = (names, filter, source) => {
const shown = new Set();
for (const name of names) {
const key = name.toLowerCase();
if (shown.has(key)) continue;
shown.add(key); chips.push({name, filter});
shown.add(key); chips.push({name, filter, source});
}
};
if (record.imageTags?.state === 'current') add(record.imageTags.tags, filterable);
if (record.imageTags?.state === 'current') add(record.imageTags.tags, filterable, 'image');
const {library} = record;
if (library?.status === 'linked') add(library.tags, filterable && library.tags_scope === 'positive');
if (library?.status === 'linked') add(library.tags, filterable && library.tags_scope === 'positive', 'linked');
return chips;
}
/** D154 (#449): a tag section's visible heading; linked tags name their owner as D146's category chip does. */
export function tagSectionLabel(source, library) {
if (source === 'image') return 'Image tags';
return library?.tags_scope === 'pair' ? 'Recipe tags' : 'Prompt tags';
}
/** A concept chip whose visible text carries a visually hidden prefix naming what it is. */
/**
* The category chip (#332 D146): the hidden prefix, the category, then `span.chip-source` " · this image", " · prompt"
@@ -295,14 +303,11 @@ export function mountFileInspector(container, {api, sender, parentScope, variant
// not what it matches, so they (and chips where no Tags filter exists, as in the viewer) stay plain. With the
// D39 fallback a recipe without tags shows its positive's, which are filter buttons again. D88: the file's own
// image tags lead the row and, where a Tags filter exists, always set it (it matches them too).
// D154 (#449): each source gets its own labelled section, so image tags never read as part of the negative prompt.
const chips = inspectorTagChips(record, {filterable:!!onTagFilter});
if (chips.length) {
const tags = el('div', undefined, 'tags');
for (const {name, filter} of chips) {
if (!filter) {tags.append(el('span', name, 'chip')); continue;}
const chip = createButton(name, 'chip'); contentScope.listen(chip, 'click', () => onTagFilter(name)); tags.append(chip);
}
inner.append(tags);
for (const source of ['image', 'linked']) {
const own = chips.filter(chip => chip.source === source);
if (own.length) inner.append(tagSection(source, tagSectionLabel(source, library), own));
}
inner.append(...generationFacts(record, 'meta-grid', 'small'));
if (full) inner.append(summaryButton(record), provenance(record, badge), rawMetadata(record));
@@ -336,6 +341,22 @@ export function mountFileInspector(container, {api, sender, parentScope, variant
}
if (!full && footer.childNodes.length) container.append(footer);
}
/**
* D154: `section.tag-section` in the prompt sections' frame, its `.role-label` heading naming the source and count.
* The region's name leaves the count out and carries the prompt blocks' context ("Current file image tags").
*/
function tagSection(source, label, chips) {
const section = el('section', undefined, `tag-section ${source}-tags`), heading = el('div', undefined, 'prompt-heading');
section.setAttribute('aria-label', promptContext ? `${promptContext} ${label.toLowerCase()}` : label);
heading.append(el('span', `${label} · ${chips.length}`, 'role-label'));
const tags = el('div', undefined, 'tags');
for (const {name: text, filter} of chips) {
if (!filter) {tags.append(el('span', text, 'chip')); continue;}
const chip = createButton(text, 'chip'); contentScope.listen(chip, 'click', () => onTagFilter(text)); tags.append(chip);
}
section.append(heading, tags);
return section;
}
/**
* D117 (LI-4): "Copy settings summary" after the six facts, the legacy COPY ALL. It copies as the concept's prompt
* copy does: the shared toast, or the select-to-copy dialog where the clipboard is unavailable or refused.
+5
View File
@@ -230,6 +230,11 @@ main:focus-visible { outline: 2px solid var(--ca-focus); outline-offset: 4px; }
details, and an unavailable download stays visibly inert next to its reason. */
.file-inspector { min-inline-size: 0; overflow-wrap: anywhere; }
.file-inspector [aria-disabled="true"] { opacity: .6; cursor: not-allowed; }
/* #449 D154: the image's own tags and the linked prompt's or recipe's tags sit in the prompt sections' frame (concept
.prompt-section), under a muted .role-label; the heading's 8px spaces the chips, as it spaces a prompt's text. */
.file-inspector .tag-section { border-top: 1px solid var(--line); padding-top: 15px; margin-top: 15px; }
.file-inspector .tag-section .role-label { color: var(--ca-muted); }
.file-inspector .tag-section > .tags { margin-top: 0; }
/* #249 D20: the viewer details' pair send and history buttons wrap inside the narrow rail at 200% zoom with wider fonts,
where the toolkit's `nowrap` would push them past its edge. */
.ca-tools #viewer-details > button { white-space: normal; /* D20 */ }