/** * 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 => ``; export const PAGE_ICONS = { home: icon(''), activity: icon(''), models: icon(''), gallery: icon(''), recipes: icon(''), combos: TOOL_ICONS.MATERIALS, prompts: icon(''), voices: icon(''), // Not a rail entry (Voice-over is a view of Voices); Home's voice-over card uses it. script: icon(''), 'audio-gallery': icon(''), }; /** Pages on the rail, top to bottom. `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: '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' }, // opens Prompt Studio by the canvas { page: 'voices', id: 'anomalous-voices-btn', labelKey: 'shellVoices', audio: true }, { page: 'audio-gallery', id: 'anomalous-audio-gallery-btn', labelKey: 'shellAudioGallery', audio: true }, ]); 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); const refreshLanguage = () => { for (const entry of RAIL_PAGES) { const button = items.get(entry.page); const text = t(entry.labelKey); button.querySelector('.anomalous-rail-label').textContent = text; button.setAttribute('aria-label', text); } syncCompact(); }; // A short window drops the labels under the icons; tooltips name the entries instead. const syncCompact = () => { root.classList.remove('is-compact'); const compact = root.clientHeight > 0 && root.scrollHeight > root.clientHeight + 1; root.classList.toggle('is-compact', compact); for (const button of items.values()) { if (compact) { button.dataset.tooltip = button.getAttribute('aria-label'); button.dataset.tooltipPos = 'right'; } else { delete button.dataset.tooltip; } } }; 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, }; }