feat(prompt-studio): enhance card tactile affordance and auto-snap drop dock

This commit is contained in:
DemonGatanjieu
2026-09-12 23:03:40 +08:00
parent dfa208b44c
commit db09ef5dcc
3 changed files with 278 additions and 38 deletions
+2 -1
View File
@@ -153,7 +153,7 @@ DOM or live LiteGraph state.
features compact vertical spacing with an inline topbar hosting the preset name input (`.anomalous-prompt-name-input`), direct `[💾 保存方案]` button, secondary action dropdown (`[··· 更多]` for new draft, preset tags, and JSON export), and window close button (`[✕]`), completely eliminating bulky standalone metadata strips;
in docked sidebar mode (`#anomalous-container.anomalous-docked`) and narrow screen layouts (<860px), adopts a Unified Dual-Zone Flow (上下联动一体流): completely eliminates clumsy tab switching (`[词卡库 | 拼装台 | 全部]`), arranging the Assembler Stage on top (`order: 1`, max-height 54%) and Candidate Cards Deck on bottom (`order: 2`, max-height 50%), both with independent scroll containers to ensure candidate cards and assembled Lego blocks remain simultaneously visible;
the Cards Deck hosts candidate prompt cards in a compact 2-column chip grid (`repeat(auto-fill, minmax(130px, 1fr))`) with 1-tap instant assembly (`cardEl.onclick` adds card to the active track with `@keyframes anomalous-just-added` animated feedback) alongside native HTML5 drag-and-drop,
features restrained dark slate backgrounds (`#24262E`) with category-specific left accent borders (Base/Style/Subject/Trigger),
features tactile glassmorphic card styling (dual-layer elevation shadows, top edge highlight, and category glow accents), a physical drag grip (`⠿`), a usage tip banner (`💡 点击词卡或拖拽至右侧拼装台`), monospace prompt snippet chips, and high-visibility `[+ 加入拼装台]` action buttons with instant click feedback,
minimalist empty state container (`.anomalous-source-empty`), a streamlined col-header holding a unified action button group (`[🎯 提取]`, `[📥 导入]`, `[➕ 新建]`),
one-click canvas node prompt extraction (`🎯 提取`, reading selected ComfyUI text nodes like `CLIPTextEncode` with downstream link connection traversal for accurate negative conditioning detection, auto-generating categorised cards) and one-click
Material Library batch sync (`📥 导入`, cancellable fetch scoped to `category=prompts`), plus on-demand persistent card creation
@@ -162,6 +162,7 @@ DOM or live LiteGraph state.
the Assembler Stage hosts the sequential Lego block track (`anomalous-assembly-track`), compact modular blocks (~52px height) allowing generous top-to-bottom sequence stacking,
supports bidirectional drag-and-drop reordering with ghost indicator lines, role-aware up/down swapping, instant A/B bypass toggles (greyscale dimming without deleting),
interactive category pills, one-click Smart Sort (`🪄 按分类排序`, Base ➔ Style ➔ Subject ➔ Trigger),
an interactive Auto-snap Dock (`.anomalous-assembly-snap-dock`) positioned at the bottom of the track that magnetically detects dragover across the empty lower track area to auto-snap dragged cards or blocks to the end with animated visual guidance,
and a permanently visible sticky Action Dock (`.anomalous-prompt-action-dock`) at the bottom of the assembler stage: displays live synthesis statistics (`✨ X字 · Y块`), quick copy button (`[📋]`), expandable compiled text preview drawer (`[📄 最终文本 ▾]`), insertion position selector (`[在后 ▾] / [在前 ▾]`), and the ComfyUI canvas node direct injection bar (`[#ID 节点名称] [⬇️ 写入正向/负向]`), ensuring canvas node injection is always 1-click away and never buried inside collapsed drawers;
`prompt_composition.js` provides bidirectional schema mapping (`planToWorkbenchDraft` and `workbenchDraftToSavedPlan`) ensuring
100% roundtrip data integrity across `anomalous-prompt-plan-v1` and `version: 2`, lossless legacy dual-role splitting and trailing text retention,
+108 -10
View File
@@ -444,6 +444,15 @@ function buildPromptComposer(owner, container, options = {}) {
const newCardForm = text(leftPanel, 'div', '', 'anomalous-workbench-new-card-form');
newCardForm.style.display = 'none';
// Usage Tip Banner
const tipBanner = text(leftPanel, 'div', '', 'anomalous-workbench-tip-banner');
tipBanner.innerHTML = `
<svg class="anomalous-tip-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M12 2v4M12 18v4M4.93 4.93l2.83 2.83M16.24 16.24l2.83 2.83M2 12h4M18 12h4M4.93 19.07l2.83-2.83M16.24 7.76l2.83-2.83"/>
</svg>
<span>${window.anomalous_browser_lang === 'zh' ? '点击词卡或拖拽至右侧拼装台,自由调换顺序' : 'Click card or drag to right stage to assemble'}</span>
`;
// Source Cards List
const sourceCardsList = text(leftPanel, 'div', '', 'anomalous-source-cards-list');
@@ -474,6 +483,47 @@ function buildPromptComposer(owner, container, options = {}) {
// Blocks Container & Dropzone
const blocksContainer = text(rightPanel, 'div', '', 'anomalous-mixer-blocks-container anomalous-assembly-track');
blocksContainer.ondragover = (e) => {
e.preventDefault();
const snapDock = blocksContainer.querySelector('.anomalous-assembly-snap-dock');
if (snapDock && (e.target === blocksContainer || e.target.closest('.anomalous-assembly-snap-dock'))) {
snapDock.classList.add('is-drag-over');
blocksContainer.classList.add('is-drag-over-end');
}
};
blocksContainer.ondragleave = (e) => {
if (!blocksContainer.contains(e.relatedTarget)) {
const snapDock = blocksContainer.querySelector('.anomalous-assembly-snap-dock');
snapDock?.classList.remove('is-drag-over');
blocksContainer.classList.remove('is-drag-over-end');
}
};
blocksContainer.ondrop = (e) => {
if (e.target === blocksContainer || e.target.closest('.anomalous-assembly-snap-dock')) {
e.preventDefault();
const snapDock = blocksContainer.querySelector('.anomalous-assembly-snap-dock');
snapDock?.classList.remove('is-drag-over');
blocksContainer.classList.remove('is-drag-over-end');
const jsonStr = e.dataTransfer.getData('application/json');
if (jsonStr) {
try {
addSourceCardToMixer(JSON.parse(jsonStr));
return;
} catch (err) {}
}
if (draggedBlockId) {
const fromIndex = draft.plan.parts.findIndex(p => p.id === draggedBlockId);
if (fromIndex >= 0) {
const [moved] = draft.plan.parts.splice(fromIndex, 1);
draft.plan.parts.push(moved);
syncDraftSynthesizedText(draft);
renderBlocksList();
updateOutputPreview();
}
}
}
};
// Bottom Live Assembled Action Dock (Always visible sticky bar at bottom of assembler)
const outputDeck = text(rightPanel, 'div', '', 'anomalous-mixer-deck-output anomalous-prompt-action-dock is-collapsed');
@@ -938,13 +988,7 @@ function buildPromptComposer(owner, container, options = {}) {
filtered.forEach(card => {
const cardEl = text(sourceCardsList, 'article', '', `anomalous-source-prompt-card is-cat-${card.category}`);
cardEl.setAttribute('draggable', 'true');
cardEl.title = window.anomalous_browser_lang === 'zh' ? '点击直接加入上方拼装台(也可拖拽)' : 'Click to add to mixer above (or drag)';
// Instant tap to add card to mixer
cardEl.onclick = (e) => {
if (e.target.closest('button') || e.target.closest('input')) return;
addSourceCardToMixer(card);
};
cardEl.title = window.anomalous_browser_lang === 'zh' ? '点击直接加入右侧拼装台(也可按住拖拽)' : 'Click to add to stage (or drag)';
// Drag Start -> transfer prompt card payload
cardEl.ondragstart = (e) => {
@@ -970,6 +1014,9 @@ function buildPromptComposer(owner, container, options = {}) {
const header = text(cardEl, 'div', '', 'anomalous-source-card-header');
const meta = CATEGORY_META[card.category] || CATEGORY_META.subject;
const grip = text(header, 'span', '⠿', 'anomalous-source-card-grip');
grip.title = window.anomalous_browser_lang === 'zh' ? '按住拖拽至拼装台' : 'Drag to assemble';
const badge = text(header, 'span', window.anomalous_browser_lang === 'zh' ? meta.zh : meta.en, 'anomalous-source-card-badge');
badge.style.color = meta.color;
badge.style.backgroundColor = meta.bg;
@@ -994,6 +1041,9 @@ function buildPromptComposer(owner, container, options = {}) {
// Card Footer Actions
const footer = text(cardEl, 'div', '', 'anomalous-source-card-footer');
const hint = text(footer, 'span', '', 'anomalous-source-card-drag-hint');
hint.innerHTML = `<svg style="width:10px;height:10px;vertical-align:-1px;margin-right:3px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"/><path d="M12 8v8M8 12h8"/></svg>${window.anomalous_browser_lang === 'zh' ? '点击或拖拽' : 'Click or Drag'}`;
const actions = text(footer, 'div', '', 'anomalous-source-card-actions');
actions.style.marginLeft = 'auto';
@@ -1021,11 +1071,32 @@ function buildPromptComposer(owner, container, options = {}) {
}
const dockBtn = text(actions, 'button', '', 'anomalous-source-action-btn is-dock');
dockBtn.innerHTML = `<svg style="width:12px;height:12px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>${window.anomalous_browser_lang === 'zh' ? '加入组合' : 'Add to Plan'}`;
dockBtn.title = window.anomalous_browser_lang === 'zh' ? '加入当前拼装组合' : 'Add into mixer track';
dockBtn.innerHTML = `<svg style="width:11px;height:11px;margin-right:4px;vertical-align:-1px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>${window.anomalous_browser_lang === 'zh' ? '加入拼装台' : 'Add to Stage'}`;
dockBtn.title = window.anomalous_browser_lang === 'zh' ? '点击加入当前拼装台(也可直接点击卡片)' : 'Add into mixer track';
const handleCardAdd = () => {
cardEl.classList.add('is-clicked');
setTimeout(() => cardEl.classList.remove('is-clicked'), 500);
const originalHtml = dockBtn.innerHTML;
dockBtn.innerHTML = `✓ ${window.anomalous_browser_lang === 'zh' ? '已加入' : 'Added'}`;
dockBtn.classList.add('is-success');
setTimeout(() => {
if (dockBtn.isConnected) {
dockBtn.innerHTML = originalHtml;
dockBtn.classList.remove('is-success');
}
}, 700);
addSourceCardToMixer(card);
};
cardEl.onclick = (e) => {
if (e.target.closest('button') || e.target.closest('input')) return;
handleCardAdd();
};
dockBtn.onclick = (e) => {
e.stopPropagation();
addSourceCardToMixer(card);
handleCardAdd();
};
});
}
@@ -1310,6 +1381,22 @@ function buildPromptComposer(owner, container, options = {}) {
updateOutputPreview();
};
});
// Interactive Auto-snap Dock at end of track
const snapDock = text(blocksContainer, 'div', '', 'anomalous-assembly-snap-dock');
snapDock.innerHTML = `
<div class="anomalous-snap-dock-content">
<svg class="anomalous-snap-icon" style="width:14px;height:14px;vertical-align:-2px;margin-right:6px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5">
<line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/>
</svg>
<span class="anomalous-snap-dock-text">${window.anomalous_browser_lang === 'zh' ? '拖拽卡片至此自动贴合至末尾' : 'Drop card here to snap to bottom'}</span>
</div>
`;
setupDropzoneListeners(snapDock);
snapDock.onclick = () => {
leftSearch?.focus?.();
showWorkbenchToast(window.anomalous_browser_lang === 'zh' ? '点击左侧词卡即可直接加入此处' : 'Click any card on left to add here');
};
}
function setupDropzoneListeners(el) {
@@ -1327,8 +1414,19 @@ function buildPromptComposer(owner, container, options = {}) {
if (jsonStr) {
try {
addSourceCardToMixer(JSON.parse(jsonStr));
return;
} catch (err) {}
}
if (draggedBlockId) {
const fromIndex = draft.plan.parts.findIndex(p => p.id === draggedBlockId);
if (fromIndex >= 0) {
const [moved] = draft.plan.parts.splice(fromIndex, 1);
draft.plan.parts.push(moved);
syncDraftSynthesizedText(draft);
renderBlocksList();
updateOutputPreview();
}
}
};
}
+168 -27
View File
@@ -10836,49 +10836,86 @@ dialog.anomalous-gallery-viewer::backdrop {
z-index: 1;
}
/* Individual Source Prompt Card (Restrained Dark Grey Base with Category Left Border) */
.anomalous-source-prompt-card {
/* Tip Banner for Prompt Studio Usage Guidance */
.anomalous-workbench-tip-banner {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 10px;
margin: 2px 0 8px;
background: rgba(56, 189, 248, 0.07);
border: 1px solid rgba(56, 189, 248, 0.2);
border-radius: var(--amb-radius-control, 6px);
background: var(--amb-bg-card, #24262E);
border: 1px solid var(--amb-border, rgba(255, 255, 255, 0.08));
border-left: 3.5px solid #64748b;
font-size: 0.72rem;
color: #93c5fd;
line-height: 1.35;
user-select: none;
}
.anomalous-workbench-tip-banner .anomalous-tip-icon {
width: 14px;
height: 14px;
flex-shrink: 0;
color: #38bdf8;
}
/* Individual Source Prompt Card (Tactile Skewed Glassmorphic Card Aesthetics) */
.anomalous-source-prompt-card {
border-radius: 8px;
background: linear-gradient(180deg, #262935 0%, #1e2029 100%);
border: 1px solid rgba(255, 255, 255, 0.11);
border-left: 4px solid #64748b;
padding: 8px 10px;
display: flex;
flex-direction: column;
gap: 6px;
cursor: grab;
user-select: none;
transition: background-color 0.15s ease, border-color 0.15s ease, transform 0.15s ease;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), inset 0 1px 0 rgba(255, 255, 255, 0.08);
transition: background 0.15s ease, border-color 0.15s ease, transform 0.15s ease, box-shadow 0.15s ease;
}
.anomalous-source-prompt-card.is-cat-base {
border-left-color: #38bdf8;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), -2px 0 8px -3px rgba(56, 189, 248, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.theme-abyssal-scarlet .anomalous-source-prompt-card.is-cat-base {
border-left-color: #dc143c;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), -2px 0 8px -3px rgba(220, 20, 60, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.anomalous-source-prompt-card.is-cat-style {
border-left-color: #c084fc;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), -2px 0 8px -3px rgba(192, 132, 252, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.anomalous-source-prompt-card.is-cat-subject {
border-left-color: #4ade80;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), -2px 0 8px -3px rgba(74, 222, 128, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.anomalous-source-prompt-card.is-cat-trigger {
border-left-color: #fb923c;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.35), -2px 0 8px -3px rgba(251, 146, 60, 0.3), inset 0 1px 0 rgba(255, 255, 255, 0.08);
}
.anomalous-source-prompt-card:hover {
background: var(--amb-bg-card-hover, #2d303a);
border-color: var(--amb-border-hover, rgba(255, 255, 255, 0.16));
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.25);
background: linear-gradient(180deg, #2e3242 0%, #232633 100%);
border-color: rgba(255, 255, 255, 0.22);
transform: translateY(-2px);
box-shadow: 0 8px 22px rgba(0, 0, 0, 0.5), inset 0 1px 0 rgba(255, 255, 255, 0.15);
}
.anomalous-source-prompt-card:active {
cursor: grabbing;
transform: translateY(0);
}
.anomalous-source-prompt-card.is-clicked {
transform: scale(0.97);
border-color: #38bdf8 !important;
box-shadow: 0 0 16px rgba(56, 189, 248, 0.45) !important;
}
.anomalous-source-prompt-card.is-dragging-source {
@@ -10892,9 +10929,25 @@ dialog.anomalous-gallery-viewer::backdrop {
gap: 6px;
}
.anomalous-source-card-grip {
color: #64748b;
font-size: 11px;
line-height: 1;
margin-right: 1px;
letter-spacing: -1px;
cursor: grab;
opacity: 0.5;
transition: opacity 0.15s ease, color 0.15s ease;
}
.anomalous-source-prompt-card:hover .anomalous-source-card-grip {
opacity: 0.95;
color: #94a3b8;
}
.anomalous-source-card-badge {
padding: 2px 6px;
border-radius: 4px;
padding: 2px 7px;
border-radius: 9999px;
font-size: 0.66rem;
font-weight: 600;
border: 1px solid transparent;
@@ -10902,8 +10955,9 @@ dialog.anomalous-gallery-viewer::backdrop {
}
.anomalous-source-card-title {
font-size: 0.8rem;
color: #e2e8f0;
font-size: 0.82rem;
font-weight: 600;
color: #f1f5f9;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -10911,16 +10965,18 @@ dialog.anomalous-gallery-viewer::backdrop {
}
.anomalous-source-card-snippet {
font-size: 0.74rem;
color: #94a3b8;
line-height: 1.4;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 0.72rem;
color: #cbd5e1;
line-height: 1.45;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
background: rgba(0, 0, 0, 0.2);
padding: 4px 6px;
border-radius: 4px;
background: rgba(0, 0, 0, 0.32);
border: 1px solid rgba(255, 255, 255, 0.06);
padding: 5px 8px;
border-radius: 5px;
}
.anomalous-source-card-footer {
@@ -10934,7 +10990,10 @@ dialog.anomalous-gallery-viewer::backdrop {
.anomalous-source-card-drag-hint {
color: #64748b;
font-size: 0.68rem;
font-size: 0.66rem;
display: inline-flex;
align-items: center;
gap: 3px;
}
.anomalous-source-card-actions {
@@ -10960,16 +11019,31 @@ dialog.anomalous-gallery-viewer::backdrop {
}
.anomalous-source-action-btn.is-dock {
background: rgba(255, 255, 255, 0.06);
border-color: rgba(255, 255, 255, 0.14);
color: var(--amb-text-main, #f1f5f9);
font-weight: 500;
background: rgba(56, 189, 248, 0.12);
border-color: rgba(56, 189, 248, 0.35);
color: #38bdf8;
font-weight: 600;
border-radius: 5px;
padding: 3px 8px;
display: inline-flex;
align-items: center;
gap: 3px;
font-size: 0.7rem;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}
.anomalous-source-action-btn.is-dock:hover {
background: var(--amb-bg-card-hover, rgba(255, 255, 255, 0.14));
border-color: var(--amb-border-hover, rgba(255, 255, 255, 0.25));
color: #ffffff;
background: #38bdf8;
color: #0b1329;
border-color: #38bdf8;
box-shadow: 0 0 12px rgba(56, 189, 248, 0.45);
}
.anomalous-source-action-btn.is-dock.is-success {
background: #22c55e !important;
border-color: #22c55e !important;
color: #ffffff !important;
box-shadow: 0 0 12px rgba(34, 197, 94, 0.5) !important;
}
.anomalous-source-action-btn.is-save-lib {
@@ -11090,6 +11164,66 @@ dialog.anomalous-gallery-viewer::backdrop {
background: rgba(45, 10, 20, 0.6);
}
/* Interactive Auto-snap Dock at the bottom of Assembler blocks */
.anomalous-assembly-snap-dock {
display: flex;
align-items: center;
justify-content: center;
min-height: 44px;
padding: 8px 14px;
border: 1.5px dashed rgba(255, 255, 255, 0.14);
border-radius: 8px;
background: rgba(0, 0, 0, 0.15);
cursor: pointer;
user-select: none;
transition: all 0.2s cubic-bezier(0.16, 1, 0.3, 1);
margin-top: 6px;
color: #94a3b8;
font-size: 0.74rem;
}
.anomalous-assembly-snap-dock:hover {
border-color: rgba(255, 255, 255, 0.28);
background: rgba(255, 255, 255, 0.04);
color: #e2e8f0;
}
.anomalous-assembly-snap-dock.is-drag-over,
.anomalous-assembly-track.is-drag-over-end .anomalous-assembly-snap-dock {
border-color: #38bdf8 !important;
background: rgba(56, 189, 248, 0.12) !important;
box-shadow: 0 0 18px rgba(56, 189, 248, 0.25), inset 0 0 10px rgba(56, 189, 248, 0.05) !important;
color: #38bdf8 !important;
transform: scale(1.01);
}
.theme-abyssal-scarlet .anomalous-assembly-snap-dock.is-drag-over,
.theme-abyssal-scarlet .anomalous-assembly-track.is-drag-over-end .anomalous-assembly-snap-dock {
border-color: #dc143c !important;
background: rgba(220, 20, 60, 0.15) !important;
box-shadow: 0 0 18px rgba(220, 20, 60, 0.3) !important;
color: #ff6b81 !important;
}
.anomalous-snap-dock-content {
display: flex;
align-items: center;
gap: 6px;
font-weight: 500;
pointer-events: none;
}
.anomalous-snap-icon {
width: 14px;
height: 14px;
transition: transform 0.2s ease;
}
.anomalous-assembly-snap-dock.is-drag-over .anomalous-snap-icon,
.anomalous-assembly-track.is-drag-over-end .anomalous-assembly-snap-dock .anomalous-snap-icon {
transform: rotate(90deg) scale(1.15);
}
.anomalous-workbench-sub-actions {
display: flex;
align-items: center;
@@ -12290,3 +12424,10 @@ button:hover .anomalous-btn-icon {
padding: 1px 4px !important;
font-size: 0.68rem !important;
}
#anomalous-container.anomalous-docked .anomalous-assembly-snap-dock {
min-height: 32px !important;
padding: 4px 8px !important;
font-size: 0.68rem !important;
margin-top: 3px !important;
}