diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index ba5a225..ab641fe 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -632,7 +632,9 @@ covered by the `styles.css` manifest. `prompt_gloss.js` looks up and keeps the tags' meanings in the picked language. `ui_prompt_source_deck.js` owns the card list (three built-in cards and the saved prompts), search, new-card form, library sync and renaming / deleting a saved prompt; `prompt_material_source.js` reads - saved prompts as cards and saves one; `ui_prompt_card_popover.js` owns the card preview (hover + saved prompts as cards and saves one; `ui_prompt_reference.js` is the reference column a + clicked card opens in, between the cards and the boxes (its tags taken one by one or all, ticks + on those the box has; it widens the drawer while open, and takes the cards' place where three columns do not fit); `ui_prompt_card_popover.js` owns the card preview (hover corridor, pin, Copy / Add / Rename / Delete); `prompt_card_drag.js` lets a card or a box be dragged out of the drawer onto a canvas prompt box or empty canvas (through `material_drag.js` and `prompt_drop.js`), passing through inside the drawer so the boxes still take it. diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ecfbe7..ecc743c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -124,9 +124,13 @@ - **Quick to open with many models, or models on a network drive** (a NAS, a cloud server's model disk): the page reads the headers of models not scanned yet 16 at a time and remembers them until the file changes, so it opens again at once; while it counts, it says **Counting your models…** instead of staying blank. On a cloud server with 1041 unscanned models it had taken 79 seconds each time. ### ⚙️ Settings in tabs (设置分组) -- The settings page is split into tabs: **Look, Models, Workflows, Prompts, Backup, Help**. "Change where downloads go" opens Models directly. +- The settings page is split into tabs: **Look, Models, Workflows, Node packs, Prompts, Backup, Help**. "Change where downloads go" opens Models directly. - **Where downloads (and now imports) go is a choice**: by base model (the new default), in Downloads then by base model, all in a Downloads folder, straight in the type's folder, or your own folder typed by hand; an example path shows the result. Without a saved setting, downloads now go by base model instead of into a Downloads folder. +### 🔖 Take single tags from a saved prompt (从存过的提示词里挑词) +- **In Prompt Studio a clicked card opens as a reference** between the cards and the prompt you are editing, as its tags (with their meanings when a language is picked). Click one tag and only that tag goes into the prompt; tags the prompt has already are ticked. **Add all** puts the whole card in, as a click on a card used to; the preview's Add and dragging a card work as before. The ✕ closes the reference; beside the canvas the drawer widens by the column while it is open. +- **It does not squeeze a narrow window**: where three columns do not fit (a side-panel browser window, a narrow drawer), the reference takes the card list's place and **‹ Cards** brings the list back, so the reference and the prompt you edit each get half the width. + ### Fixes - **Edit emotions: every clip in the list, and the line follows the clip (编辑情绪:下拉列出全部音频,换音频台词跟着换)**: a box that already held a file listed only the files whose names contain it (the browser filters by the box's text), so a character's other clips seemed missing. Pressing the box now lists all of them, with the current file shown greyed; leaving without a pick keeps it. Picking another clip no longer keeps the old clip's line beside it: the line the node already reads for that clip is filled in, otherwise the line the node finds for the clip (its .txt / .lab or the annotation list) is filled in, with Anomalous_TTS from interface 14 on (older versions leave the box blank and the node reads the line when it speaks); going back to the first clip brings its line back. A blank line box shows, greyed, the line the node uses for its clip. - **Search a character's clips by line (按台词找参考音频)**: Edit emotions has a search box above the clips: type part of a line or a file name and the matches list with their lines, to hear (▶), **Use as main voice** or **Add as emotion** (a new emotion row with that clip and its line, ready for its name). Searching lines needs Anomalous_TTS interface 14; with an older version it searches file names. diff --git a/docs/architecture/frontend.md b/docs/architecture/frontend.md index 0c79245..05eddc8 100644 --- a/docs/architecture/frontend.md +++ b/docs/architecture/frontend.md @@ -343,7 +343,10 @@ while the page is visible (scheduled after the previous request), on focus/visib and after a save; a newer request cancels the older one and a failed one keeps the current cards. List summaries are not prompt bodies: a prompt's text is read through `material_prompt_data.js` once per filename and timestamp, since a saved prompt's text -never changes. A card's role decides its box: a positive card never goes into a +never changes. A clicked card opens as a reference (`ui_prompt_reference.js`, read only, +one at a time) between the cards and the boxes: a click on one of its tags adds that tag, +"Add all" the whole card (as the preview's Add does); the ticks follow the box's text on the +same 400 ms poll. A card's role decides its box: a positive card never goes into a negative box or the reverse; boxes of unknown or both roles take either. A saved card's preview renames it (`update_material`, tags kept) or deletes it (`delete_material`, to the Recycle Bin). A card, or a box's handle with the box's text diff --git a/web/modules/locales.js b/web/modules/locales.js index 105d860..cb7a0c3 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -1798,6 +1798,15 @@ export const i18n = { promptStudioRemoveTag: "删掉", promptStudioTagHint: "点一下选中(可调权重),双击改字,拖动换位置", promptStudioAllThere: "这些词已经都有了", + promptStudioRefTitle: "参考:{name}", + promptStudioRefClose: "关掉参考", + promptStudioRefBack: "‹ 卡片", + promptStudioRefAddAll: "全部加入", + promptStudioRefAddAllHint: "把这张卡片的词一次全加进去(已经有的不重复加)", + promptStudioRefNote: "点一个词,只把它加进正在改的提示词;打勾的是已经有的。", + promptStudioRefTake: "只把这个词加进去", + promptStudioRefThere: "这个词已经有了", + promptStudioRefHint: "点卡片现在会先把它打开成参考,可以一个词一个词地挑;想整张加,点参考上的“全部加入”。", promptStudioAddedSkipped: "加了 {count} 个,{skipped} 个已经有了", promptDeckSearch: "搜索词卡…", promptDeckNew: "新建词卡", @@ -4485,6 +4494,15 @@ export const i18n = { promptStudioRemoveTag: "Remove", promptStudioTagHint: "Click to select (and set its weight), double-click to edit, drag to move", promptStudioAllThere: "Those tags are there already", + promptStudioRefTitle: "Reference: {name}", + promptStudioRefClose: "Close the reference", + promptStudioRefBack: "‹ Cards", + promptStudioRefAddAll: "Add all", + promptStudioRefAddAllHint: "Add every tag of this card at once (those there already are not added twice)", + promptStudioRefNote: "Click a tag to add just that one to the prompt being edited; ticked ones are there already.", + promptStudioRefTake: "Add just this tag", + promptStudioRefThere: "This tag is there already", + promptStudioRefHint: "A clicked card now opens as a reference, to pick its tags one by one; to add it whole, press “Add all” on the reference.", promptStudioAddedSkipped: "Added {count}; {skipped} were there already", promptDeckSearch: "Search cards…", promptDeckNew: "New card", diff --git a/web/modules/prompt_tags.js b/web/modules/prompt_tags.js index b7b324f..b8fd5cd 100644 --- a/web/modules/prompt_tags.js +++ b/web/modules/prompt_tags.js @@ -67,7 +67,8 @@ export function withWeight(tag, weight) { return rounded === 1 ? core : `(${core}:${rounded})`; } -const tagKey = tag => tagWeight(tag).core.trim().toLowerCase(); +/** What makes two tags the same: their words, whatever the weight or case. */ +export const tagKey = tag => tagWeight(tag).core.trim().toLowerCase(); // New tags before item `at` (the end when past it); the text's ending stays at the end. function spliceTags(parsed, at, texts) { diff --git a/web/modules/ui_prompt_box_editor.js b/web/modules/ui_prompt_box_editor.js index fa4d11b..66fc074 100644 --- a/web/modules/ui_prompt_box_editor.js +++ b/web/modules/ui_prompt_box_editor.js @@ -45,7 +45,7 @@ const asTag = text => (/^[A-Z][a-z]/.test(text) ? text[0].toLowerCase() + text.s * (false when it did not). `prefs` is the studio's { view: 'tags' | 'text', glossLang }. `refuse(card)` * says why a card may not go in ('' when it may); `onSave(value)` keeps the text as a card. * `drawer` is the studio drawer, for the handle that drags the text onto the canvas. - * Returns { element, role, render(), refresh(), addText(text) }. + * Returns { element, role, value(), render(), refresh(), addText(text) }. */ export function createPromptBoxEditor({ role, name, getValue, write, prefs, refuse, onSave, drawer, scope }) { const id = nextEditorId++; @@ -406,6 +406,7 @@ export function createPromptBoxEditor({ role, name, getValue, write, prefs, refu return { element, role, + value: getValue, render, /** Redraws when the text changed elsewhere (not while it is being edited here). */ refresh() { diff --git a/web/modules/ui_prompt_reference.js b/web/modules/ui_prompt_reference.js new file mode 100644 index 0000000..b3ddf28 --- /dev/null +++ b/web/modules/ui_prompt_reference.js @@ -0,0 +1,163 @@ +/** + * Prompt Studio's reference column: a clicked card opens here, between the cards and the prompt + * boxes, as its tags (with their meanings when a language is picked), so single tags can be + * taken from it: a click puts that tag into the box the card goes into (ui_prompt_target.js), + * "Add all" the whole card. Tags the box has already are ticked. Read only: the card itself is + * never changed here. In the drawer it widens the drawer by the column while it is open; where + * three columns do not fit it takes the cards' place, and "‹ Cards" goes back (the stylesheet). + */ + +import { translate as t } from './locales.js'; +import { fetchGlosses, glossOf } from './prompt_gloss.js'; +import { promptTagsOf, tagKey, tagWeight } from './prompt_tags.js'; +import { showWorkbenchToast } from './ui_prompt_toast.js'; + +const POLL_MS = 400; +const EXTRA_WIDTH = 280; // the drawer grows by this while the column is open +const HINT_KEY = 'anomalous_prompt_reference_hint'; +const ROLE_KEYS = { positive: 'recipePromptRolePositive', negative: 'recipePromptRoleNegative' }; + +function el(tag, className, content) { + const node = document.createElement(tag); + if (className) node.className = className; + if (content !== undefined) node.textContent = content; + return node; +} + +function button(className, label, title, onClick) { + const node = el('button', className, label); + node.type = 'button'; + if (title) node.title = title; + node.onclick = onClick; + return node; +} + +/** Says once, the first time a card opens here, that a click no longer adds the whole card. */ +function hintOnce() { + try { + if (localStorage.getItem(HINT_KEY)) return; + localStorage.setItem(HINT_KEY, '1'); + } catch { /* no storage here: say it each visit */ } + showWorkbenchToast(t('promptStudioRefHint')); +} + +/** + * Fills `grid` (between the cards and the boxes). `target` is the prompt side ({ addCard(card, + * text), valueFor(card) }); `prefs` the studio's { glossLang }. Returns { open(card), close(), render() }. + */ +export function createPromptReference(grid, { drawer, scope, prefs, target }) { + const panel = el('section', 'anomalous-ps-ref'); + panel.hidden = true; + const head = el('header', 'anomalous-ps-target-head'); + const label = el('div', 'anomalous-ps-target-label'); + const title = el('span', 'anomalous-ps-target-name'); + const closeButton = button('anomalous-ps-target-x', '✕', t('promptStudioRefClose'), () => close()); + closeButton.setAttribute('aria-label', t('promptStudioRefClose')); + label.append(title, closeButton); + const addAll = button('anomalous-ps-mini', t('promptStudioRefAddAll'), t('promptStudioRefAddAllHint'), () => { + if (card) void target.addCard(card).then(render); + }); + const back = button('anomalous-ps-mini anomalous-ps-ref-back', t('promptStudioRefBack'), t('promptStudioRefClose'), () => close()); + head.append(back, label, addAll); + const role = el('span', 'anomalous-ps-role'); + const note = el('p', 'anomalous-ps-target-note', t('promptStudioRefNote')); + const tagsEl = el('div', 'anomalous-ps-tags'); + panel.append(head, role, note, tagsEl); + grid.insertBefore(panel, grid.children[1] || null); // right after the cards + + let card = null; + let seen = null; // the box text the ticks were drawn from + let taken = new Set(); // tags taken from this card here (a translated one reads differently in the box) + let widened = null; // { from, to } drawer widths while the column is open + let glossTimer = null; + let glossFailed = ''; + const timer = setInterval(() => { + if (card && target.valueFor(card) !== seen) render(); + }, POLL_MS); + scope.onDispose(() => { clearInterval(timer); clearTimeout(glossTimer); }); + + function widen(open) { + if (!drawer.classList.contains('anomalous-prompt-studio-drawer')) return; + if (open && !widened) { + const from = drawer.getBoundingClientRect().width; + const to = Math.round(Math.min(window.innerWidth * 0.85, from + EXTRA_WIDTH)); + drawer.style.width = `${to}px`; + widened = { from, to }; + } else if (!open && widened) { + // Dragged wider or narrower meanwhile: that width is the user's now. + if (Math.round(drawer.getBoundingClientRect().width) === widened.to) drawer.style.width = `${widened.from}px`; + widened = null; + } + } + + function glossText(tag) { + const meaning = glossOf(tag, prefs.glossLang); + return meaning === undefined ? (glossFailed === prefs.glossLang ? '' : '…') : meaning; + } + + function wantGlosses(tags) { + clearTimeout(glossTimer); + const lang = prefs.glossLang; + if (!lang || glossFailed === lang || tags.every(tag => glossOf(tag, lang) !== undefined)) return; + glossTimer = setTimeout(async () => { + const ok = await fetchGlosses(tags, lang, scope.signal); + if (scope.signal.aborted || !card) return; + if (!ok) glossFailed = lang; + render(); + }, 250); + } + + function chipFor(tag, have) { + const { core, weight } = tagWeight(tag); + const there = have.has(tagKey(tag)) || taken.has(tagKey(tag)); + const chip = el('button', `anomalous-ps-tag is-ref${there ? ' is-there' : ''}`); + chip.type = 'button'; + chip.title = t(there ? 'promptStudioRefThere' : 'promptStudioRefTake'); + const main = el('span', 'anomalous-ps-tag-main'); + if (there) main.append(el('span', 'anomalous-ps-ref-tick', '✓')); + main.append(el('span', 'anomalous-ps-tag-text', core)); + if (weight !== 1) main.append(el('span', 'anomalous-ps-tag-weight', String(weight))); + chip.append(main); + if (prefs.glossLang) chip.append(el('span', 'anomalous-ps-tag-gloss', glossText(tag))); + chip.onclick = async () => { + if (there) return; + chip.disabled = true; + if (await target.addCard(card, tag)) taken.add(tagKey(tag)); + render(); + }; + return chip; + } + + /** Draws the open card's tags, ticking those its box has. */ + function render() { + if (!card) return; + seen = target.valueFor(card); + const have = new Set(promptTagsOf(seen ?? '').map(tagKey)); + const tags = promptTagsOf(card.content); + title.textContent = t('promptStudioRefTitle', { name: card.title }); + const cardRole = card.role === 'negative' ? 'negative' : 'positive'; + role.className = `anomalous-ps-role is-${cardRole}`; + role.textContent = t(ROLE_KEYS[cardRole]); + tagsEl.replaceChildren(...tags.map(tag => chipFor(tag, have))); + wantGlosses(tags); + } + + function open(next) { + hintOnce(); + if (card?.id !== next.id) taken = new Set(); + card = next; + panel.hidden = false; + grid.classList.add('has-ref'); + widen(true); + render(); + } + + function close() { + card = null; + panel.hidden = true; + grid.classList.remove('has-ref'); + widen(false); + } + + return { open, close, render }; +} diff --git a/web/modules/ui_prompt_source_deck.js b/web/modules/ui_prompt_source_deck.js index fb103c4..4fb30aa 100644 --- a/web/modules/ui_prompt_source_deck.js +++ b/web/modules/ui_prompt_source_deck.js @@ -1,7 +1,8 @@ /** * Prompt Studio's cards: three common ones and the prompts you saved (prompt_material_source.js), - * with search and a small form for a new card. A card is clicked (or added from its preview, - * ui_prompt_card_popover.js) to go into the prompt box of its role, dragged onto a box, or + * with search and a small form for a new card. A card is clicked to open it as a reference + * (ui_prompt_reference.js, tags taken one by one), added whole from its preview + * (ui_prompt_card_popover.js) into the prompt box of its role, dragged onto a box, or * dragged out onto the canvas (prompt_card_drag.js); a saved one is renamed or deleted from its * preview. The saved list is checked again every 30 seconds and when the window comes back. */ @@ -32,15 +33,15 @@ function button(className, label, title, onClick) { return node; } -/** Fills `parent`; `onPick(card)` takes a clicked card. Returns { sync() }. */ -export function createPromptSourceDeck(parent, drawer, scope, onPick) { +/** Fills `parent`; `onPick(card)` takes a clicked card, `onAdd(card)` one added whole from its preview. Returns { sync() }. */ +export function createPromptSourceDeck(parent, drawer, scope, { onPick, onAdd }) { const presets = PROMPT_PRESETS.map(card => ({ ...card, title: t(card.titleKey) })); let saved = []; let keyword = ''; const popover = createCardPopover({ drawer, scope, - onAdd: card => onPick(card), + onAdd, onRename: card => void rename(card), onDelete: card => void remove(card), }); diff --git a/web/modules/ui_prompt_target.js b/web/modules/ui_prompt_target.js index c564646..0613a9e 100644 --- a/web/modules/ui_prompt_target.js +++ b/web/modules/ui_prompt_target.js @@ -48,7 +48,7 @@ function refusal(card, boxRole) { /** * Fills `parent`. `owner` keeps the draft; `prefs` is the studio's { view, glossLang }; - * `onSave(text, role)` keeps a box's text as a card. Returns { addCard(card), render() }. + * `onSave(text, role)` keeps a box's text as a card. Returns { addCard(card, text), valueFor(card), render() }. */ export function createPromptTarget(parent, { owner, drawer, scope, prefs, onSave }) { const draft = owner.promptStudioDraft ||= { positive: '', negative: '' }; @@ -162,17 +162,24 @@ export function createPromptTarget(parent, { owner, drawer, scope, prefs, onSave const timer = setInterval(tick, POLL_MS); scope.onDispose(() => clearInterval(timer)); + /** The box a card goes into: the one of its role, or one that takes either; null when none does. */ + const editorFor = card => { + const role = card.role === 'negative' ? 'negative' : 'positive'; + return editors.find(item => item.role === role) || editors.find(item => !refusal(card, item.role)) || null; + }; + return { - /** A card clicked: into the box of its role (or one that takes either), at the end. */ - addCard(card) { - const role = card.role === 'negative' ? 'negative' : 'positive'; - const editor = editors.find(item => item.role === role) || editors.find(item => !refusal(card, item.role)); + /** `text` (the whole card when left out) into the box the card goes into, at the end; false when it did not go in. */ + async addCard(card, text = card.content) { + const editor = editorFor(card); if (!editor) { - showWorkbenchToast(t('promptStudioNoBoxFor', { card: t(ROLE_NAMES[role]) })); - return; + showWorkbenchToast(t('promptStudioNoBoxFor', { card: t(ROLE_NAMES[card.role === 'negative' ? 'negative' : 'positive']) })); + return false; } - void editor.addText(card.content); + return editor.addText(text); }, + /** The text of the box the card goes into, null when there is none. */ + valueFor: card => editorFor(card)?.value() ?? null, render: () => editors.forEach(editor => editor.render()), }; } \ No newline at end of file diff --git a/web/modules/ui_prompt_workbench.js b/web/modules/ui_prompt_workbench.js index 0e50be1..315387b 100644 --- a/web/modules/ui_prompt_workbench.js +++ b/web/modules/ui_prompt_workbench.js @@ -3,8 +3,9 @@ * a top bar with the view switch (tags or the whole text), the language tags' meanings are shown * in (or none), and on the page "Beside the canvas", in the drawer its dock side, "Back to the * window" and Close; the cards on the drawer's outer side (ui_prompt_source_deck.js) and - * the prompt boxes beside the canvas (ui_prompt_target.js). A clicked card goes into the box of - * its role; Save on a box keeps its text as a card under My prompts. The view and the meaning + * the prompt boxes beside the canvas (ui_prompt_target.js). A clicked card opens between them as + * a reference (ui_prompt_reference.js) to take its tags one by one; Save on a box keeps its text + * as a card under My prompts. The view and the meaning * language are remembered in this browser. */ @@ -13,6 +14,7 @@ import { GLOSS_LANGUAGES } from './prompt_gloss.js'; import { promptTitle } from './prompt_composition.js'; import { savePromptCard } from './prompt_material_source.js'; import { anomalousPrompt } from './ui_dialog.js'; +import { createPromptReference } from './ui_prompt_reference.js'; import { createPromptSourceDeck } from './ui_prompt_source_deck.js'; import { createPromptTarget } from './ui_prompt_target.js'; import { showWorkbenchToast } from './ui_prompt_toast.js'; @@ -91,6 +93,7 @@ export function createPromptWorkbench(owner, drawer, scope, { mode = 'docked', o savePrefs(prefs); drawSwitches(); target.render(); + reference.render(); }; controls.append(glossPicker); @@ -135,6 +138,11 @@ export function createPromptWorkbench(owner, drawer, scope, { mode = 'docked', o } } - const deck = createPromptSourceDeck(grid, drawer, scope, card => target.addCard(card)); + const deck = createPromptSourceDeck(grid, drawer, scope, { + onPick: card => reference.open(card), + onAdd: card => void target.addCard(card), + }); const target = createPromptTarget(grid, { owner, drawer, scope, prefs, onSave: save }); + // Between the cards and the boxes, whichever side the drawer is docked on. + const reference = createPromptReference(grid, { drawer, scope, prefs, target }); } \ No newline at end of file diff --git a/web/styles/21-prompt-studio.css b/web/styles/21-prompt-studio.css index 9cb0798..5800fa1 100644 --- a/web/styles/21-prompt-studio.css +++ b/web/styles/21-prompt-studio.css @@ -20,6 +20,10 @@ grid-template-columns: 240px minmax(0, 1fr); } +.anomalous-prompt-page .anomalous-ps-grid.has-ref { + grid-template-columns: 240px minmax(0, 1fr) minmax(0, 1fr); +} + .anomalous-ps { container: anomalous-ps / inline-size; display: flex; @@ -165,6 +169,51 @@ grid-template-columns: minmax(0, 1fr) 200px; } +/* A card opened as a reference (ui_prompt_reference.js) sits between the cards and the boxes. */ +.anomalous-ps-grid.has-ref { + grid-template-columns: 200px minmax(0, 1fr) minmax(0, 1fr); +} + +.anomalous-prompt-studio-drawer:not(.is-dock-left) .anomalous-ps-grid.has-ref { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr) 200px; +} + +.anomalous-ps-ref { + display: flex; + flex-direction: column; + gap: 8px; + min-height: 0; + padding: 10px 12px 16px; + overflow-y: auto; + border-right: 1px solid var(--amb-border); + background: var(--amb-bg-panel); +} + +.anomalous-prompt-studio-drawer:not(.is-dock-left) .anomalous-ps-ref { + order: 1; + border-right: 0; + border-left: 1px solid var(--amb-border); +} + +.anomalous-ps-ref .anomalous-ps-role { + align-self: flex-start; +} + +.anomalous-ps-tag.is-ref { + font-family: inherit; + text-align: left; + cursor: copy; +} + +.anomalous-ps-tag.is-ref.is-there { + opacity: 0.55; + cursor: default; +} + +.anomalous-ps-ref-tick { + color: var(--amb-ok-text); +} + .anomalous-ps-deck { display: flex; flex-direction: column; @@ -648,6 +697,7 @@ /* These blocks set their own display, which would outrank the hidden attribute. */ .anomalous-ps-new[hidden], +.anomalous-ps-ref[hidden], .anomalous-ps-target-note[hidden], .anomalous-ps-foreign[hidden], .anomalous-ps-tags[hidden], @@ -656,7 +706,33 @@ display: none; } -/* A narrow drawer stacks the cards over the prompt boxes. */ +/* No room for three columns: the reference takes the cards' place, its "‹ Cards" goes back. */ +.anomalous-ps-ref-back { + display: none; +} + +@container anomalous-ps (max-width: 980px) { + .anomalous-ps-grid.has-ref .anomalous-ps-deck { + display: none; + } + + .anomalous-ps-grid.has-ref, + .anomalous-prompt-page .anomalous-ps-grid.has-ref, + .anomalous-prompt-studio-drawer:not(.is-dock-left) .anomalous-ps-grid.has-ref { + grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); + } + + .anomalous-ps-ref-back { + display: inline-flex; + flex-shrink: 0; + } + + .anomalous-ps-ref .anomalous-ps-target-x { + display: none; + } +} + +/* A narrow drawer stacks the cards (or the reference) over the prompt boxes. */ @container anomalous-ps (max-width: 540px) { .anomalous-ps-grid, .anomalous-prompt-studio-drawer:not(.is-dock-left) .anomalous-ps-grid { @@ -664,8 +740,16 @@ grid-template-rows: minmax(120px, 34%) minmax(0, 1fr); } + .anomalous-ps-grid.has-ref, + .anomalous-prompt-studio-drawer:not(.is-dock-left) .anomalous-ps-grid.has-ref { + grid-template-columns: minmax(0, 1fr); + grid-template-rows: minmax(120px, 40%) minmax(0, 1fr); + } + .anomalous-ps-deck, - .anomalous-prompt-studio-drawer:not(.is-dock-left) .anomalous-ps-deck { + .anomalous-prompt-studio-drawer:not(.is-dock-left) .anomalous-ps-deck, + .anomalous-ps-ref, + .anomalous-prompt-studio-drawer:not(.is-dock-left) .anomalous-ps-ref { order: 0; border: 0; border-bottom: 1px solid var(--amb-border);