Every page in both languages, all themes and at 480: - Give the window its own text colour: text with none took ComfyUI's, white on the light theme (Tidy headings and file names, the site chip). - Keep a task's link chip in its row, its x in reach, and colour the next-task chip. - Let choices in Settings wrap, the Models search shrink, and a workflow's preset row go to two lines. - Add --amb-on-link for text on the accent: pale in the dark themes, so white was hard to read. - Lists and colons follow the language (listSeparator, wordColon); tests/words_in_language.mjs keeps it so. - Right plurals in English, "AI" on the rail, one line left in Chinese translated. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
127 lines
6.7 KiB
JavaScript
127 lines
6.7 KiB
JavaScript
/**
|
|
* The shell's left icon rail: one entry per page (image pages, then the audio pages),
|
|
* the canvas tools and settings. Entries open pages through `owner.goTo(page)`;
|
|
* ui_rail_tools.js fills `owner.railTools` (scan, doctor, current node) and
|
|
* `owner.railFoot` (settings). Old ids are kept (`#anomalous-models-btn` …) so tours
|
|
* and other modules still find them.
|
|
*/
|
|
|
|
import { translate as t } from './locales.js';
|
|
import { TOOL_ICONS } from './tool_registry.js';
|
|
|
|
const icon = paths => `<svg width="19" height="19" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`;
|
|
|
|
export const PAGE_ICONS = {
|
|
home: icon('<path d="m3 10 9-7 9 7v10a1 1 0 0 1-1 1h-5v-6H9v6H4a1 1 0 0 1-1-1z"/>'),
|
|
activity: icon('<circle cx="12" cy="12" r="9"/><path d="M12 7v5l3 2"/>'),
|
|
ai: icon('<path d="M12 3l1.9 4.6L18.5 9.5l-4.6 1.9L12 16l-1.9-4.6L5.5 9.5l4.6-1.9z"/><path d="M19 15l.8 1.9 1.9.8-1.9.8L19 20.4l-.8-1.9-1.9-.8 1.9-.8z"/><path d="M5 16l.6 1.4L7 18l-1.4.6L5 20l-.6-1.4L3 18l1.4-.6z"/>'),
|
|
models: icon('<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>'),
|
|
gallery: icon('<rect width="18" height="18" x="3" y="3" rx="2"/><circle cx="9" cy="9" r="2"/><path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"/>'),
|
|
recipes: icon('<circle cx="18" cy="18" r="3"/><circle cx="6" cy="6" r="3"/><circle cx="18" cy="6" r="3"/><path d="M18 9v6"/><path d="M9 6h6"/><path d="M7.8 7.8l8.4 8.4"/>'),
|
|
combos: TOOL_ICONS.MATERIALS,
|
|
prompts: icon('<path d="M4 7V4h16v3"/><path d="M9 20h6"/><path d="M12 4v16"/>'),
|
|
voices: icon('<path d="M12 2a3 3 0 0 0-3 3v7a3 3 0 0 0 6 0V5a3 3 0 0 0-3-3Z"/><path d="M19 10v2a7 7 0 0 1-14 0v-2"/><line x1="12" y1="19" x2="12" y2="22"/>'),
|
|
// Not rail entries (Voice-over and Audio are views of Voices); Home's voice-over card uses this one.
|
|
script: icon('<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/><path d="M8 9h8"/><path d="M8 13h5"/>'),
|
|
'audio-gallery': icon('<path d="M9 18V5l12-2v13"/><circle cx="6" cy="18" r="3"/><circle cx="18" cy="16" r="3"/>'),
|
|
};
|
|
|
|
/** Pages on the rail, top to bottom: no more than the nine number keys. `audio`: the page belongs to the audio domain. */
|
|
export const RAIL_PAGES = Object.freeze([
|
|
{ page: 'home', id: 'anomalous-home-btn', labelKey: 'shellHome' },
|
|
{ page: 'activity', id: 'anomalous-activity-btn', labelKey: 'shellActivity' },
|
|
{ page: 'ai', id: 'anomalous-ai-btn', labelKey: 'shellAi', railKey: 'shellAiRail' }, // the AI workbench (ui_ai_workbench.js)
|
|
{ page: 'models', id: 'anomalous-models-btn', labelKey: 'models' },
|
|
{ page: 'gallery', id: 'anomalous-gallery-btn', labelKey: 'gallery' },
|
|
{ page: 'recipes', id: 'anomalous-notebook-btn', labelKey: 'recipeTitle' },
|
|
{ page: 'combos', id: 'anomalous-combos-btn', labelKey: 'shellCombos' },
|
|
{ page: 'prompts', id: 'anomalous-prompts-btn', labelKey: 'shellPrompts' }, // Prompt Studio as a page
|
|
{ page: 'voices', id: 'anomalous-voices-btn', labelKey: 'shellVoices', audio: true }, // with Voice-over and Audio as its views
|
|
]);
|
|
|
|
const div = className => Object.assign(document.createElement('div'), { className });
|
|
|
|
/**
|
|
* Builds the rail; its logo is `.anomalous-rail-brand`.
|
|
* Returns `{ root, items: Map(page → button), setActive(page | null), refreshLanguage() }`.
|
|
*/
|
|
export function createShellRail(owner) {
|
|
const root = document.createElement('nav');
|
|
root.id = 'anomalous-rail';
|
|
root.setAttribute('aria-label', 'Anomalous');
|
|
|
|
const brand = document.createElement('button');
|
|
brand.type = 'button';
|
|
brand.className = 'anomalous-rail-brand';
|
|
brand.textContent = 'A';
|
|
brand.tabIndex = -1;
|
|
brand.setAttribute('aria-hidden', 'true');
|
|
|
|
const nav = div('anomalous-rail-nav');
|
|
const marker = div('anomalous-rail-marker');
|
|
nav.appendChild(marker);
|
|
const items = new Map();
|
|
for (const entry of RAIL_PAGES) {
|
|
if (entry.audio && !nav.querySelector('.anomalous-rail-sep')) nav.appendChild(div('anomalous-rail-sep'));
|
|
const button = document.createElement('button');
|
|
button.type = 'button';
|
|
button.id = entry.id;
|
|
button.className = 'anomalous-rail-item';
|
|
button.dataset.page = entry.page;
|
|
button.innerHTML = PAGE_ICONS[entry.page]; // static markup only
|
|
button.appendChild(Object.assign(document.createElement('span'), { className: 'anomalous-rail-label' }));
|
|
button.onclick = () => owner.goTo(entry.page, { fromRail: true });
|
|
items.set(entry.page, button);
|
|
nav.appendChild(button);
|
|
}
|
|
|
|
owner.railTools = div('anomalous-rail-tools');
|
|
owner.railFoot = div('anomalous-rail-foot');
|
|
root.append(brand, nav, div('anomalous-rail-sep'), owner.railTools, div('anomalous-rail-spacer'), owner.railFoot);
|
|
|
|
// The tooltip names the entry and its number key (1-9 from the top, ui_shell_nav.js).
|
|
const refreshLanguage = () => {
|
|
RAIL_PAGES.forEach((entry, index) => {
|
|
const button = items.get(entry.page);
|
|
const text = t(entry.labelKey);
|
|
button.querySelector('.anomalous-rail-label').textContent = t(entry.railKey || entry.labelKey);
|
|
button.setAttribute('aria-label', text);
|
|
// Only the first nine have a number key.
|
|
button.dataset.tooltip = index < 9 ? t('shellRailKey', { name: text, key: index + 1 }) : text;
|
|
button.dataset.tooltipPos = 'right';
|
|
});
|
|
syncCompact();
|
|
};
|
|
|
|
const moveMarker = () => {
|
|
const current = [...items.values()].find(button => button.classList.contains('active'));
|
|
marker.classList.toggle('is-shown', !!current);
|
|
if (!current) return;
|
|
marker.style.translate = `0 ${current.offsetTop}px`;
|
|
marker.style.height = `${current.offsetHeight}px`;
|
|
};
|
|
|
|
// A short window drops the labels under the icons; the tooltips still name the entries.
|
|
const syncCompact = () => {
|
|
root.classList.remove('is-compact');
|
|
root.classList.toggle('is-compact', root.clientHeight > 0 && root.scrollHeight > root.clientHeight + 1);
|
|
moveMarker();
|
|
};
|
|
if (typeof ResizeObserver === 'function') new ResizeObserver(syncCompact).observe(root);
|
|
refreshLanguage();
|
|
|
|
return {
|
|
root,
|
|
items,
|
|
setActive(page) {
|
|
for (const [key, button] of items) {
|
|
button.classList.toggle('active', key === page);
|
|
if (key === page) button.setAttribute('aria-current', 'page');
|
|
else button.removeAttribute('aria-current');
|
|
}
|
|
moveMarker();
|
|
},
|
|
refreshLanguage,
|
|
};
|
|
}
|