diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
index cf01bec..0593492 100644
--- a/ARCHITECTURE.md
+++ b/ARCHITECTURE.md
@@ -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
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 98a5ca8..379cee6 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -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.
diff --git a/docs/architecture/recipes.md b/docs/architecture/recipes.md
index 8768bdc..e560908 100644
--- a/docs/architecture/recipes.md
+++ b/docs/architecture/recipes.md
@@ -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`
diff --git a/web/modules/locales.js b/web/modules/locales.js
index c651bb7..6d08b91 100644
--- a/web/modules/locales.js
+++ b/web/modules/locales.js
@@ -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',
diff --git a/web/modules/ui_browser_navigation.js b/web/modules/ui_browser_navigation.js
index e0c22bc..fc30266 100644
--- a/web/modules/ui_browser_navigation.js
+++ b/web/modules/ui_browser_navigation.js
@@ -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();
diff --git a/web/modules/ui_param_set_keys.js b/web/modules/ui_param_set_keys.js
index b5bc95e..c049a51 100644
--- a/web/modules/ui_param_set_keys.js
+++ b/web/modules/ui_param_set_keys.js
@@ -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);
});
}
diff --git a/web/modules/ui_popup_menu.js b/web/modules/ui_popup_menu.js
new file mode 100644
index 0000000..a534e49
--- /dev/null
+++ b/web/modules/ui_popup_menu.js
@@ -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;
+}
\ No newline at end of file
diff --git a/web/modules/ui_recipe_cards.js b/web/modules/ui_recipe_cards.js
index 5790626..3c6010f 100644
--- a/web/modules/ui_recipe_cards.js
+++ b/web/modules/ui_recipe_cards.js
@@ -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 = `${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 = `${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 = `${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 = '';
- 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
- ? ``
- : ``;
- scopePill.innerHTML = `${scopeSvg}${t(isPartial ? 'recipeScopePill_partial' : 'recipeScopePill_complete')}`;
- mediaWrap.appendChild(scopePill);
-
- // Drag Affordance Badge (Top-left of cover)
- const dragBadge = document.createElement('span');
- dragBadge.className = 'anomalous-recipe-drag-badge';
- dragBadge.innerHTML = `${t('recipeDraggableBadge') || (window.anomalous_browser_lang === 'zh' ? '可拖拽' : 'Draggable')}`;
- 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 = ``;
- appendBtn.innerHTML = `${actionSvg}${t(isPartial ? 'recipeAppendCanvas' : 'recipeOpenCanvas')}`;
- 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 = ``;
- 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 = ``;
- 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 = ``;
- 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;
-}
-
+}
\ No newline at end of file
diff --git a/web/modules/ui_recipe_catalog.js b/web/modules/ui_recipe_catalog.js
index 21e387c..4375d24 100644
--- a/web/modules/ui_recipe_catalog.js
+++ b/web/modules/ui_recipe_catalog.js
@@ -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 = ''; // 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 = '';
- 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 = '';
- 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 = `${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 = `💡${t('recipeDragGlobalHint') || '提示:按住卡片直接拖拽到 ComfyUI 画布空白处释放,即可立即载入工作流'}`;
- 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();
diff --git a/web/modules/ui_recipe_detail.js b/web/modules/ui_recipe_detail.js
index 311e3c5..c976121 100644
--- a/web/modules/ui_recipe_detail.js
+++ b/web/modules/ui_recipe_detail.js
@@ -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();
diff --git a/web/styles.css b/web/styles.css
index 01ae70f..274d499 100644
--- a/web/styles.css
+++ b/web/styles.css
@@ -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");
diff --git a/web/styles/00-foundation-models.css b/web/styles/00-foundation-models.css
index b3a56c8..7cd80af 100644
--- a/web/styles/00-foundation-models.css
+++ b/web/styles/00-foundation-models.css
@@ -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;
diff --git a/web/styles/03-gallery-recipes.css b/web/styles/03-gallery-recipes.css
index 9d60b05..95b2739 100644
--- a/web/styles/03-gallery-recipes.css
+++ b/web/styles/03-gallery-recipes.css
@@ -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;
diff --git a/web/styles/04-dialogs-materials.css b/web/styles/04-dialogs-materials.css
index 13c5fd9..3ddb527 100644
--- a/web/styles/04-dialogs-materials.css
+++ b/web/styles/04-dialogs-materials.css
@@ -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 */
diff --git a/web/styles/08-theme-gallery-overrides.css b/web/styles/08-theme-gallery-overrides.css
index bbfdde6..2197302 100644
--- a/web/styles/08-theme-gallery-overrides.css
+++ b/web/styles/08-theme-gallery-overrides.css
@@ -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);
-}
diff --git a/web/styles/28-workflows.css b/web/styles/28-workflows.css
new file mode 100644
index 0000000..06102b6
--- /dev/null
+++ b/web/styles/28-workflows.css
@@ -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; }
+}
\ No newline at end of file