From 617a7a3bc408df823c320d7fdb11fce3bc1672f4 Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <207458273+DemonGatanjieu@users.noreply.github.com> Date: Sat, 10 Oct 2026 21:50:53 +0800 Subject: [PATCH] Workflows page: one bar, quieter cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- ARCHITECTURE.md | 18 +- CHANGELOG.md | 4 + docs/architecture/recipes.md | 3 +- web/modules/locales.js | 18 +- web/modules/ui_browser_navigation.js | 8 +- web/modules/ui_param_set_keys.js | 10 +- web/modules/ui_popup_menu.js | 79 ++++ web/modules/ui_recipe_cards.js | 547 ++++------------------ web/modules/ui_recipe_catalog.js | 225 ++++----- web/modules/ui_recipe_detail.js | 2 +- web/styles.css | 11 +- web/styles/00-foundation-models.css | 3 +- web/styles/03-gallery-recipes.css | 220 --------- web/styles/04-dialogs-materials.css | 455 +----------------- web/styles/08-theme-gallery-overrides.css | 47 +- web/styles/28-workflows.css | 448 ++++++++++++++++++ 16 files changed, 753 insertions(+), 1345 deletions(-) create mode 100644 web/modules/ui_popup_menu.js create mode 100644 web/styles/28-workflows.css 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