Files
DemonGatanjieu-Anomalous_Mo…/web/modules/ui_shell_rail.js
T
DemonGatanjieuandClaude Opus 5.5 cae7fd2a4a Audio gallery becomes a view of Voices; the rail fits the number keys
With the AI workbench the rail had ten entries for nine number keys, so
the audio gallery had none. It is now the third tab of Voices (Characters,
Voice-over, Audio); 9 reopens Voices on the view used last, and a test
keeps the rail within nine entries.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 20:26:31 +08:00

115 lines
6.2 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' }, // 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 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 = text;
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();
};
// 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);
};
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');
}
},
refreshLanguage,
};
}