feat(webui): console tokens and shell restyle (#1542, row 53)
The console takes the design package's tokens.css and icons.svg as byte
copies, guarded by a test, and a new shell.css holds the components:
command bar with freshness line, section list (a strip that scrolls
inside itself below 760px), dense table, inspector, status mark, chips,
copy command, toast, Ctrl+K palette, not found, banners and skeleton
rows. JetBrains Mono 2.304 ships from the official archive with sha256
and OFL. Harbor following the system setting is the default palette.
A refusal (403, not-configured, no-bus-host, or the board's 403) fails
closed everywhere: no kept rows, counts, inspector, palette entries or
conversation history, and a later 500 or 503 doesn't bring them back.
Dewey built it in three rounds. Round 1 (75569953) and round 2
(78c3aef9) got changes from Darkwing (27148, 27165) and Filbert (27143,
27163) for refusal leaks outside the main view; round 3 (2562c05d) was
approved by Darkwing (27171) and Filbert (27174). Sage's gate on
ea080fd8 plus the candidate: runs 41/0, queue 148/0, webui 27/0,
conversation 182/0, control-board 124/0, build-tokens --check current,
every scripts/test-*.sh 0 failed (task 98/0 and release 14/0 with
Docker). The round 2 gate had one discord engine timing failure in the
full run, 66/0 alone, filed as #1553.
Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
@@ -7,12 +7,13 @@
|
||||
// Delivered replies with no newer answer yet, by row: the board's sentAt. Page memory only.
|
||||
const awaiting = new Map();
|
||||
let data = null, project = null, selected = null, returnFocus = null, paused = false, busy = false, timer;
|
||||
let board = 'the configured board', palette = 'harbor', mode = 'light';
|
||||
let board = 'the configured board', palette = 'harbor', mode = 'system', failed = false;
|
||||
const labels = { registration: 'registered', 'first-user-message': 'first message', 'tmux-pane': 'tmux pane', 'session-cwd': 'session cwd', 'workspace-git-root': 'git root' };
|
||||
const source = value => value ? `<span class="source">${esc(labels[value] || value)}</span>` : '';
|
||||
// Who set a registered task (#1511): null from the scan unless the task shown is the registered one. A caller's claim, not verified; it grants nothing.
|
||||
const setBy = r => r.taskSetBy ? `<span class="source">set by ${esc(r.taskSetBy)}</span>` : '';
|
||||
const badge = state => `<span class="badge ${ { working: 'badge-ok', waiting: 'badge-attn', error: 'badge-danger' }[state] || 'badge-muted'}">${esc(state || 'unknown')}</span>`;
|
||||
// Status mark: the class adds the glyph (shell.css); the word is the text, so colour is never the only signal.
|
||||
const badge = state => `<span class="badge st st-${esc(state || 'unknown')} ${ { working: 'badge-ok', waiting: 'badge-attn', error: 'badge-danger' }[state] || 'badge-muted'}">${esc(state || 'unknown')}</span>`;
|
||||
const connectorStatus = r => r.connector ? `<span class="source">${r.connector.braked === true ? 'braked (STOP)' : r.connector.braked === false ? 'not braked' : 'brake unknown'} · owner ${esc(r.connector.ownerState)}</span>` : '';
|
||||
const relaunchNotice = r => r.relaunchedAt ? `relaunched at ${r.relaunchedAt}, no messages since` : '';
|
||||
// Relative age is the board's ageSeconds: time since last activity as of the scan, not session lifetime (#1507).
|
||||
@@ -34,12 +35,13 @@
|
||||
const canReply = r => !r.connector && r.registered && r.registered.alive !== false && r.registered.tmux?.session;
|
||||
const row = id => data?.sessions.find(r => key(r) === id);
|
||||
const announce = text => { $('announce').textContent = text; };
|
||||
// Colours come from tokens.css by data-palette and data-mode. System removes data-mode, so the OS setting picks light or dark.
|
||||
function theme() {
|
||||
const p = Brand.palettes.find(p => p.id === palette) || Brand.palettes[0];
|
||||
palette = p.id;
|
||||
if (!['light', 'dim', 'dark'].includes(mode)) mode = 'light';
|
||||
for (const [name, value] of Object.entries(Brand.tokens(p, mode))) document.documentElement.style.setProperty('--' + name, value);
|
||||
document.documentElement.dataset.palette = palette; document.documentElement.dataset.mode = mode;
|
||||
if (!['system', 'light', 'dim', 'dark'].includes(mode)) mode = 'system';
|
||||
document.documentElement.dataset.palette = palette;
|
||||
if (mode === 'system') delete document.documentElement.dataset.mode; else document.documentElement.dataset.mode = mode;
|
||||
$('palette').value = palette; $('mode').value = mode;
|
||||
try { localStorage.setItem('mosaic-console-appearance', JSON.stringify({ palette, mode })); } catch {}
|
||||
}
|
||||
@@ -59,8 +61,8 @@
|
||||
return `<ul class="waiting">${rows.map(r => `<li class="wait-item"><div class="wait-head">${openButton(r)} ${badge(r.state)}${connectorStatus(r)}</div><p class="small muted">${esc(r.project)} · ${esc(r.activeProject || 'unknown')}</p><p>${esc(r.task || 'unknown')}${source(r.taskSource)}${setBy(r)}</p><p class="preview">${esc(relaunchNotice(r) || r.lastError || r.lastAssistantText || 'No assistant text yet.')}</p><div class="actions"><span class="small muted">${esc(relaunchNotice(r) || (activity(r) ? `Last activity ${activity(r)}` : 'No activity'))}</span><span>${historyButton(r)}${seenButton(r)}</span></div></li>`).join('')}</ul>`;
|
||||
}
|
||||
function table(rows) {
|
||||
if (!rows.length) return '<p class="state">No sessions match these filters.</p>';
|
||||
return `<div class="table-wrap" tabindex="0" role="region" aria-label="Session table, scroll horizontally for all columns"><table class="sessions"><thead><tr>${['Agent', 'State', 'Task', 'Active project', 'Workspace', 'Model', 'Registered', 'Last activity'].map(h => `<th scope="col">${h}</th>`).join('')}</tr></thead><tbody>${rows.map(r => `<tr data-row="${esc(key(r))}"><td>${openButton(r)}<span class="source">${esc(r.project)}</span>${historyButton(r)}</td><td>${badge(r.state)}${connectorStatus(r)}${r.seen ? '<span class="source">seen</span>' : ''}</td><td class="task">${esc(r.task || 'unknown')}${source(r.taskSource)}${setBy(r)}</td><td>${esc(r.activeProject || 'unknown')}${source(r.activeProjectSource)}</td><td>${esc(r.workspace || 'unknown')}${source(r.workspaceSource)}</td><td>${esc(r.model || 'unknown')}<span class="source">${esc(r.provider)}</span></td><td>${r.registered ? r.registered.alive === false ? 'stale' : 'registered' : 'no'}</td><td>${relaunchNotice(r) ? esc(relaunchNotice(r)) : activity(r) ? `${esc(activity(r))}${activity(r) === r.lastActivity ? '' : `<span class="source">${esc(r.lastActivity)}</span>`}` : 'unknown'}</td></tr>`).join('')}</tbody></table></div>`;
|
||||
if (!rows.length) return `<div class="empty"><b>No sessions match these filters.</b><p>${data.sessions.length ? 'Clear Hide offline or Hide seen, or pick All projects, to see the rest.' : 'Sessions appear here when the board scan finds an agent session on this machine.'}</p></div>`;
|
||||
return `<div class="table-wrap" tabindex="0" role="region" aria-label="Session table, scroll horizontally for all columns"><table class="sessions"><thead><tr>${['Agent', 'State', 'Task', 'Active project', 'Workspace', 'Model', 'Registered', 'Last activity'].map(h => `<th scope="col">${h}</th>`).join('')}</tr></thead><tbody>${rows.map(r => `<tr data-row="${esc(key(r))}"${selected === key(r) ? ' class="is-selected"' : ''}><td>${openButton(r)}<span class="source">${esc(r.project)}</span>${historyButton(r)}</td><td>${badge(r.state)}${connectorStatus(r)}${r.seen ? '<span class="source">seen</span>' : ''}</td><td class="task">${esc(r.task || 'unknown')}${source(r.taskSource)}${setBy(r)}</td><td>${esc(r.activeProject || 'unknown')}${source(r.activeProjectSource)}</td><td>${esc(r.workspace || 'unknown')}${source(r.workspaceSource)}</td><td>${esc(r.model || 'unknown')}<span class="source">${esc(r.provider)}</span></td><td>${r.registered ? r.registered.alive === false ? 'stale' : 'registered' : 'no'}</td><td>${relaunchNotice(r) ? esc(relaunchNotice(r)) : activity(r) ? `${esc(activity(r))}${activity(r) === r.lastActivity ? '' : `<span class="source">${esc(r.lastActivity)}</span>`}` : 'unknown'}</td></tr>`).join('')}</tbody></table></div>`;
|
||||
}
|
||||
function registered(r) {
|
||||
const reg = r.registered;
|
||||
@@ -267,7 +269,7 @@
|
||||
$('projects').innerHTML = `<ul class="tree-list"><li><button type="button" data-project="" aria-pressed="${project === null}">All projects <span class="count">${data.sessions.length}</span></button></li>${projects.map(p => `<li><button type="button" data-project="${esc(p)}" aria-pressed="${project === p}">${esc(p)} <span class="count">${data.sessions.filter(r => r.project === p).length}</span></button></li>`).join('')}</ul>`;
|
||||
const waiting = data.sessions.filter(r => r.waitingOnYou);
|
||||
const seen = data.sessions.filter(r => r.seen);
|
||||
$('waiting-count').textContent = waiting.length; $('waiting').innerHTML = waiting.length ? cards(waiting) : '<p class="muted">Nothing is waiting on you.</p>';
|
||||
$('waiting-count').textContent = waiting.length; $('waiting').innerHTML = waiting.length ? cards(waiting) : '<div class="empty"><b>Nothing is waiting on you.</b><p>A session shows here when its agent stops on a question or an error and you have not marked it seen.</p></div>';
|
||||
$('seen-count').textContent = seen.length; $('seen').innerHTML = cards(seen);
|
||||
const group = data.sessions.filter(r => !project || r.project === project);
|
||||
const visible = group.filter(r => !($('hide-offline').checked && r.state === 'offline') && !($('hide-seen').checked && r.seen));
|
||||
@@ -275,21 +277,54 @@
|
||||
$('sessions').innerHTML = table(visible);
|
||||
$('footer').textContent = Object.entries(data.counts || {}).map(([s, n]) => `${s} ${n}`).join(' · ') || 'No sessions';
|
||||
$('status').textContent = `Scanned ${data.generatedAt || 'unknown'} · ${paused ? 'auto-refresh paused' : 'refresh every 10s'}`;
|
||||
inspect(); convForm(); if (conv) convStatus(); restore(focus);
|
||||
fresh(); inspect(); convForm(); if (conv) convStatus(); restore(focus);
|
||||
}
|
||||
// Freshness line in the command bar: when the board last scanned, and warning words when the last refresh failed.
|
||||
function fresh() {
|
||||
const at = Date.parse(data?.generatedAt), ago = Number.isFinite(at) ? age(Math.max(0, Math.round((Date.now() - at) / 1000))) : null;
|
||||
if (!data) { $('fresh-board').innerHTML = failed === 'refused' ? 'Board <span class="warn">not read: refused</span>' : failed ? 'Board <span class="warn">not read: the read failed</span>' : 'Board not read yet'; return; }
|
||||
const scan = `Board scanned <b>${esc(Number.isFinite(at) ? `${new Date(at).toISOString().slice(11, 19)} UTC` : 'unknown')}</b>${ago ? ` (${ago} ago)` : ''}`;
|
||||
$('fresh-board').innerHTML = failed ? `${scan} · <span class="warn">stale: the last refresh failed</span>` : scan;
|
||||
}
|
||||
setInterval(() => { if (data) fresh(); }, 5000);
|
||||
async function api(path, value) {
|
||||
const res = await fetch(path, value === undefined ? { cache: 'no-store' } : { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify(value) });
|
||||
const result = await res.json();
|
||||
if (!res.ok) throw new Error(result.error || `HTTP ${res.status}`);
|
||||
if (!res.ok) throw Object.assign(new Error(result.error || `HTTP ${res.status}`), { status: res.status });
|
||||
return result;
|
||||
}
|
||||
function accept(value) {
|
||||
if (!value || !Array.isArray(value.sessions) || value.sessions.some(r => !r || typeof r.project !== 'string' || typeof r.agent !== 'string')) throw new Error('Invalid board session data');
|
||||
data = value; $('error').hidden = true;
|
||||
data = value; failed = false; $('error').hidden = true; $('error').className = 'banner err';
|
||||
$('board-refused').hidden = true; document.body.classList.remove('board-refused');
|
||||
for (const id of awaiting.keys()) { const r = row(id); if (r && !pending(r)) awaiting.delete(id); }
|
||||
render();
|
||||
}
|
||||
function error(err) { $('error').hidden = false; $('error').textContent = `${err.message} Board: ${board}. ${data ? 'Showing last known data.' : 'No board data loaded.'} Use Refresh to try again.`; }
|
||||
// A refusal (403) fails closed: the last data is dropped, not kept or shown as current, and so is
|
||||
// everything drawn from it: the cards, the table, the inspector, an open conversation and its
|
||||
// history, the project tree, the counts and the footer. Any other failure keeps the last data with
|
||||
// its scan time. With no data, from a refusal or before the first scan, the board is drawn empty
|
||||
// and says why, so a later failure never brings back old rows.
|
||||
function error(err) {
|
||||
const refused = err.status === 403;
|
||||
failed = refused ? 'refused' : true; $('error').hidden = false;
|
||||
if (refused) { data = null; selected = null; project = null; if (conv) { $('conv-log').replaceChildren(); closeConversation(); } }
|
||||
if (!data) {
|
||||
const why = refused ? 'the board refused the read' : 'the read failed';
|
||||
$('projects').innerHTML = `<p class="muted">Not read: ${why}.</p>`;
|
||||
for (const id of ['waiting', 'seen', 'sessions']) $(id).replaceChildren();
|
||||
for (const id of ['waiting-count', 'seen-count', 'session-count']) $(id).textContent = '–';
|
||||
$('footer').textContent = `Board not read: ${refused ? 'refused' : 'the read failed'}`;
|
||||
$('status').textContent = `Not read: ${why}`;
|
||||
inspect();
|
||||
}
|
||||
$('error').className = refused ? 'banner ref' : 'banner err';
|
||||
$('board-refused').hidden = !refused; document.body.classList.toggle('board-refused', refused);
|
||||
$('error').innerHTML = refused
|
||||
? `<p><b>The board refused the read.</b> refused: <code>${esc(err.message)}</code> Board: ${esc(board)}. Console shows nothing rather than a guess. Use Refresh to try again.</p>`
|
||||
: `<p><b>${esc(err.message)}</b> Board: ${esc(board)}. ${data ? `Showing last known data.${data.generatedAt ? ` Scanned ${esc(data.generatedAt)}.` : ''}` : 'No board data loaded.'} Use Refresh to try again.</p>`;
|
||||
fresh();
|
||||
}
|
||||
function schedule() { clearTimeout(timer); if (!paused) timer = setTimeout(refresh, 10000); }
|
||||
async function refresh() {
|
||||
if (busy) return;
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,12 @@
|
||||
JetBrains Mono 2.304
|
||||
Release: https://github.com/JetBrains/JetBrainsMono/releases/tag/v2.304
|
||||
Archive: https://github.com/JetBrains/JetBrainsMono/releases/download/v2.304/JetBrainsMono-2.304.zip
|
||||
Archive sha256: 6f6376c6ed2960ea8a963cd7387ec9d76e3f629125bc33d1fdcd7eb7012f7bbf
|
||||
License: OFL.txt in the archive, copied as jetbrains-mono-OFL.txt
|
||||
400: fonts/webfonts/JetBrainsMono-Regular.woff2 -> jetbrains-mono-400.woff2
|
||||
500: fonts/webfonts/JetBrainsMono-Medium.woff2 -> jetbrains-mono-500.woff2
|
||||
700: fonts/webfonts/JetBrainsMono-Bold.woff2 -> jetbrains-mono-700.woff2
|
||||
a9cb1cd82332b23a47e3a1239d25d13c86d16c4220695e34b243effa999f45f2 jetbrains-mono-400.woff2
|
||||
086c48dfbea9ddaff1320f7e09399b8e2924e88ce67453721255db3bdbb5a353 jetbrains-mono-500.woff2
|
||||
c503cc5ec5f8b2c7666b7ecda1adf44bd45f2e6579b2eba0fc292150416588a2 jetbrains-mono-700.woff2
|
||||
30f0c136e3c88e422d0791acd97238870f9054a9729bc34cf2ff0d4ed8cac4ad jetbrains-mono-OFL.txt
|
||||
@@ -181,6 +181,12 @@
|
||||
const when = iso => iso ? `<time datetime="${esc(iso)}" title="${esc(iso)}">${esc(ago(iso))}</time>` : 'never';
|
||||
const clock = iso => iso ? `<time datetime="${esc(iso)}" title="${esc(iso)}">${esc(String(iso).slice(11, 16))}</time>` : '';
|
||||
const badge = (text, kind = 'muted') => `<span class="badge badge-${kind}">${txt(text)}</span>`;
|
||||
// Class chip: the decision class, the action in mono. BLOCKING is its own chip (CSS capitalises it; the text stays lowercase).
|
||||
const CLASSES = ['routine', 'within-role', 'cross-role', 'gated'];
|
||||
const chip = (cls, action) => `<span class="tag${CLASSES.includes(cls) ? ` tag-${cls}` : ''}">${txt(cls)} · <code>${txt(action)}</code></span>`;
|
||||
const blockChip = text => `<span class="tag tag-block">${txt(text)}</span>`;
|
||||
// An empty state says what would appear and where it comes from.
|
||||
const empty = (what, from) => `<div class="empty"><b>${what}</b>${from ? `<p>${from}</p>` : ''}</div>`;
|
||||
const taskLink = ref => ref ? `<a href="#/tasks/${enc(ref)}">#${esc(String(ref).split('/')[1])}</a>` : '';
|
||||
// Rows without an actor role come from the human (CLI, outside any run) or from the broker itself.
|
||||
const actor = (a, by) => `<span class="source">${a ? `${txt(a.role)} · ${txt(a.run)}` : by === 'human' ? 'human, CLI outside any run' : 'broker'}</span>`;
|
||||
@@ -200,10 +206,10 @@
|
||||
// ---------------------------------------------------------------- views
|
||||
let inboxCount = null;
|
||||
function sections(route) {
|
||||
const items = [['/', 'Control board', ''], ['/inbox', 'Inbox', inboxCount ? `<span class="count" aria-label="${inboxCount.open} open for you">${inboxCount.open}</span>${inboxCount.blocking ? ` ${badge(`${inboxCount.blocking} blocking`, 'attn')}` : ''}` : ''], ['/tasks', 'Tasks', ''], ['/agents', 'Agents', '']];
|
||||
$('sections').innerHTML = items.map(([h, l, extra]) => {
|
||||
const items = [['/', 'Board', '', 'board'], ['/inbox', 'Inbox', inboxCount ? `<span class="count" aria-label="${inboxCount.open} open for you">${inboxCount.open}</span>${inboxCount.blocking ? ` ${blockChip(`${inboxCount.blocking} blocking`)}` : ''}` : '', 'inbox'], ['/tasks', 'Tasks', '', 'tasks'], ['/agents', 'Agents', '', 'agents']];
|
||||
$('sections').innerHTML = items.map(([h, l, extra, icon]) => {
|
||||
const cur = h === '/' ? route === '/' : route.startsWith(h) || (h === '/tasks' && route.startsWith('/trail/task')) || (h === '/inbox' && route.startsWith('/trail/decision'));
|
||||
return `<li><a href="#${h}" class="s1-section"${cur ? ' aria-current="page"' : ''}>${l}${extra ? ` ${extra}` : ''}</a></li>`;
|
||||
return `<li><a href="#${h}" class="s1-section"${cur ? ' aria-current="page"' : ''}><svg class="i" aria-hidden="true"><use href="/icons.svg#i-${icon}"/></svg>${l}${extra ? ` ${extra}` : ''}</a></li>`;
|
||||
}).join('');
|
||||
}
|
||||
function countInbox(rows) { inboxCount = { open: rows.length, blocking: rows.filter(d => d.blocking).length }; }
|
||||
@@ -211,7 +217,7 @@
|
||||
// Inbox
|
||||
function decisionCard(d) {
|
||||
return `<li class="wait-item s1-dec${d.blocking ? ' is-attn' : ''}"><div class="wait-head"><a href="#/inbox/${enc(d.id)}" class="s1-dec-open">${txt(first(d.question))}</a></div>
|
||||
<p class="s1-badges">${d.blocking ? badge('blocking', 'attn') : ''}${badge(`${d.class} · ${d.action}`)}</p>
|
||||
<p class="s1-badges">${d.blocking ? blockChip('blocking') : ''}${chip(d.class, d.action)}</p>
|
||||
<p class="small s1-auth">${approves(d.authorization)}</p>
|
||||
<p class="small muted">Raised by ${txt(d.raised_by_role)} ${when(d.at)}${d.task_ref ? ` · task ${taskLink(d.task_ref)}` : ''}${d.requirement_ref ? ` · ${txt(d.requirement_ref)}` : ''}</p></li>`;
|
||||
}
|
||||
@@ -219,7 +225,7 @@
|
||||
// The broker orders gated first, then oldest; lift blocking ones to the top.
|
||||
const rows = (await get('inbox')).slice().sort((a, b) => (b.blocking - a.blocking) || String(a.at).localeCompare(String(b.at)));
|
||||
countInbox(rows);
|
||||
const body = rows.length ? `<ul class="waiting s1-decs">${rows.map(decisionCard).join('')}</ul>` : '<p class="state">Nothing is waiting on you.</p>';
|
||||
const body = rows.length ? `<ul class="waiting s1-decs">${rows.map(decisionCard).join('')}</ul>` : empty('Nothing is waiting on you.', 'A decision shows here when an agent raises one routed to you through the bus.');
|
||||
return `${head('Inbox', `${readNote()} Decisions are answered in a terminal with <code>mosaic decide</code>; Console only shows them.`)}
|
||||
<h2 id="b-for-you">For you <span class="count">${rows.length}</span></h2>${body}
|
||||
<p class="small muted">Not shown here yet: ${gap('Decisions routed to roles')} ${gap('Closed decisions')} ${gap('Seen and DM state per row')} Open a decision for its seen, DM and closing history.</p>`;
|
||||
@@ -245,8 +251,8 @@
|
||||
: !c ? `<p class="small muted">This decision is routed to ${txt(d.routeTo)}, not to you, so Console offers no command for it.</p>` : '';
|
||||
return `${head(`Decision ${txt(short(d.id))}`, status)}
|
||||
<section aria-labelledby="b-q" class="panel"><h2 id="b-q">Question</h2><p class="s1-q">${txt(d.question)}</p>
|
||||
<dl class="kv"><dt>Class</dt><dd>${txt(d.class)} · ${txt(d.action)}${d.blocking ? ` ${badge('blocking', 'attn')}` : ''}</dd><dt>Approval</dt><dd class="s1-auth">${approves(d.authorization)}</dd><dt>Routed to</dt><dd>${txt(d.routeTo)}</dd><dt>Raised by</dt><dd>${txt(d.raisedBy.role)} <span class="source">${txt(d.raisedBy.run)}</span></dd><dt>Raised</dt><dd>${when(d.at)}</dd>${d.taskRef ? `<dt>Task</dt><dd>${taskLink(d.taskRef)} <span class="source">${txt(d.taskRef)}</span></dd>` : ''}${d.requirementRef ? `<dt>Requirement</dt><dd>${txt(d.requirementRef)}</dd>` : ''}<dt>DM</dt><dd>${d.dm ? `sent ${when(d.dm.at)}` : d.blocking ? 'not sent' : 'not needed (not blocking)'}</dd><dt>Id</dt><dd><code>${txt(d.id)}</code></dd></dl></section>
|
||||
<section aria-labelledby="b-o"><h2 id="b-o">Options</h2>${help}${options}<p class="small muted" id="copy-status" role="status"></p></section>
|
||||
<dl class="kv"><dt>Class</dt><dd>${chip(d.class, d.action)}${d.blocking ? ` ${blockChip('blocking')}` : ''}</dd><dt>Approval</dt><dd class="s1-auth">${approves(d.authorization)}</dd><dt>Routed to</dt><dd>${txt(d.routeTo)}</dd><dt>Raised by</dt><dd>${txt(d.raisedBy.role)} <span class="source">${txt(d.raisedBy.run)}</span></dd><dt>Raised</dt><dd>${when(d.at)}</dd>${d.taskRef ? `<dt>Task</dt><dd>${taskLink(d.taskRef)} <span class="source">${txt(d.taskRef)}</span></dd>` : ''}${d.requirementRef ? `<dt>Requirement</dt><dd>${txt(d.requirementRef)}</dd>` : ''}<dt>DM</dt><dd>${d.dm ? `sent ${when(d.dm.at)}` : d.blocking ? 'not sent' : 'not needed (not blocking)'}</dd><dt>Id</dt><dd><code>${txt(d.id)}</code></dd></dl></section>
|
||||
<section aria-labelledby="b-o"><h2 id="b-o">Options</h2>${help}${options}<p class="small muted" id="copy-status"></p></section>
|
||||
<section aria-labelledby="b-c"><h2 id="b-c">Earlier versions</h2>${earlier.length ? `<ul class="s1-plain">${earlier.map(x => `<li><a href="#/inbox/${enc(x.id)}">${txt(short(x.id))}</a> ${badge(x.closed ? x.closed.op : 'open')} ${txt(first(x.question))}${x.closed?.note ? `<span class="source">${txt(x.closed.note)}</span>` : ''}</li>`).join('')}</ul>` : '<p class="muted">None.</p>'}<p class="small muted">${gap('A later version that replaces this one')}</p></section>
|
||||
<section aria-labelledby="b-h"><h2 id="b-h">History</h2>${history.length ? `<ul class="s1-plain">${history.join('')}</ul>` : '<p class="muted">Not seen yet.</p>'}<p><a href="#/trail/decision/${enc(d.id)}">Full trail for this decision</a></p></section>`;
|
||||
}
|
||||
@@ -273,7 +279,7 @@
|
||||
const byChanged = (x, y) => String(y.changedAt).localeCompare(String(x.changedAt));
|
||||
async function tasksView(get) {
|
||||
const rows = await get('tasks'), ts = rows.map(r => task(r));
|
||||
if (!ts.length) return `${head('Tasks', freshness(rows))}<p class="state">No tasks in this business yet. Tasks appear after the PM creates them or the poller reads them from Vikunja.</p>`;
|
||||
if (!ts.length) return `${head('Tasks', freshness(rows))}${empty('No tasks in this business yet.', 'Tasks appear after the PM creates them or the poller reads them from Vikunja.')}`;
|
||||
const open = ts.filter(t => !t.missing && !t.done), done = ts.filter(t => !t.missing && t.done).sort(byChanged), missing = ts.filter(t => t.missing);
|
||||
const buckets = [...new Set(open.map(t => t.bucket))].sort((a, b) => a - b);
|
||||
const groups = buckets.map(b => {
|
||||
@@ -282,7 +288,7 @@
|
||||
}).join('');
|
||||
const doneHtml = done.length ? `<details class="s1-done" id="b-done"><summary><h2 id="b-done-title">Done <span class="count">${done.length}</span></h2></summary>${taskTable(done.slice(0, 50), 'Done tasks')}${done.length > 50 ? `<p class="small muted">The 50 most recently changed of ${done.length}.</p>` : ''}</details>` : '';
|
||||
const missingHtml = missing.length ? `<section aria-labelledby="b-missing"><h2 id="b-missing">No longer readable <span class="count">${missing.length}</span></h2><ul class="s1-plain">${missing.map(t => `<li><a href="#/tasks/${enc(t.ref)}">${title(t)}</a> ${badge(`missing: ${t.missing.reason}`, 'danger')} <span class="source">since ${clock(t.missing.at)}</span></li>`).join('')}</ul></section>` : '';
|
||||
return `${head('Tasks', freshness(rows))}<p class="small muted">Edit tasks in Vikunja; Console only reads them. ${gap('Bucket names, bot names and the Vikunja address')}</p>${groups || '<p class="state">No open tasks.</p>'}${doneHtml}${missingHtml}`;
|
||||
return `${head('Tasks', freshness(rows))}<p class="small muted">Edit tasks in Vikunja; Console only reads them. ${gap('Bucket names, bot names and the Vikunja address')}</p>${groups || empty('No open tasks.', 'Open tasks are read from Vikunja; done and unreadable ones are listed below.')}${doneHtml}${missingHtml}`;
|
||||
}
|
||||
async function taskView(get, ref) {
|
||||
const row = (await get('tasks')).find(r => r.task_ref === ref);
|
||||
@@ -305,7 +311,7 @@
|
||||
const rows = claims.map(c => `<tr><th scope="row">${txt(c.role)}</th><td>${txt(c.holder_run)}</td><td>${txt(c.harness)}${c.address ? `<span class="source">${txt(c.address)}</span>` : ''}</td><td>${when(c.at)}</td></tr>`).join('');
|
||||
return `${head('Agents', readNote())}
|
||||
<section aria-labelledby="b-claims"><h2 id="b-claims">Held roles <span class="count">${claims.length}</span></h2><p class="small muted">The current holder of each claimed role. A role nobody holds does not appear.</p>
|
||||
${claims.length ? `<div class="table-wrap" tabindex="0" role="region" aria-label="Held roles, scroll for all columns"><table class="s1-table s1-agents"><thead><tr>${['Role', 'Holder', 'Harness', 'Held since'].map(h => `<th scope="col">${h}</th>`).join('')}</tr></thead><tbody>${rows}</tbody></table></div>` : '<p class="state">No role is held right now.</p>'}</section>
|
||||
${claims.length ? `<div class="table-wrap" tabindex="0" role="region" aria-label="Held roles, scroll for all columns"><table class="s1-table s1-agents"><thead><tr>${['Role', 'Holder', 'Harness', 'Held since'].map(h => `<th scope="col">${h}</th>`).join('')}</tr></thead><tbody>${rows}</tbody></table></div>` : empty('No role is held right now.', 'A role shows here while a run holds its claim on the bus.')}</section>
|
||||
<p class="small muted">Not shown here yet: ${gap('Role instances, launch limits and model families')} ${gap('Launch state')} ${gap('Live and ended sessions')} ${gap('Credential notices')}</p>
|
||||
<p class="small muted">Board seats stay on the <a href="#/">control board</a>. This view is about roles.</p>`;
|
||||
}
|
||||
@@ -314,7 +320,7 @@
|
||||
const GROUPS = [['all', 'All'], ['request', 'Requests'], ['decision', 'Decisions'], ['launch', 'Launches'], ['task', 'Task changes'], ['review', 'Review']];
|
||||
function trailList(rows, filter) {
|
||||
const shown = filter === 'all' ? rows : rows.filter(r => r.group === filter);
|
||||
if (!shown.length) return '<p class="muted">Nothing of this kind in the trail.</p>';
|
||||
if (!shown.length) return empty('Nothing of this kind in the trail.', 'Every step the stack takes on this subject writes a trail row.');
|
||||
return `<ol class="s1-trail">${shown.map(r => `<li class="s1-trow s1-g-${esc(r.group)}"><span class="s1-twhen">${clock(r.at)}</span><span class="s1-tkind">${txt(r.kind)}</span><div class="s1-ttext"><p>${txt(r.text)}${r.decision && !r.kind.startsWith('decision.') && !r.kind.startsWith('delivery.') && SAFE.test(String(r.decision)) ? ` <a href="#/inbox/${enc(r.decision)}">decision ${txt(short(r.decision))}</a>` : ''}</p>${actor(r.actor, r.by)}${r.note ? `<span class="source">${esc(r.note)}</span>` : ''}</div></li>`).join('')}</ol>`;
|
||||
}
|
||||
async function trailView(get, kind, ref, q) {
|
||||
@@ -341,8 +347,8 @@
|
||||
}
|
||||
|
||||
// Not found and states
|
||||
function notFound(why) { return `${head('Not found')}<p class="state">${why || 'No page at this address.'} <a href="#/inbox">Go to the inbox</a></p>`; }
|
||||
const loadingView = () => `${head('Reading…')}<p class="state" aria-busy="true"><span class="spinner" aria-hidden="true"></span>Reading from the bus.</p>`;
|
||||
function notFound(why) { return `${head('Not found')}<div class="empty nf"><b>${why || 'No page at this address.'}</b><p>Nothing is at <code>${txt(location.hash || '#/')}</code>. <a href="#/">Go to the Board</a> or press <kbd>Ctrl K</kbd> to search.</p></div>`; }
|
||||
const loadingView = () => `${head('Reading…')}<div class="skel-rows" aria-busy="true"><span class="sr-only">Reading from the bus.</span><span class="skel"></span><span class="skel"></span><span class="skel"></span><span class="skel"></span></div>`;
|
||||
const retry = '<div class="actions"><button type="button" class="btn" data-retry>Read again</button></div>';
|
||||
// Why a read failed, in words, with the bus code as given. Never a path or a guess.
|
||||
function why(err) {
|
||||
@@ -361,8 +367,18 @@
|
||||
return `${say} (${code})`;
|
||||
}
|
||||
const failTitle = err => err.status === 403 ? 'Bus refused the read' : ['not-configured', 'no-bus-host'].includes(err.code) ? 'No bus to read' : 'The read failed';
|
||||
const failedView = err => `${head(failTitle(err))}<div class="state state-error" role="alert"><p><b>${why(err)}</b> Console shows nothing rather than a guess. Nothing was written.</p>${retry}</div>`;
|
||||
const staleBanner = err => `<div class="state state-error s1-stale" role="alert"><p><b>The last read failed.</b> ${why(err)} This is what was read before, ${when(readAt)}. Nothing was changed.</p>${retry}</div>`;
|
||||
// A refusal fails closed: no kept data, the code in mono. Any other failure keeps the last read, labelled with its time.
|
||||
const refusal = err => err.status === 403 || ['not-configured', 'no-bus-host'].includes(err.code);
|
||||
// After a refusal nothing read before is shown anywhere: not in a view, the palette or the Inbox count.
|
||||
function forget() { last.clear(); inboxCount = null; }
|
||||
const failedView = err => `${head(failTitle(err))}<div class="banner ${refusal(err) ? 'ref' : 'err'}" role="alert"><p>${refusal(err) ? `<b>refused: <code>${txt(err.code)}</code>.</b> ` : ''}${why(err)} Console shows nothing rather than a guess. Nothing was written.</p>${retry}</div>${empty('Nothing to show.', refusal(err) ? 'The view stays empty until the problem above is fixed.' : 'Read again, or come back when the bus answers.')}`;
|
||||
const staleBanner = err => `<div class="banner err s1-stale" role="alert"><p><b>The last read failed.</b> ${why(err)} This is what was read before, ${when(readAt)}. Nothing was changed.</p>${retry}</div>`;
|
||||
// Freshness line in the command bar for bus pages.
|
||||
const stamp = iso => { const t = Date.parse(iso); return Number.isFinite(t) ? `${new Date(t).toISOString().slice(11, 19)} UTC` : 'unknown'; };
|
||||
function freshBus(state) {
|
||||
$('fresh-bus').innerHTML = state === 'refused' ? 'Bus <span class="warn">not read: refused</span>' : state === 'failed' ? 'Bus <span class="warn">not read: the read failed</span>'
|
||||
: `Bus read <b>${esc(stamp(readAt))}</b>${readAt ? ` (${esc(ago(readAt))})` : ''}${state === 'stale' ? ' · <span class="warn">stale: the last read failed</span>' : ''}`;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- router
|
||||
// #/ and anything that is not a #/ route is the control board; the rest is this file's.
|
||||
@@ -406,20 +422,23 @@
|
||||
if (board) {
|
||||
// The board page keeps its own title; the Inbox count still follows the bus.
|
||||
if (shown !== null) { shown = null; $('bus-view').innerHTML = ''; document.title = 'Mosaic Console'; if (navigated) $('main').focus(); }
|
||||
try { const r = await fetchRead('inbox'); last.set('inbox', r); if (g === gen) { countInbox(r.rows); sections(path); } } catch {}
|
||||
try { const r = await fetchRead('inbox'); last.set('inbox', r); if (g === gen) { countInbox(r.rows); sections(path); } }
|
||||
catch (err) { if (refusal(err)) { forget(); if (g === gen) sections(path); } }
|
||||
if (g === gen) schedule();
|
||||
return;
|
||||
}
|
||||
if (navigated || shown !== route) { $('bus-view').innerHTML = loadingView(); }
|
||||
let html;
|
||||
let html, state = 'fresh';
|
||||
const live = reader(true);
|
||||
try { html = await view(live, path, new URLSearchParams(query)); readAt = live.oldest(); }
|
||||
catch (err) {
|
||||
const kept = reader(false);
|
||||
try { html = await view(kept, path, new URLSearchParams(query)); readAt = kept.oldest(); html = staleBanner(err) + html; }
|
||||
catch { html = failedView(err); }
|
||||
try { if (refusal(err)) throw err; html = await view(kept, path, new URLSearchParams(query)); readAt = kept.oldest(); html = staleBanner(err) + html; state = 'stale'; }
|
||||
catch { html = failedView(err); state = refusal(err) ? 'refused' : 'failed'; }
|
||||
if (refusal(err)) forget();
|
||||
}
|
||||
if (g !== gen) return;
|
||||
freshBus(state);
|
||||
const k = !navigated && shown === route ? keep() : null;
|
||||
$('bus-view').innerHTML = html; shown = route;
|
||||
sections(path);
|
||||
@@ -439,16 +458,82 @@
|
||||
const b = e.target.closest('#bus-view [data-copy]');
|
||||
if (b) {
|
||||
const text = b.dataset.copy, out = $('copy-status');
|
||||
try { await navigator.clipboard.writeText(text); out.textContent = `Copied: ${text}`; }
|
||||
try { await navigator.clipboard.writeText(text); out.textContent = `Copied: ${text}`; toast(out.textContent); }
|
||||
catch {
|
||||
const r = document.createRange(); r.selectNodeContents(b.previousElementSibling);
|
||||
const s = getSelection(); s.removeAllRanges(); s.addRange(r);
|
||||
out.textContent = 'The clipboard is unavailable. The command is selected; press Ctrl+C to copy it.';
|
||||
toast(out.textContent, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('#toast [data-dismiss]')) { toast(null); return; }
|
||||
const item = e.target.closest('#cmdk-list [data-href]');
|
||||
if (item) { pkGo(item.dataset.href); return; }
|
||||
if (e.target.closest('#cmdk')) { pkOpen(); return; }
|
||||
if (e.target.closest('#bus-view [data-retry]')) render(false);
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------- toast
|
||||
// One line in a polite live region. Success goes after 2.6 s; an error stays until dismissed.
|
||||
let toastTimer;
|
||||
function toast(text, error = false) {
|
||||
const t = $('toast'); clearTimeout(toastTimer);
|
||||
if (text === null) { t.className = 'toast'; t.textContent = ''; return; }
|
||||
t.className = `toast on${error ? ' err' : ''}`;
|
||||
t.innerHTML = `<span>${esc(text)}</span>${error ? '<button type="button" data-dismiss>Dismiss</button>' : ''}`;
|
||||
if (!error) toastTimer = setTimeout(() => toast(null), 2600);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- command palette
|
||||
// Ctrl+K opens it. It lists the views, and the decisions and tasks from the last inbox and
|
||||
// tasks reads (opening it reads both again, GET only). Enter goes; Esc closes and puts focus
|
||||
// back where it was. It only navigates.
|
||||
const VIEWS = [['#/', 'Board'], ['#/inbox', 'Inbox'], ['#/tasks', 'Tasks'], ['#/agents', 'Agents']];
|
||||
let pkAll = [], pkShown = [], pkAt = 0, pkBack = null;
|
||||
function pkItems() {
|
||||
const decisions = (last.get('inbox')?.rows || []).map(d => ({ href: `#/inbox/${enc(d.id)}`, label: first(d.question), group: `decision ${short(d.id)}` }));
|
||||
const tasks = (last.get('tasks')?.rows || []).filter(r => TASK.test(String(r.task_ref))).map(r => { const t = task(r); return { href: `#/tasks/${enc(t.ref)}`, label: `#${t.id} ${t.title ?? '(no title in the current read)'}`, group: 'task' }; });
|
||||
pkAll = [...VIEWS.map(([href, label]) => ({ href, label, group: 'view' })), ...decisions, ...tasks];
|
||||
}
|
||||
function pkFill() {
|
||||
const q = $('cmdk-input').value.trim().toLowerCase();
|
||||
pkShown = pkAll.filter(i => !q || `${i.label} ${i.group}`.toLowerCase().includes(q)).slice(0, 50);
|
||||
pkAt = Math.min(pkAt, Math.max(0, pkShown.length - 1));
|
||||
$('cmdk-list').innerHTML = pkShown.length ? pkShown.map((i, n) => `<li role="option" id="cmdk-opt-${n}" data-href="${esc(i.href)}" aria-selected="${n === pkAt}"><span>${txt(i.label)}</span><span class="g">${txt(i.group)}</span></li>`).join('')
|
||||
: '<li class="pk-none" role="option" aria-disabled="true" aria-selected="false">No view, decision or task matches.</li>';
|
||||
if (pkShown.length) { $('cmdk-input').setAttribute('aria-activedescendant', `cmdk-opt-${pkAt}`); $(`cmdk-opt-${pkAt}`)?.scrollIntoView({ block: 'nearest' }); }
|
||||
else $('cmdk-input').removeAttribute('aria-activedescendant');
|
||||
}
|
||||
function pkOpen() {
|
||||
const d = $('cmdk-dialog');
|
||||
if (d.open) return;
|
||||
pkBack = document.activeElement; pkAt = 0; $('cmdk-input').value = '';
|
||||
pkItems(); pkFill(); d.showModal(); $('cmdk-input').focus();
|
||||
Promise.allSettled(['inbox', 'tasks'].map(async v => last.set(v, await fetchRead(v)))).then(rs => {
|
||||
if (rs.some(r => r.status === 'rejected' && refusal(r.reason))) { forget(); sections(routeOf().split('?')[0]); }
|
||||
if (d.open) { pkItems(); pkFill(); }
|
||||
});
|
||||
}
|
||||
function pkGo(href) {
|
||||
pkBack = null; $('cmdk-dialog').close();
|
||||
if (location.hash === href || (href === '#/' && !location.hash.startsWith('#/'))) { render(true); return; }
|
||||
location.hash = href;
|
||||
}
|
||||
$('cmdk-dialog').addEventListener('close', () => { const b = pkBack; pkBack = null; if (b && document.contains(b)) b.focus(); });
|
||||
$('cmdk-input').addEventListener('input', () => { pkAt = 0; pkFill(); });
|
||||
$('cmdk-input').addEventListener('keydown', e => {
|
||||
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') { e.preventDefault(); if (pkShown.length) { pkAt = (pkAt + (e.key === 'ArrowDown' ? 1 : -1) + pkShown.length) % pkShown.length; pkFill(); } }
|
||||
else if (e.key === 'Enter') { e.preventDefault(); if (pkShown[pkAt]) pkGo(pkShown[pkAt].href); }
|
||||
});
|
||||
document.addEventListener('keydown', e => {
|
||||
if ((e.ctrlKey || e.metaKey) && !e.altKey && e.key.toLowerCase() === 'k') { e.preventDefault(); pkOpen(); return; }
|
||||
// Up and down move between the task links of a table, as on the board's session table.
|
||||
const a = e.target.closest?.('#bus-view table.s1-table td.s1-title a');
|
||||
if (!a || (e.key !== 'ArrowDown' && e.key !== 'ArrowUp')) return;
|
||||
const links = [...a.closest('table').querySelectorAll('td.s1-title a')], i = links.indexOf(a);
|
||||
e.preventDefault(); links[i + (e.key === 'ArrowDown' ? 1 : -1)]?.focus();
|
||||
});
|
||||
window.addEventListener('hashchange', () => render(true));
|
||||
$('refresh').addEventListener('click', () => render(false));
|
||||
$('pause').addEventListener('click', () => setTimeout(schedule)); // after app.js flips aria-pressed
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg">
|
||||
<!-- Mosaic Stack icons, draft 1. 24-unit grid, 1.75 stroke, round caps and joins, no fill.
|
||||
Use: <svg class="i" aria-hidden="true"><use href="icons.svg#i-inbox"/></svg> with color set by CSS.
|
||||
"mark" is the placeholder Relay mark (fill, not stroke). -->
|
||||
<symbol id="mark" viewBox="0 0 16 17" fill="currentColor"><path fill-rule="evenodd" d="M1 7 5 3 9 3 12 6 9 9 6 6 4 8 4 9 7 12 5 14 1 10ZM15 10 11 14 7 14 4 11 7 8 10 11 12 9 12 8 9 5 11 3 15 7Z"/></symbol>
|
||||
<symbol id="i-board" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="3.5" width="7" height="7" rx="1.5"/><rect x="13.5" y="3.5" width="7" height="7" rx="1.5"/><rect x="3.5" y="13.5" width="7" height="7" rx="1.5"/><rect x="13.5" y="13.5" width="7" height="7" rx="1.5"/></symbol>
|
||||
<symbol id="i-inbox" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M3.5 13.5 6 5.5h12l2.5 8v5a1 1 0 0 1-1 1h-15a1 1 0 0 1-1-1z"/><path d="M3.5 13.5h5l1 2.5h5l1-2.5h5"/></symbol>
|
||||
<symbol id="i-talk" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 5h15a1 1 0 0 1 1 1v9a1 1 0 0 1-1 1H10l-4.5 3.5V16h-1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1z"/><path d="M8 10.5h8"/></symbol>
|
||||
<symbol id="i-tasks" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M10 6.5h10M10 12h10M10 17.5h10"/><path d="m3.5 6.5 1.5 1.5 2.5-3"/><path d="m3.5 12 1.5 1.5 2.5-3"/><path d="M4 17.5h3"/></symbol>
|
||||
<symbol id="i-agents" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="9" cy="8.5" r="3"/><path d="M3.5 19.5c.6-3.2 2.8-5 5.5-5s4.9 1.8 5.5 5"/><circle cx="17" cy="9.5" r="2.4"/><path d="M15.8 14.6c2.2.2 3.9 1.8 4.7 4.9"/></symbol>
|
||||
<symbol id="i-trail" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="5.5" r="2"/><circle cx="18" cy="18.5" r="2"/><path d="M8 5.5h7.5a3.25 3.25 0 0 1 0 6.5h-7a3.25 3.25 0 0 0 0 6.5H16"/></symbol>
|
||||
<symbol id="i-queue" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 6.5h16M4 12h16M4 17.5h9"/><path d="m16.5 15.5 2 2 2-2"/></symbol>
|
||||
<symbol id="i-runs" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3.5 20 8v8l-8 4.5L4 16V8z"/><path d="m4 8 8 4.5L20 8M12 12.5v8"/></symbol>
|
||||
<symbol id="i-ledger" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5 20.5V10M10 20.5V4M15 20.5v-8M20 20.5v-5"/></symbol>
|
||||
<symbol id="i-business" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 20.5V8.5l6-4v16M10.5 20.5h9v-9l-9-3"/><path d="M3 20.5h18M14 13.5h2M14 16.5h2M6.5 10.5h2M6.5 13.5h2"/></symbol>
|
||||
<symbol id="i-library" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4.5 4.5h4v15h-4zM10 4.5h4v15h-4z"/><path d="m15.5 5.3 3.4-.9 3.1 14.3-3.4.9z"/></symbol>
|
||||
<symbol id="i-settings" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M4 7h9M17 7h3M4 17h3M11 17h9"/><circle cx="15" cy="7" r="2"/><circle cx="9" cy="17" r="2"/></symbol>
|
||||
<symbol id="i-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><circle cx="11" cy="11" r="6"/><path d="m20 20-4.5-4.5"/></symbol>
|
||||
<symbol id="i-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="8.5" y="8.5" width="11" height="11" rx="1.5"/><path d="M15.5 8.5v-3a1 1 0 0 0-1-1h-9a1 1 0 0 0-1 1v9a1 1 0 0 0 1 1h3"/></symbol>
|
||||
<symbol id="i-close" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M6 6l12 12M18 6 6 18"/></symbol>
|
||||
<symbol id="i-term" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><rect x="3.5" y="4.5" width="17" height="15" rx="1.5"/><path d="m7.5 9.5 3 2.5-3 2.5M12.5 15h4"/></symbol>
|
||||
<symbol id="i-flag" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round"><path d="M5.5 20.5v-16M5.5 4.5h12l-2.5 4 2.5 4h-12"/></symbol>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.8 KiB |
@@ -1,24 +1,29 @@
|
||||
<!doctype html>
|
||||
<html lang="en" data-design="console" data-palette="harbor" data-mode="light">
|
||||
<html lang="en" data-design="console" data-palette="harbor">
|
||||
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Mosaic Console</title>
|
||||
<link rel="stylesheet" href="/shared/app.css"><link rel="stylesheet" href="/console.css"><link rel="stylesheet" href="/live.css"><link rel="stylesheet" href="/bus.css">
|
||||
<link rel="stylesheet" href="/shared/app.css"><link rel="stylesheet" href="/tokens.css"><link rel="stylesheet" href="/console.css"><link rel="stylesheet" href="/live.css"><link rel="stylesheet" href="/bus.css"><link rel="stylesheet" href="/shell.css">
|
||||
<script src="/brand.js" defer></script><script src="/app.js" defer></script><script src="/bus.js" defer></script></head>
|
||||
<body>
|
||||
<a class="skip" href="#main">Skip to content</a>
|
||||
<header class="cmdbar"><a href="/" class="wordmark"><i>M</i>Mosaic<small>Console</small></a>
|
||||
<div class="cmd-right"><label>Palette<select id="palette"></select></label><label>Appearance<select id="mode"><option>light</option><option>dim</option><option>dark</option></select></label><button class="btn" id="refresh" type="button">Refresh</button><button class="btn" id="pause" type="button" aria-pressed="false">Pause</button></div></header>
|
||||
<header class="cmdbar"><a href="/" class="wordmark"><svg class="mark" viewBox="0 0 16 17" aria-hidden="true"><use href="/icons.svg#mark"/></svg>Mosaic<small>Console</small></a>
|
||||
<button type="button" class="cmdk" id="cmdk" aria-haspopup="dialog" aria-controls="cmdk-dialog"><svg class="i" aria-hidden="true"><use href="/icons.svg#i-search"/></svg><span>Go to a view, decision or task</span><kbd>Ctrl K</kbd></button>
|
||||
<p class="fresh" id="fresh"><span id="fresh-board"></span><span id="fresh-bus"></span></p>
|
||||
<div class="cmd-right"><label>Palette<select id="palette"></select></label><label>Appearance<select id="mode"><option value="system">system</option><option value="light">light</option><option value="dim">dim</option><option value="dark">dark</option></select></label><button class="btn" id="refresh" type="button">Refresh</button><button class="btn" id="pause" type="button" aria-pressed="false">Pause</button></div></header>
|
||||
<div class="frame">
|
||||
<nav class="tree" aria-label="Console"><h2 class="tree-title">Console</h2><ul class="s1-sections" id="sections"></ul><div id="board-tree"><h2 class="tree-title">Projects</h2><div id="projects"><p class="muted">Loading projects…</p></div><p class="small muted">Select a project to filter sessions. Waiting on you always shows all projects.</p></div></nav>
|
||||
<main id="main" class="content" tabindex="-1"><div class="page-head" id="board-head"><div><h1>Control board</h1><p id="status" class="small muted" role="status">Loading board…</p></div></div>
|
||||
<div id="error" class="state state-error" role="alert" hidden></div>
|
||||
<div id="error" class="banner err" role="alert" hidden></div>
|
||||
<div id="board-refused" class="empty" hidden><b>Nothing to show.</b><p>The board refused the read, so Console shows nothing rather than a guess. The reason is above.</p></div>
|
||||
<section id="conversation" class="conversation" aria-labelledby="conv-title" hidden><div class="page-head"><div><h2 id="conv-title" tabindex="-1">Conversation</h2><p id="conv-meta" class="small muted"></p></div><div class="conv-actions"><label>Session<select id="conv-pick"></select></label><button class="btn" id="conv-back" type="button">Back to board</button></div></div>
|
||||
<div id="conv-markers"></div><p id="conv-status" class="small muted" role="status"></p><ol id="conv-log" class="turns conv-log" aria-label="Conversation history"></ol>
|
||||
<form id="conv-form" class="conv-form" hidden><label for="conv-reply" id="conv-reply-label">Reply</label><textarea id="conv-reply" maxlength="2000" rows="3" placeholder="Message this seat"></textarea><button class="btn primary" type="submit" id="conv-send">Send</button><p id="conv-receipt" class="conv-receipt" role="status" hidden></p></form><p id="conv-no-reply" class="muted" hidden></p></section>
|
||||
<div id="board-view"><section class="console-waiting" aria-labelledby="waiting-title"><h2 id="waiting-title">Waiting on you <span id="waiting-count" class="count">0</span></h2><div id="waiting"><p class="muted">Loading sessions…</p></div></section>
|
||||
<details id="seen-section"><summary>Seen <span id="seen-count" class="count">0</span></summary><div id="seen"></div></details>
|
||||
<section aria-labelledby="sessions-title"><div class="page-head"><h2 id="sessions-title">All sessions <span id="session-count" class="count">0</span></h2><div class="filters"><label><input id="hide-offline" type="checkbox" checked>Hide offline</label><label><input id="hide-seen" type="checkbox" checked>Hide seen</label></div></div><p class="small muted">Select an agent to inspect. Arrow keys move between agents; Enter opens the inspector. Scroll the table for all columns.</p><div id="sessions"></div></section></div>
|
||||
<section aria-labelledby="sessions-title"><div class="page-head"><h2 id="sessions-title">All sessions <span id="session-count" class="count">0</span></h2><div class="filters"><label><input id="hide-offline" type="checkbox" checked>Hide offline</label><label><input id="hide-seen" type="checkbox" checked>Hide seen</label></div></div><p class="small muted">Select an agent to inspect. Arrow keys move between agents; Enter opens the inspector. Scroll the table for all columns.</p><div id="sessions"><div class="skel-rows" aria-busy="true"><span class="sr-only">Loading sessions…</span><span class="skel"></span><span class="skel"></span><span class="skel"></span><span class="skel"></span></div></div></section></div>
|
||||
<div id="bus-view" hidden></div>
|
||||
</main>
|
||||
<aside class="inspector" id="inspector" aria-labelledby="inspector-title" hidden><div class="inspector-head"><h2 id="inspector-title" tabindex="-1">Inspector</h2><button type="button" class="btn" id="close">Close</button></div><div id="inspection"></div></aside>
|
||||
</div><footer class="foot"><span id="footer">Mosaic Stack</span><span id="board-url"></span></footer><div id="announce" class="sr-only" aria-live="polite"></div>
|
||||
<div id="toast" class="toast" role="status" aria-live="polite"></div>
|
||||
<dialog class="pk" id="cmdk-dialog" aria-label="Go to"><div class="pk-in"><svg class="i" aria-hidden="true"><use href="/icons.svg#i-search"/></svg><input id="cmdk-input" type="text" role="combobox" aria-expanded="true" aria-controls="cmdk-list" aria-autocomplete="list" aria-label="Go to a view, decision or task" autocomplete="off" spellcheck="false"></div><ul id="cmdk-list" class="pk-list" role="listbox" aria-label="Results"></ul><p class="pk-foot small"><kbd>↑</kbd><kbd>↓</kbd> move · <kbd>Enter</kbd> open · <kbd>Esc</kbd> close</p></dialog>
|
||||
</body></html>
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
/* Row 53 (#1542): the console shell restyled to docs/design (tokens.css, icons.svg, IMPLEMENTING.md).
|
||||
Loaded last, after tokens.css, so it overrides app.css, console.css, live.css and bus.css
|
||||
without editing them. Colours come only from tokens.css; nothing here is set inline. */
|
||||
@font-face{font-family:"JetBrains Mono";font-style:normal;font-weight:400;font-display:swap;src:url(/assets/fonts/jetbrains-mono-400.woff2) format("woff2")}
|
||||
@font-face{font-family:"JetBrains Mono";font-style:normal;font-weight:500;font-display:swap;src:url(/assets/fonts/jetbrains-mono-500.woff2) format("woff2")}
|
||||
@font-face{font-family:"JetBrains Mono";font-style:normal;font-weight:700;font-display:swap;src:url(/assets/fonts/jetbrains-mono-700.woff2) format("woff2")}
|
||||
:root{--bar-h:53px;--foot-h:33px}
|
||||
/* app.css sets a light --shadow and a darker one for html[data-mode=dark|dim]; System on a dark OS needs the darker one too. */
|
||||
@media (prefers-color-scheme:dark){:root:not([data-mode]){--shadow:0 1px 2px rgb(0 0 0/.3),0 8px 24px rgb(0 0 0/.35)}}
|
||||
|
||||
/* Type scale: page title 28/800, section 20/800, question 16/700, text 14/1.5, table 13, labels 11/700 caps, mono 12.5. */
|
||||
body{font:400 14px/1.5 var(--font);color:var(--text);background:var(--canvas)}
|
||||
.content h1{font-size:28px;font-weight:800;letter-spacing:-.02em;color:var(--strong)}
|
||||
.content h2,.inspector h2{font-size:20px;font-weight:800;letter-spacing:-.015em;color:var(--strong)}
|
||||
.content h3{font-size:16px;font-weight:700}
|
||||
.s1-q{font-size:16px;font-weight:700;color:var(--strong)}
|
||||
.tree-title,.label{font:700 11px/1.3 var(--font);letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
|
||||
code,kbd,pre,.mono{font-family:var(--mono);font-size:12.5px}
|
||||
kbd{border:1px solid var(--line);border-bottom-width:2px;border-radius:4px;padding:0 .35em;background:var(--surface);color:var(--strong);white-space:nowrap;font-size:11.5px}
|
||||
.count,td,.s1-twhen,time,.fresh,.kv dd{font-variant-numeric:tabular-nums}
|
||||
:focus-visible{outline:2px solid var(--focus);outline-offset:2px}
|
||||
table.sessions tbody tr:focus-visible,table.s1-table tbody tr:focus-visible{outline:2px solid var(--focus);outline-offset:-2px}
|
||||
svg.i{width:18px;height:18px;flex:none;fill:none;stroke:currentColor;stroke-width:1.75;stroke-linecap:round;stroke-linejoin:round}
|
||||
|
||||
/* Command bar: mark, Ctrl+K, freshness line, appearance. */
|
||||
.cmdbar{flex-wrap:nowrap;gap:10px;padding:8px 12px;min-height:var(--bar-h);min-width:0}
|
||||
.wordmark{color:var(--strong);font-weight:800;font-size:16px;flex-wrap:nowrap;white-space:nowrap}
|
||||
.wordmark .mark{width:20px;height:auto;fill:var(--action);flex:none}
|
||||
.wordmark small{font-size:11px}
|
||||
.cmdk{flex:1 1 160px;max-width:460px;display:flex;align-items:center;gap:8px;min-height:36px;border:1px solid var(--line);background:var(--canvas);border-radius:var(--r);padding:5px 10px;color:var(--muted);cursor:pointer;text-align:left;min-width:0;font:inherit;font-size:13px}
|
||||
.cmdk span{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.cmdk:hover{border-color:var(--border)}
|
||||
.fresh{font-size:12px;color:var(--muted);white-space:nowrap;margin:0 0 0 auto;min-width:0;overflow:hidden;text-overflow:ellipsis}
|
||||
.fresh b{color:var(--success);font-weight:600}
|
||||
.fresh .warn{color:var(--warning);font-weight:700}
|
||||
#fresh-bus,body.on-bus #fresh-board{display:none}
|
||||
body.on-bus #fresh-bus{display:inline}
|
||||
.fresh+.cmd-right{margin-left:0}
|
||||
.cmd-right{flex-wrap:nowrap}
|
||||
.cmd-right .btn{min-height:32px;padding:4px 10px;font-size:13px}
|
||||
.cmd-right select{min-height:32px;font-size:13px;border-radius:var(--r)}
|
||||
/* Labels sit beside their selects so the bar stays one row of --bar-h. */
|
||||
.cmd-right label{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--muted)}
|
||||
@media (max-width:1279px){.cmd-right label{font-size:0;gap:0}.cmd-right label select{font-size:13px}}
|
||||
|
||||
/* Section list: icon, label, count. */
|
||||
.s1-section{gap:9px;padding:6px 8px;border:0;border-radius:var(--r);color:var(--text);font-size:13.5px;min-height:34px;flex-wrap:nowrap}
|
||||
.s1-section:hover{background:var(--raised)}
|
||||
.s1-section[aria-current=page]{background:color-mix(in srgb,var(--action) 13%,var(--surface));color:var(--strong);border:0}
|
||||
.s1-section[aria-current=page] svg{color:var(--action)}
|
||||
.s1-section .count{font:600 11px var(--mono);background:none;padding:0;color:var(--muted)}
|
||||
.s1-section .count.hot{color:var(--surface);background:var(--danger);border-radius:999px;padding:0 6px}
|
||||
|
||||
/* Dense table. */
|
||||
.table-wrap{border-radius:var(--r-table)}
|
||||
table.sessions,table.s1-table{border-collapse:collapse;width:100%;font-size:13px}
|
||||
table.sessions th,table.s1-table thead th{font-size:11px;font-weight:700;letter-spacing:.06em;text-transform:uppercase;color:var(--muted);text-align:left;padding:7px 10px;border-bottom:1px solid var(--line);white-space:nowrap;background:var(--surface)}
|
||||
table.sessions td,table.s1-table td,table.s1-table tbody th{padding:7px 10px;border-bottom:1px solid color-mix(in srgb,var(--line) 60%,transparent);vertical-align:top}
|
||||
table.sessions tbody tr:last-child td,table.s1-table tbody tr:last-child>*{border-bottom:0}
|
||||
table.sessions tbody tr:hover,table.s1-table tbody tr:hover{background:color-mix(in srgb,var(--raised) 55%,var(--surface))}
|
||||
table.sessions tbody tr.is-selected{background:color-mix(in srgb,var(--action) 10%,var(--surface));box-shadow:inset 3px 0 0 var(--action)}
|
||||
table.s1-table td code{font-size:12.5px;white-space:nowrap}
|
||||
|
||||
/* Status mark: glyph and word, never colour alone. The glyph is generated content with empty alt text, so the word is what is read. */
|
||||
.badge{font-size:12px;padding:1px 7px}
|
||||
.st::before{width:auto;height:auto;background:none;border-radius:0;line-height:1;content:"?" / ""}
|
||||
.st-working::before{content:"●" / ""}
|
||||
.st-waiting::before{content:"◐" / ""}
|
||||
.st-error::before{content:"▲" / ""}
|
||||
.st-offline::before{content:"○" / ""}
|
||||
.st-idle::before{content:"–" / ""}
|
||||
|
||||
/* Class chip. BLOCKING is its own chip. */
|
||||
/* Inline-block, not flex: flex drops the space after the dot in "gated · <code>action</code>". */
|
||||
.tag{display:inline-block;vertical-align:middle;font-size:12px;font-weight:600;line-height:18px;padding:1px 7px;border-radius:4px;background:var(--raised);color:var(--strong);white-space:nowrap;max-width:100%;overflow:hidden;text-overflow:ellipsis}
|
||||
.tag code{font-size:11.5px}
|
||||
.tag-gated{background:color-mix(in srgb,var(--danger) 14%,var(--surface));color:var(--danger)}
|
||||
.tag-block{background:var(--danger);color:var(--surface);text-transform:uppercase;letter-spacing:.04em}
|
||||
.tag-cross-role{background:color-mix(in srgb,var(--accent) 16%,var(--surface));color:var(--accent)}
|
||||
.tag-within-role,.tag-routine{background:var(--raised);color:var(--muted)}
|
||||
|
||||
/* Copy command. */
|
||||
.s1-cmd code{font-size:12.5px;border-radius:var(--r)}
|
||||
|
||||
/* View states: empty, banner (error and refusal), skeleton rows. */
|
||||
.empty{padding:28px 16px;text-align:center;color:var(--muted);border:1px dashed var(--line);border-radius:var(--r-lg);background:var(--surface)}
|
||||
.empty b{display:block;color:var(--strong);font-size:15px;margin-bottom:4px}
|
||||
.empty p{margin:4px auto 0;max-width:62ch}
|
||||
.banner{display:flex;flex-wrap:wrap;gap:6px 10px;align-items:flex-start;padding:10px 12px;border-radius:var(--r-table);margin-bottom:12px;border:1px solid;font-size:13px;color:var(--text);text-align:left}
|
||||
.banner>p,.banner>div:not(.actions){flex:1 1 280px;min-width:0;margin:0;overflow-wrap:anywhere}
|
||||
.banner .actions{margin:0}
|
||||
.banner.err{border-color:color-mix(in srgb,var(--danger) 50%,var(--line));background:color-mix(in srgb,var(--danger) 8%,var(--surface))}
|
||||
.banner.ref{border-color:color-mix(in srgb,var(--warning) 50%,var(--line));background:color-mix(in srgb,var(--warning) 9%,var(--surface))}
|
||||
.banner b,.banner strong{color:var(--strong)}
|
||||
.banner code{overflow-wrap:anywhere}
|
||||
.skel-rows{display:grid;gap:10px;padding:14px 12px;border:1px solid var(--line);border-radius:var(--r-table);background:var(--surface)}
|
||||
.skel{display:block;height:12px;border-radius:4px;background:var(--raised);animation:skel 1.4s ease-in-out infinite}
|
||||
.skel:nth-child(odd){width:82%}
|
||||
@keyframes skel{50%{opacity:.45}}
|
||||
body.board-refused #board-view{display:none}
|
||||
body.on-bus #board-refused{display:none}
|
||||
|
||||
/* Toast: one line, polite, bottom centre. */
|
||||
.toast{position:fixed;left:50%;bottom:16px;transform:translateX(-50%);display:flex;align-items:center;gap:10px;background:var(--strong);color:var(--surface);padding:6px 8px 6px 14px;border-radius:var(--r);font-size:13px;z-index:60;max-width:calc(100% - 24px);box-shadow:var(--shadow)}
|
||||
/* Off, the toast stays in the accessibility tree, empty and visually hidden, so the first copy is announced. */
|
||||
.toast:not(.on){width:1px;height:1px;padding:0;overflow:hidden;clip-path:inset(50%);white-space:nowrap;box-shadow:none}
|
||||
.toast span{min-width:0;overflow-wrap:anywhere}
|
||||
.toast.err{background:var(--danger)}
|
||||
.toast button{font:inherit;font-weight:600;border:1px solid currentColor;background:none;color:inherit;border-radius:4px;padding:1px 8px;cursor:pointer}
|
||||
.toast:not(.err) button{display:none}
|
||||
|
||||
/* Command palette. */
|
||||
.pk{width:min(560px,calc(100% - 24px));padding:0;margin:10vh auto auto;background:var(--surface);color:var(--text);border:1px solid var(--line);border-radius:var(--r-lg);box-shadow:var(--shadow);overflow:hidden}
|
||||
.pk::backdrop{background:color-mix(in srgb,var(--strong) 28%,transparent)}
|
||||
.pk-in{display:flex;align-items:center;gap:8px;padding:0 14px;border-bottom:1px solid var(--line);color:var(--muted)}
|
||||
.pk input{flex:1;min-width:0;border:0;padding:12px 0;background:none;font:inherit;font-size:15px;color:var(--strong)}
|
||||
.pk input:focus{outline:none}
|
||||
.pk-in:focus-within{box-shadow:inset 0 -2px 0 var(--focus)}
|
||||
.pk-list{list-style:none;margin:0;padding:6px;max-height:min(340px,60vh);overflow:auto}
|
||||
.pk-list li{display:flex;gap:10px;align-items:center;padding:7px 10px;border-radius:var(--r);cursor:pointer;font-size:13.5px;min-width:0}
|
||||
.pk-list li>span:first-of-type{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
|
||||
.pk-list li[aria-selected=true]{background:color-mix(in srgb,var(--action) 14%,var(--surface));color:var(--strong)}
|
||||
.pk-list li .g{margin-left:auto;font-size:11px;color:var(--muted);white-space:nowrap}
|
||||
.pk-list .pk-none{cursor:default;color:var(--muted)}
|
||||
.pk-foot{margin:0;padding:6px 14px;border-top:1px solid var(--line);color:var(--muted)}
|
||||
|
||||
/* Not found. */
|
||||
.nf code{overflow-wrap:anywhere}
|
||||
|
||||
/* Two columns and up: the footer is a one-line status bar held at the bottom, and the side column
|
||||
fits between it and the command bar, so a short page does not scroll and a long one keeps the nav in view. */
|
||||
@media (min-width:760px){
|
||||
footer.foot{position:sticky;bottom:0;z-index:15;background:var(--surface);min-height:var(--foot-h);padding:6px var(--pad);align-items:center;flex-wrap:nowrap;font-size:12px}
|
||||
footer.foot>*{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;overflow-wrap:normal}
|
||||
.frame{min-height:calc(100vh - var(--bar-h) - var(--foot-h))}
|
||||
.tree,.inspector{top:var(--bar-h);height:calc(100vh - var(--bar-h) - var(--foot-h))}
|
||||
}
|
||||
/* The live region after the footer sits at its static place, one pixel past the screen; pin it so it adds no scroll. */
|
||||
#announce{top:0;left:0}
|
||||
|
||||
/* Breakpoints. 760 to 959 px keeps the side column (console.css drops it at 959). */
|
||||
@media (min-width:760px) and (max-width:959px){
|
||||
.frame,body.has-inspector .frame,body.on-bus .frame,body.on-bus.has-inspector .frame{grid-template-columns:200px minmax(0,1fr)}
|
||||
.tree{position:sticky;top:var(--bar-h);height:calc(100vh - var(--bar-h) - var(--foot-h));border-right:1px solid var(--line);border-bottom:0;flex-direction:column;flex-wrap:nowrap;align-items:stretch}
|
||||
.tree-title{flex:none}
|
||||
.tree-list{display:grid;flex-wrap:nowrap;overflow:visible}
|
||||
.tree .small{flex-basis:auto;margin-top:auto}
|
||||
.s1-sections{display:grid;gap:2px}
|
||||
.s1-section{border-radius:var(--r);padding:6px 8px}
|
||||
.cmdbar{position:sticky}
|
||||
}
|
||||
@media (max-width:1099px){
|
||||
body.has-inspector .frame{grid-template-columns:240px minmax(0,1fr)}
|
||||
.inspector{position:fixed;top:var(--bar-h);right:0;bottom:0;left:auto;width:min(400px,100%);height:auto;max-height:none;border-left:1px solid var(--line);border-top:0;border-radius:0;z-index:30;box-shadow:-12px 0 30px -12px color-mix(in srgb,#000 35%,transparent)}
|
||||
}
|
||||
@media (max-width:959px){body.has-inspector .frame{grid-template-columns:minmax(0,1fr)}}
|
||||
@media (min-width:760px) and (max-width:959px){body.has-inspector .frame{grid-template-columns:200px minmax(0,1fr)}}
|
||||
@media (max-width:759px){
|
||||
.cmdbar{position:static;flex-wrap:wrap}
|
||||
/* One column: the nav and the page take their own height, not a share of the screen. */
|
||||
.frame{align-content:start}
|
||||
.cmdk{flex:1 1 100%;max-width:none;order:3}
|
||||
.fresh{display:none}
|
||||
/* Both selects and both buttons share one row; the selects give up width first. */
|
||||
.cmd-right{margin-left:0;flex:1 1 100%;flex-wrap:nowrap;min-width:0}
|
||||
.cmd-right label{flex:1 1 0;min-width:0}
|
||||
.cmd-right .btn{flex:none}
|
||||
/* The section list is one strip that scrolls inside itself (IMPLEMENTING.md), so more sections never add rows. */
|
||||
.s1-sections{display:flex;flex-wrap:nowrap;overflow-x:auto;flex-basis:100%;min-width:0;max-width:100%;gap:2px;padding-bottom:2px;scrollbar-width:thin}
|
||||
.s1-sections>li{flex:none}
|
||||
.s1-section{border-radius:var(--r);padding:5px 8px;white-space:nowrap;flex:none}
|
||||
.inspector{inset:0;top:0;width:100%;z-index:40;box-shadow:none}
|
||||
}
|
||||
/* Below 360 px one row would cut the select labels; the buttons wrap under them instead. */
|
||||
@media (max-width:359px){.cmd-right{flex-wrap:wrap}.cmd-right label{flex:1 1 110px}}
|
||||
@media (prefers-reduced-motion:reduce){*,*::before,*::after{animation:none!important;transition:none!important;scroll-behavior:auto!important}}
|
||||
@media (forced-colors:active){.tag,.empty,.banner,.toast,.pk{border:1px solid CanvasText}.s1-section[aria-current=page]{outline:2px solid Highlight}table.sessions tbody tr.is-selected{outline:2px solid Highlight}}
|
||||
@@ -0,0 +1,813 @@
|
||||
/* Generated from tokens.json by tools/build-tokens.mjs. Do not edit by hand. */
|
||||
|
||||
/* Fonts: Manrope and JetBrains Mono. Self-host the woff2 files: the console's CSP blocks external hosts. packages/webui already serves Manrope 400-700; weight 800 and JetBrains Mono 400/500/700 still need adding (or drop 800 and use 700). The design preview loads both from Google Fonts only because it is a standalone page. */
|
||||
|
||||
:root {
|
||||
--font: "Manrope", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
--mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, Consolas, monospace;
|
||||
--r: 6px;
|
||||
--r-lg: 10px;
|
||||
--r-table: 8px;
|
||||
--pad: 16px;
|
||||
--gap: 12px;
|
||||
}
|
||||
|
||||
/* Harbor: the default palette */
|
||||
|
||||
:root {
|
||||
--canvas: #f3f5f7;
|
||||
--surface: #ffffff;
|
||||
--raised: #e6eaef;
|
||||
--line: #ced5df;
|
||||
--border: #636e7e;
|
||||
--text: #434d5b;
|
||||
--muted: #5e6978;
|
||||
--action: #2266bf;
|
||||
--onAction: #ffffff;
|
||||
--accent: #8d5e20;
|
||||
--focus: #2266bf;
|
||||
--success: #287658;
|
||||
--warning: #8f5c19;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #12161c;
|
||||
--surface: #1c232b;
|
||||
--raised: #28323e;
|
||||
--line: #384556;
|
||||
--border: #a4acb7;
|
||||
--text: #b9c1cb;
|
||||
--muted: #a4acb7;
|
||||
--action: #74a6e7;
|
||||
--onAction: #10141a;
|
||||
--accent: #e1b47a;
|
||||
--focus: #74a6e7;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-mode="dark"] {
|
||||
--canvas: #12161c;
|
||||
--surface: #1c232b;
|
||||
--raised: #28323e;
|
||||
--line: #384556;
|
||||
--border: #a4acb7;
|
||||
--text: #b9c1cb;
|
||||
--muted: #a4acb7;
|
||||
--action: #74a6e7;
|
||||
--onAction: #10141a;
|
||||
--accent: #e1b47a;
|
||||
--focus: #74a6e7;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-mode="dim"] {
|
||||
--canvas: #242d38;
|
||||
--surface: #2e3947;
|
||||
--raised: #384556;
|
||||
--line: #47576c;
|
||||
--border: #a4acb7;
|
||||
--text: #d6dbe1;
|
||||
--muted: #a9b1bc;
|
||||
--action: #8ab4ea;
|
||||
--onAction: #10141a;
|
||||
--accent: #e1b47a;
|
||||
--focus: #8ab4ea;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #ea9d99;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Carmine */
|
||||
|
||||
:root[data-palette="carmine"] {
|
||||
--canvas: #f7f3f3;
|
||||
--surface: #ffffff;
|
||||
--raised: #efe7e8;
|
||||
--line: #deced1;
|
||||
--border: #7e6367;
|
||||
--text: #5e4549;
|
||||
--muted: #7b6065;
|
||||
--action: #b92740;
|
||||
--onAction: #ffffff;
|
||||
--accent: #2e706c;
|
||||
--focus: #b92740;
|
||||
--success: #287658;
|
||||
--warning: #8f5c19;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="carmine"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #1b1214;
|
||||
--surface: #2b1d1f;
|
||||
--raised: #3d292c;
|
||||
--line: #55393e;
|
||||
--border: #b7a4a7;
|
||||
--text: #c9b6b9;
|
||||
--muted: #b7a4a7;
|
||||
--action: #e2788a;
|
||||
--onAction: #10141a;
|
||||
--accent: #8bd0cb;
|
||||
--focus: #e2788a;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="carmine"][data-mode="dark"] {
|
||||
--canvas: #1b1214;
|
||||
--surface: #2b1d1f;
|
||||
--raised: #3d292c;
|
||||
--line: #55393e;
|
||||
--border: #b7a4a7;
|
||||
--text: #c9b6b9;
|
||||
--muted: #b7a4a7;
|
||||
--action: #e2788a;
|
||||
--onAction: #10141a;
|
||||
--accent: #8bd0cb;
|
||||
--focus: #e2788a;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="carmine"][data-mode="dim"] {
|
||||
--canvas: #372528;
|
||||
--surface: #462f33;
|
||||
--raised: #55393e;
|
||||
--line: #6b484e;
|
||||
--border: #b7a4a7;
|
||||
--text: #ded3d5;
|
||||
--muted: #bca9ac;
|
||||
--action: #e996a4;
|
||||
--onAction: #10141a;
|
||||
--accent: #8bd0cb;
|
||||
--focus: #e996a4;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e99995;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Atlantic */
|
||||
|
||||
:root[data-palette="atlantic"] {
|
||||
--canvas: #f3f6f7;
|
||||
--surface: #ffffff;
|
||||
--raised: #e6ecef;
|
||||
--line: #cdd9df;
|
||||
--border: #63757e;
|
||||
--text: #3f4e55;
|
||||
--muted: #5c6d75;
|
||||
--action: #19719a;
|
||||
--onAction: #ffffff;
|
||||
--accent: #257467;
|
||||
--focus: #19719a;
|
||||
--success: #287658;
|
||||
--warning: #935f1a;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="atlantic"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #12191c;
|
||||
--surface: #1c272b;
|
||||
--raised: #28373e;
|
||||
--line: #384d57;
|
||||
--border: #a4b1b7;
|
||||
--text: #bcc8cd;
|
||||
--muted: #a4b1b7;
|
||||
--action: #73c3e8;
|
||||
--onAction: #10141a;
|
||||
--accent: #83d8ca;
|
||||
--focus: #73c3e8;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e4817c;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="atlantic"][data-mode="dark"] {
|
||||
--canvas: #12191c;
|
||||
--surface: #1c272b;
|
||||
--raised: #28373e;
|
||||
--line: #384d57;
|
||||
--border: #a4b1b7;
|
||||
--text: #bcc8cd;
|
||||
--muted: #a4b1b7;
|
||||
--action: #73c3e8;
|
||||
--onAction: #10141a;
|
||||
--accent: #83d8ca;
|
||||
--focus: #73c3e8;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e4817c;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="atlantic"][data-mode="dim"] {
|
||||
--canvas: #243238;
|
||||
--surface: #2e3f47;
|
||||
--raised: #384d57;
|
||||
--line: #46606d;
|
||||
--border: #a4b1b7;
|
||||
--text: #e2e7e9;
|
||||
--muted: #afbbc0;
|
||||
--action: #73c3e8;
|
||||
--onAction: #10141a;
|
||||
--accent: #83d8ca;
|
||||
--focus: #73c3e8;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #edaaa6;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Terracotta */
|
||||
|
||||
:root[data-palette="terracotta"] {
|
||||
--canvas: #f7f4f3;
|
||||
--surface: #ffffff;
|
||||
--raised: #eee9e7;
|
||||
--line: #ded3cf;
|
||||
--border: #7e6b63;
|
||||
--text: #584841;
|
||||
--muted: #78665e;
|
||||
--action: #a74f2a;
|
||||
--onAction: #ffffff;
|
||||
--accent: #356f88;
|
||||
--focus: #a74f2a;
|
||||
--success: #287658;
|
||||
--warning: #8f5c19;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="terracotta"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #1b1513;
|
||||
--surface: #2a211d;
|
||||
--raised: #3c2f2a;
|
||||
--line: #54423b;
|
||||
--border: #b7a9a4;
|
||||
--text: #cbbeb9;
|
||||
--muted: #b7a9a4;
|
||||
--action: #de9a7c;
|
||||
--onAction: #10141a;
|
||||
--accent: #89bcd1;
|
||||
--focus: #de9a7c;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="terracotta"][data-mode="dark"] {
|
||||
--canvas: #1b1513;
|
||||
--surface: #2a211d;
|
||||
--raised: #3c2f2a;
|
||||
--line: #54423b;
|
||||
--border: #b7a9a4;
|
||||
--text: #cbbeb9;
|
||||
--muted: #b7a9a4;
|
||||
--action: #de9a7c;
|
||||
--onAction: #10141a;
|
||||
--accent: #89bcd1;
|
||||
--focus: #de9a7c;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="terracotta"][data-mode="dim"] {
|
||||
--canvas: #362b26;
|
||||
--surface: #453630;
|
||||
--raised: #54423b;
|
||||
--line: #695349;
|
||||
--border: #b7a9a4;
|
||||
--text: #e5dfdc;
|
||||
--muted: #beb2ac;
|
||||
--action: #e2a68d;
|
||||
--onAction: #10141a;
|
||||
--accent: #89bcd1;
|
||||
--focus: #e2a68d;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #eba29e;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Aubergine */
|
||||
|
||||
:root[data-palette="aubergine"] {
|
||||
--canvas: #f5f4f6;
|
||||
--surface: #ffffff;
|
||||
--raised: #ece8ed;
|
||||
--line: #d9d2db;
|
||||
--border: #78637e;
|
||||
--text: #59455e;
|
||||
--muted: #75607b;
|
||||
--action: #88469b;
|
||||
--onAction: #ffffff;
|
||||
--accent: #77672c;
|
||||
--focus: #88469b;
|
||||
--success: #287658;
|
||||
--warning: #8f5c19;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="aubergine"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #18141a;
|
||||
--surface: #262028;
|
||||
--raised: #362d39;
|
||||
--line: #4c3f50;
|
||||
--border: #b3a4b7;
|
||||
--text: #c7b9cb;
|
||||
--muted: #b3a4b7;
|
||||
--action: #bf8ecc;
|
||||
--onAction: #10141a;
|
||||
--accent: #d3c388;
|
||||
--focus: #bf8ecc;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="aubergine"][data-mode="dark"] {
|
||||
--canvas: #18141a;
|
||||
--surface: #262028;
|
||||
--raised: #362d39;
|
||||
--line: #4c3f50;
|
||||
--border: #b3a4b7;
|
||||
--text: #c7b9cb;
|
||||
--muted: #b3a4b7;
|
||||
--action: #bf8ecc;
|
||||
--onAction: #10141a;
|
||||
--accent: #d3c388;
|
||||
--focus: #bf8ecc;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="aubergine"][data-mode="dim"] {
|
||||
--canvas: #312933;
|
||||
--surface: #3e3441;
|
||||
--raised: #4c3f50;
|
||||
--line: #5f4f63;
|
||||
--border: #b3a4b7;
|
||||
--text: #e1d9e3;
|
||||
--muted: #baacbe;
|
||||
--action: #cba4d6;
|
||||
--onAction: #10141a;
|
||||
--accent: #d3c388;
|
||||
--focus: #cba4d6;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #ea9d99;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Mineral */
|
||||
|
||||
:root[data-palette="mineral"] {
|
||||
--canvas: #f3f6f6;
|
||||
--surface: #ffffff;
|
||||
--raised: #e8eded;
|
||||
--line: #d0dcda;
|
||||
--border: #637e7a;
|
||||
--text: #3d524f;
|
||||
--muted: #586f6c;
|
||||
--action: #2a7469;
|
||||
--onAction: #ffffff;
|
||||
--accent: #9f414f;
|
||||
--focus: #2a7469;
|
||||
--success: #287658;
|
||||
--warning: #935f1a;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="mineral"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #141a19;
|
||||
--surface: #1f2927;
|
||||
--raised: #2c3a38;
|
||||
--line: #3d514e;
|
||||
--border: #a4b7b4;
|
||||
--text: #b9cbc8;
|
||||
--muted: #a4b7b4;
|
||||
--action: #87d4c8;
|
||||
--onAction: #10141a;
|
||||
--accent: #d18f99;
|
||||
--focus: #87d4c8;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e58580;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="mineral"][data-mode="dark"] {
|
||||
--canvas: #141a19;
|
||||
--surface: #1f2927;
|
||||
--raised: #2c3a38;
|
||||
--line: #3d514e;
|
||||
--border: #a4b7b4;
|
||||
--text: #b9cbc8;
|
||||
--muted: #a4b7b4;
|
||||
--action: #87d4c8;
|
||||
--onAction: #10141a;
|
||||
--accent: #d18f99;
|
||||
--focus: #87d4c8;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e58580;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="mineral"][data-mode="dim"] {
|
||||
--canvas: #273432;
|
||||
--surface: #324340;
|
||||
--raised: #3d514e;
|
||||
--line: #4d6662;
|
||||
--border: #a4b7b4;
|
||||
--text: #e5ebea;
|
||||
--muted: #b2c2c0;
|
||||
--action: #87d4c8;
|
||||
--onAction: #10141a;
|
||||
--accent: #e0b3ba;
|
||||
--focus: #87d4c8;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #eeaeaa;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Cobalt */
|
||||
|
||||
:root[data-palette="cobalt"] {
|
||||
--canvas: #f3f3f7;
|
||||
--surface: #ffffff;
|
||||
--raised: #e6e8ef;
|
||||
--line: #cdd1df;
|
||||
--border: #63687e;
|
||||
--text: #454a5e;
|
||||
--muted: #60657b;
|
||||
--action: #1c3bc4;
|
||||
--onAction: #ffffff;
|
||||
--accent: #79671b;
|
||||
--focus: #1c3bc4;
|
||||
--success: #277255;
|
||||
--warning: #8f5c19;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="cobalt"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #12141c;
|
||||
--surface: #1c1f2c;
|
||||
--raised: #282c3e;
|
||||
--line: #383d57;
|
||||
--border: #a4a7b7;
|
||||
--text: #b6b9c9;
|
||||
--muted: #a4a7b7;
|
||||
--action: #798eec;
|
||||
--onAction: #10141a;
|
||||
--accent: #e1ce7a;
|
||||
--focus: #798eec;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="cobalt"][data-mode="dark"] {
|
||||
--canvas: #12141c;
|
||||
--surface: #1c1f2c;
|
||||
--raised: #282c3e;
|
||||
--line: #383d57;
|
||||
--border: #a4a7b7;
|
||||
--text: #b6b9c9;
|
||||
--muted: #a4a7b7;
|
||||
--action: #798eec;
|
||||
--onAction: #10141a;
|
||||
--accent: #e1ce7a;
|
||||
--focus: #798eec;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="cobalt"][data-mode="dim"] {
|
||||
--canvas: #242838;
|
||||
--surface: #2e3248;
|
||||
--raised: #383d57;
|
||||
--line: #464d6d;
|
||||
--border: #a4a7b7;
|
||||
--text: #d0d2dc;
|
||||
--muted: #a6aab9;
|
||||
--action: #94a5f0;
|
||||
--onAction: #10141a;
|
||||
--accent: #e1ce7a;
|
||||
--focus: #94a5f0;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e89591;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Rosewood */
|
||||
|
||||
:root[data-palette="rosewood"] {
|
||||
--canvas: #f6f3f5;
|
||||
--surface: #ffffff;
|
||||
--raised: #ede8eb;
|
||||
--line: #dcd0d6;
|
||||
--border: #7e6371;
|
||||
--text: #5e4552;
|
||||
--muted: #7b606e;
|
||||
--action: #a53b72;
|
||||
--onAction: #ffffff;
|
||||
--accent: #416c95;
|
||||
--focus: #a53b72;
|
||||
--success: #287658;
|
||||
--warning: #8f5c19;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="rosewood"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #1a1417;
|
||||
--surface: #291f24;
|
||||
--raised: #3a2c33;
|
||||
--line: #513d48;
|
||||
--border: #b7a4ae;
|
||||
--text: #cbb9c2;
|
||||
--muted: #b7a4ae;
|
||||
--action: #d487af;
|
||||
--onAction: #10141a;
|
||||
--accent: #8eaecd;
|
||||
--focus: #d487af;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="rosewood"][data-mode="dark"] {
|
||||
--canvas: #1a1417;
|
||||
--surface: #291f24;
|
||||
--raised: #3a2c33;
|
||||
--line: #513d48;
|
||||
--border: #b7a4ae;
|
||||
--text: #cbb9c2;
|
||||
--muted: #b7a4ae;
|
||||
--action: #d487af;
|
||||
--onAction: #10141a;
|
||||
--accent: #8eaecd;
|
||||
--focus: #d487af;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="rosewood"][data-mode="dim"] {
|
||||
--canvas: #34272e;
|
||||
--surface: #43323b;
|
||||
--raised: #513d48;
|
||||
--line: #664d5a;
|
||||
--border: #b7a4ae;
|
||||
--text: #e1d6db;
|
||||
--muted: #beacb5;
|
||||
--action: #dc9ebe;
|
||||
--onAction: #10141a;
|
||||
--accent: #95b3d0;
|
||||
--focus: #dc9ebe;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #ea9d99;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Graphite */
|
||||
|
||||
:root[data-palette="graphite"] {
|
||||
--canvas: #f5f5f5;
|
||||
--surface: #ffffff;
|
||||
--raised: #eaeaeb;
|
||||
--line: #d5d6d7;
|
||||
--border: #636e7e;
|
||||
--text: #434d5b;
|
||||
--muted: #5e6978;
|
||||
--action: #616975;
|
||||
--onAction: #ffffff;
|
||||
--accent: #5e6978;
|
||||
--focus: #616975;
|
||||
--success: #287658;
|
||||
--warning: #8f5c19;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="graphite"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #161718;
|
||||
--surface: #232425;
|
||||
--raised: #323334;
|
||||
--line: #454749;
|
||||
--border: #a4abb7;
|
||||
--text: #bcc3cd;
|
||||
--muted: #a4abb7;
|
||||
--action: #a6acb5;
|
||||
--onAction: #10141a;
|
||||
--accent: #a4abb7;
|
||||
--focus: #a6acb5;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="graphite"][data-mode="dark"] {
|
||||
--canvas: #161718;
|
||||
--surface: #232425;
|
||||
--raised: #323334;
|
||||
--line: #454749;
|
||||
--border: #a4abb7;
|
||||
--text: #bcc3cd;
|
||||
--muted: #a4abb7;
|
||||
--action: #a6acb5;
|
||||
--onAction: #10141a;
|
||||
--accent: #a4abb7;
|
||||
--focus: #a6acb5;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e37d78;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="graphite"][data-mode="dim"] {
|
||||
--canvas: #2d2e2f;
|
||||
--surface: #393a3c;
|
||||
--raised: #454749;
|
||||
--line: #57595c;
|
||||
--border: #a4abb7;
|
||||
--text: #dfe2e7;
|
||||
--muted: #afb6c0;
|
||||
--action: #b1b6be;
|
||||
--onAction: #10141a;
|
||||
--accent: #afb6c0;
|
||||
--focus: #b1b6be;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #eba29e;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
/* Grove */
|
||||
|
||||
:root[data-palette="grove"] {
|
||||
--canvas: #f4f6f5;
|
||||
--surface: #ffffff;
|
||||
--raised: #e8edea;
|
||||
--line: #d2dbd6;
|
||||
--border: #637e6e;
|
||||
--text: #3d5246;
|
||||
--muted: #586f62;
|
||||
--action: #377752;
|
||||
--onAction: #ffffff;
|
||||
--accent: #9c444a;
|
||||
--focus: #377752;
|
||||
--success: #287658;
|
||||
--warning: #935f1a;
|
||||
--danger: #ba2e26;
|
||||
--strong: color-mix(in srgb, var(--text) 72%, #000);
|
||||
color-scheme: light;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root[data-palette="grove"]:not([data-mode="light"]):not([data-mode="dim"]) {
|
||||
--canvas: #141917;
|
||||
--surface: #202823;
|
||||
--raised: #2d3932;
|
||||
--line: #3f4f46;
|
||||
--border: #a4b7ac;
|
||||
--text: #b9cbc1;
|
||||
--muted: #a4b7ac;
|
||||
--action: #8fcca9;
|
||||
--onAction: #10141a;
|
||||
--accent: #cd8e92;
|
||||
--focus: #8fcca9;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e58580;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
}
|
||||
|
||||
:root[data-palette="grove"][data-mode="dark"] {
|
||||
--canvas: #141917;
|
||||
--surface: #202823;
|
||||
--raised: #2d3932;
|
||||
--line: #3f4f46;
|
||||
--border: #a4b7ac;
|
||||
--text: #b9cbc1;
|
||||
--muted: #a4b7ac;
|
||||
--action: #8fcca9;
|
||||
--onAction: #10141a;
|
||||
--accent: #cd8e92;
|
||||
--focus: #8fcca9;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #e58580;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
|
||||
:root[data-palette="grove"][data-mode="dim"] {
|
||||
--canvas: #29332d;
|
||||
--surface: #34413a;
|
||||
--raised: #3f4f46;
|
||||
--line: #4f6358;
|
||||
--border: #a4b7ac;
|
||||
--text: #e2e9e5;
|
||||
--muted: #afc0b6;
|
||||
--action: #8fcca9;
|
||||
--onAction: #10141a;
|
||||
--accent: #ddb1b4;
|
||||
--focus: #8fcca9;
|
||||
--success: #85d5b7;
|
||||
--warning: #e7b574;
|
||||
--danger: #edaaa6;
|
||||
--strong: color-mix(in srgb, var(--text) 70%, #fff);
|
||||
color-scheme: dark;
|
||||
}
|
||||
@@ -21,8 +21,10 @@ const root = resolve(import.meta.dirname, 'public');
|
||||
const files = new Map([
|
||||
['/', ['index.html', 'text/html; charset=utf-8']],
|
||||
...['app.js', 'brand.js', 'bus.js'].map(f => ['/' + f, [f, 'text/javascript; charset=utf-8']]),
|
||||
...['shared/app.css', 'console.css', 'live.css', 'bus.css'].map(f => ['/' + f, [f, 'text/css; charset=utf-8']]),
|
||||
...['shared/app.css', 'tokens.css', 'console.css', 'live.css', 'bus.css', 'shell.css'].map(f => ['/' + f, [f, 'text/css; charset=utf-8']]),
|
||||
['/icons.svg', ['icons.svg', 'image/svg+xml']],
|
||||
...[400, 500, 600, 700].map(w => [`/assets/fonts/manrope-${w}.woff2`, [`assets/fonts/manrope-${w}.woff2`, 'font/woff2']]),
|
||||
...[400, 500, 700].map(w => [`/assets/fonts/jetbrains-mono-${w}.woff2`, [`assets/fonts/jetbrains-mono-${w}.woff2`, 'font/woff2']]),
|
||||
]);
|
||||
function json(res, status, body) {
|
||||
res.writeHead(status, { 'content-type': 'application/json', 'cache-control': 'no-store' });
|
||||
|
||||
Reference in New Issue
Block a user