feat(ui): redesign prompt composer with paper-like note cards, active drag, canvas reveal, and material import drawer
This commit is contained in:
+6
-2
@@ -94,10 +94,14 @@ DOM or live LiteGraph state.
|
||||
and global collapse/expand toggles; delegates image inspection to `ui_gallery_detail.js`;
|
||||
`node_material_actions.js` owns shared transactional node application and guarded undo;
|
||||
the library and Node Assistant use `ui_material_application.js` for the same receipt.
|
||||
- `ui_prompt_composer.js` owns Prompt Combinations inside the library;
|
||||
- `ui_prompt_composer.js` owns the Prompt Combinations paper note studio, featuring
|
||||
paper-like note cards with colored washi tapes, active whole-card dragging, quick
|
||||
actions, and a slide-out drawer to conveniently import prompts from the Material Library;
|
||||
`prompt_composition.js` joins text at the beginning/end and reads older fragment plans.
|
||||
- `material_drag.js` owns temporary canvas drop listeners, target hit testing and
|
||||
window restoration. Dropped materials use replacement; prompt-panel drops insert text.
|
||||
window restoration. During active dragging, the main modal smoothly transitions to
|
||||
full transparency (`opacity: 0; pointer-events: none`) to fully reveal the underlying
|
||||
ComfyUI canvas, restoring on drop/cancel. Dropped materials use replacement; prompt-panel drops insert text.
|
||||
- `material_inspector.js` owns shared image-metadata parsing and node-parameter
|
||||
rendering used by the library and Image Detail Workbench. Node cards feature leading
|
||||
aligned frosted-glass checkboxes with cyan micro-interactions for batch selection.
|
||||
|
||||
+28
-2
@@ -58,8 +58,21 @@ export const i18n = {
|
||||
promptImportPlan: "导入提示词方案",
|
||||
promptAlreadyImported: "素材库中已有相同的提示词方案。",
|
||||
promptImportError: "导入失败,请选择有效的提示词方案 JSON 文件(不超过 2 MiB)。",
|
||||
promptExportLocation: "提示词方案的导出按钮位于方案编辑页。",
|
||||
promptNewDraft: "新建搭配",
|
||||
promptImportFromMaterials: "从素材库导入",
|
||||
promptImportOverwrite: "覆盖",
|
||||
promptImportAppend: "追加",
|
||||
promptImportOverwritePrompt: "覆盖当前",
|
||||
promptImportAppendPrompt: "追加到当前",
|
||||
promptPositiveNote: "正向提示词便签",
|
||||
promptNegativeNote: "负向提示词便签",
|
||||
promptWordCount: "{count} 字符",
|
||||
promptDragNoteHint: "按住卡头或便签,直接拖拽至画布节点",
|
||||
promptDrawerTitle: "素材库提示词速选",
|
||||
promptDrawerSearchPlaceholder: "搜索素材名称或提示词...",
|
||||
promptDrawerEmpty: "未在素材库中找到可用的提示词素材",
|
||||
promptClearText: "清空便签",
|
||||
promptClearConfirm: "确定要清空该便签的内容吗?",
|
||||
promptEnablePart: "启用这个片段",
|
||||
promptUnnamedPart: "未命名片段",
|
||||
promptPartCategory: "片段用途",
|
||||
@@ -1067,8 +1080,21 @@ export const i18n = {
|
||||
promptImportPlan: "Import prompt plan",
|
||||
promptAlreadyImported: "An identical prompt plan is already in the library.",
|
||||
promptImportError: "Import failed. Choose a valid prompt plan JSON file (up to 2 MiB).",
|
||||
promptExportLocation: "Open a prompt plan to export it from its editor.",
|
||||
promptNewDraft: "New combination",
|
||||
promptImportFromMaterials: "Import from Materials",
|
||||
promptImportOverwrite: "Overwrite",
|
||||
promptImportAppend: "Append",
|
||||
promptImportOverwritePrompt: "Overwrite current",
|
||||
promptImportAppendPrompt: "Append to current",
|
||||
promptPositiveNote: "Positive Prompt Note",
|
||||
promptNegativeNote: "Negative Prompt Note",
|
||||
promptWordCount: "{count} chars",
|
||||
promptDragNoteHint: "Hold card or header to drag onto canvas node",
|
||||
promptDrawerTitle: "Material Library Prompts",
|
||||
promptDrawerSearchPlaceholder: "Search material title or prompt text...",
|
||||
promptDrawerEmpty: "No prompt materials found in library",
|
||||
promptClearText: "Clear note",
|
||||
promptClearConfirm: "Are you sure you want to clear this note?",
|
||||
promptEnablePart: "Enable this fragment",
|
||||
promptUnnamedPart: "Untitled fragment",
|
||||
promptPartCategory: "Fragment category",
|
||||
|
||||
@@ -33,7 +33,8 @@ export function bindMaterialDrag(element, owner, { payload, accepts, drop }) {
|
||||
event.stopPropagation();
|
||||
event.dataTransfer.setData('application/x-anomalous-material', 'local-drag');
|
||||
event.dataTransfer.effectAllowed = 'copy';
|
||||
const hint = text(document.body, 'div', t('materialDropHint'), 'anomalous-material-drag-hint');
|
||||
const defaultHint = data.dragHint || t('materialDropHint');
|
||||
const hint = text(document.body, 'div', defaultHint, 'anomalous-material-drag-hint');
|
||||
hint.setAttribute('role', 'status');
|
||||
const cleanup = () => {
|
||||
clearTimeout(reveal);
|
||||
@@ -48,7 +49,8 @@ export function bindMaterialDrag(element, owner, { payload, accepts, drop }) {
|
||||
const node = target(event);
|
||||
const valid = !!node && accepts(node, data);
|
||||
event.dataTransfer.dropEffect = valid ? 'copy' : 'none';
|
||||
hint.textContent = valid ? t('materialDropTarget', { name: materialNodeHeading(node) }) : t('materialDropHint');
|
||||
hint.classList.toggle('is-target-valid', valid);
|
||||
hint.textContent = valid ? t('materialDropTarget', { name: materialNodeHeading(node) }) : defaultHint;
|
||||
hint.style.left = `${Math.max(8, Math.min(event.clientX + 16, window.innerWidth - 250))}px`;
|
||||
hint.style.top = `${Math.max(8, event.clientY - 48)}px`;
|
||||
};
|
||||
|
||||
@@ -451,11 +451,10 @@ function renderMaterialCard(owner, material) {
|
||||
card.onclick = activate;
|
||||
if (material.node_types?.length) {
|
||||
bindMaterialDrag(card, owner, {
|
||||
payload: () => ({ ...material, node_types: [...material.node_types] }),
|
||||
payload: () => ({ ...material, node_types: [...material.node_types], dragHint: t('materialDragParameters') || '拖拽素材参数至目标节点' }),
|
||||
accepts: (node, source) => source.node_types.includes(node.type),
|
||||
drop: (node, source, graph) => applyLibraryMaterial(owner, source, node, graph),
|
||||
});
|
||||
text(card, 'span', t('materialDragParameters'), 'anomalous-material-drag-label');
|
||||
}
|
||||
card.tabIndex = 0;
|
||||
card.setAttribute('aria-label', `${material.name} — ${t('materialViewDetails')}`);
|
||||
|
||||
@@ -15,7 +15,11 @@ export function addPromptToDraft(owner, name, positive, negative = '') {
|
||||
owner.promptPlanDraft ||= newDraft();
|
||||
const current = composePromptPlan(owner.promptPlanDraft.plan);
|
||||
owner.promptPlanDraft.name ||= String(name).slice(0, 120);
|
||||
owner.promptPlanDraft.plan = { parts: [], positive: joinPromptText(current.positive, positive, 'after'), negative: joinPromptText(current.negative, negative, 'after') };
|
||||
owner.promptPlanDraft.plan = {
|
||||
parts: [],
|
||||
positive: joinPromptText(current.positive, positive, 'after'),
|
||||
negative: joinPromptText(current.negative, negative, 'after'),
|
||||
};
|
||||
showPromptComposer(owner);
|
||||
}
|
||||
|
||||
@@ -35,8 +39,11 @@ export async function showPromptComposer(owner, material) {
|
||||
} catch (error) {
|
||||
if (error.name !== 'AbortError') await anomalousAlert(t('materialDetailLoadError'));
|
||||
return;
|
||||
} finally { if (owner.materialDetailController === controller) owner.materialDetailController = null; }
|
||||
} finally {
|
||||
if (owner.materialDetailController === controller) owner.materialDetailController = null;
|
||||
}
|
||||
}
|
||||
|
||||
owner.promptPlanDraft ||= newDraft();
|
||||
clearTimeout(owner.materialSearchTimer);
|
||||
owner.materialListController?.abort();
|
||||
@@ -44,136 +51,442 @@ export async function showPromptComposer(owner, material) {
|
||||
owner.materialDetailController?.abort();
|
||||
owner.materialDetailView?.remove();
|
||||
owner.materialDetailView = null;
|
||||
for (const panel of [owner.materialIntro, owner.materialList, owner.materialToolbar, owner.materialPager, owner.materialContext]) if (panel) panel.style.display = 'none';
|
||||
|
||||
for (const panel of [owner.materialIntro, owner.materialList, owner.materialToolbar, owner.materialPager, owner.materialContext]) {
|
||||
if (panel) panel.style.display = 'none';
|
||||
}
|
||||
owner.promptComposerView?.remove();
|
||||
|
||||
const view = text(owner.materialView, 'section', '', 'anomalous-prompt-composer');
|
||||
owner.promptComposerView = view;
|
||||
const draft = owner.promptPlanDraft;
|
||||
draft.plan = { parts: [], ...composePromptPlan(draft.plan) };
|
||||
const header = text(view, 'div', '', 'anomalous-prompt-actions');
|
||||
const back = text(header, 'button', t('materialBackToLibrary'), 'anomalous-btn-ghost');
|
||||
back.onclick = () => owner.showMaterials();
|
||||
text(header, 'h3', t('promptCombinations'));
|
||||
const name = text(view, 'input', '', 'anomalous-nb-select');
|
||||
name.placeholder = t('promptPlanName'); name.setAttribute('aria-label', t('promptPlanName')); name.maxLength = 120; name.value = draft.name;
|
||||
name.oninput = () => { draft.name = name.value; };
|
||||
const tags = text(view, 'input', '', 'anomalous-nb-select');
|
||||
tags.placeholder = t('materialTagsHint'); tags.setAttribute('aria-label', t('materialTags')); tags.value = draft.tags.join(', '); tags.maxLength = 1200;
|
||||
tags.oninput = () => { draft.tags = tags.value.split(/[,,]/).map(value => value.trim()).filter(Boolean); };
|
||||
text(view, 'p', t('promptCompositionHint'), 'anomalous-material-muted');
|
||||
text(view, 'small', t('promptDraftHint'), 'anomalous-material-muted');
|
||||
const field = (parent, label, value, oninput, readOnly = false) => {
|
||||
const wrap = text(parent, 'label', label, 'anomalous-prompt-field');
|
||||
const input = text(wrap, 'textarea', ''); input.value = value; input.rows = 3; input.readOnly = readOnly;
|
||||
input.setAttribute('aria-label', label); input.oninput = () => oninput(input.value);
|
||||
return input;
|
||||
|
||||
// 1. Topbar
|
||||
const topbar = text(view, 'div', '', 'anomalous-prompt-topbar');
|
||||
const topLeft = text(topbar, 'div', '', 'anomalous-prompt-topbar-left');
|
||||
const backBtn = text(topLeft, 'button', `← ${t('materialBackToLibrary')}`, 'anomalous-btn-ghost');
|
||||
backBtn.onclick = () => owner.showMaterials();
|
||||
text(topLeft, 'h3', t('promptCombinations'));
|
||||
|
||||
const topActions = text(topbar, 'div', '', 'anomalous-prompt-topbar-actions');
|
||||
const importBtn = text(topActions, 'button', `📥 ${t('promptImportFromMaterials')}`, 'anomalous-btn-ghost');
|
||||
importBtn.title = t('promptDrawerTitle');
|
||||
|
||||
const saveBtn = text(topActions, 'button', `💾 ${t('promptSavePlan')}`, 'anomalous-btn-primary');
|
||||
const exportBtn = text(topActions, 'button', `📤 ${t('promptExportPlan')}`, 'anomalous-btn-ghost');
|
||||
const newBtn = text(topActions, 'button', `✨ ${t('promptNewDraft')}`, 'anomalous-btn-ghost');
|
||||
|
||||
// 2. Metadata strip (Name, Tags, Position)
|
||||
const metaStrip = text(view, 'div', '', 'anomalous-prompt-meta-strip');
|
||||
const nameInput = text(metaStrip, 'input', '', 'anomalous-prompt-name-input');
|
||||
nameInput.placeholder = t('promptPlanName');
|
||||
nameInput.maxLength = 120;
|
||||
nameInput.value = draft.name;
|
||||
nameInput.oninput = () => { draft.name = nameInput.value; };
|
||||
|
||||
const tagsInput = text(metaStrip, 'input', '', 'anomalous-prompt-tags-input');
|
||||
tagsInput.placeholder = t('materialTagsHint');
|
||||
tagsInput.value = draft.tags.join(', ');
|
||||
tagsInput.maxLength = 1200;
|
||||
tagsInput.oninput = () => {
|
||||
draft.tags = tagsInput.value.split(/[,,]/).map(val => val.trim()).filter(Boolean);
|
||||
};
|
||||
const placement = text(view, 'label', t('promptInsertPosition'), 'anomalous-prompt-field');
|
||||
const position = text(placement, 'select', '');
|
||||
position.setAttribute('aria-label', t('promptInsertPosition'));
|
||||
for (const value of ['before', 'after']) text(position, 'option', t(`promptInsert_${value}`)).value = value;
|
||||
position.value = owner.promptInsertPosition || 'after';
|
||||
position.onchange = () => { owner.promptInsertPosition = position.value; };
|
||||
for (const role of ['positive', 'negative']) {
|
||||
const card = text(view, 'div', '', 'anomalous-prompt-drag-card');
|
||||
field(card, t(`promptFinal_${role}`), draft.plan[role], value => { draft.plan[role] = value; });
|
||||
const drag = text(card, 'button', t('promptDragText'), 'anomalous-btn-primary');
|
||||
drag.type = 'button';
|
||||
bindMaterialDrag(drag, owner, {
|
||||
payload: () => draft.plan[role].trim() ? { content: draft.plan[role], position: position.value } : null,
|
||||
accepts: node => promptWidgetTargets(node).length > 0,
|
||||
drop: (node, data, graph) => applyPromptDrop(node, data, graph, targetArea),
|
||||
});
|
||||
const copy = text(card, 'button', t(`promptCopy_${role}`), 'anomalous-btn-ghost');
|
||||
copy.onclick = async () => {
|
||||
try { await navigator.clipboard.writeText(draft.plan[role]); copy.textContent = t('materialCopied'); }
|
||||
catch (error) { await anomalousAlert(t('materialCopyError')); }
|
||||
};
|
||||
|
||||
const posWrap = text(metaStrip, 'label', '', 'anomalous-prompt-insert-pos');
|
||||
text(posWrap, 'span', `${t('promptInsertPosition')}:`);
|
||||
const posSelect = text(posWrap, 'select', '');
|
||||
for (const value of ['before', 'after']) {
|
||||
text(posSelect, 'option', t(`promptInsert_${value}`)).value = value;
|
||||
}
|
||||
const targetArea = text(view, 'div', '', 'anomalous-prompt-target');
|
||||
const renderTarget = () => {
|
||||
posSelect.value = owner.promptInsertPosition || 'after';
|
||||
posSelect.onchange = () => { owner.promptInsertPosition = posSelect.value; };
|
||||
|
||||
// 3. Paper-like Note Cards Grid
|
||||
const notesGrid = text(view, 'div', '', 'anomalous-paper-notes-grid');
|
||||
|
||||
const renderNoteCard = (role, isPositive) => {
|
||||
const card = text(notesGrid, 'article', '', `anomalous-paper-note ${isPositive ? 'is-positive' : 'is-negative'}`);
|
||||
card.setAttribute('draggable', 'true');
|
||||
card.setAttribute('aria-label', t(isPositive ? 'promptPositiveNote' : 'promptNegativeNote'));
|
||||
|
||||
// Tape effect on top
|
||||
text(card, 'div', '', 'anomalous-paper-note-tape');
|
||||
|
||||
// Header
|
||||
const noteHeader = text(card, 'div', '', 'anomalous-paper-note-header');
|
||||
const noteTitle = text(noteHeader, 'div', '', 'anomalous-paper-note-title');
|
||||
noteTitle.innerHTML = isPositive ? `✨ ${t('promptPositiveNote')}` : `🚫 ${t('promptNegativeNote')}`;
|
||||
|
||||
const noteActions = text(noteHeader, 'div', '', 'anomalous-paper-note-actions');
|
||||
const charCount = text(noteActions, 'span', t('promptWordCount', { count: draft.plan[role].length }), 'anomalous-paper-note-count');
|
||||
|
||||
const copyBtn = text(noteActions, 'button', `📋 ${t('copy')}`, 'anomalous-paper-note-btn');
|
||||
copyBtn.title = t(`promptCopy_${role}`);
|
||||
copyBtn.onclick = async event => {
|
||||
event.stopPropagation();
|
||||
try {
|
||||
await navigator.clipboard.writeText(draft.plan[role]);
|
||||
copyBtn.textContent = '✅ 已复制';
|
||||
setTimeout(() => { if (copyBtn.isConnected) copyBtn.textContent = `📋 ${t('copy')}`; }, 1500);
|
||||
} catch (err) {
|
||||
await anomalousAlert(t('materialCopyError'));
|
||||
}
|
||||
};
|
||||
|
||||
const clearBtn = text(noteActions, 'button', `🧹`, 'anomalous-paper-note-btn');
|
||||
clearBtn.title = t('promptClearText');
|
||||
clearBtn.onclick = async event => {
|
||||
event.stopPropagation();
|
||||
if (draft.plan[role].trim() && await anomalousConfirm(t('promptClearConfirm'))) {
|
||||
draft.plan[role] = '';
|
||||
textarea.value = '';
|
||||
charCount.textContent = t('promptWordCount', { count: 0 });
|
||||
}
|
||||
};
|
||||
|
||||
// Textarea body
|
||||
const noteBody = text(card, 'div', '', 'anomalous-paper-note-body');
|
||||
const textarea = text(noteBody, 'textarea', '', 'anomalous-paper-note-textarea');
|
||||
textarea.value = draft.plan[role] || '';
|
||||
textarea.placeholder = isPositive ? '输入或导入正面提示词...' : '输入或导入负面提示词...';
|
||||
textarea.rows = 5;
|
||||
|
||||
textarea.oninput = () => {
|
||||
draft.plan[role] = textarea.value;
|
||||
charCount.textContent = t('promptWordCount', { count: textarea.value.length });
|
||||
};
|
||||
|
||||
// Footer with drag hint
|
||||
const noteFooter = text(card, 'div', '', 'anomalous-paper-note-footer');
|
||||
const dragPrompt = text(noteFooter, 'span', '', 'anomalous-paper-note-drag-prompt');
|
||||
dragPrompt.innerHTML = `🖐️ ${t('promptDragNoteHint')}`;
|
||||
|
||||
// Bind Whole Card Drag
|
||||
bindMaterialDrag(card, owner, {
|
||||
payload: () => draft.plan[role].trim() ? {
|
||||
content: draft.plan[role],
|
||||
position: posSelect.value,
|
||||
dragHint: isPositive ? `✨ 拖拽正面提示词便签至目标节点` : `🚫 拖拽负面提示词便签至目标节点`,
|
||||
} : null,
|
||||
accepts: node => promptWidgetTargets(node).length > 0,
|
||||
drop: (node, data, graph) => applyPromptDrop(node, data, graph, targetBar),
|
||||
});
|
||||
|
||||
return {
|
||||
update: () => {
|
||||
textarea.value = draft.plan[role] || '';
|
||||
charCount.textContent = t('promptWordCount', { count: textarea.value.length });
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
const posCardControl = renderNoteCard('positive', true);
|
||||
const negCardControl = renderNoteCard('negative', false);
|
||||
|
||||
const refreshAllNotes = () => {
|
||||
posCardControl.update();
|
||||
negCardControl.update();
|
||||
nameInput.value = draft.name || '';
|
||||
tagsInput.value = (draft.tags || []).join(', ');
|
||||
};
|
||||
|
||||
// 4. Target Node Bar & Direct Application
|
||||
const targetBar = text(view, 'div', '', 'anomalous-prompt-target-bar');
|
||||
const renderTargetBar = () => {
|
||||
if (!view.isConnected) return;
|
||||
targetArea.replaceChildren();
|
||||
targetBar.replaceChildren();
|
||||
const node = selectedMaterialNode(app);
|
||||
const targets = promptWidgetTargets(node);
|
||||
if (!node || !targets.length) { text(targetArea, 'p', t('promptSelectTextNode')); return; }
|
||||
text(targetArea, 'strong', t('materialApplyingTo', { name: materialNodeHeading(node), id: node.id }));
|
||||
const widget = text(targetArea, 'select', ''); widget.setAttribute('aria-label', t('promptTargetWidget'));
|
||||
for (const target of targets) text(widget, 'option', target.name).value = String(target.index);
|
||||
const role = text(targetArea, 'select', ''); role.setAttribute('aria-label', t('promptTargetRole'));
|
||||
for (const value of ['positive', 'negative']) text(role, 'option', t(`promptFinal_${value}`)).value = value;
|
||||
for (const placement of ['before', 'after']) {
|
||||
const action = text(targetArea, 'button', t(`promptInsert_${placement}`), 'anomalous-btn-primary');
|
||||
action.onclick = async () => {
|
||||
|
||||
if (!node || !targets.length) {
|
||||
text(targetBar, 'span', `💡 ${t('promptSelectTextNode')}`, 'anomalous-prompt-target-info');
|
||||
return;
|
||||
}
|
||||
|
||||
const info = text(targetBar, 'div', '', 'anomalous-prompt-target-info');
|
||||
info.innerHTML = `🎯 ${t('materialApplyingTo', { name: materialNodeHeading(node), id: node.id })}`;
|
||||
|
||||
const actions = text(targetBar, 'div', '', 'anomalous-prompt-target-actions');
|
||||
const widgetSelect = text(actions, 'select', '');
|
||||
widgetSelect.setAttribute('aria-label', t('promptTargetWidget'));
|
||||
for (const target of targets) {
|
||||
text(widgetSelect, 'option', target.name).value = String(target.index);
|
||||
}
|
||||
|
||||
const applyRole = (role, label) => {
|
||||
const btn = text(actions, 'button', label, 'anomalous-btn-primary');
|
||||
btn.type = 'button';
|
||||
btn.onclick = async () => {
|
||||
try {
|
||||
if (selectedMaterialNode(app) !== node) throw new Error('materialTargetChanged');
|
||||
const index = Number(widget.value);
|
||||
if (!promptWidgetTargets(node).some(target => target.index === index)) throw new Error('materialTargetChanged');
|
||||
const content = composePromptPlan(draft.plan)[role.value];
|
||||
if (!content.trim()) return;
|
||||
const value = joinPromptText(node.widgets[index].value, content, placement);
|
||||
showMaterialApplication(targetArea, applyNodeMaterialValues(app, node, [{ index, value }]), node);
|
||||
} catch (error) { await anomalousAlert(t(error.message) === error.message ? t('materialApplyFailed') : t(error.message)); }
|
||||
const index = Number(widgetSelect.value);
|
||||
if (!promptWidgetTargets(node).some(t => t.index === index)) throw new Error('materialTargetChanged');
|
||||
const content = draft.plan[role];
|
||||
if (!content || !content.trim()) return;
|
||||
const value = joinPromptText(node.widgets[index].value, content, posSelect.value);
|
||||
showMaterialApplication(targetBar, applyNodeMaterialValues(app, node, [{ index, value }]), node);
|
||||
} catch (error) {
|
||||
await anomalousAlert(t(error.message) === error.message ? t('materialApplyFailed') : t(error.message));
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
applyRole('positive', `写入正面 (${t(`promptInsert_${posSelect.value}`)})`);
|
||||
applyRole('negative', `写入负面 (${t(`promptInsert_${posSelect.value}`)})`);
|
||||
};
|
||||
owner.refreshPromptTarget = renderTarget;
|
||||
const actions = text(view, 'div', '', 'anomalous-prompt-actions');
|
||||
const save = text(actions, 'button', t('promptSavePlan'), 'anomalous-btn-primary');
|
||||
save.onclick = async () => {
|
||||
if (!draft.name.trim()) { name.focus(); return; }
|
||||
save.disabled = true;
|
||||
|
||||
owner.refreshPromptTarget = renderTargetBar;
|
||||
renderTargetBar();
|
||||
|
||||
// 5. Drawer Integration: Import Prompts from Material Library
|
||||
importBtn.onclick = () => {
|
||||
openMaterialImportDrawer(owner, draft, refreshAllNotes);
|
||||
};
|
||||
|
||||
// 6. Action Handlers (Save, Export, Reset)
|
||||
saveBtn.onclick = async () => {
|
||||
if (!draft.name.trim()) {
|
||||
nameInput.focus();
|
||||
return;
|
||||
}
|
||||
saveBtn.disabled = true;
|
||||
const body = clone(draft);
|
||||
const send = () => fetch('/anomalous/save_prompt_plan', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body) });
|
||||
const send = () => fetch('/anomalous/save_prompt_plan', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
try {
|
||||
let response = await send();
|
||||
if (response.status === 409) {
|
||||
const duplicate = await response.json();
|
||||
if (!await anomalousConfirm(t('materialDuplicateConfirm', { name: duplicate.name }))) return;
|
||||
body.allow_duplicate = true; response = await send();
|
||||
body.allow_duplicate = true;
|
||||
response = await send();
|
||||
}
|
||||
const payload = await jsonResponse(response, 'prompt save failed');
|
||||
if (payload.status !== 'success') throw new Error('prompt save failed');
|
||||
showMaterialSaved(owner, payload.material);
|
||||
} catch (error) { await anomalousAlert(t('materialSaveError')); }
|
||||
finally { save.disabled = false; }
|
||||
} catch (error) {
|
||||
await anomalousAlert(t('materialSaveError'));
|
||||
} finally {
|
||||
saveBtn.disabled = false;
|
||||
}
|
||||
};
|
||||
const exportPlan = text(actions, 'button', t('promptExportPlan'), 'anomalous-btn-ghost');
|
||||
exportPlan.onclick = () => {
|
||||
if (!draft.name.trim()) { name.focus(); return; }
|
||||
|
||||
exportBtn.onclick = () => {
|
||||
if (!draft.name.trim()) {
|
||||
nameInput.focus();
|
||||
return;
|
||||
}
|
||||
const blob = new Blob([JSON.stringify({ format: 'anomalous-prompt-plan-v1', ...draft }, null, 2)], { type: 'application/json' });
|
||||
if (blob.size > 2 * 1024 * 1024 || draft.tags.length > 20 || draft.tags.some(tag => tag.length > 60)) {
|
||||
return anomalousAlert(t('promptExportError'));
|
||||
}
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a'); a.href = url; a.download = 'prompt-plan.json'; a.click();
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = `${draft.name || 'prompt-plan'}.json`;
|
||||
a.click();
|
||||
setTimeout(() => URL.revokeObjectURL(url), 1000);
|
||||
};
|
||||
const reset = text(actions, 'button', t('promptNewDraft'), 'anomalous-btn-ghost');
|
||||
reset.onclick = async () => { if (await anomalousConfirm(t('promptReplaceDraft'))) { owner.promptPlanDraft = newDraft(); showPromptComposer(owner); } };
|
||||
renderTarget();
|
||||
}
|
||||
|
||||
newBtn.onclick = async () => {
|
||||
if (await anomalousConfirm(t('promptReplaceDraft'))) {
|
||||
owner.promptPlanDraft = newDraft();
|
||||
showPromptComposer(owner);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function applyPromptDrop(node, data, graph, parent) {
|
||||
const targets = promptWidgetTargets(node);
|
||||
const apply = index => {
|
||||
if (app.graph !== graph || graph.getNodeById(node.id) !== node || !promptWidgetTargets(node).some(target => target.index === index)) throw new Error('materialTargetChanged');
|
||||
if (app.graph !== graph || graph.getNodeById(node.id) !== node || !promptWidgetTargets(node).some(target => target.index === index)) {
|
||||
throw new Error('materialTargetChanged');
|
||||
}
|
||||
const value = joinPromptText(node.widgets[index].value, data.content, data.position);
|
||||
showMaterialApplication(parent, applyNodeMaterialValues(app, node, [{ index, value }]), node);
|
||||
};
|
||||
if (targets.length === 1) { apply(targets[0].index); return; }
|
||||
if (targets.length === 1) {
|
||||
apply(targets[0].index);
|
||||
return;
|
||||
}
|
||||
if (!targets.length) throw new Error('materialNoCompatibleValues');
|
||||
|
||||
const dialog = text(document.body, 'dialog', '', 'anomalous-material-choice');
|
||||
text(dialog, 'h3', t('promptChooseTarget'));
|
||||
const status = text(dialog, 'p', ''); status.setAttribute('role', 'alert');
|
||||
const status = text(dialog, 'p', '');
|
||||
status.setAttribute('role', 'alert');
|
||||
for (const target of targets) {
|
||||
const choose = text(dialog, 'button', target.name, 'anomalous-btn-primary');
|
||||
choose.onclick = () => {
|
||||
try { apply(target.index); dialog.close(); }
|
||||
catch (error) { status.textContent = t(error.message) === error.message ? t('materialApplyFailed') : t(error.message); }
|
||||
try {
|
||||
apply(target.index);
|
||||
dialog.close();
|
||||
} catch (error) {
|
||||
status.textContent = t(error.message) === error.message ? t('materialApplyFailed') : t(error.message);
|
||||
}
|
||||
};
|
||||
}
|
||||
const close = text(dialog, 'button', t('close'), 'anomalous-btn-ghost'); close.onclick = () => dialog.close();
|
||||
dialog.onclose = () => dialog.remove(); dialog.showModal();
|
||||
const close = text(dialog, 'button', t('close'), 'anomalous-btn-ghost');
|
||||
close.onclick = () => dialog.close();
|
||||
dialog.onclose = () => dialog.remove();
|
||||
dialog.showModal();
|
||||
}
|
||||
|
||||
/**
|
||||
* Slide-out Drawer: Quick Prompt Importer from Material Library
|
||||
*/
|
||||
async function openMaterialImportDrawer(owner, draft, onUpdated) {
|
||||
document.querySelector('.anomalous-prompt-import-drawer')?.remove();
|
||||
|
||||
const drawer = document.createElement('div');
|
||||
drawer.className = 'anomalous-prompt-import-drawer';
|
||||
|
||||
// Header
|
||||
const header = text(drawer, 'div', '', 'anomalous-drawer-header');
|
||||
text(header, 'h4', `📥 ${t('promptDrawerTitle')}`);
|
||||
const closeBtn = text(header, 'button', '✕', 'anomalous-paper-note-btn');
|
||||
closeBtn.onclick = () => drawer.remove();
|
||||
|
||||
// Search bar
|
||||
const searchWrap = text(drawer, 'div', '', 'anomalous-drawer-search');
|
||||
const searchInput = text(searchWrap, 'input', '');
|
||||
searchInput.placeholder = t('promptDrawerSearchPlaceholder');
|
||||
searchInput.type = 'search';
|
||||
|
||||
// List container
|
||||
const listContainer = text(drawer, 'div', '', 'anomalous-drawer-list');
|
||||
text(listContainer, 'div', '⏳ 正在加载素材库提示词...', 'anomalous-material-muted');
|
||||
|
||||
document.body.appendChild(drawer);
|
||||
|
||||
const onKeydown = e => {
|
||||
if (e.key === 'Escape') {
|
||||
drawer.remove();
|
||||
window.removeEventListener('keydown', onKeydown);
|
||||
}
|
||||
};
|
||||
window.addEventListener('keydown', onKeydown);
|
||||
|
||||
try {
|
||||
const response = await fetch('/anomalous/materials?limit=100');
|
||||
const payload = await jsonResponse(response, 'load materials failed');
|
||||
const items = payload.data?.items || [];
|
||||
|
||||
// Extract and shape prompt data
|
||||
const promptMaterials = [];
|
||||
for (const item of items) {
|
||||
let pos = '';
|
||||
let neg = '';
|
||||
|
||||
if (item.kind === 'prompt_plan' && item.plan) {
|
||||
pos = item.plan.positive || '';
|
||||
neg = item.plan.negative || '';
|
||||
} else if (item.kind === 'prompt_text') {
|
||||
pos = item.content || item.summary || '';
|
||||
} else if (Array.isArray(item.node_blocks)) {
|
||||
for (const block of item.node_blocks) {
|
||||
const textVal = Array.isArray(block.widgets_values) && typeof block.widgets_values[0] === 'string'
|
||||
? block.widgets_values[0]
|
||||
: '';
|
||||
if (!textVal) continue;
|
||||
if (block.promptRole === 'positive') pos = pos ? `${pos}, ${textVal}` : textVal;
|
||||
else if (block.promptRole === 'negative') neg = neg ? `${neg}, ${textVal}` : textVal;
|
||||
else if (/cliptextencode/i.test(block.type || '')) pos = pos ? `${pos}, ${textVal}` : textVal;
|
||||
}
|
||||
}
|
||||
|
||||
if (pos || neg || item.kind === 'prompt_plan') {
|
||||
promptMaterials.push({
|
||||
raw: item,
|
||||
name: item.name || '未命名素材',
|
||||
positive: pos,
|
||||
negative: neg,
|
||||
tags: item.tags || [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const renderList = (filter = '') => {
|
||||
listContainer.replaceChildren();
|
||||
const lower = filter.toLowerCase().trim();
|
||||
const filtered = promptMaterials.filter(m =>
|
||||
!lower ||
|
||||
m.name.toLowerCase().includes(lower) ||
|
||||
m.positive.toLowerCase().includes(lower) ||
|
||||
m.negative.toLowerCase().includes(lower) ||
|
||||
m.tags.some(tag => tag.toLowerCase().includes(lower))
|
||||
);
|
||||
|
||||
if (!filtered.length) {
|
||||
text(listContainer, 'div', t('promptDrawerEmpty'), 'anomalous-material-empty');
|
||||
return;
|
||||
}
|
||||
|
||||
for (const item of filtered) {
|
||||
const card = text(listContainer, 'div', '', 'anomalous-drawer-item');
|
||||
text(card, 'div', item.name, 'anomalous-drawer-item-title');
|
||||
|
||||
if (item.positive) {
|
||||
const prev = text(card, 'div', `✨ 正面: ${item.positive}`, 'anomalous-drawer-item-preview');
|
||||
prev.title = item.positive;
|
||||
}
|
||||
if (item.negative) {
|
||||
const prev = text(card, 'div', `🚫 负面: ${item.negative}`, 'anomalous-drawer-item-preview');
|
||||
prev.title = item.negative;
|
||||
}
|
||||
|
||||
const actions = text(card, 'div', '', 'anomalous-drawer-item-actions');
|
||||
|
||||
if (item.positive) {
|
||||
const overwritePos = text(actions, 'button', `覆盖正面`, 'anomalous-drawer-item-btn');
|
||||
overwritePos.onclick = () => {
|
||||
draft.plan.positive = item.positive;
|
||||
onUpdated();
|
||||
};
|
||||
|
||||
const appendPos = text(actions, 'button', `追加正面`, 'anomalous-drawer-item-btn');
|
||||
appendPos.onclick = () => {
|
||||
draft.plan.positive = joinPromptText(draft.plan.positive, item.positive, owner.promptInsertPosition || 'after');
|
||||
onUpdated();
|
||||
};
|
||||
}
|
||||
|
||||
if (item.negative) {
|
||||
const overwriteNeg = text(actions, 'button', `覆盖负面`, 'anomalous-drawer-item-btn');
|
||||
overwriteNeg.onclick = () => {
|
||||
draft.plan.negative = item.negative;
|
||||
onUpdated();
|
||||
};
|
||||
|
||||
const appendNeg = text(actions, 'button', `追加负面`, 'anomalous-drawer-item-btn');
|
||||
appendNeg.onclick = () => {
|
||||
draft.plan.negative = joinPromptText(draft.plan.negative, item.negative, owner.promptInsertPosition || 'after');
|
||||
onUpdated();
|
||||
};
|
||||
}
|
||||
|
||||
const loadAll = text(actions, 'button', `整套导入`, 'anomalous-drawer-item-btn');
|
||||
loadAll.onclick = () => {
|
||||
if (item.positive) draft.plan.positive = item.positive;
|
||||
if (item.negative) draft.plan.negative = item.negative;
|
||||
if (!draft.name.trim() && item.name) draft.name = item.name;
|
||||
if (item.tags?.length) draft.tags = [...new Set([...draft.tags, ...item.tags])];
|
||||
onUpdated();
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
renderList();
|
||||
searchInput.oninput = () => renderList(searchInput.value);
|
||||
searchInput.focus();
|
||||
|
||||
} catch (err) {
|
||||
listContainer.replaceChildren();
|
||||
text(listContainer, 'div', `加载素材库失败: ${err.message}`, 'anomalous-material-empty');
|
||||
}
|
||||
}
|
||||
|
||||
+458
-13
@@ -7366,17 +7366,462 @@ dialog.anomalous-gallery-viewer::backdrop {
|
||||
background: #202428;
|
||||
color: #eee;
|
||||
}
|
||||
.anomalous-material-drag-label {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
left: 10px;
|
||||
padding: 4px 8px;
|
||||
background: #202428;
|
||||
color: #eee;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
pointer-events: none;
|
||||
/* ==========================================================================
|
||||
Prompt Composer & Paper Note Studio / Material Drag System
|
||||
========================================================================== */
|
||||
|
||||
/* 1. Modal Dragging State: Instantly ghost modal to reveal ComfyUI canvas */
|
||||
#anomalous-modal.anomalous-material-dragging {
|
||||
pointer-events: none !important;
|
||||
}
|
||||
.anomalous-material-card[draggable="true"], .anomalous-prompt-drag-card button[draggable="true"] { cursor: grab; }
|
||||
.anomalous-prompt-drag-card { padding: 12px; border: 1px solid var(--border-color, #454545); border-radius: 8px; }
|
||||
.anomalous-prompt-drag-card > button { margin: 10px 8px 0 0; }
|
||||
|
||||
#anomalous-modal.anomalous-material-dragging #anomalous-container {
|
||||
opacity: 0 !important;
|
||||
pointer-events: none !important;
|
||||
transform: scale(0.985);
|
||||
transition: opacity 0.16s cubic-bezier(0.4, 0, 0.2, 1), transform 0.16s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
/* 2. Drag Cursor & Hover Feedback */
|
||||
.anomalous-material-card[draggable="true"],
|
||||
.anomalous-paper-note[draggable="true"] {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.anomalous-material-card[draggable="true"]:active,
|
||||
.anomalous-paper-note[draggable="true"]:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
/* Hide legacy bulky drag label to restore clean card visuals */
|
||||
.anomalous-material-drag-label {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* 3. Drag Follower Floating Badge */
|
||||
.anomalous-material-drag-hint {
|
||||
position: fixed;
|
||||
z-index: 999999;
|
||||
pointer-events: none;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 7px 14px;
|
||||
border-radius: 20px;
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border: 1.5px solid rgba(45, 212, 191, 0.55);
|
||||
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.65), 0 0 16px rgba(45, 212, 191, 0.2);
|
||||
color: #f1f5f9;
|
||||
font-size: 0.8rem;
|
||||
font-weight: 500;
|
||||
line-height: 1.2;
|
||||
backdrop-filter: blur(8px);
|
||||
transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-material-drag-hint.is-target-valid {
|
||||
border-color: #2dd4bf;
|
||||
background: rgba(13, 37, 42, 0.94);
|
||||
box-shadow: 0 12px 36px rgba(0, 0, 0, 0.75), 0 0 24px rgba(45, 212, 191, 0.45);
|
||||
color: #5eead4;
|
||||
}
|
||||
|
||||
/* 4. Prompt Composer Layout (Warm Paper Studio) */
|
||||
.anomalous-prompt-composer {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
padding: 16px 20px 24px;
|
||||
overflow-y: auto;
|
||||
max-height: 100%;
|
||||
color: #e2e8f0;
|
||||
}
|
||||
|
||||
.anomalous-prompt-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.anomalous-prompt-topbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.anomalous-prompt-topbar-left h3 {
|
||||
margin: 0;
|
||||
font-size: 1.05rem;
|
||||
font-weight: 600;
|
||||
color: #f8fafc;
|
||||
letter-spacing: 0.02em;
|
||||
}
|
||||
|
||||
.anomalous-prompt-topbar-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.anomalous-prompt-meta-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
background: rgba(255, 255, 255, 0.03);
|
||||
border: 1px solid rgba(255, 255, 255, 0.07);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.anomalous-prompt-meta-strip input {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 6px;
|
||||
padding: 6px 10px;
|
||||
color: #f1f5f9;
|
||||
font-size: 0.82rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-prompt-meta-strip input:focus {
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
|
||||
.anomalous-prompt-name-input {
|
||||
flex: 1 1 200px;
|
||||
}
|
||||
|
||||
.anomalous-prompt-tags-input {
|
||||
flex: 1 1 180px;
|
||||
}
|
||||
|
||||
.anomalous-prompt-insert-pos {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 0.8rem;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
.anomalous-prompt-insert-pos select {
|
||||
background: rgba(15, 23, 42, 0.5);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
border-radius: 6px;
|
||||
padding: 5px 8px;
|
||||
color: #f1f5f9;
|
||||
font-size: 0.78rem;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 5. Paper-like Note Cards Grid */
|
||||
.anomalous-paper-notes-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(360px, 1fr));
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.anomalous-paper-note {
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
border-radius: 12px;
|
||||
background: linear-gradient(175deg, #1c2028 0%, #15181f 100%);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
box-shadow: 0 10px 24px -4px rgba(0, 0, 0, 0.4), 0 4px 10px -2px rgba(0, 0, 0, 0.25);
|
||||
overflow: hidden;
|
||||
transition: transform 0.18s cubic-bezier(0.4, 0, 0.2, 1), box-shadow 0.18s cubic-bezier(0.4, 0, 0.2, 1), border-color 0.18s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.anomalous-paper-note:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 14px 30px -4px rgba(0, 0, 0, 0.55), 0 6px 14px -2px rgba(0, 0, 0, 0.35);
|
||||
border-color: rgba(255, 255, 255, 0.18);
|
||||
}
|
||||
|
||||
/* Note Top Tape Effect (纸胶带) */
|
||||
.anomalous-paper-note-tape {
|
||||
height: 6px;
|
||||
width: 100%;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.anomalous-paper-note.is-positive .anomalous-paper-note-tape {
|
||||
background: linear-gradient(90deg, #2dd4bf 0%, #0d9488 100%);
|
||||
box-shadow: 0 0 10px rgba(45, 212, 191, 0.35);
|
||||
}
|
||||
|
||||
.anomalous-paper-note.is-negative .anomalous-paper-note-tape {
|
||||
background: linear-gradient(90deg, #fb7185 0%, #e11d48 100%);
|
||||
box-shadow: 0 0 10px rgba(251, 113, 133, 0.35);
|
||||
}
|
||||
|
||||
/* Note Header */
|
||||
.anomalous-paper-note-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 14px 6px;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.anomalous-paper-note-title {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 7px;
|
||||
font-size: 0.86rem;
|
||||
font-weight: 600;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
|
||||
.anomalous-paper-note.is-positive .anomalous-paper-note-title {
|
||||
color: #5eead4;
|
||||
}
|
||||
|
||||
.anomalous-paper-note.is-negative .anomalous-paper-note-title {
|
||||
color: #fda4af;
|
||||
}
|
||||
|
||||
.anomalous-paper-note-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.anomalous-paper-note-count {
|
||||
font-size: 0.72rem;
|
||||
color: #64748b;
|
||||
font-family: monospace;
|
||||
padding: 2px 6px;
|
||||
background: rgba(0, 0, 0, 0.25);
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.anomalous-paper-note-btn {
|
||||
padding: 3px 8px;
|
||||
border-radius: 5px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #cbd5e1;
|
||||
font-size: 0.74rem;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease, color 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-paper-note-btn:hover {
|
||||
background: rgba(255, 255, 255, 0.12);
|
||||
color: #f8fafc;
|
||||
border-color: rgba(255, 255, 255, 0.25);
|
||||
}
|
||||
|
||||
/* Note Textarea */
|
||||
.anomalous-paper-note-body {
|
||||
padding: 6px 14px 10px;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.anomalous-paper-note-textarea {
|
||||
width: 100%;
|
||||
min-height: 120px;
|
||||
resize: vertical;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
color: #f1f5f9;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
padding: 10px;
|
||||
border-radius: 6px;
|
||||
font-family: inherit;
|
||||
box-sizing: border-box;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-paper-note-textarea:focus {
|
||||
background: rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
/* Note Footer Drag Hint */
|
||||
.anomalous-paper-note-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 6px 14px 10px;
|
||||
font-size: 0.73rem;
|
||||
color: #64748b;
|
||||
border-top: 1px dashed rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.anomalous-paper-note-drag-prompt {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #94a3b8;
|
||||
}
|
||||
|
||||
/* 6. Active Target Status & Injection Bar */
|
||||
.anomalous-prompt-target-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
padding: 10px 14px;
|
||||
border-radius: 10px;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid rgba(45, 212, 191, 0.2);
|
||||
}
|
||||
|
||||
.anomalous-prompt-target-info {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 0.82rem;
|
||||
color: #cbd5e1;
|
||||
}
|
||||
|
||||
.anomalous-prompt-target-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
/* 7. Slide-out Material Import Drawer */
|
||||
.anomalous-prompt-import-drawer {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 440px;
|
||||
max-width: 90vw;
|
||||
height: 100vh;
|
||||
background: rgba(18, 22, 30, 0.96);
|
||||
border-left: 1px solid rgba(255, 255, 255, 0.12);
|
||||
box-shadow: -10px 0 40px rgba(0, 0, 0, 0.75);
|
||||
z-index: 10005;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
backdrop-filter: blur(12px);
|
||||
animation: drawerSlideIn 0.22s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
}
|
||||
|
||||
@keyframes drawerSlideIn {
|
||||
from { transform: translateX(100%); }
|
||||
to { transform: translateX(0); }
|
||||
}
|
||||
|
||||
.anomalous-drawer-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 14px 18px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
|
||||
.anomalous-drawer-header h4 {
|
||||
margin: 0;
|
||||
font-size: 0.98rem;
|
||||
font-weight: 600;
|
||||
color: #f1f5f9;
|
||||
}
|
||||
|
||||
.anomalous-drawer-search {
|
||||
padding: 10px 18px;
|
||||
border-bottom: 1px solid rgba(255, 255, 255, 0.06);
|
||||
}
|
||||
|
||||
.anomalous-drawer-search input {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 7px 12px;
|
||||
border-radius: 6px;
|
||||
background: rgba(15, 23, 42, 0.6);
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
color: #f1f5f9;
|
||||
font-size: 0.82rem;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.anomalous-drawer-search input:focus {
|
||||
border-color: #2dd4bf;
|
||||
}
|
||||
|
||||
.anomalous-drawer-list {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 14px 18px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.anomalous-drawer-item {
|
||||
padding: 12px;
|
||||
border-radius: 8px;
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
border: 1px solid rgba(255, 255, 255, 0.08);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-drawer-item:hover {
|
||||
background: rgba(255, 255, 255, 0.06);
|
||||
border-color: rgba(45, 212, 191, 0.3);
|
||||
}
|
||||
|
||||
.anomalous-drawer-item-title {
|
||||
font-size: 0.84rem;
|
||||
font-weight: 600;
|
||||
color: #f8fafc;
|
||||
}
|
||||
|
||||
.anomalous-drawer-item-preview {
|
||||
font-size: 0.76rem;
|
||||
color: #94a3b8;
|
||||
line-height: 1.4;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 3;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 6px 8px;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.anomalous-drawer-item-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.anomalous-drawer-item-btn {
|
||||
padding: 4px 9px;
|
||||
border-radius: 4px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.12);
|
||||
background: rgba(255, 255, 255, 0.05);
|
||||
color: #cbd5e1;
|
||||
font-size: 0.72rem;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-drawer-item-btn:hover {
|
||||
background: #2dd4bf;
|
||||
color: #0f172a;
|
||||
border-color: #2dd4bf;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user