Workflows page: one bar, quieter cards
The page's three rows (kind pills, search, tag select, count and close; view, share and save; the drag tip strip) become one bar: search, a Filter menu (kind and tag), the count, grid or list, share and import, save the canvas. Cards keep the picture, the name, a line of steps, sampler, LoRAs and size, and the model; the models' state is a dot (words only when some are missing), 功能模块 marks a partial workflow, and Open and ⋯ (edit, export, delete) show on hover. ui_popup_menu.js is the small menu both use. The page's styles move to styles/28-workflows.css; the card and bar rules spread over four stylesheets and the ~240 lines of CSS injected from ui_recipe_cards.js are removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
a0aee2ec38
commit
617a7a3bc4
+10
-8
@@ -458,11 +458,8 @@ covered by the `styles.css` manifest.
|
||||
with stage interaction in `ui_image_stage.js` and metadata tabs in
|
||||
`ui_image_inspector.js`.
|
||||
- `ui_recipes.js` / `ui_recipe_detail.js` and `ui_notebooks.js`
|
||||
own their respective workspace surfaces and persistence flows. `ui_recipes.js` owns
|
||||
the Workflow Recipe studio catalog workspace with search/filter tags, grid/list layout toggle,
|
||||
dedicated top-right modal close anchor (permanently decoupled from the tool button row to prevent wrapping displacement),
|
||||
streamlined action header (preserving active workflow saving while pruning unfinished package
|
||||
import entrypoints), and card browsing. `ui_notebooks.js` owns the Combos workspace (its list or one combo's editor,
|
||||
own their respective workspace surfaces and persistence flows. `ui_recipes.js` hands the
|
||||
Workflows page (its catalog) the recipe services (save, edit, fetch) and opens a recipe by name. `ui_notebooks.js` owns the Combos workspace (its list or one combo's editor,
|
||||
with Back to the list) and combo persistence; `ui_notebook_editor.js` owns unfolded card editing (modularized into single-responsibility
|
||||
sub-functions adhering to the 50-line rule: sticky top action toolbar with floating More popover dropdown and timed two-step delete safety guard,
|
||||
unfolded companion models card with unconstrained multi-column tile flow eliminating nested gallery scrollbars,
|
||||
@@ -497,9 +494,14 @@ covered by the `styles.css` manifest.
|
||||
`(node_id, widget_index, category, saved_value)`. `recipe_parser.js` summarizes
|
||||
models, LoRAs, samplers and prompts (linked prompt nodes before embedded loader
|
||||
prompts) and keeps the summary in step with widget edits using the same node rules.
|
||||
`ui_recipe_catalog.js` owns recipe
|
||||
filters, navigation, dismissible topbar drag guidance strip with localStorage persistence, the 3-step empty-state onboarding blueprint (`renderRecipeEmptyGuide`), and background catalog-wide model readiness resolution (`resolveCatalogRecipeReadiness`), `ui_recipe_cards.js` owns cards and card actions
|
||||
(including `grab` drag affordance, cover `可拖拽` badge, harmonized multi-state model readiness pill with `getRecipeReadiness` synchronizing available, missing, and pending matches with detail overview, and direct canvas drag-and-drop), `ui_recipe_dialogs.js` owns save/edit dialogs, and `ui_recipe_media.js` owns shared cover helpers. Detail sessions synchronize detected model availability back to `owner.recipeRecords` via `syncRecipeReferencesToCatalog`.
|
||||
`ui_recipe_catalog.js` is the Workflows page: one bar (search, the filter menu with kind and tag,
|
||||
the count, grid or list, share and import, save the canvas), the list, the empty page's three-step
|
||||
guide (`renderRecipeEmptyGuide`) and the catalog-wide model readiness check
|
||||
(`resolveCatalogRecipeReadiness`). `ui_recipe_cards.js` is one card: the cover with the models'
|
||||
state (`getRecipeReadiness`: a dot, or words when some are missing) and 功能模块 for a partial
|
||||
workflow, the name, a line of steps, sampler, LoRAs and size, then its model; Open and ⋯ (edit, export, delete)
|
||||
over the cover on hover; a press opens the details, a drag opens it on the canvas. Its styles are
|
||||
`styles/28-workflows.css`. `ui_popup_menu.js` is the small menu under a button they share, `ui_recipe_dialogs.js` owns save/edit dialogs, and `ui_recipe_media.js` owns shared cover helpers. Detail sessions synchronize detected model availability back to `owner.recipeRecords` via `syncRecipeReferencesToCatalog`.
|
||||
- `ui_prompt_composer.js` owns the standalone Prompt Studio drawer (dock side, width, Esc);
|
||||
`ui_prompt_workbench.js` fills it: the top bar (tags or text view, the meanings' language) and
|
||||
saving a box as a card. `ui_prompt_target.js` decides what is edited, the prompt boxes of
|
||||
|
||||
@@ -27,6 +27,10 @@
|
||||
- **Key parameters (关键参数)**: press ☆ before a value's name (tick **Show equal values too** to star one that matches) and it stays at the top of every comparison, for every workflow, kept with your ComfyUI settings.
|
||||
- **Parameter sets in view (参数方案更好找)**: a workflow card with saved sets shows **▤ N** (how many), which opens them directly. Each set there shows its key values (those unlike the workflow's own in colour), how many values differ, and ⇄ to compare it with the workflow.
|
||||
|
||||
### 🗂️ A tidier Workflows page (工作流页面重做)
|
||||
- One bar on top: search, **Filter** (kind and tag in one menu), grid or list, share and import, and **Save the canvas**. The drag tip strip and the red ✕ are gone (Esc still closes the page; the empty page explains dragging).
|
||||
- Cards keep the picture, the name, a line of steps, sampler, LoRAs and size, and the model. The models' state is a dot on the cover, with words only when some are missing; 功能模块 marks a partial workflow. **Open** and **⋯** (edit, export, delete) show when the pointer is on a card.
|
||||
|
||||
### ⚙️ Settings in tabs (设置分组)
|
||||
- The settings page is split into tabs: **Look, Models, Workflows, 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.
|
||||
|
||||
@@ -98,7 +98,8 @@ parameter editing, raw nodes, and preset saving; and `ui_recipe_models.js`
|
||||
lists the models with previews and whether each is here under its saved name. Inline persistence is centralized
|
||||
in `ui_recipe_metadata.js`, with pure ordering/value helpers in
|
||||
`ui_recipe_parameter_utils.js`. The catalog shell lives in `ui_recipe_catalog.js`,
|
||||
cards and card actions in `ui_recipe_cards.js`, save/edit dialogs in
|
||||
cards and card actions in `ui_recipe_cards.js` (styles in `styles/28-workflows.css`; the ⋯
|
||||
and filter menus are `ui_popup_menu.js`), save/edit dialogs in
|
||||
`ui_recipe_dialogs.js`, and shared cover helpers in `ui_recipe_media.js`.
|
||||
`ui_recipe_versions.js` owns version comparison and restore, while
|
||||
`ui_recipe_gallery.js` owns result rendering and opens `ui_gallery_detail.js`
|
||||
|
||||
+10
-8
@@ -606,6 +606,11 @@ export const i18n = {
|
||||
paramSetSame: "与默认参数相同",
|
||||
paramSetCompare: "和默认参数对比",
|
||||
paramSetCount: "参数方案 {count}",
|
||||
workflowsSteps: "{count} 步",
|
||||
workflowsFilter: "筛选",
|
||||
workflowsFilterOn: "筛选 · {count}",
|
||||
workflowsFilterKind: "类型",
|
||||
workflowsFilterTag: "标签",
|
||||
paramSetCountHint: "打开这个工作流的参数方案",
|
||||
compareModeOn: "启用",
|
||||
compareModeMuted: "已静音",
|
||||
@@ -1610,7 +1615,6 @@ export const i18n = {
|
||||
recipeDialogNo: '否',
|
||||
recipeNoTags: '暂无标签',
|
||||
recipeNoMatches: '没有匹配的配方。请清除搜索或标签筛选。',
|
||||
recipeFilterByTag: '按此标签筛选',
|
||||
recipeImport: '导入配方包',
|
||||
recipeExport: '导出配方包',
|
||||
recipePackageExportTitle: '导出配方包:{name}',
|
||||
@@ -1641,10 +1645,8 @@ export const i18n = {
|
||||
recipeDragHint: '按住拖拽至画布打开工作流',
|
||||
recipeDragTargetCanvas: '释放以在画布打开工作流',
|
||||
recipeCardDragHint: '按住卡片可拖拽至画布打开工作流',
|
||||
recipeDraggableBadge: '可拖拽',
|
||||
recipeCardPrimaryActionTooltipComplete: '在新画布中打开工作流(也可直接按住卡片拖至画布)',
|
||||
recipeCardPrimaryActionTooltipPartial: '追加模块至当前画布(也可直接按住卡片拖至画布)',
|
||||
recipeDragGlobalHint: '提示:按住卡片直接拖拽到 ComfyUI 画布空白处释放,即可立即载入工作流',
|
||||
recipeEmptyGuideTitle: '工作流配方工坊暂无配方',
|
||||
recipeEmptyGuideSubtitle: "把调好的工作流连同模型和参数存下来,下次直接用。",
|
||||
recipeEmptyStep1Title: '1. 保存当前工作流',
|
||||
@@ -1853,7 +1855,6 @@ export const i18n = {
|
||||
recipeRawNodesShowMore: '展开其余 {count} 个节点…',
|
||||
recipeRawNodesLoadedCount: '已显示 {shown} / {total} 个节点',
|
||||
recipeCardSpecsCheckpoint: '大模型',
|
||||
recipeCardSpecsLoras: 'LoRA',
|
||||
recipeCardSpecsSteps: '步数',
|
||||
recipeCardSpecsSampler: '采样',
|
||||
recipeCardSpecsResolution: '尺寸',
|
||||
@@ -2908,6 +2909,11 @@ export const i18n = {
|
||||
paramSetSame: "Same as the default",
|
||||
paramSetCompare: "Compare with the default",
|
||||
paramSetCount: "{count} parameter sets",
|
||||
workflowsSteps: "{count} steps",
|
||||
workflowsFilter: "Filter",
|
||||
workflowsFilterOn: "Filter · {count}",
|
||||
workflowsFilterKind: "Kind",
|
||||
workflowsFilterTag: "Tag",
|
||||
paramSetCountHint: "Open this workflow's parameter sets",
|
||||
compareModeOn: "On",
|
||||
compareModeMuted: "Muted",
|
||||
@@ -3911,7 +3917,6 @@ export const i18n = {
|
||||
recipeClearFilters: 'Clear filters',
|
||||
recipeNoTags: 'No tags yet',
|
||||
recipeNoMatches: 'No recipes match. Clear the search or tag filters.',
|
||||
recipeFilterByTag: 'Filter by this tag',
|
||||
recipeImport: 'Import package',
|
||||
recipeExport: 'Export recipe package',
|
||||
recipePackageExportTitle: 'Export recipe package: {name}',
|
||||
@@ -3943,10 +3948,8 @@ export const i18n = {
|
||||
recipeDragHint: 'Drag onto canvas to load workflow',
|
||||
recipeDragTargetCanvas: 'Drop to load workflow onto canvas',
|
||||
recipeCardDragHint: 'Hold & drag card onto canvas to load workflow',
|
||||
recipeDraggableBadge: 'Draggable',
|
||||
recipeCardPrimaryActionTooltipComplete: 'Open workflow in new canvas (or drag & drop card directly)',
|
||||
recipeCardPrimaryActionTooltipPartial: 'Append module to current canvas (or drag & drop card directly)',
|
||||
recipeDragGlobalHint: 'Tip: Drag and drop cards directly onto ComfyUI canvas to load workflows',
|
||||
recipeEmptyGuideTitle: 'Workflow Recipe Studio is Empty',
|
||||
recipeEmptyGuideSubtitle: "Keep a tuned workflow with its models and settings, and use it again next time.",
|
||||
recipeEmptyStep1Title: '1. Save Current Workflow',
|
||||
@@ -4155,7 +4158,6 @@ export const i18n = {
|
||||
recipeRawNodesShowMore: 'Show {count} more nodes…',
|
||||
recipeRawNodesLoadedCount: 'Loaded {shown} / {total} nodes',
|
||||
recipeCardSpecsCheckpoint: 'Base',
|
||||
recipeCardSpecsLoras: 'LoRA',
|
||||
recipeCardSpecsSteps: 'Steps',
|
||||
recipeCardSpecsSampler: 'Sampler',
|
||||
recipeCardSpecsResolution: 'Size',
|
||||
|
||||
@@ -40,8 +40,8 @@ export function closeWorkspace() {
|
||||
this.recipeReturnState = null;
|
||||
delete this.recipeDetailPayload;
|
||||
if (this.recipeListContainer) this.recipeListContainer.style.display = '';
|
||||
const actionbar = this.recipeView?.querySelector('.anomalous-recipe-actionbar');
|
||||
if (actionbar) actionbar.style.display = '';
|
||||
const bar = this.recipeView?.querySelector('.anomalous-wf-bar');
|
||||
if (bar) bar.style.display = '';
|
||||
if (this.detailPanel) {
|
||||
this.stopMediaInContainer?.(this.detailPanel);
|
||||
this.detailPanel.replaceChildren();
|
||||
@@ -83,8 +83,8 @@ export function hideAllPanels() {
|
||||
this.recipeReturnState = null;
|
||||
delete this.recipeDetailPayload;
|
||||
if (this.recipeListContainer) this.recipeListContainer.style.display = '';
|
||||
const actionbar = this.recipeView?.querySelector('.anomalous-recipe-actionbar');
|
||||
if (actionbar) actionbar.style.display = '';
|
||||
const bar = this.recipeView?.querySelector('.anomalous-wf-bar');
|
||||
if (bar) bar.style.display = '';
|
||||
if (this.detailPanel) {
|
||||
this.stopMediaInContainer?.(this.detailPanel);
|
||||
this.detailPanel.replaceChildren();
|
||||
|
||||
@@ -63,11 +63,8 @@ export function showParameterSetCount(owner, card, recipe, signatures) {
|
||||
const own = recipe?.data?.parameter_signature?.value;
|
||||
const sets = Array.isArray(map[recipe?.filename]) ? map[recipe.filename] : [];
|
||||
const count = sets.filter(signature => !own || signature !== own).length;
|
||||
// First in the specs line: the card's height is fixed, so a line of its own would push into it
|
||||
// (a card without specs has that line free).
|
||||
const strip = card.querySelector('.anomalous-recipe-specs-strip');
|
||||
const header = card.querySelector('.anomalous-recipe-card-header');
|
||||
if (!count || !(strip || header)) return;
|
||||
const line = card.querySelector('.anomalous-wf-extras');
|
||||
if (!count || !line) return;
|
||||
const chip = el('button', 'anomalous-recipe-set-chip', `▤ ${count}`);
|
||||
chip.type = 'button';
|
||||
chip.title = `${t('paramSetCount', { count })} · ${t('paramSetCountHint')}`;
|
||||
@@ -77,8 +74,7 @@ export function showParameterSetCount(owner, card, recipe, signatures) {
|
||||
owner.recipeReturnState = { activeTab: 'parameters' };
|
||||
card.click();
|
||||
};
|
||||
if (strip) strip.prepend(chip);
|
||||
else header.after(chip);
|
||||
line.append(chip);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* A small menu that opens under a button (the workflow card's ⋯, the Workflows page's
|
||||
* filter): rows that run an action, rows ticked as chosen, and headings. One is open at a
|
||||
* time; a press outside it, Esc, scrolling or a chosen row closes it.
|
||||
*/
|
||||
|
||||
let openMenu = null;
|
||||
|
||||
function el(tag, className, text) {
|
||||
const node = document.createElement(tag);
|
||||
if (className) node.className = className;
|
||||
if (text !== undefined) node.textContent = text;
|
||||
return node;
|
||||
}
|
||||
|
||||
/** Closes the open menu, if any. */
|
||||
export function closePopupMenu() {
|
||||
openMenu?.();
|
||||
}
|
||||
|
||||
/**
|
||||
* Opens `items` under `anchor`: { heading } or { label, run, checked?, danger? }. A row with
|
||||
* `checked` (true or false) shows a tick when true. Pressing the anchor again closes it.
|
||||
*/
|
||||
export function openPopupMenu(anchor, items) {
|
||||
const wasOpenHere = openMenu?.anchor === anchor;
|
||||
closePopupMenu();
|
||||
if (wasOpenHere) return;
|
||||
const menu = el('div', 'anomalous-popup-menu');
|
||||
menu.setAttribute('role', 'menu');
|
||||
for (const item of items) {
|
||||
if (item.heading) {
|
||||
menu.append(el('div', 'anomalous-popup-menu-heading', item.heading));
|
||||
continue;
|
||||
}
|
||||
const row = el('button', `anomalous-popup-menu-item${item.danger ? ' is-danger' : ''}`);
|
||||
row.type = 'button';
|
||||
row.setAttribute('role', item.checked === undefined ? 'menuitem' : 'menuitemradio');
|
||||
if (item.checked !== undefined) {
|
||||
row.setAttribute('aria-checked', String(Boolean(item.checked)));
|
||||
row.append(el('span', 'anomalous-popup-menu-tick', item.checked ? '✓' : ''));
|
||||
}
|
||||
row.append(el('span', 'anomalous-popup-menu-label', item.label));
|
||||
row.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
close();
|
||||
item.run?.();
|
||||
};
|
||||
menu.append(row);
|
||||
}
|
||||
document.body.append(menu);
|
||||
const box = anchor.getBoundingClientRect();
|
||||
const { width, height } = menu.getBoundingClientRect();
|
||||
const left = Math.min(Math.max(8, box.right - width), window.innerWidth - width - 8);
|
||||
const below = box.bottom + 4;
|
||||
menu.style.left = `${left}px`;
|
||||
menu.style.top = `${below + height > window.innerHeight - 8 ? Math.max(8, box.top - height - 4) : below}px`;
|
||||
|
||||
const onPointer = (event) => {
|
||||
if (!menu.contains(event.target) && !anchor.contains(event.target)) close();
|
||||
};
|
||||
const onKey = (event) => {
|
||||
if (event.key !== 'Escape') return;
|
||||
event.stopPropagation();
|
||||
close();
|
||||
};
|
||||
const close = () => {
|
||||
menu.remove();
|
||||
window.removeEventListener('pointerdown', onPointer, true);
|
||||
window.removeEventListener('keydown', onKey, true);
|
||||
window.removeEventListener('scroll', close, true);
|
||||
if (openMenu === close) openMenu = null;
|
||||
};
|
||||
close.anchor = anchor;
|
||||
window.addEventListener('pointerdown', onPointer, true);
|
||||
window.addEventListener('keydown', onKey, true);
|
||||
window.addEventListener('scroll', close, true);
|
||||
openMenu = close;
|
||||
}
|
||||
+102
-445
@@ -1,4 +1,11 @@
|
||||
/** Workflow Recipe cards and card-level actions. */
|
||||
/**
|
||||
* A Workflow Recipe's card on the Workflows page: its cover (a dot when its models are all
|
||||
* here, words when some are missing; 功能模块 marks a partial workflow), its name, a line of
|
||||
* its steps, sampler, LoRAs and size, and its model. Open (or append, for a partial one) and ⋯ (edit, export,
|
||||
* delete) show over the cover while the pointer is on the card; a press elsewhere opens its
|
||||
* details, and the card drags onto the canvas. The page lays cards as a grid or a list
|
||||
* (styles/28-workflows.css).
|
||||
*/
|
||||
|
||||
import { app } from "../../../scripts/app.js";
|
||||
import { translate } from "./locales.js";
|
||||
@@ -8,6 +15,7 @@ import { applyRecipeToCanvas, showRecipeDetail } from "./ui_recipe_detail.js";
|
||||
import { exportRecipePackage } from "./ui_recipe_package.js";
|
||||
import { appendText } from "./ui_recipe_detail_dom.js";
|
||||
import { appendRecipeCover, outputImageUrl, previewIsVideo, recipeAssetUrl, safeThumbnail } from "./ui_recipe_media.js";
|
||||
import { openPopupMenu } from "./ui_popup_menu.js";
|
||||
|
||||
const t = (key, params) => translate(key, params);
|
||||
|
||||
@@ -26,11 +34,6 @@ async function runRecipeCardAction(actionButton, action, errorKey) {
|
||||
}
|
||||
}
|
||||
|
||||
function compactText(value, limit = 110) {
|
||||
const text = String(value || '').trim().replace(/\s+/g, ' ');
|
||||
return text.length > limit ? `${text.slice(0, limit - 1)}…` : text;
|
||||
}
|
||||
|
||||
function modelDisplayName(value) {
|
||||
const path = String(value || '').replace(/\\/g, '/');
|
||||
const filename = path.split('/').pop() || '';
|
||||
@@ -40,42 +43,25 @@ function modelDisplayName(value) {
|
||||
function formatRecipeResolution(res) {
|
||||
if (!res) return '';
|
||||
if (typeof res === 'string' || typeof res === 'number') return String(res);
|
||||
if (Array.isArray(res) && res.length >= 2) return `${res[0]}x${res[1]}`;
|
||||
if (Array.isArray(res) && res.length >= 2) return `${res[0]}×${res[1]}`;
|
||||
if (typeof res === 'object') {
|
||||
const w = res.width ?? res.w ?? res.x;
|
||||
const h = res.height ?? res.h ?? res.y;
|
||||
if (w && h) return `${w}x${h}`;
|
||||
if (w && h) return `${w}×${h}`;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function createRecipeQuickSpecs(params) {
|
||||
if (!params || typeof params !== 'object') return null;
|
||||
const strip = document.createElement('div');
|
||||
strip.className = 'anomalous-recipe-specs-strip';
|
||||
|
||||
/** The line of how a recipe draws: steps, sampler, LoRAs, size. */
|
||||
function recipeMetaLine(params) {
|
||||
if (!params || typeof params !== 'object') return '';
|
||||
const loras = Array.isArray(params.loras) ? params.loras : [];
|
||||
if (loras.length > 0) {
|
||||
const loraTag = appendText(strip, 'span', '', 'anomalous-recipe-spec-tag is-lora');
|
||||
loraTag.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"><path d="M20.59 13.41l-7.17 7.17a2 2 0 01-2.83 0L2 12V2h10l8.59 8.59a2 2 0 010 2.82z"/><line x1="7" y1="7" x2="7.01" y2="7"/></svg>${loras.length} ${t('recipeCardSpecsLoras')}`;
|
||||
loraTag.title = loras.map((l) => (typeof l === 'object' && l?.name ? l.name : String(l))).join(', ');
|
||||
}
|
||||
|
||||
if (params.steps) {
|
||||
const samplerName = params.sampler_name ? ` · ${params.sampler_name}` : '';
|
||||
const stepTag = appendText(strip, 'span', '', 'anomalous-recipe-spec-tag is-step');
|
||||
stepTag.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"><circle cx="12" cy="12" r="10"/><polyline points="12 6 12 12 16 14"/></svg>${params.steps} ${t('recipeCardSpecsSteps')}${samplerName}`;
|
||||
stepTag.title = `${params.steps} ${t('recipeCardSpecsSteps')}${samplerName}`;
|
||||
}
|
||||
|
||||
const formattedRes = formatRecipeResolution(params.resolution);
|
||||
if (formattedRes) {
|
||||
const resTag = appendText(strip, 'span', '', 'anomalous-recipe-spec-tag');
|
||||
resTag.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"><rect x="3" y="3" width="18" height="18" rx="2"/><line x1="9" y1="3" x2="9" y2="21"/></svg>${formattedRes}`;
|
||||
resTag.title = `${t('recipeCardSpecsResolution')}: ${formattedRes}`;
|
||||
}
|
||||
|
||||
return strip.childElementCount ? strip : null;
|
||||
return [
|
||||
params.steps ? t('workflowsSteps', { count: params.steps }) : '',
|
||||
params.sampler_name || '',
|
||||
loras.length ? `${loras.length} LoRA` : '',
|
||||
formatRecipeResolution(params.resolution),
|
||||
].filter(Boolean).join(' · ');
|
||||
}
|
||||
|
||||
export function getRecipeReadiness(recipeData) {
|
||||
@@ -106,426 +92,98 @@ export function getRecipeReadiness(recipeData) {
|
||||
return { status: 'ready', label: t('recipeStatusReady') };
|
||||
}
|
||||
|
||||
export function ensureRecipeGuideStyles() {
|
||||
if (typeof document === 'undefined') return;
|
||||
if (document.querySelector?.('#anomalous-recipe-guide-styles')) return;
|
||||
if (typeof document.createElement !== 'function') return;
|
||||
|
||||
const style = document.createElement('style');
|
||||
style.id = 'anomalous-recipe-guide-styles';
|
||||
style.textContent = `
|
||||
.anomalous-recipe-card {
|
||||
cursor: grab !important;
|
||||
}
|
||||
.anomalous-recipe-card:active {
|
||||
cursor: grabbing !important;
|
||||
}
|
||||
.anomalous-recipe-card button,
|
||||
.anomalous-recipe-card a,
|
||||
.anomalous-recipe-card select,
|
||||
.anomalous-recipe-card input {
|
||||
cursor: pointer;
|
||||
}
|
||||
.anomalous-recipe-drag-badge {
|
||||
position: absolute;
|
||||
top: 6px;
|
||||
left: 6px;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
background: rgba(10, 12, 16, 0.75);
|
||||
backdrop-filter: blur(6px);
|
||||
-webkit-backdrop-filter: blur(6px);
|
||||
border: 1px solid var(--amb-border);
|
||||
color: var(--amb-text-soft);
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease, background 0.15s ease, border-color 0.15s ease;
|
||||
opacity: 0.85;
|
||||
z-index: 2;
|
||||
}
|
||||
.anomalous-recipe-card:hover .anomalous-recipe-drag-badge {
|
||||
opacity: 1;
|
||||
background: rgba(15, 23, 42, 0.9);
|
||||
border-color: color-mix(in srgb, var(--amb-link) 40%, transparent);
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
/* 修复封面底部模型就绪状态徽标对比度与穿透:深色高对比毛玻璃底板 */
|
||||
.anomalous-recipe-readiness-pill {
|
||||
display: inline-flex !important;
|
||||
align-items: center !important;
|
||||
gap: 5px !important;
|
||||
background: rgba(10, 12, 18, 0.88) !important;
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--amb-border-strong) !important;
|
||||
padding: 2.5px 8px !important;
|
||||
border-radius: 6px !important;
|
||||
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45) !important;
|
||||
color: var(--amb-text-main) !important;
|
||||
}
|
||||
.anomalous-recipe-readiness-pill.is-ready {
|
||||
background: rgba(10, 12, 18, 0.9) !important;
|
||||
border-color: rgba(16, 185, 129, 0.5) !important;
|
||||
}
|
||||
.anomalous-recipe-readiness-pill.is-ready .anomalous-recipe-readiness-text {
|
||||
color: var(--amb-ok-soft) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
.anomalous-recipe-readiness-pill.is-warning {
|
||||
background: rgba(10, 12, 18, 0.9) !important;
|
||||
border-color: rgba(245, 158, 11, 0.6) !important;
|
||||
}
|
||||
.anomalous-recipe-readiness-pill.is-warning .anomalous-recipe-readiness-text {
|
||||
color: var(--amb-warn-soft) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
.anomalous-recipe-readiness-pill.is-missing {
|
||||
background: rgba(10, 12, 18, 0.9) !important;
|
||||
border-color: rgba(239, 68, 68, 0.6) !important;
|
||||
}
|
||||
.anomalous-recipe-readiness-pill.is-missing .anomalous-recipe-readiness-text {
|
||||
color: var(--amb-danger-text) !important;
|
||||
font-weight: 600 !important;
|
||||
}
|
||||
.anomalous-recipe-readiness-dot {
|
||||
width: 7px !important;
|
||||
height: 7px !important;
|
||||
flex-shrink: 0 !important;
|
||||
}
|
||||
.anomalous-recipe-drag-hint-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 6px 14px;
|
||||
margin: 8px 16px 4px 16px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
color: var(--amb-text-muted);
|
||||
background: var(--amb-lt-chip, rgba(30, 41, 59, 0.5));
|
||||
border: 1px solid var(--amb-border);
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
|
||||
flex-shrink: 0;
|
||||
line-height: 1.4;
|
||||
}
|
||||
.anomalous-recipe-drag-hint-content {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
}
|
||||
.anomalous-recipe-drag-hint-icon {
|
||||
font-size: 14px;
|
||||
line-height: 1;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.anomalous-recipe-drag-hint-text {
|
||||
color: var(--amb-text-soft);
|
||||
font-size: 12px;
|
||||
}
|
||||
.anomalous-recipe-drag-hint-close {
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--amb-text-dim);
|
||||
font-size: 16px;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
transition: all 0.15s ease;
|
||||
flex-shrink: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
.anomalous-recipe-drag-hint-close:hover {
|
||||
color: var(--amb-text-main);
|
||||
background: rgba(var(--amb-ink-rgb), 0.08);
|
||||
}
|
||||
.anomalous-recipe-empty-guide {
|
||||
grid-column: 1 / -1;
|
||||
max-width: 580px;
|
||||
margin: 32px auto;
|
||||
padding: 28px 24px;
|
||||
background: rgba(var(--amb-ink-rgb), 0.025);
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: 16px;
|
||||
text-align: center;
|
||||
box-sizing: border-box;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
}
|
||||
.anomalous-recipe-empty-icon {
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--amb-link) 10%, transparent);
|
||||
border: 1px solid color-mix(in srgb, var(--amb-link) 25%, transparent);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 24px;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.anomalous-recipe-empty-title {
|
||||
margin: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
color: var(--amb-text-main);
|
||||
letter-spacing: 0.2px;
|
||||
}
|
||||
.anomalous-recipe-empty-subtitle {
|
||||
margin: 0;
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
color: var(--amb-text-muted);
|
||||
max-width: 440px;
|
||||
}
|
||||
.anomalous-recipe-empty-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
.anomalous-recipe-empty-step {
|
||||
background: rgba(var(--amb-ink-rgb), 0.03);
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: 10px;
|
||||
padding: 14px 14px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
transition: background 0.15s ease, border-color 0.15s ease;
|
||||
}
|
||||
.anomalous-recipe-empty-step:hover {
|
||||
background: rgba(var(--amb-ink-rgb), 0.05);
|
||||
border-color: var(--amb-border-strong);
|
||||
}
|
||||
.anomalous-recipe-step-icon {
|
||||
font-size: 20px;
|
||||
line-height: 1;
|
||||
margin-bottom: 2px;
|
||||
}
|
||||
.anomalous-recipe-empty-step strong {
|
||||
font-size: 13px;
|
||||
color: var(--amb-text-main);
|
||||
font-weight: 600;
|
||||
}
|
||||
.anomalous-recipe-empty-step p {
|
||||
margin: 0;
|
||||
font-size: 11.5px;
|
||||
line-height: 1.5;
|
||||
color: var(--amb-text-muted);
|
||||
}
|
||||
.anomalous-recipe-empty-action-btn {
|
||||
margin-top: 6px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 8px 18px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--amb-btn-primary-text);
|
||||
background: var(--amb-btn-primary-bg);
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
transition: background 0.15s ease;
|
||||
}
|
||||
.anomalous-recipe-empty-action-btn:hover {
|
||||
background: #4338ca;
|
||||
}
|
||||
`;
|
||||
(document.head || document.body)?.appendChild(style);
|
||||
/** The models' state on the cover: a dot when all is well, words when something is missing. */
|
||||
function readinessMark(data) {
|
||||
const readiness = getRecipeReadiness(data);
|
||||
const label = readiness.label.replace(/^[🟢🟡🔴⏳]\s*/u, '');
|
||||
const mark = document.createElement('span');
|
||||
mark.className = `anomalous-wf-status is-${readiness.status}`;
|
||||
mark.title = `${label} — ${t('recipeReadinessHint')}`;
|
||||
if (readiness.status === 'missing') mark.textContent = label;
|
||||
else mark.setAttribute('aria-label', label);
|
||||
return mark;
|
||||
}
|
||||
|
||||
export function createRecipeCard(owner, recipe, services) {
|
||||
ensureRecipeGuideStyles();
|
||||
const data = recipe?.data || {};
|
||||
const card = document.createElement('article');
|
||||
card.className = 'anomalous-recipe-card';
|
||||
card.style.cursor = 'grab';
|
||||
card.title = `${data.name || t('recipeUntitled')} — ${t('recipeCardDragHint') || '按住可拖拽至画布打开工作流'}`;
|
||||
async function deleteRecipe(owner, recipe) {
|
||||
if (!await anomalousConfirm(t('recipeDeleteConfirm'))) return;
|
||||
const response = await fetch('/anomalous/delete_recipe', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ filename: recipe.filename }),
|
||||
});
|
||||
if (!response.ok) throw new Error('recipe deletion failed');
|
||||
await owner.refreshRecipes();
|
||||
}
|
||||
|
||||
// 1. Bento Media Wrap: Cover on TOP (136px)
|
||||
const mediaWrap = document.createElement('div');
|
||||
mediaWrap.className = 'anomalous-recipe-card-media-wrap';
|
||||
const sourceImageUrl = outputImageUrl(data.source_image);
|
||||
const savedCover = recipeAssetUrl(recipe.filename, data.presentation?.cover_asset_id);
|
||||
const thumbnail = savedCover || (previewIsVideo(sourceImageUrl)
|
||||
? sourceImageUrl
|
||||
: safeThumbnail(data.thumbnail) || sourceImageUrl);
|
||||
appendRecipeCover(mediaWrap, thumbnail, data.name || t('recipeThumbnail'), recipe?.filename || data.name);
|
||||
|
||||
// Frosted Glass Chips at bottom-left of cover (Readiness Pill + Base Model)
|
||||
const bottomChips = document.createElement('div');
|
||||
bottomChips.className = 'anomalous-recipe-cover-bottom-chips';
|
||||
|
||||
const readiness = getRecipeReadiness(data);
|
||||
const readinessPill = document.createElement('span');
|
||||
readinessPill.className = `anomalous-recipe-readiness-pill is-${readiness.status}`;
|
||||
readinessPill.title = `${readiness.label} — ${t('recipeReadinessHint') || (window.anomalous_browser_lang === 'zh' ? '检测工作流所需模型在本地环境中的就绪状态,点击卡片进入详情可进行智能比对与替换' : 'Detects model dependencies; open details for smart matching and replacement')}`;
|
||||
const dot = document.createElement('span');
|
||||
dot.className = `anomalous-recipe-readiness-dot is-${readiness.status}`;
|
||||
const rText = document.createElement('span');
|
||||
rText.className = 'anomalous-recipe-readiness-text';
|
||||
rText.textContent = readiness.label.replace(/^[🟢🟡🔴⏳]\s*/, '');
|
||||
readinessPill.append(dot, rText);
|
||||
bottomChips.appendChild(readinessPill);
|
||||
|
||||
const baseModelStr = data.params?.baseModel || data.params?.baseModels;
|
||||
if (baseModelStr) {
|
||||
const pill = document.createElement('span');
|
||||
pill.className = 'anomalous-recipe-cover-pill';
|
||||
pill.innerHTML = '<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:-1px;margin-right:4px;"><path d="m21 16-9 5-9-5V8l9-5 9 5v8z"/><path d="m3.27 6.96 8.73 4.84 8.73-4.84"/><path d="M12 22.08V11.8"/></svg>';
|
||||
appendText(pill, 'span', modelDisplayName(baseModelStr));
|
||||
pill.title = String(baseModelStr);
|
||||
bottomChips.appendChild(pill);
|
||||
}
|
||||
mediaWrap.appendChild(bottomChips);
|
||||
|
||||
// Scope Pill (Top-right of cover)
|
||||
const isPartial = data.workflow_scope === 'partial';
|
||||
const scopePill = document.createElement('span');
|
||||
scopePill.className = `anomalous-recipe-scope-pill ${isPartial ? 'is-partial' : 'is-complete'}`;
|
||||
const scopeSvg = isPartial
|
||||
? `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:-1px;margin-right:3px;"><circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v6"/><path d="M9 6h6"/></svg>`
|
||||
: `<svg width="11" height="11" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" style="display:inline-block;vertical-align:-1px;margin-right:3px;"><polygon points="13 2 3 14 12 14 11 22 21 10 12 10 13 2"/></svg>`;
|
||||
scopePill.innerHTML = `${scopeSvg}<span>${t(isPartial ? 'recipeScopePill_partial' : 'recipeScopePill_complete')}</span>`;
|
||||
mediaWrap.appendChild(scopePill);
|
||||
|
||||
// Drag Affordance Badge (Top-left of cover)
|
||||
const dragBadge = document.createElement('span');
|
||||
dragBadge.className = 'anomalous-recipe-drag-badge';
|
||||
dragBadge.innerHTML = `<svg width="10" height="10" viewBox="0 0 24 24" fill="currentColor" style="display:inline-block;vertical-align:-1px;margin-right:3px;"><circle cx="9" cy="5" r="2"/><circle cx="15" cy="5" r="2"/><circle cx="9" cy="12" r="2"/><circle cx="15" cy="12" r="2"/><circle cx="9" cy="19" r="2"/><circle cx="15" cy="19" r="2"/></svg><span>${t('recipeDraggableBadge') || (window.anomalous_browser_lang === 'zh' ? '可拖拽' : 'Draggable')}</span>`;
|
||||
dragBadge.title = t('recipeCardDragHint') || '按住可拖拽至画布打开工作流';
|
||||
mediaWrap.appendChild(dragBadge);
|
||||
|
||||
card.appendChild(mediaWrap);
|
||||
|
||||
// 2. Card Body
|
||||
const body = document.createElement('div');
|
||||
body.className = 'anomalous-recipe-card-body';
|
||||
|
||||
// Header: Clean Title (Readiness relocated to cover pill)
|
||||
const header = document.createElement('div');
|
||||
header.className = 'anomalous-recipe-card-header';
|
||||
const title = appendText(header, 'h3', data.name || t('recipeUntitled'), 'anomalous-recipe-card-title');
|
||||
title.title = data.name || t('recipeUntitled');
|
||||
body.appendChild(header);
|
||||
|
||||
// Quick Specs Strip
|
||||
const specs = createRecipeQuickSpecs(data.params);
|
||||
if (specs) body.appendChild(specs);
|
||||
|
||||
// Compact Tags (first 3)
|
||||
if (Array.isArray(data.tags) && data.tags.length) {
|
||||
const tags = document.createElement('div');
|
||||
tags.className = 'anomalous-recipe-tags';
|
||||
for (const tag of data.tags.slice(0, 3)) {
|
||||
const tagButton = appendText(tags, 'button', compactText(tag, 18), 'anomalous-recipe-badge anomalous-recipe-badge-tag');
|
||||
tagButton.type = 'button';
|
||||
tagButton.title = t('recipeFilterByTag');
|
||||
tagButton.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
if (!owner.recipeSelectedTags) owner.recipeSelectedTags = new Set();
|
||||
owner.recipeSelectedTags = new Set([tag]);
|
||||
if (owner.recipeTagSelect) owner.recipeTagSelect.value = tag;
|
||||
owner.renderRecipeList(owner.recipeRecords || []);
|
||||
};
|
||||
}
|
||||
body.appendChild(tags);
|
||||
}
|
||||
|
||||
// Footer: Primary Action Button + Mini Actions
|
||||
const footer = document.createElement('div');
|
||||
footer.className = 'anomalous-recipe-card-footer';
|
||||
|
||||
const appendBtn = document.createElement('button');
|
||||
appendBtn.type = 'button';
|
||||
appendBtn.className = 'anomalous-recipe-btn-primary-action anomalous-tooltip-target';
|
||||
const actionSvg = `<svg width="13" height="13" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" style="display:inline-block;vertical-align:-1px;margin-right:4px;"><polygon points="5 3 19 12 5 21 5 3"/></svg>`;
|
||||
appendBtn.innerHTML = `${actionSvg}<span>${t(isPartial ? 'recipeAppendCanvas' : 'recipeOpenCanvas')}</span>`;
|
||||
appendBtn.removeAttribute('title');
|
||||
const primaryTooltip = isPartial
|
||||
? (t('recipeCardPrimaryActionTooltipPartial') || `${t('recipeAppendCanvas')}(也可按住卡片直接拖至画布)`)
|
||||
: (t('recipeCardPrimaryActionTooltipComplete') || `${t('recipeOpenCanvas')}(也可按住卡片直接拖至画布)`);
|
||||
appendBtn.setAttribute('data-tooltip', primaryTooltip);
|
||||
appendBtn.setAttribute('data-tooltip-pos', 'top');
|
||||
appendBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
runRecipeCardAction(appendBtn, async () => {
|
||||
/** Open / append and ⋯, shown over the cover while the pointer is on the card. */
|
||||
function cardActions(owner, recipe, services, isPartial) {
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'anomalous-wf-actions';
|
||||
const open = appendText(actions, 'button', `▶ ${t(isPartial ? 'recipeAppendCanvas' : 'recipeOpenCanvas')}`, 'anomalous-wf-open');
|
||||
open.type = 'button';
|
||||
open.title = t(isPartial ? 'recipeCardPrimaryActionTooltipPartial' : 'recipeCardPrimaryActionTooltipComplete');
|
||||
open.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
runRecipeCardAction(open, async () => {
|
||||
const graph = app.graph;
|
||||
const fullRecipe = await services.fetchRecipeData(recipe.filename);
|
||||
if (app.graph !== graph) throw new Error('materialTargetChanged');
|
||||
await applyRecipeToCanvas(owner, fullRecipe);
|
||||
}, isPartial ? 'recipeAppendError' : 'recipeOpenError');
|
||||
};
|
||||
footer.appendChild(appendBtn);
|
||||
|
||||
const miniActions = document.createElement('div');
|
||||
miniActions.className = 'anomalous-recipe-card-mini-actions';
|
||||
|
||||
const editBtn = document.createElement('button');
|
||||
editBtn.type = 'button';
|
||||
editBtn.className = 'anomalous-recipe-card-mini-btn anomalous-tooltip-target';
|
||||
editBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/></svg>`;
|
||||
editBtn.removeAttribute('title');
|
||||
editBtn.setAttribute('data-tooltip', t('recipeEdit'));
|
||||
editBtn.setAttribute('data-tooltip-pos', 'top');
|
||||
editBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
runRecipeCardAction(editBtn, () => services.editRecipe(owner, recipe?.data || {}, recipe.filename), 'recipeUpdateError');
|
||||
const more = appendText(actions, 'button', '⋯', 'anomalous-wf-more');
|
||||
more.type = 'button';
|
||||
more.title = t('notebookMore');
|
||||
more.setAttribute('aria-label', t('notebookMore'));
|
||||
more.onclick = (event) => {
|
||||
event.stopPropagation();
|
||||
openPopupMenu(more, [
|
||||
{ label: t('recipeEdit'), run: () => runRecipeCardAction(more, () => services.editRecipe(owner, recipe?.data || {}, recipe.filename), 'recipeUpdateError') },
|
||||
{ label: t('recipeExport'), run: () => exportRecipePackage(recipe) },
|
||||
{ label: t('recipeDelete'), danger: true, run: () => runRecipeCardAction(more, () => deleteRecipe(owner, recipe), 'recipeDeleteError') },
|
||||
]);
|
||||
};
|
||||
return actions;
|
||||
}
|
||||
|
||||
const exportBtn = document.createElement('button');
|
||||
exportBtn.type = 'button';
|
||||
exportBtn.className = 'anomalous-recipe-card-mini-btn anomalous-tooltip-target';
|
||||
exportBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
|
||||
exportBtn.removeAttribute('title');
|
||||
exportBtn.setAttribute('data-tooltip', t('recipeExport'));
|
||||
exportBtn.setAttribute('aria-label', t('recipeExport'));
|
||||
exportBtn.setAttribute('data-tooltip-pos', 'top');
|
||||
exportBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
exportRecipePackage(recipe);
|
||||
};
|
||||
/** The model's line; the parameter set count joins it on the right (ui_param_set_keys.js). */
|
||||
function modelLine(params) {
|
||||
const line = document.createElement('div');
|
||||
line.className = 'anomalous-wf-extras';
|
||||
const model = modelDisplayName(params?.baseModel || params?.baseModels);
|
||||
const name = appendText(line, 'span', model, 'anomalous-wf-model');
|
||||
name.title = model;
|
||||
return line;
|
||||
}
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.type = 'button';
|
||||
removeBtn.className = 'anomalous-recipe-card-mini-btn is-delete anomalous-tooltip-target';
|
||||
removeBtn.innerHTML = `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="display:block;"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/></svg>`;
|
||||
removeBtn.removeAttribute('title');
|
||||
removeBtn.setAttribute('data-tooltip', t('recipeDelete'));
|
||||
removeBtn.setAttribute('data-tooltip-pos', 'top');
|
||||
removeBtn.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
runRecipeCardAction(removeBtn, async () => {
|
||||
if (!await anomalousConfirm(t('recipeDeleteConfirm'))) return;
|
||||
const response = await fetch('/anomalous/delete_recipe', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ filename: recipe.filename }),
|
||||
});
|
||||
if (!response.ok) throw new Error('recipe deletion failed');
|
||||
await owner.refreshRecipes();
|
||||
}, 'recipeDeleteError');
|
||||
};
|
||||
export function createRecipeCard(owner, recipe, services) {
|
||||
const data = recipe?.data || {};
|
||||
const isPartial = data.workflow_scope === 'partial';
|
||||
const card = document.createElement('article');
|
||||
card.className = 'anomalous-wf-card';
|
||||
card.title = `${data.name || t('recipeUntitled')} — ${t('recipeCardDragHint')}`;
|
||||
|
||||
miniActions.append(editBtn, exportBtn, removeBtn);
|
||||
footer.appendChild(miniActions);
|
||||
body.appendChild(footer);
|
||||
card.appendChild(body);
|
||||
const cover = document.createElement('div');
|
||||
cover.className = 'anomalous-wf-cover';
|
||||
const sourceImageUrl = outputImageUrl(data.source_image);
|
||||
const savedCover = recipeAssetUrl(recipe.filename, data.presentation?.cover_asset_id);
|
||||
const thumbnail = savedCover || (previewIsVideo(sourceImageUrl)
|
||||
? sourceImageUrl
|
||||
: safeThumbnail(data.thumbnail) || sourceImageUrl);
|
||||
appendRecipeCover(cover, thumbnail, data.name || t('recipeThumbnail'), recipe?.filename || data.name);
|
||||
cover.append(readinessMark(data));
|
||||
if (isPartial) appendText(cover, 'span', t('recipeScopePill_partial'), 'anomalous-wf-kind');
|
||||
card.append(cover, cardActions(owner, recipe, services, isPartial));
|
||||
|
||||
const info = document.createElement('div');
|
||||
info.className = 'anomalous-wf-info';
|
||||
const title = appendText(info, 'h3', data.name || t('recipeUntitled'), 'anomalous-wf-title');
|
||||
title.title = data.name || t('recipeUntitled');
|
||||
const meta = appendText(info, 'div', recipeMetaLine(data.params), 'anomalous-wf-meta');
|
||||
meta.title = meta.textContent;
|
||||
info.append(modelLine(data.params));
|
||||
card.append(info);
|
||||
|
||||
// Card click for Detail Modal
|
||||
card.onclick = () => runRecipeCardAction(card, async () => {
|
||||
const bundle = await services.fetchRecipeBundle(recipe.filename);
|
||||
const result = await showRecipeDetail(owner, {
|
||||
@@ -536,14 +194,14 @@ export function createRecipeCard(owner, recipe, services) {
|
||||
if (result?.mode === 'edit') await services.editRecipe(owner, bundle.data, recipe.filename, bundle.history);
|
||||
}, 'recipeLoadError');
|
||||
|
||||
// 3. 一拖直达画布: "拖入画布后直接打开一个新的画布,就像拖入一个ComfyUI的图片一样"
|
||||
// Dragged onto the canvas, the card opens its workflow like a dropped ComfyUI picture.
|
||||
bindMaterialDrag(card, owner, {
|
||||
payload: () => ({
|
||||
type: 'recipe',
|
||||
filename: recipe.filename,
|
||||
scope: data.workflow_scope || 'complete',
|
||||
dragHint: t('recipeDragHint') || '按住拖拽至画布打开工作流',
|
||||
dragTargetHint: t('recipeDragTargetCanvas') || '释放以在画布打开工作流',
|
||||
dragHint: t('recipeDragHint'),
|
||||
dragTargetHint: t('recipeDragTargetCanvas'),
|
||||
}),
|
||||
accepts: () => false,
|
||||
dropOnCanvas: async (event, dragData, graph) => {
|
||||
@@ -554,5 +212,4 @@ export function createRecipeCard(owner, recipe, services) {
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
/** Workflow Recipe catalog shell: navigation, filters, view mode, and loading. */
|
||||
/** The Workflows page (Workflow Recipe catalog): its bar (search, filter, view), the list, loading. Cards: ui_recipe_cards.js. */
|
||||
|
||||
import { translate } from './locales.js';
|
||||
import { appendText } from './ui_recipe_detail_dom.js';
|
||||
import { createRecipeCard, ensureRecipeGuideStyles } from './ui_recipe_cards.js';
|
||||
import { createRecipeCard } from './ui_recipe_cards.js';
|
||||
import { openPopupMenu } from './ui_popup_menu.js';
|
||||
import { moveWorkflowMaterials, workflowMaterialCount } from './recipe_save.js';
|
||||
import { openShareDialog } from './ui_share.js';
|
||||
import { fetchParameterSetSignatures, showParameterSetCount } from './ui_param_set_keys.js';
|
||||
@@ -34,57 +35,52 @@ function buildMoveStrip(owner) {
|
||||
return strip;
|
||||
}
|
||||
|
||||
export function updateRecipeFilterControls(owner, recipes) {
|
||||
if (!owner.recipeTagSelect) return;
|
||||
const tags = [...new Set((recipes || []).flatMap(item => item?.data?.tags || []))]
|
||||
/** The page's tags, for the filter menu. */
|
||||
function recipeTags(owner) {
|
||||
return [...new Set((owner.recipeRecords || []).flatMap(item => item?.data?.tags || []))]
|
||||
.filter(Boolean)
|
||||
.sort((a, b) => a.localeCompare(b));
|
||||
const selected = [...(owner.recipeSelectedTags || [])][0] || '';
|
||||
owner.recipeSelectedTags = new Set(selected ? [selected] : []);
|
||||
owner.recipeTagSelect.replaceChildren();
|
||||
appendText(owner.recipeTagSelect, 'option', t('materialAllTags')).value = '';
|
||||
if (selected && !tags.includes(selected)) tags.push(selected);
|
||||
for (const tag of tags) appendText(owner.recipeTagSelect, 'option', tag).value = tag;
|
||||
owner.recipeTagSelect.value = selected;
|
||||
}
|
||||
|
||||
function buildRecipeStudioTopbar(owner) {
|
||||
const topbar = document.createElement('div');
|
||||
topbar.className = 'anomalous-recipe-topbar';
|
||||
/** The filter button says how many filters are on. */
|
||||
export function updateRecipeFilterControls(owner) {
|
||||
const button = owner.recipeFilterButton;
|
||||
if (!button) return;
|
||||
const active = (owner.recipeScopeFilter && owner.recipeScopeFilter !== 'all' ? 1 : 0) + (owner.recipeSelectedTags?.size || 0);
|
||||
button.textContent = active ? t('workflowsFilterOn', { count: active }) : t('workflowsFilter');
|
||||
button.classList.toggle('is-active', Boolean(active));
|
||||
}
|
||||
|
||||
const left = document.createElement('div');
|
||||
left.className = 'anomalous-recipe-topbar-left';
|
||||
const pillsWrap = document.createElement('div');
|
||||
pillsWrap.className = 'anomalous-recipe-pills';
|
||||
const scopes = [
|
||||
{ id: 'all', label: t('recipeScopePill_all') },
|
||||
{ id: 'complete', label: t('recipeScopePill_complete') },
|
||||
{ id: 'partial', label: t('recipeScopePill_partial') },
|
||||
];
|
||||
owner.recipeScopeFilter = owner.recipeScopeFilter || 'all';
|
||||
for (const { id, label } of scopes) {
|
||||
const scopeButton = appendText(pillsWrap, 'button', label, 'anomalous-recipe-pill');
|
||||
scopeButton.type = 'button';
|
||||
if (owner.recipeScopeFilter === id) scopeButton.classList.add('is-active');
|
||||
scopeButton.onclick = () => {
|
||||
if (owner.recipeScopeFilter === id) return;
|
||||
owner.recipeScopeFilter = id;
|
||||
pillsWrap.querySelectorAll('.anomalous-recipe-pill').forEach(element => element.classList.remove('is-active'));
|
||||
scopeButton.classList.add('is-active');
|
||||
owner.renderRecipeList(owner.recipeRecords || []);
|
||||
};
|
||||
/** The filter menu: what kind of workflow, then one tag. */
|
||||
function openFilterMenu(owner, anchor) {
|
||||
const rerender = () => owner.renderRecipeList(owner.recipeRecords || []);
|
||||
const scope = owner.recipeScopeFilter || 'all';
|
||||
const tag = [...(owner.recipeSelectedTags || [])][0] || '';
|
||||
const items = [{ heading: t('workflowsFilterKind') }];
|
||||
for (const id of ['all', 'complete', 'partial']) {
|
||||
items.push({ label: t(`recipeScopePill_${id}`), checked: scope === id, run: () => { owner.recipeScopeFilter = id; rerender(); } });
|
||||
}
|
||||
left.appendChild(pillsWrap);
|
||||
topbar.appendChild(left);
|
||||
const tags = recipeTags(owner);
|
||||
if (tags.length) {
|
||||
items.push({ heading: t('workflowsFilterTag') });
|
||||
items.push({ label: t('materialAllTags'), checked: !tag, run: () => { owner.recipeSelectedTags = new Set(); rerender(); } });
|
||||
for (const name of tags) {
|
||||
items.push({ label: `#${name}`, checked: tag === name, run: () => { owner.recipeSelectedTags = new Set([name]); rerender(); } });
|
||||
}
|
||||
}
|
||||
openPopupMenu(anchor, items);
|
||||
}
|
||||
|
||||
const center = document.createElement('div');
|
||||
center.className = 'anomalous-recipe-topbar-center';
|
||||
const searchWrap = document.createElement('div');
|
||||
searchWrap.className = 'anomalous-recipe-search-wrap';
|
||||
appendText(searchWrap, 'span', '🔍', 'anomalous-recipe-search-icon');
|
||||
/** The one row over the list: search, filter, count, view, share and import, save the canvas. */
|
||||
function buildWorkflowBar(owner) {
|
||||
const bar = document.createElement('div');
|
||||
bar.className = 'anomalous-wf-bar';
|
||||
|
||||
const search = document.createElement('label');
|
||||
search.className = 'anomalous-wf-search';
|
||||
search.innerHTML = '<svg viewBox="0 0 24 24" width="14" height="14" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true"><circle cx="11" cy="11" r="7"/><line x1="16.5" y1="16.5" x2="21" y2="21"/></svg>'; // static markup only
|
||||
const searchInput = document.createElement('input');
|
||||
searchInput.type = 'search';
|
||||
searchInput.className = 'anomalous-recipe-search-input';
|
||||
searchInput.placeholder = t('recipeSearchPlaceholder');
|
||||
searchInput.value = owner.recipeSearchQuery || '';
|
||||
searchInput.oninput = () => {
|
||||
@@ -92,64 +88,46 @@ function buildRecipeStudioTopbar(owner) {
|
||||
owner.renderRecipeList(owner.recipeRecords || []);
|
||||
};
|
||||
owner.recipeSearchInput = searchInput;
|
||||
searchWrap.appendChild(searchInput);
|
||||
center.appendChild(searchWrap);
|
||||
search.append(searchInput);
|
||||
|
||||
const tagSelect = document.createElement('select');
|
||||
tagSelect.className = 'anomalous-recipe-tag-select';
|
||||
tagSelect.onchange = () => {
|
||||
owner.recipeSelectedTags = new Set(tagSelect.value ? [tagSelect.value] : []);
|
||||
owner.renderRecipeList(owner.recipeRecords || []);
|
||||
};
|
||||
owner.recipeTagSelect = tagSelect;
|
||||
center.appendChild(tagSelect);
|
||||
owner.recipeFilterSummary = appendText(center, 'small', '0/0', 'anomalous-recipe-filter-summary');
|
||||
topbar.appendChild(center);
|
||||
const filter = appendText(bar, 'button', '', 'anomalous-wf-filter');
|
||||
filter.type = 'button';
|
||||
filter.onclick = () => openFilterMenu(owner, filter);
|
||||
owner.recipeFilterButton = filter;
|
||||
bar.prepend(search);
|
||||
owner.recipeFilterSummary = appendText(bar, 'span', '0/0', 'anomalous-wf-count');
|
||||
|
||||
const right = document.createElement('div');
|
||||
right.className = 'anomalous-recipe-topbar-right';
|
||||
const viewSwitch = document.createElement('div');
|
||||
viewSwitch.className = 'anomalous-recipe-view-switch';
|
||||
owner.recipeViewMode = owner.recipeViewMode || 'grid';
|
||||
const gridButton = document.createElement('button');
|
||||
gridButton.type = 'button';
|
||||
gridButton.className = 'anomalous-recipe-view-btn' + (owner.recipeViewMode === 'grid' ? ' is-active' : '');
|
||||
gridButton.title = t('recipeViewGrid');
|
||||
gridButton.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M3 3h7v7H3V3zm11 0h7v7h-7V3zm-11 11h7v7H3v-7zm11 0h7v7h-7v-7z"/></svg>';
|
||||
const listButton = document.createElement('button');
|
||||
listButton.type = 'button';
|
||||
listButton.className = 'anomalous-recipe-view-btn' + (owner.recipeViewMode === 'list' ? ' is-active' : '');
|
||||
listButton.title = t('recipeViewList');
|
||||
listButton.innerHTML = '<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor"><path d="M3 4h18v3H3V4zm0 7h18v3H3v-3zm0 7h18v3H3v-3z"/></svg>';
|
||||
const setViewMode = mode => {
|
||||
if (owner.recipeViewMode === mode) return;
|
||||
owner.recipeViewMode = mode;
|
||||
gridButton.classList.toggle('is-active', mode === 'grid');
|
||||
listButton.classList.toggle('is-active', mode === 'list');
|
||||
owner.recipeListContainer?.classList.toggle('is-grid', mode === 'grid');
|
||||
owner.recipeListContainer?.classList.toggle('is-list', mode === 'list');
|
||||
const view = appendText(bar, 'button', '', 'anomalous-wf-icon-btn');
|
||||
view.type = 'button';
|
||||
const showViewButton = () => {
|
||||
const list = owner.recipeViewMode === 'list';
|
||||
view.textContent = list ? '▦' : '☰';
|
||||
view.title = t(list ? 'recipeViewGrid' : 'recipeViewList');
|
||||
view.setAttribute('aria-label', view.title);
|
||||
};
|
||||
gridButton.onclick = () => setViewMode('grid');
|
||||
listButton.onclick = () => setViewMode('list');
|
||||
viewSwitch.append(gridButton, listButton);
|
||||
right.appendChild(viewSwitch);
|
||||
view.onclick = () => {
|
||||
owner.recipeViewMode = owner.recipeViewMode === 'list' ? 'grid' : 'list';
|
||||
owner.recipeListContainer?.classList.toggle('is-grid', owner.recipeViewMode === 'grid');
|
||||
owner.recipeListContainer?.classList.toggle('is-list', owner.recipeViewMode === 'list');
|
||||
showViewButton();
|
||||
};
|
||||
showViewButton();
|
||||
|
||||
// Share and import (ui_share.js): share codes, workflow files, images, backups.
|
||||
const shareButton = appendText(right, 'button', '⇅', 'anomalous-recipe-topbar-btn anomalous-tooltip-target');
|
||||
shareButton.type = 'button';
|
||||
shareButton.setAttribute('data-tooltip', t('toolWorkflowTransferTitle'));
|
||||
shareButton.setAttribute('aria-label', t('toolWorkflowTransferTitle'));
|
||||
shareButton.onclick = () => openShareDialog(owner);
|
||||
const share = appendText(bar, 'button', '⇅', 'anomalous-wf-icon-btn');
|
||||
share.type = 'button';
|
||||
share.title = t('toolWorkflowTransferTitle');
|
||||
share.setAttribute('aria-label', share.title);
|
||||
share.onclick = () => openShareDialog(owner);
|
||||
|
||||
const saveButton = appendText(right, 'button', '', 'anomalous-recipe-topbar-btn is-primary');
|
||||
saveButton.dataset.recipeSaveCurrent = 'true';
|
||||
saveButton.type = 'button';
|
||||
saveButton.innerHTML = `<svg style="width:14px;height:14px;margin-right:6px;vertical-align:-2px;" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M19 21H5a2 2 0 01-2-2V5a2 2 0 012-2h11l5 5v11a2 2 0 01-2 2z"/><polyline points="17 21 17 13 7 13 7 21"/><polyline points="7 3 7 8 15 8"/></svg>${t('recipeSaveCurrent')}`;
|
||||
saveButton.onclick = () => owner.handleSaveRecipe();
|
||||
topbar.appendChild(right);
|
||||
return topbar;
|
||||
const save = appendText(bar, 'button', `+ ${t('recipeSaveCurrent')}`, 'anomalous-wf-save');
|
||||
save.type = 'button';
|
||||
save.dataset.recipeSaveCurrent = 'true';
|
||||
save.onclick = () => owner.handleSaveRecipe();
|
||||
updateRecipeFilterControls(owner);
|
||||
return bar;
|
||||
}
|
||||
|
||||
function normaliseSearchText(value) {
|
||||
const text = String(value || '').trim().toLocaleLowerCase();
|
||||
try { return text.normalize('NFKC'); } catch (error) { return text; }
|
||||
@@ -200,6 +178,7 @@ function renderRecipeEmptyGuide(owner) {
|
||||
|
||||
export function renderRecipeList(recipes, services) {
|
||||
this.recipeListContainer.replaceChildren();
|
||||
updateRecipeFilterControls(this);
|
||||
const records = Array.isArray(recipes) ? recipes : [];
|
||||
const selectedTags = this.recipeSelectedTags || new Set();
|
||||
const scopeFilter = this.recipeScopeFilter || 'all';
|
||||
@@ -215,14 +194,7 @@ export function renderRecipeList(recipes, services) {
|
||||
});
|
||||
|
||||
if (this.recipeFilterSummary) this.recipeFilterSummary.textContent = `${filtered.length}/${records.length}`;
|
||||
if (this.recipeHintStrip) {
|
||||
const isDismissed = (() => {
|
||||
try { return localStorage.getItem('amb_hide_recipe_drag_hint') === 'true'; } catch (_) { return false; }
|
||||
})();
|
||||
this.recipeHintStrip.style.display = (records.length > 0 && !isDismissed) ? 'flex' : 'none';
|
||||
}
|
||||
if (!records.length) {
|
||||
ensureRecipeGuideStyles();
|
||||
if (this.recipeSearchQuery || (this.recipeSelectedTags && this.recipeSelectedTags.size > 0) || (this.recipeScopeFilter && this.recipeScopeFilter !== 'all')) {
|
||||
appendText(this.recipeListContainer, 'p', t('recipeNoMatches'), 'anomalous-recipe-empty');
|
||||
} else {
|
||||
@@ -256,13 +228,6 @@ export async function showRecipes() {
|
||||
if (!this.recipeContainer) {
|
||||
this.recipeContainer = document.createElement('div');
|
||||
this.recipeContainer.className = 'anomalous-nb-container anomalous-recipe-container';
|
||||
const modalClose = document.createElement('button');
|
||||
modalClose.type = 'button';
|
||||
modalClose.className = 'anomalous-recipe-modal-close';
|
||||
modalClose.innerHTML = '×';
|
||||
modalClose.title = t('close');
|
||||
modalClose.onclick = () => this.closeWorkspace();
|
||||
this.recipeContainer.appendChild(modalClose);
|
||||
this.nbPanel.appendChild(this.recipeContainer);
|
||||
}
|
||||
this.recipeContainer.style.display = 'flex';
|
||||
@@ -279,21 +244,15 @@ export async function showRecipes() {
|
||||
this.recipeDetailView.remove();
|
||||
this.recipeDetailView = null;
|
||||
if (this.recipeListContainer) this.recipeListContainer.style.display = '';
|
||||
const topbar = this.recipeView?.querySelector('.anomalous-recipe-topbar');
|
||||
if (topbar) topbar.style.display = '';
|
||||
const bar = this.recipeView?.querySelector('.anomalous-wf-bar');
|
||||
if (bar) bar.style.display = '';
|
||||
}
|
||||
if (this.recipeView) {
|
||||
this.recipeView.style.display = 'flex';
|
||||
if (!this.recipeDetailView) {
|
||||
if (this.recipeListContainer) this.recipeListContainer.style.display = '';
|
||||
const topbar = this.recipeView?.querySelector('.anomalous-recipe-topbar');
|
||||
if (topbar) topbar.style.display = '';
|
||||
const isDismissed = (() => {
|
||||
try { return localStorage.getItem('amb_hide_recipe_drag_hint') === 'true'; } catch (_) { return false; }
|
||||
})();
|
||||
if (this.recipeHintStrip && (this.recipeRecords || []).length > 0 && !isDismissed) {
|
||||
this.recipeHintStrip.style.display = 'flex';
|
||||
}
|
||||
const bar = this.recipeView?.querySelector('.anomalous-wf-bar');
|
||||
if (bar) bar.style.display = '';
|
||||
this.recipeReturnState = null;
|
||||
delete this.recipeDetailPayload;
|
||||
}
|
||||
@@ -302,7 +261,6 @@ export async function showRecipes() {
|
||||
await this.refreshRecipes();
|
||||
return;
|
||||
}
|
||||
ensureRecipeGuideStyles();
|
||||
this.recipesInitialized = true;
|
||||
this.recipeSelectedTags = this.recipeSelectedTags || new Set();
|
||||
this.recipeSearchQuery = this.recipeSearchQuery || '';
|
||||
@@ -310,34 +268,11 @@ export async function showRecipes() {
|
||||
this.recipeViewMode = this.recipeViewMode || 'grid';
|
||||
this.recipeView = document.createElement('div');
|
||||
this.recipeView.className = 'anomalous-recipe-body';
|
||||
this.recipeView.appendChild(buildRecipeStudioTopbar(this));
|
||||
|
||||
this.recipeHintStrip = document.createElement('div');
|
||||
this.recipeHintStrip.className = 'anomalous-recipe-actionbar anomalous-recipe-drag-hint-strip';
|
||||
this.recipeHintStrip.style.display = 'none';
|
||||
|
||||
const hintContent = document.createElement('div');
|
||||
hintContent.className = 'anomalous-recipe-drag-hint-content';
|
||||
hintContent.innerHTML = `<span class="anomalous-recipe-drag-hint-icon">💡</span><span class="anomalous-recipe-drag-hint-text">${t('recipeDragGlobalHint') || '提示:按住卡片直接拖拽到 ComfyUI 画布空白处释放,即可立即载入工作流'}</span>`;
|
||||
this.recipeHintStrip.appendChild(hintContent);
|
||||
|
||||
const hintClose = document.createElement('button');
|
||||
hintClose.type = 'button';
|
||||
hintClose.className = 'anomalous-recipe-drag-hint-close';
|
||||
hintClose.innerHTML = '×';
|
||||
hintClose.title = t('close') || (window.anomalous_browser_lang === 'zh' ? '关闭提示' : 'Dismiss tip');
|
||||
hintClose.onclick = (e) => {
|
||||
e.stopPropagation();
|
||||
this.recipeHintStrip.style.display = 'none';
|
||||
try { localStorage.setItem('amb_hide_recipe_drag_hint', 'true'); } catch (_) {}
|
||||
};
|
||||
this.recipeHintStrip.appendChild(hintClose);
|
||||
|
||||
this.recipeView.appendChild(this.recipeHintStrip);
|
||||
this.recipeView.appendChild(buildWorkflowBar(this));
|
||||
this.recipeView.appendChild(buildMoveStrip(this));
|
||||
|
||||
this.recipeListContainer = document.createElement('div');
|
||||
this.recipeListContainer.className = `anomalous-recipe-list ${this.recipeViewMode === 'list' ? 'is-list' : 'is-grid'}`;
|
||||
this.recipeListContainer.className = `anomalous-wf-list ${this.recipeViewMode === 'list' ? 'is-list' : 'is-grid'}`;
|
||||
this.recipeView.appendChild(this.recipeListContainer);
|
||||
this.recipeContainer.appendChild(this.recipeView);
|
||||
await this.refreshRecipes();
|
||||
@@ -408,7 +343,7 @@ export async function refreshRecipes() {
|
||||
if (!response.ok) throw new Error('recipe list request failed');
|
||||
const payload = await response.json();
|
||||
this.recipeRecords = payload.recipes || [];
|
||||
updateRecipeFilterControls(this, this.recipeRecords);
|
||||
updateRecipeFilterControls(this);
|
||||
} catch (error) {
|
||||
console.error('Could not load Workflow Recipes from server:', error);
|
||||
this.recipeListContainer.replaceChildren();
|
||||
|
||||
@@ -95,7 +95,7 @@ export function showRecipeDetail(owner, { recipe, filename, history = [] }) {
|
||||
owner.recipeDetailPayload = { recipe, filename, history };
|
||||
owner.recipeDetailFilename = filename;
|
||||
owner.recipeListContainer.style.display = 'none';
|
||||
const topbars = owner.recipeView ? Array.from(owner.recipeView.querySelectorAll('.anomalous-recipe-topbar, .anomalous-recipe-actionbar')) : [];
|
||||
const topbars = owner.recipeView ? Array.from(owner.recipeView.querySelectorAll('.anomalous-wf-bar')) : [];
|
||||
topbars.forEach(bar => { bar.style.display = 'none'; });
|
||||
if (owner.recipeDetailView) owner.recipeDetailView.remove();
|
||||
|
||||
|
||||
+6
-5
@@ -1,12 +1,12 @@
|
||||
@import url("./styles/00-foundation-models.css?v=20261007-theme-2");
|
||||
@import url("./styles/00-foundation-models.css?v=20261010-wf-1");
|
||||
@import url("./styles/01-shell-tools.css?v=20261007-theme-2");
|
||||
@import url("./styles/02-notebooks.css?v=20261009-click-1");
|
||||
@import url("./styles/03-gallery-recipes.css?v=20261009-click-1");
|
||||
@import url("./styles/04-dialogs-materials.css?v=20261007-theme-2");
|
||||
@import url("./styles/03-gallery-recipes.css?v=20261010-wf-1");
|
||||
@import url("./styles/04-dialogs-materials.css?v=20261010-wf-1");
|
||||
@import url("./styles/05-recipe-studio-overrides.css?v=20261007-theme-2");
|
||||
@import url("./styles/06-material-workbench.css?v=20261007-theme-2");
|
||||
@import url("./styles/07-prompt-studio.css?v=20261007-theme-2");
|
||||
@import url("./styles/08-theme-gallery-overrides.css?v=20261007-theme-2");
|
||||
@import url("./styles/08-theme-gallery-overrides.css?v=20261010-wf-1");
|
||||
@import url("./styles/09-prompt-drawers.css?v=20261007-theme-2");
|
||||
@import url("./styles/10-model-sources.css?v=20261007-theme-2");
|
||||
@import url("./styles/11-audio-studio.css?v=20261010-search-1");
|
||||
@@ -25,4 +25,5 @@
|
||||
@import url("./styles/24-backup.css?v=20261007-theme-2");
|
||||
@import url("./styles/25-model-import.css?v=20261008-import");
|
||||
@import url("./styles/26-model-tidy.css?v=20261008-tidy");
|
||||
@import url("./styles/27-param-compare.css?v=20261010-compare-7");
|
||||
@import url("./styles/27-param-compare.css?v=20261010-compare-8");
|
||||
@import url("./styles/28-workflows.css?v=20261010-wf-2");
|
||||
|
||||
@@ -138,8 +138,7 @@ html.amb-theme-light {
|
||||
html.amb-theme-light :is(.anomalous-workbench-overlay, .anomalous-gallery-viewer,
|
||||
.anomalous-card-labels, .anomalous-card-action-btn, .anomalous-card-badge.is-unscanned,
|
||||
.anomalous-gallery-details, .anomalous-gallery-keep, .anomalous-gallery-compare,
|
||||
.anomalous-recipe-readiness-pill, .anomalous-recipe-cover-pill, .anomalous-recipe-scope-pill,
|
||||
.anomalous-recipe-drag-badge, .anomalous-recipe-model-preview small, .anomalous-preset-compact-gallery-badge) {
|
||||
.anomalous-recipe-scope-pill, .anomalous-recipe-model-preview small, .anomalous-preset-compact-gallery-badge) {
|
||||
--amb-bg-page: #131315;
|
||||
--amb-bg-panel: #1a1a1d;
|
||||
--amb-bg-card: #202024;
|
||||
|
||||
@@ -380,210 +380,6 @@ dialog.anomalous-gallery-viewer[open] {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* 一体化 Studio 沉浸顶栏 (无厚重底色,细微下划线,支持弹性折行) */
|
||||
.anomalous-recipe-topbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
min-height: 48px;
|
||||
height: auto;
|
||||
padding: 8px 52px 8px 16px;
|
||||
background: var(--amb-lt-panel, rgba(18, 22, 32, 0.65));
|
||||
backdrop-filter: blur(10px);
|
||||
-webkit-backdrop-filter: blur(10px);
|
||||
border-bottom: 1px solid var(--amb-border);
|
||||
flex-shrink: 0;
|
||||
flex-wrap: wrap;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.anomalous-recipe-topbar-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.anomalous-recipe-pills {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
background: rgba(var(--amb-ink-rgb), 0.02);
|
||||
padding: 3px 4px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--amb-border);
|
||||
}
|
||||
|
||||
.anomalous-recipe-pill {
|
||||
padding: 3px 11px;
|
||||
border-radius: 14px;
|
||||
font-size: 12px;
|
||||
color: var(--amb-text-muted);
|
||||
background: transparent;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.anomalous-recipe-pill:hover {
|
||||
color: var(--amb-text-main);
|
||||
background: rgba(var(--amb-ink-rgb), 0.05);
|
||||
}
|
||||
|
||||
.anomalous-recipe-pill.is-active {
|
||||
color: var(--amb-text-main);
|
||||
text-shadow: none;
|
||||
font-weight: 600;
|
||||
box-shadow: none;
|
||||
border: 1px solid color-mix(in srgb, var(--amb-link) 35%, transparent);
|
||||
background: var(--amb-bg-card-hover);
|
||||
border-color: var(--amb-border-hover);
|
||||
}
|
||||
|
||||
.anomalous-recipe-topbar-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex: 1 1 260px;
|
||||
max-width: 460px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.anomalous-recipe-search-wrap {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.anomalous-recipe-search-icon {
|
||||
position: absolute;
|
||||
left: 10px;
|
||||
pointer-events: none;
|
||||
color: var(--amb-text-dim);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-search-input {
|
||||
width: 100%;
|
||||
height: 30px;
|
||||
padding: 0 12px 0 30px;
|
||||
background: var(--amb-lt-sunken, rgba(0, 0, 0, 0.28));
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: 16px;
|
||||
color: var(--amb-text-main);
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
transition: border-color 0.15s ease, background 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-recipe-search-input:focus {
|
||||
border-color: color-mix(in srgb, var(--amb-link) 45%, transparent);
|
||||
background: var(--amb-lt-sunken, rgba(0, 0, 0, 0.4));
|
||||
}
|
||||
|
||||
.anomalous-recipe-tag-select {
|
||||
height: 30px;
|
||||
padding: 0 10px;
|
||||
background: var(--amb-lt-sunken, rgba(0, 0, 0, 0.28));
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: 16px;
|
||||
color: var(--amb-text-muted);
|
||||
font-size: 12px;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
max-width: 130px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-tag-select:hover {
|
||||
border-color: var(--amb-border-strong);
|
||||
}
|
||||
|
||||
.anomalous-recipe-topbar-right {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-wrap: wrap;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.anomalous-recipe-view-switch {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
background: var(--amb-lt-sunken, rgba(0, 0, 0, 0.32));
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: 8px;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-view-btn {
|
||||
width: 26px;
|
||||
height: 26px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
background: transparent;
|
||||
color: var(--amb-text-dim);
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-recipe-view-btn:hover {
|
||||
color: var(--amb-text-soft);
|
||||
}
|
||||
|
||||
.anomalous-recipe-view-btn.is-active {
|
||||
background: rgba(var(--amb-ink-rgb), 0.09);
|
||||
color: var(--amb-link);
|
||||
}
|
||||
|
||||
.anomalous-recipe-topbar-btn {
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
border-radius: 8px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
transition: all 0.15s ease;
|
||||
border: 1px solid var(--amb-border);
|
||||
background: rgba(var(--amb-ink-rgb), 0.03);
|
||||
color: var(--amb-text-soft);
|
||||
}
|
||||
|
||||
.anomalous-recipe-topbar-btn:hover {
|
||||
background: rgba(var(--amb-ink-rgb), 0.08);
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-recipe-topbar-btn.is-primary {
|
||||
background: var(--amb-btn-primary-bg);
|
||||
border-color: var(--amb-border-hover);
|
||||
color: var(--amb-btn-primary-text);
|
||||
}
|
||||
|
||||
.anomalous-recipe-topbar-btn.is-primary:hover {
|
||||
background: var(--amb-btn-primary-hover);
|
||||
border-color: var(--amb-border-hover);
|
||||
color: var(--amb-btn-primary-text);
|
||||
}
|
||||
|
||||
/* 列表容器 */
|
||||
.anomalous-recipe-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.anomalous-recipe-node-details {
|
||||
margin-top: 2px;
|
||||
border-top: 1px solid var(--amb-border-strong);
|
||||
@@ -1712,22 +1508,6 @@ dialog.anomalous-gallery-viewer[open] {
|
||||
}
|
||||
|
||||
/* Recipe library discovery and semantic version diff. */
|
||||
.anomalous-recipe-actionbar {
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-search {
|
||||
min-width: 220px;
|
||||
flex: 1 1 240px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-filter-summary {
|
||||
color: var(--amb-lt-muted, #9caec4);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.anomalous-recipe-badge-tag {
|
||||
border: 0;
|
||||
cursor: pointer;
|
||||
|
||||
@@ -118,125 +118,8 @@ dialog.anomalous-gallery-viewer::backdrop {
|
||||
background: rgba(0, 0, 0, 0.95);
|
||||
}
|
||||
|
||||
/* ========================================================================== */
|
||||
/* 🎨 Workflow Recipes Modern UI/UX Redesign Overrides */
|
||||
/* ========================================================================== */
|
||||
|
||||
.anomalous-recipe-list {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 360px));
|
||||
gap: 18px;
|
||||
overflow-y: auto;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-list[style*="display: none"],
|
||||
.anomalous-recipe-actionbar[style*="display: none"] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
/* ==========================================================================
|
||||
Recipe Cards: Uniform 264px Bento Grid & 44px Compact List System
|
||||
========================================================================== */
|
||||
|
||||
/* 1. 网格模式下的卡片几何规范 (Bento 架构 + 发光电路回路隐喻) */
|
||||
.anomalous-recipe-list.is-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
|
||||
grid-auto-rows: 264px;
|
||||
align-content: start;
|
||||
gap: 14px;
|
||||
padding: 16px;
|
||||
background-image:
|
||||
radial-gradient(circle at 50% 50%, rgba(99, 102, 241, 0.06) 1px, transparent 1px),
|
||||
linear-gradient(rgba(var(--amb-ink-rgb), 0.015) 1px, transparent 1px),
|
||||
linear-gradient(90deg, rgba(var(--amb-ink-rgb), 0.015) 1px, transparent 1px);
|
||||
background-size: 24px 24px, 48px 48px, 48px 48px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 12px 16px;
|
||||
}
|
||||
|
||||
/* 卡片基础骨架 */
|
||||
.anomalous-recipe-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--amb-border, var(--amb-border));
|
||||
background: var(--amb-bg-card, var(--amb-bg-card-hover));
|
||||
border-radius: var(--amb-radius-card, 10px);
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
||||
cursor: pointer;
|
||||
transition: border-color 0.15s ease, background-color 0.15s ease, box-shadow 0.15s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.anomalous-recipe-card:hover {
|
||||
border-color: var(--amb-border-hover, var(--amb-border-strong));
|
||||
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
|
||||
.anomalous-recipe-card:focus-visible {
|
||||
outline: 2px solid var(--amb-accent-gold, var(--amb-link));
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
/* 网格模式卡片 */
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card {
|
||||
height: 264px;
|
||||
max-height: 264px;
|
||||
flex-direction: column;
|
||||
border-radius: var(--amb-radius-card, 10px);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* Bento Wide 横卡支持 (Featured / Pinned Recipes) */
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card.is-featured,
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card.is-wide {
|
||||
grid-column: span 2;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card.is-featured .anomalous-recipe-card-media-wrap,
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card.is-wide .anomalous-recipe-card-media-wrap {
|
||||
width: 44%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card:hover {
|
||||
transform: translateY(-3px);
|
||||
}
|
||||
|
||||
/* 顶部封面展示区 (标准黄金高度 136px) */
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card-media-wrap {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 136px;
|
||||
flex-shrink: 0;
|
||||
background: var(--amb-bg-page);
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card-media-wrap img,
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card-media-wrap video {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
transition: transform 0.25s ease;
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card:hover .anomalous-recipe-card-media-wrap img,
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card:hover .anomalous-recipe-card-media-wrap video {
|
||||
transform: scale(1.03);
|
||||
}
|
||||
/* Workflow Recipes: the cover placeholder and the scope pill the recipe details use
|
||||
(the Workflows page and its cards: 28-workflows.css). */
|
||||
|
||||
/* 默认封面占位图微光与材质渲染 (AI Abstract Art Blind Box) */
|
||||
.anomalous-recipe-thumbnail.is-default-placeholder {
|
||||
@@ -245,71 +128,6 @@ dialog.anomalous-gallery-viewer::backdrop {
|
||||
transition: filter 0.25s ease, transform 0.25s ease, opacity 0.25s ease;
|
||||
}
|
||||
|
||||
.anomalous-recipe-card:hover .anomalous-recipe-thumbnail.is-default-placeholder {
|
||||
filter: brightness(1.08) contrast(1.15);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.anomalous-recipe-card-media-wrap:not(:has(img, video)) {
|
||||
background-image: url('assets/default_cover_1.webp');
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
}
|
||||
|
||||
/* 封面内嵌浮动底模与就绪微胶囊组 (Frosted Glass Chips at Bottom-Left) */
|
||||
.anomalous-recipe-cover-bottom-chips {
|
||||
position: absolute;
|
||||
bottom: 8px;
|
||||
left: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
max-width: calc(100% - 16px);
|
||||
pointer-events: none;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.anomalous-recipe-readiness-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 2px 7px;
|
||||
border-radius: 12px;
|
||||
background: rgba(12, 14, 20, 0.78);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--amb-border-strong);
|
||||
font-size: 0.65rem;
|
||||
font-weight: 500;
|
||||
color: var(--amb-text-main);
|
||||
white-space: nowrap;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.anomalous-recipe-readiness-text {
|
||||
font-size: 0.64rem;
|
||||
line-height: 1;
|
||||
color: var(--amb-text-soft);
|
||||
}
|
||||
|
||||
.anomalous-recipe-cover-pill {
|
||||
position: relative;
|
||||
padding: 2px 7px;
|
||||
border-radius: 6px;
|
||||
background: rgba(12, 14, 18, 0.82);
|
||||
backdrop-filter: blur(8px);
|
||||
-webkit-backdrop-filter: blur(8px);
|
||||
border: 1px solid var(--amb-border-strong);
|
||||
color: var(--amb-text-main);
|
||||
font-size: 0.66rem;
|
||||
font-weight: 500;
|
||||
max-width: 140px;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
/* 封面右上角作用域徽章 */
|
||||
.anomalous-recipe-scope-pill {
|
||||
position: absolute;
|
||||
@@ -336,98 +154,6 @@ dialog.anomalous-gallery-viewer::backdrop {
|
||||
color: var(--amb-link);
|
||||
}
|
||||
|
||||
/* 网格卡片信息区 */
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card-body {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
padding: 9px 12px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card-title {
|
||||
flex: 1;
|
||||
margin: 0;
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 2;
|
||||
overflow: hidden;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: 13px;
|
||||
line-height: 17px;
|
||||
color: var(--amb-text-main, var(--amb-text-main));
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 环境就绪指示灯微点 */
|
||||
.anomalous-recipe-readiness-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
flex-shrink: 0;
|
||||
box-shadow: 0 0 6px currentColor;
|
||||
}
|
||||
|
||||
.anomalous-recipe-readiness-dot.is-ready {
|
||||
background-color: #10b981;
|
||||
color: rgba(16, 185, 129, 0.7);
|
||||
}
|
||||
|
||||
.anomalous-recipe-readiness-dot.is-warning {
|
||||
background-color: #f59e0b;
|
||||
color: rgba(245, 158, 11, 0.7);
|
||||
}
|
||||
|
||||
.anomalous-recipe-readiness-dot.is-missing {
|
||||
background-color: #ef4444;
|
||||
color: rgba(239, 68, 68, 0.7);
|
||||
}
|
||||
|
||||
/* 规格微胶囊行 */
|
||||
.anomalous-recipe-specs-strip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-spec-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
padding: 1px 5px;
|
||||
border-radius: 4px;
|
||||
background: rgba(var(--amb-ink-rgb), 0.04);
|
||||
border: 1px solid var(--amb-border);
|
||||
color: var(--amb-text-muted);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.anomalous-recipe-spec-tag.is-lora {
|
||||
color: var(--amb-link);
|
||||
background: color-mix(in srgb, var(--amb-link) 8%, transparent);
|
||||
border-color: color-mix(in srgb, var(--amb-link) 20%, transparent);
|
||||
}
|
||||
|
||||
.anomalous-recipe-spec-tag.is-step {
|
||||
color: var(--amb-ok-text);
|
||||
background: rgba(16, 185, 129, 0.08);
|
||||
border-color: rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
/* 配方单行文本标签 (收拢至单行 Flex,溢出截断) */
|
||||
.anomalous-recipe-tags {
|
||||
display: flex;
|
||||
@@ -461,17 +187,6 @@ dialog.anomalous-gallery-viewer::backdrop {
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
/* 网格卡片底部主动作与悬浮微操作 */
|
||||
.anomalous-recipe-list.is-grid .anomalous-recipe-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 6px;
|
||||
margin-top: 2px;
|
||||
padding-top: 6px;
|
||||
border-top: 1px solid var(--amb-border);
|
||||
}
|
||||
|
||||
.anomalous-recipe-btn-primary-action {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
@@ -508,120 +223,6 @@ dialog.anomalous-gallery-viewer::backdrop {
|
||||
filter: brightness(0.95);
|
||||
}
|
||||
|
||||
.anomalous-recipe-card-mini-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 3px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-recipe-card:hover .anomalous-recipe-card-mini-actions {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.anomalous-recipe-card-mini-btn {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
padding: 0;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 5px;
|
||||
background: rgba(var(--amb-ink-rgb), 0.05);
|
||||
border: 1px solid var(--amb-border);
|
||||
color: var(--amb-text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-recipe-card-mini-btn:hover {
|
||||
background: rgba(var(--amb-ink-rgb), 0.12);
|
||||
color: var(--amb-text-main);
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
.anomalous-recipe-card-mini-btn.is-delete:hover {
|
||||
background: rgba(239, 68, 68, 0.3);
|
||||
border-color: rgba(239, 68, 68, 0.5);
|
||||
color: var(--amb-danger-text);
|
||||
}
|
||||
|
||||
/* 2. 紧凑行列表模式 (44px 极速视图) */
|
||||
.anomalous-recipe-list.is-list .anomalous-recipe-card {
|
||||
height: 44px;
|
||||
min-height: 44px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
gap: 12px;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-list .anomalous-recipe-card:hover {
|
||||
transform: translateX(2px);
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-list .anomalous-recipe-card-media-wrap {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
border-radius: 6px;
|
||||
flex-shrink: 0;
|
||||
overflow: hidden;
|
||||
background: var(--amb-bg-page);
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-list .anomalous-recipe-cover-pill,
|
||||
.anomalous-recipe-list.is-list .anomalous-recipe-scope-pill,
|
||||
.anomalous-recipe-list.is-list .anomalous-recipe-specs-strip {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-list .anomalous-recipe-card-body {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-list .anomalous-recipe-card-header {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-list .anomalous-recipe-card-title {
|
||||
margin: 0;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
font-size: 13px;
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-list .anomalous-recipe-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
flex-shrink: 0;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.anomalous-recipe-list.is-list .anomalous-recipe-btn-primary-action {
|
||||
flex: 0 0 auto;
|
||||
height: 26px;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
/* Detail View: Readiness Banner */
|
||||
.anomalous-recipe-readiness-banner {
|
||||
display: flex;
|
||||
@@ -697,34 +298,6 @@ dialog.anomalous-gallery-viewer::backdrop {
|
||||
border-color: var(--amb-border-strong);
|
||||
}
|
||||
|
||||
.anomalous-recipe-readiness-pill.is-ready {
|
||||
background: rgba(16, 185, 129, 0.16);
|
||||
border-color: rgba(16, 185, 129, 0.45);
|
||||
color: var(--amb-ok-soft);
|
||||
}
|
||||
|
||||
.anomalous-recipe-readiness-pill.is-warning {
|
||||
background: rgba(245, 158, 11, 0.16);
|
||||
border-color: rgba(245, 158, 11, 0.45);
|
||||
color: var(--amb-warn-text);
|
||||
}
|
||||
|
||||
.anomalous-recipe-readiness-pill.is-missing {
|
||||
background: rgba(239, 68, 68, 0.16);
|
||||
border-color: rgba(239, 68, 68, 0.45);
|
||||
color: var(--amb-danger-text);
|
||||
}
|
||||
|
||||
.anomalous-recipe-card-footer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-top: auto;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--amb-border);
|
||||
}
|
||||
|
||||
.anomalous-recipe-btn-primary-action {
|
||||
flex: 1;
|
||||
padding: 8px 14px;
|
||||
@@ -745,30 +318,6 @@ dialog.anomalous-gallery-viewer::backdrop {
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-recipe-card-mini-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-card-mini-btn {
|
||||
padding: 6px 8px;
|
||||
border-radius: 6px;
|
||||
background: rgba(var(--amb-ink-rgb), 0.04);
|
||||
border: 1px solid var(--amb-border);
|
||||
color: var(--amb-text-muted);
|
||||
cursor: pointer;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1;
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-recipe-card-mini-btn:hover {
|
||||
background: rgba(var(--amb-ink-rgb), 0.1);
|
||||
color: var(--amb-text-main);
|
||||
border-color: var(--amb-border-strong);
|
||||
}
|
||||
|
||||
/* Metric Cards for Sampling Parameters */
|
||||
|
||||
/* Lazy Loaded Raw Nodes Container */
|
||||
|
||||
@@ -204,8 +204,7 @@ html.theme-abyssal-scarlet,
|
||||
background: linear-gradient(180deg, rgba(25, 12, 20, 0.9) 0%, rgba(14, 6, 14, 0.98) 100%) !important;
|
||||
}
|
||||
|
||||
.theme-abyssal-scarlet .anomalous-card:hover,
|
||||
.theme-abyssal-scarlet .anomalous-recipe-card:hover {
|
||||
.theme-abyssal-scarlet .anomalous-card:hover {
|
||||
border-color: #dc143c !important;
|
||||
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.95), inset 0 0 12px rgba(0, 0, 0, 0.7) !important;
|
||||
}
|
||||
@@ -274,24 +273,6 @@ html.theme-abyssal-scarlet,
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.theme-abyssal-scarlet .anomalous-recipe-card {
|
||||
border-color: rgba(220, 20, 60, 0.22) !important;
|
||||
background: rgba(18, 10, 26, 0.88) !important;
|
||||
}
|
||||
|
||||
/* 6. Active Pills in Deep Velvet Wine (Inner Shadow & Sharp Border) */
|
||||
.theme-abyssal-scarlet .anomalous-recipe-pill.is-active {
|
||||
background-color: rgba(60, 10, 20, 0.85) !important;
|
||||
border-color: #dc143c !important;
|
||||
color: #e0d8db !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.theme-abyssal-scarlet .anomalous-recipe-readiness-pill {
|
||||
border-color: rgba(220, 20, 60, 0.35) !important;
|
||||
color: #e0d8db !important;
|
||||
}
|
||||
|
||||
/* 7. Error State Abyssal Corruption Animations */
|
||||
@keyframes abyssal-corruption {
|
||||
0% {
|
||||
@@ -857,29 +838,3 @@ button:hover .anomalous-btn-icon {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.anomalous-recipe-modal-close {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 14px;
|
||||
z-index: 100;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: transparent;
|
||||
border: none;
|
||||
color: var(--amb-lt-danger, #ff4444);
|
||||
font-size: 1.8rem;
|
||||
line-height: 1;
|
||||
cursor: pointer;
|
||||
border-radius: 6px;
|
||||
transition: all 0.18s ease;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.anomalous-recipe-modal-close:hover {
|
||||
background: rgba(239, 68, 68, 0.16);
|
||||
color: #ff6666;
|
||||
transform: scale(1.08);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,448 @@
|
||||
/* The Workflows page (ui_recipe_catalog.js, ui_recipe_cards.js): one bar, then cards as a grid
|
||||
or a list; the small menu of ui_popup_menu.js; the empty page's guide. */
|
||||
|
||||
.anomalous-wf-bar {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 12px 16px;
|
||||
border-bottom: 1px solid var(--amb-border);
|
||||
}
|
||||
|
||||
.anomalous-wf-search {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
height: 32px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid var(--amb-border-strong);
|
||||
border-radius: var(--amb-radius-control);
|
||||
background: var(--amb-bg-input);
|
||||
color: var(--amb-text-dim);
|
||||
}
|
||||
|
||||
.anomalous-wf-search:focus-within { border-color: var(--amb-link); }
|
||||
|
||||
.anomalous-wf-search input {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
background: transparent;
|
||||
color: var(--amb-text-main);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.anomalous-wf-filter,
|
||||
.anomalous-wf-icon-btn,
|
||||
.anomalous-wf-save {
|
||||
flex: 0 0 auto;
|
||||
height: 32px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--amb-border-strong);
|
||||
border-radius: var(--amb-radius-control);
|
||||
background: transparent;
|
||||
color: var(--amb-text-soft);
|
||||
font-size: 13px;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.anomalous-wf-icon-btn {
|
||||
width: 32px;
|
||||
padding: 0;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
.anomalous-wf-filter:hover,
|
||||
.anomalous-wf-icon-btn:hover {
|
||||
border-color: var(--amb-border-hover);
|
||||
background: var(--amb-bg-card-hover);
|
||||
color: var(--amb-text-main);
|
||||
}
|
||||
|
||||
.anomalous-wf-filter.is-active {
|
||||
border-color: var(--amb-link);
|
||||
color: var(--amb-link);
|
||||
}
|
||||
|
||||
.anomalous-wf-count {
|
||||
flex: 0 0 auto;
|
||||
color: var(--amb-text-dim);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.anomalous-wf-save {
|
||||
border-color: transparent;
|
||||
background: var(--amb-btn-primary-bg);
|
||||
color: var(--amb-btn-primary-text);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.anomalous-wf-save:hover { background: var(--amb-btn-primary-hover); }
|
||||
|
||||
.anomalous-wf-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.anomalous-wf-list.is-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(220px, 1fr));
|
||||
align-content: start;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.anomalous-wf-list.is-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.anomalous-wf-card {
|
||||
--wf-cover-height: 150px;
|
||||
position: relative;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: var(--amb-radius-panel);
|
||||
background: var(--amb-bg-card);
|
||||
cursor: grab;
|
||||
transition: border-color 0.15s ease, background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-wf-card:active { cursor: grabbing; }
|
||||
.anomalous-wf-card button { cursor: pointer; }
|
||||
|
||||
.anomalous-wf-card:hover,
|
||||
.anomalous-wf-card:focus-within {
|
||||
border-color: var(--amb-border-hover);
|
||||
background: var(--amb-bg-card-hover);
|
||||
}
|
||||
|
||||
.anomalous-wf-cover {
|
||||
position: relative;
|
||||
flex: 0 0 var(--wf-cover-height);
|
||||
height: var(--wf-cover-height);
|
||||
overflow: hidden;
|
||||
background: var(--amb-bg-sunken);
|
||||
}
|
||||
|
||||
.anomalous-wf-cover img,
|
||||
.anomalous-wf-cover video {
|
||||
display: block;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.anomalous-wf-status {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
right: 8px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 2px rgba(10, 12, 18, 0.7);
|
||||
background: var(--amb-ok);
|
||||
}
|
||||
|
||||
.anomalous-wf-status.is-warning { background: var(--amb-text-muted); }
|
||||
|
||||
.anomalous-wf-status.is-missing {
|
||||
width: auto;
|
||||
height: auto;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
box-shadow: none;
|
||||
background: rgba(127, 29, 29, 0.9);
|
||||
color: #fecaca;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.anomalous-wf-kind {
|
||||
position: absolute;
|
||||
top: 8px;
|
||||
left: 8px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 999px;
|
||||
background: rgba(10, 12, 18, 0.78);
|
||||
color: #fde68a;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.anomalous-wf-actions {
|
||||
position: absolute;
|
||||
top: calc(var(--wf-cover-height) - 42px);
|
||||
right: 8px;
|
||||
left: 8px;
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
opacity: 0;
|
||||
transform: translateY(4px);
|
||||
pointer-events: none;
|
||||
transition: opacity 0.15s ease, transform 0.15s ease;
|
||||
}
|
||||
|
||||
.anomalous-wf-card:hover .anomalous-wf-actions,
|
||||
.anomalous-wf-card:focus-within .anomalous-wf-actions {
|
||||
opacity: 1;
|
||||
transform: none;
|
||||
pointer-events: auto;
|
||||
}
|
||||
|
||||
.anomalous-wf-open,
|
||||
.anomalous-wf-more {
|
||||
height: 32px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.16);
|
||||
border-radius: var(--amb-radius-control);
|
||||
background: rgba(15, 20, 30, 0.88);
|
||||
color: #f3f4f6;
|
||||
font-size: 12.5px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.anomalous-wf-open { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.anomalous-wf-more { flex: 0 0 34px; font-size: 16px; }
|
||||
|
||||
.anomalous-wf-open:hover,
|
||||
.anomalous-wf-more:hover { background: rgba(30, 38, 56, 0.96); }
|
||||
|
||||
.anomalous-wf-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
min-width: 0;
|
||||
padding: 10px 12px 10px;
|
||||
}
|
||||
|
||||
.anomalous-wf-title {
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: var(--amb-text-main);
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.anomalous-wf-meta {
|
||||
overflow: hidden;
|
||||
color: var(--amb-text-muted);
|
||||
font-size: 12px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.anomalous-wf-meta:empty { display: none; }
|
||||
|
||||
.anomalous-wf-extras {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
min-height: 20px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.anomalous-wf-model {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
color: var(--amb-text-dim);
|
||||
font-size: 11.5px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.anomalous-wf-extras .anomalous-recipe-set-chip { margin-left: auto; }
|
||||
|
||||
.anomalous-wf-list.is-list .anomalous-wf-card {
|
||||
--wf-cover-height: 64px;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.anomalous-wf-list.is-list .anomalous-wf-cover { flex: 0 0 96px; width: 96px; }
|
||||
.anomalous-wf-list.is-list .anomalous-wf-kind { display: none; }
|
||||
|
||||
.anomalous-wf-list.is-list .anomalous-wf-info {
|
||||
flex: 1;
|
||||
padding: 6px 220px 6px 14px;
|
||||
}
|
||||
|
||||
.anomalous-wf-list.is-list .anomalous-wf-extras { display: none; }
|
||||
|
||||
.anomalous-wf-list.is-list .anomalous-wf-actions {
|
||||
top: 16px;
|
||||
right: 12px;
|
||||
left: auto;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
/* Abyssal Scarlet: wine cards, crimson on hover. */
|
||||
.theme-abyssal-scarlet .anomalous-wf-card {
|
||||
border-color: rgba(220, 20, 60, 0.22);
|
||||
background: rgba(18, 10, 26, 0.88);
|
||||
}
|
||||
|
||||
.theme-abyssal-scarlet .anomalous-wf-card:hover,
|
||||
.theme-abyssal-scarlet .anomalous-wf-card:focus-within { border-color: #dc143c; }
|
||||
|
||||
.theme-abyssal-scarlet .anomalous-wf-filter.is-active {
|
||||
border-color: #dc143c;
|
||||
color: #e0d8db;
|
||||
}
|
||||
|
||||
/* The small menu (ui_popup_menu.js) over the page and its dialogs. */
|
||||
.anomalous-popup-menu {
|
||||
position: fixed;
|
||||
z-index: 1000003;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 168px;
|
||||
max-height: 60vh;
|
||||
padding: 4px;
|
||||
overflow-y: auto;
|
||||
border: 1px solid var(--amb-border-strong);
|
||||
border-radius: var(--amb-radius-card);
|
||||
background: var(--amb-bg-panel);
|
||||
box-shadow: 0 12px 32px rgba(var(--amb-shadow-rgb), 0.45);
|
||||
}
|
||||
|
||||
.anomalous-popup-menu-heading {
|
||||
padding: 6px 10px 2px;
|
||||
color: var(--amb-text-dim);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.anomalous-popup-menu-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 7px 10px;
|
||||
border: none;
|
||||
border-radius: var(--amb-radius-control);
|
||||
background: transparent;
|
||||
color: var(--amb-text-main);
|
||||
font-size: 13px;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.anomalous-popup-menu-item:hover { background: var(--amb-bg-card-hover); }
|
||||
.anomalous-popup-menu-item.is-danger { color: var(--amb-danger-text); }
|
||||
|
||||
.anomalous-popup-menu-tick {
|
||||
width: 12px;
|
||||
color: var(--amb-link);
|
||||
}
|
||||
|
||||
/* The empty page: what Workflows are for, three steps and Save. */
|
||||
.anomalous-recipe-empty-guide {
|
||||
grid-column: 1 / -1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
max-width: 580px;
|
||||
margin: 32px auto;
|
||||
padding: 28px 24px;
|
||||
box-sizing: border-box;
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: 16px;
|
||||
background: rgba(var(--amb-ink-rgb), 0.025);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.anomalous-recipe-empty-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 52px;
|
||||
height: 52px;
|
||||
border: 1px solid color-mix(in srgb, var(--amb-link) 25%, transparent);
|
||||
border-radius: 50%;
|
||||
background: color-mix(in srgb, var(--amb-link) 10%, transparent);
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-empty-title {
|
||||
margin: 0;
|
||||
color: var(--amb-text-main);
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.anomalous-recipe-empty-subtitle {
|
||||
max-width: 440px;
|
||||
margin: 0;
|
||||
color: var(--amb-text-muted);
|
||||
font-size: 12.5px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.anomalous-recipe-empty-steps {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
margin-top: 8px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.anomalous-recipe-empty-step {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--amb-border);
|
||||
border-radius: 10px;
|
||||
background: rgba(var(--amb-ink-rgb), 0.03);
|
||||
}
|
||||
|
||||
.anomalous-recipe-step-icon { font-size: 20px; line-height: 1; }
|
||||
|
||||
.anomalous-recipe-empty-step strong {
|
||||
color: var(--amb-text-main);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.anomalous-recipe-empty-step p {
|
||||
margin: 0;
|
||||
color: var(--amb-text-muted);
|
||||
font-size: 11.5px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.anomalous-recipe-empty-action-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
margin-top: 6px;
|
||||
padding: 8px 18px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
background: var(--amb-btn-primary-bg);
|
||||
color: var(--amb-btn-primary-text);
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.anomalous-recipe-empty-action-btn:hover { background: var(--amb-btn-primary-hover); }
|
||||
|
||||
@media (max-width: 560px) {
|
||||
.anomalous-wf-bar { flex-wrap: wrap; }
|
||||
.anomalous-wf-search { flex-basis: 100%; }
|
||||
.anomalous-wf-list.is-list .anomalous-wf-info { padding-right: 14px; }
|
||||
.anomalous-wf-list.is-list .anomalous-wf-actions { display: none; }
|
||||
}
|
||||
Reference in New Issue
Block a user