@giamme/forge 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +106 -1766
- package/SKILL.md +3 -2
- package/VERSION +1 -1
- package/docs/README.md +26 -0
- package/docs/add-ons.md +33 -0
- package/docs/artifacts.md +69 -0
- package/docs/commands.md +315 -0
- package/docs/dataset.md +20 -0
- package/docs/decomposed-runs.md +268 -0
- package/docs/development.md +95 -0
- package/docs/forge-ui.md +92 -0
- package/docs/fractal.md +181 -0
- package/docs/install.md +160 -0
- package/docs/memory.md +156 -0
- package/docs/models.md +118 -0
- package/docs/reviews.md +231 -0
- package/docs/safety.md +55 -0
- package/docs/troubleshooting.md +54 -0
- package/forge +5 -0
- package/package.json +3 -2
- package/references/decompose.md +1 -1
- package/references/fractal.md +1 -1
- package/references/harnesses.md +18 -6
- package/references/solo.md +1 -1
- package/scripts/forge-dispatch.sh +7 -1
- package/scripts/forge-runs.py +121 -0
- package/scripts/forge-runtime.py +59 -6
- package/scripts/forge-version.py +54 -12
- package/scripts/forge_dataset/harvest.py +1 -1
- package/scripts/forge_dataset/importer.py +1 -1
- package/scripts/forge_fractal/__init__.py +2 -1
- package/ui/README.md +80 -20
- package/ui/THIRD_PARTY_NOTICES.md +108 -0
- package/ui/docs/api.md +99 -17
- package/ui/forge_ui/export.py +16 -6
- package/ui/forge_ui/fractal.py +1 -2
- package/ui/forge_ui/reader.py +95 -7
- package/ui/forge_ui/server.py +10 -1
- package/ui/forge_ui/static/D-DINCondensed-Bold.woff2 +0 -0
- package/ui/forge_ui/static/app.js +279 -124
- package/ui/forge_ui/static/cloud3d.js +3 -2
- package/ui/forge_ui/static/cloudui.js +8 -5
- package/ui/forge_ui/static/convo.js +295 -0
- package/ui/forge_ui/static/datatable.js +232 -0
- package/ui/forge_ui/static/diffview.js +29 -10
- package/ui/forge_ui/static/findings3d.js +134 -0
- package/ui/forge_ui/static/graph.js +3 -3
- package/ui/forge_ui/static/icons.js +94 -0
- package/ui/forge_ui/static/index.html +25 -13
- package/ui/forge_ui/static/iso.js +70 -19
- package/ui/forge_ui/static/leaderboard.js +44 -48
- package/ui/forge_ui/static/panel.js +68 -32
- package/ui/forge_ui/static/runmodel.js +17 -1
- package/ui/forge_ui/static/scene3d.js +518 -43
- package/ui/forge_ui/static/search.js +2 -1
- package/ui/forge_ui/static/shell.js +137 -0
- package/ui/forge_ui/static/stagectl.js +150 -0
- package/ui/forge_ui/static/style.css +924 -469
- package/ui/forge_ui/static/theme.js +25 -0
- package/ui/forge_ui/transcripts.py +608 -0
|
@@ -107,7 +107,8 @@ window.ForgeSearch = (() => {
|
|
|
107
107
|
dlg.addEventListener('click', e => { if (e.target === dlg) close(); }); // the backdrop
|
|
108
108
|
document.addEventListener('keydown', e => {
|
|
109
109
|
if ((e.key === 'k' || e.key === 'K') && (e.ctrlKey || e.metaKey) && !e.altKey) { e.preventDefault(); if (dlg.open) close(); else open(); }
|
|
110
|
-
|
|
110
|
+
// a view that handles / itself (a conversation's search) keeps it
|
|
111
|
+
else if (e.key === '/' && !e.defaultPrevented && !e.ctrlKey && !e.metaKey && !e.altKey && !dlg.open && !typing(e.target)) { e.preventDefault(); open(); }
|
|
111
112
|
});
|
|
112
113
|
for (const b of document.querySelectorAll('[data-jump]')) {
|
|
113
114
|
b.addEventListener('click', open);
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/* forge-ui shell: Immersive (modules float over a full-bleed stage) or Workbench (modules docked, resized by
|
|
2
|
+
splitters), which modules are open, and their sizes. Loaded at the top of <body> without defer, so the page's first
|
|
3
|
+
paint already has the user's layout; the controls are wired once the document is parsed.
|
|
4
|
+
State on <body>: data-shell="immersive|workbench", data-runs / data-insp / data-tasks="open|closed", and the sizes as
|
|
5
|
+
--runs-w, --insp-w, --tasks-h. Keys 1, 2, 3 toggle the runs, inspector and tasks modules (digits need no Shift on US
|
|
6
|
+
or Italian keyboards). On a first visit the inspector and the tasks start folded, so a run opens on its plate; choosing
|
|
7
|
+
something opens the inspector (app.js), and once a module is opened or folded that choice is kept.
|
|
8
|
+
Remembered in localStorage under forge-ui-shell.
|
|
9
|
+
window.ForgeShell = { state, toggle(panel, open?), setShell(name), head(panel, title, count?), splitter(panel), onChange(fn) } */
|
|
10
|
+
window.ForgeShell = (() => {
|
|
11
|
+
'use strict';
|
|
12
|
+
const KEY = 'forge-ui-shell', body = document.body;
|
|
13
|
+
const PANELS = { runs: { label: 'Runs', key: '1', icon: 'panel-left' }, insp: { label: 'Inspector', key: '2', icon: 'panel-right' }, tasks: { label: 'Tasks', key: '3', icon: 'panel-bottom' } };
|
|
14
|
+
const SIZE = { runs: ['--runs-w', 220, 560, 300], insp: ['--insp-w', 300, 760, 380], tasks: ['--tasks-h', 120, 900, 208] };
|
|
15
|
+
const read = () => { try { const v = JSON.parse(localStorage.getItem(KEY)); return v && typeof v === 'object' ? v : {}; } catch (_) { return {}; } };
|
|
16
|
+
const saved = read();
|
|
17
|
+
const narrow = window.matchMedia && window.matchMedia('(max-width: 760px)').matches;
|
|
18
|
+
const state = {
|
|
19
|
+
shell: saved.shell === 'workbench' ? 'workbench' : 'immersive',
|
|
20
|
+
runs: typeof saved.runs === 'boolean' ? saved.runs : !narrow, insp: saved.insp === true, tasks: saved.tasks === true,
|
|
21
|
+
size: { runs: num(saved.size && saved.size.runs, 'runs'), insp: num(saved.size && saved.size.insp, 'insp'), tasks: num(saved.size && saved.size.tasks, 'tasks') },
|
|
22
|
+
};
|
|
23
|
+
function num(v, panel) { const [, lo, hi, d] = SIZE[panel]; return typeof v === 'number' && Number.isFinite(v) ? Math.max(lo, Math.min(hi, Math.round(v))) : d; }
|
|
24
|
+
const listeners = new Set();
|
|
25
|
+
const icon = name => window.ForgeIcons ? window.ForgeIcons.icon(name) : document.createTextNode('');
|
|
26
|
+
function save() { try { localStorage.setItem(KEY, JSON.stringify(state)); } catch (_) { /* private mode: not remembered */ } }
|
|
27
|
+
function apply() {
|
|
28
|
+
body.dataset.shell = state.shell;
|
|
29
|
+
for (const p of Object.keys(PANELS)) body.dataset[p] = state[p] ? 'open' : 'closed';
|
|
30
|
+
for (const [p, [prop]] of Object.entries(SIZE)) body.style.setProperty(prop, state.size[p] + 'px');
|
|
31
|
+
for (const b of document.querySelectorAll('[data-panel-toggle]')) {
|
|
32
|
+
const p = b.dataset.panelToggle; b.setAttribute('aria-pressed', String(!!state[p]));
|
|
33
|
+
b.title = `${state[p] ? 'Hide' : 'Show'} the ${PANELS[p].label.toLowerCase()} (${PANELS[p].key})`;
|
|
34
|
+
}
|
|
35
|
+
for (const b of document.querySelectorAll('[data-shell-pick]')) b.setAttribute('aria-pressed', String(b.dataset.shellPick === state.shell));
|
|
36
|
+
for (const b of document.querySelectorAll('[data-theme-pick]')) b.setAttribute('aria-pressed', String(window.ForgeTheme && b.dataset.themePick === window.ForgeTheme.get()));
|
|
37
|
+
for (const s of document.querySelectorAll('.splitter[data-panel]')) s.setAttribute('aria-valuenow', String(state.size[s.dataset.panel]));
|
|
38
|
+
}
|
|
39
|
+
function changed() { apply(); save(); for (const f of listeners) try { f(state); } catch (_) { /* a listener's own problem */ } }
|
|
40
|
+
function toggle(panel, open) { if (!PANELS[panel]) return; state[panel] = open === undefined ? !state[panel] : !!open; changed(); }
|
|
41
|
+
function setShell(name) { state.shell = name === 'workbench' ? 'workbench' : 'immersive'; changed(); }
|
|
42
|
+
apply();
|
|
43
|
+
|
|
44
|
+
function el(tag, cls, text) { const n = document.createElement(tag); if (cls) n.className = cls; if (text !== undefined) n.textContent = text; return n; }
|
|
45
|
+
function bank(label, cls) { const g = el('div', 'seg ' + cls); g.setAttribute('role', 'group'); g.setAttribute('aria-label', label); return g; }
|
|
46
|
+
// a module's head: its plate, an optional count, room for tools, and the key that folds it away
|
|
47
|
+
function head(panel, title, count) {
|
|
48
|
+
const h = el('div', 'module-head');
|
|
49
|
+
const plate = el('h2', 'module-title', title); h.append(plate);
|
|
50
|
+
if (count !== undefined) h.append(el('span', 'module-count', count));
|
|
51
|
+
const tools = el('span', 'module-tools');
|
|
52
|
+
tools.append(foldButton(panel));
|
|
53
|
+
h.append(tools);
|
|
54
|
+
return h;
|
|
55
|
+
}
|
|
56
|
+
function foldButton(panel) {
|
|
57
|
+
const b = el('button', 'icon-btn fold-btn'); b.type = 'button'; b.dataset.fold = panel;
|
|
58
|
+
const sync = () => { b.replaceChildren(icon(state[panel] ? (panel === 'tasks' ? 'chevron-down' : 'close') : panel === 'tasks' ? 'chevron-up' : PANELS[panel].icon));
|
|
59
|
+
b.setAttribute('aria-label', `${state[panel] ? 'Fold away' : 'Open'} the ${PANELS[panel].label.toLowerCase()} (${PANELS[panel].key})`); b.title = b.getAttribute('aria-label'); };
|
|
60
|
+
sync(); listeners.add(() => { if (b.isConnected) sync(); });
|
|
61
|
+
b.addEventListener('click', () => toggle(panel));
|
|
62
|
+
return b;
|
|
63
|
+
}
|
|
64
|
+
// a splitter on a module's inner edge: drag it, or focus it and use the arrow keys
|
|
65
|
+
function splitter(panel) {
|
|
66
|
+
const [prop, lo, hi] = SIZE[panel], rows = panel === 'tasks';
|
|
67
|
+
const s = el('div', 'splitter ' + (rows ? 'h' : 'v'));
|
|
68
|
+
s.dataset.panel = panel; s.tabIndex = 0;
|
|
69
|
+
s.setAttribute('role', 'separator'); s.setAttribute('aria-orientation', rows ? 'horizontal' : 'vertical');
|
|
70
|
+
s.setAttribute('aria-label', `Resize the ${PANELS[panel].label.toLowerCase()}`);
|
|
71
|
+
s.setAttribute('aria-valuemin', String(lo)); s.setAttribute('aria-valuemax', String(hi)); s.setAttribute('aria-valuenow', String(state.size[panel]));
|
|
72
|
+
const set = v => { state.size[panel] = num(v, panel); body.style.setProperty(prop, state.size[panel] + 'px'); s.setAttribute('aria-valuenow', String(state.size[panel])); };
|
|
73
|
+
let start = null;
|
|
74
|
+
s.addEventListener('pointerdown', e => {
|
|
75
|
+
if (e.button !== 0) return;
|
|
76
|
+
e.preventDefault(); s.setPointerCapture(e.pointerId); s.classList.add('dragging'); body.classList.add('resizing'); body.classList.toggle('rows', rows);
|
|
77
|
+
start = { x: e.clientX, y: e.clientY, v: state.size[panel] };
|
|
78
|
+
});
|
|
79
|
+
s.addEventListener('pointermove', e => {
|
|
80
|
+
if (!start) return;
|
|
81
|
+
const d = rows ? start.y - e.clientY : panel === 'insp' ? start.x - e.clientX : e.clientX - start.x;
|
|
82
|
+
set(start.v + d);
|
|
83
|
+
});
|
|
84
|
+
const end = () => { if (!start) return; start = null; s.classList.remove('dragging'); body.classList.remove('resizing', 'rows'); changed(); };
|
|
85
|
+
s.addEventListener('pointerup', end); s.addEventListener('pointercancel', end);
|
|
86
|
+
s.addEventListener('keydown', e => {
|
|
87
|
+
const grow = rows ? 'ArrowUp' : panel === 'insp' ? 'ArrowLeft' : 'ArrowRight', shrink = rows ? 'ArrowDown' : panel === 'insp' ? 'ArrowRight' : 'ArrowLeft';
|
|
88
|
+
const step = e.shiftKey ? 64 : 16;
|
|
89
|
+
if (e.key === grow) set(state.size[panel] + step); else if (e.key === shrink) set(state.size[panel] - step);
|
|
90
|
+
else if (e.key === 'Home') set(lo); else if (e.key === 'End') set(hi); else return;
|
|
91
|
+
e.preventDefault(); changed();
|
|
92
|
+
});
|
|
93
|
+
return s;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function wire() {
|
|
97
|
+
const tools = document.querySelector('.desk-tools');
|
|
98
|
+
if (tools) {
|
|
99
|
+
const panels = bank('Modules', 'panel-bank');
|
|
100
|
+
for (const [p, d] of Object.entries(PANELS)) {
|
|
101
|
+
const b = el('button'); b.type = 'button'; b.dataset.panelToggle = p; b.dataset.key = d.key;
|
|
102
|
+
b.append(icon(d.icon), el('span', 'panel-label', d.label)); panels.append(b); // the label, or on a narrow desk the panel's picture
|
|
103
|
+
b.addEventListener('click', () => toggle(p));
|
|
104
|
+
}
|
|
105
|
+
const shells = bank('Layout', 'icons shell-switch');
|
|
106
|
+
for (const [name, label] of [['immersive', 'Immersive: modules float over the stage'], ['workbench', 'Workbench: modules docked beside the stage']]) {
|
|
107
|
+
const b = el('button'); b.type = 'button'; b.dataset.shellPick = name; b.setAttribute('aria-label', label); b.title = label;
|
|
108
|
+
b.append(icon(name)); b.addEventListener('click', () => setShell(name)); shells.append(b);
|
|
109
|
+
}
|
|
110
|
+
const themes = bank('Theme', 'icons theme-switch');
|
|
111
|
+
for (const [name, label] of [['system', 'Theme: follow the system'], ['light', 'Theme: light'], ['dark', 'Theme: dark']]) {
|
|
112
|
+
const b = el('button'); b.type = 'button'; b.dataset.themePick = name; b.setAttribute('aria-label', label); b.title = label;
|
|
113
|
+
b.append(icon(name === 'light' ? 'sun' : name === 'dark' ? 'moon' : 'system'));
|
|
114
|
+
b.addEventListener('click', () => { if (window.ForgeTheme) window.ForgeTheme.set(name); apply(); }); themes.append(b);
|
|
115
|
+
}
|
|
116
|
+
tools.prepend(panels, shells, themes);
|
|
117
|
+
}
|
|
118
|
+
const bell = document.getElementById('bell'); if (bell && !bell.querySelector('.icon')) bell.prepend(icon('bell'));
|
|
119
|
+
const search = document.querySelector('.top-search-label'); if (search && !search.querySelector('.icon')) search.prepend(icon('search'));
|
|
120
|
+
const side = document.querySelector('.sidebar');
|
|
121
|
+
if (side) {
|
|
122
|
+
const t = side.querySelector('.module-tools'); if (t) t.append(foldButton('runs'));
|
|
123
|
+
side.append(splitter('runs'));
|
|
124
|
+
}
|
|
125
|
+
document.addEventListener('keydown', e => {
|
|
126
|
+
if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey || e.repeat) return;
|
|
127
|
+
const t = e.target, typing = t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));
|
|
128
|
+
if (typing || document.querySelector('dialog[open]')) return;
|
|
129
|
+
const p = Object.keys(PANELS).find(k => PANELS[k].key === e.key);
|
|
130
|
+
if (p) { e.preventDefault(); toggle(p); }
|
|
131
|
+
});
|
|
132
|
+
apply();
|
|
133
|
+
}
|
|
134
|
+
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', wire, { once: true }); else wire();
|
|
135
|
+
|
|
136
|
+
return { state, toggle, setShell, head, splitter, onChange: f => listeners.add(f) };
|
|
137
|
+
})();
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/* forge-ui stage controls: the bank of keys over the 3D stage and the keyboard behind it. Fit (F), zoom (+ −),
|
|
2
|
+
projection (V: perspective, true isometric, top-down), reset (0), full screen, the legend of what the stage
|
|
3
|
+
draws now (L), the route (I) and the sheet of every key (?). scene3d.js says what each key does and what the
|
|
4
|
+
legend holds (ctl); this module only draws the bank, the legend and the sheet, with createElement and
|
|
5
|
+
textContent like the rest of the page. The keys work wherever focus is, while the stage is on screen and
|
|
6
|
+
nothing is being typed. */
|
|
7
|
+
export const PROJECTIONS = [['persp', 'Perspective', 'persp'], ['iso', 'Isometric', 'iso'], ['top', 'Top-down', 'top']];
|
|
8
|
+
// the sheet: [group, [[keys, what]]]
|
|
9
|
+
const SHEET = [
|
|
10
|
+
['Modules', [['1', 'Runs'], ['2', 'Inspector'], ['3', 'Tasks']]],
|
|
11
|
+
['Stage', [['F', 'Fit what is shown'], ['+ −', 'Zoom in, out'], ['V', 'Perspective, isometric, top-down'], ['0', 'Reset the view'],
|
|
12
|
+
['L', 'Legend'], ['I', 'Route: keep a chosen task’s route lit'], ['Shift F', 'Full screen'], ['?', 'These keys']]],
|
|
13
|
+
['With the stage focused', [['← →', 'Previous, next task (on the map: run)'], ['↑ ↓', 'On the map: previous, next repository'], ['Enter', 'On the map: open the run'],
|
|
14
|
+
['E', 'Plan or execution'], ['Esc', 'Clear the selection, then back to the map']]],
|
|
15
|
+
['In a diff', [['n p', 'Next, previous finding'], ['j k', 'Next, previous file']]],
|
|
16
|
+
['In a conversation', [['/', 'Search what it said and ran'], ['E', 'Next error']]],
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const el = (tag, cls, text) => { const n = document.createElement(tag); if (cls) n.className = cls; if (text != null) n.textContent = text; return n; };
|
|
20
|
+
const icon = name => window.ForgeIcons ? window.ForgeIcons.icon(name) : el('span');
|
|
21
|
+
const typing = t => t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName));
|
|
22
|
+
|
|
23
|
+
// ctl: { view() → 'run'|'atlas'|'cloud'|null, fit(), reset(), zoom(k), projection, setProjection(name),
|
|
24
|
+
// legend() → [{title, note?, rows: [{sw, label}]}], toggleRoute() }
|
|
25
|
+
export function createControls(host, ctl) {
|
|
26
|
+
const bank = el('div', 'stage-ctl'); bank.setAttribute('role', 'toolbar'); bank.setAttribute('aria-label', 'Stage view'); bank.setAttribute('aria-orientation', 'vertical');
|
|
27
|
+
const key = (name, label, keys, fn, ico = name) => {
|
|
28
|
+
const b = el('button', 'icon-btn'); b.type = 'button'; b.dataset.ctl = name;
|
|
29
|
+
b.setAttribute('aria-label', label); b.title = `${label} (${keys})`; b.setAttribute('aria-keyshortcuts', keys);
|
|
30
|
+
b.append(icon(ico)); b.addEventListener('click', fn); return b;
|
|
31
|
+
};
|
|
32
|
+
// momentary keys (fit, zoom, reset) have no lamp; a latching one lights while it is in (the projection bank, legend, full screen)
|
|
33
|
+
const latch = b => { b.classList.add('latch'); b.setAttribute('aria-pressed', 'false'); return b; };
|
|
34
|
+
const projs = el('div', 'stage-ctl-group'); projs.setAttribute('role', 'group'); projs.setAttribute('aria-label', 'Projection (V)');
|
|
35
|
+
for (const [name, label, ico] of PROJECTIONS) projs.append(latch(key('proj-' + name, label, 'V', () => { ctl.setProjection(name); sync(); }, ico)));
|
|
36
|
+
const full = latch(key('fullscreen', 'Full screen', 'Shift+F', () => fullscreen(), 'fullscreen'));
|
|
37
|
+
const leg = latch(key('legend', 'Legend', 'L', () => legend(), 'legend'));
|
|
38
|
+
leg.setAttribute('aria-expanded', 'false');
|
|
39
|
+
bank.append(
|
|
40
|
+
key('fit', 'Fit what is shown', 'F', () => ctl.fit()),
|
|
41
|
+
key('zoom-in', 'Zoom in', '+', () => ctl.zoom(1.35)),
|
|
42
|
+
key('zoom-out', 'Zoom out', '-', () => ctl.zoom(1 / 1.35)),
|
|
43
|
+
key('reset', 'Reset the view', '0', () => { ctl.reset(); sync(); }),
|
|
44
|
+
el('span', 'stage-ctl-rule'), projs, el('span', 'stage-ctl-rule'),
|
|
45
|
+
leg, full,
|
|
46
|
+
key('keys', 'Keys', '?', () => sheet(), 'keyboard'),
|
|
47
|
+
);
|
|
48
|
+
if (!document.fullscreenEnabled) full.hidden = true;
|
|
49
|
+
host.append(bank);
|
|
50
|
+
|
|
51
|
+
// the legend: what the stage draws now, rebuilt each time it opens and when the view changes under it
|
|
52
|
+
const panel = el('section', 'stage-legend'); panel.id = 'stage-legend'; panel.hidden = true; panel.setAttribute('aria-label', 'Legend');
|
|
53
|
+
leg.setAttribute('aria-controls', panel.id);
|
|
54
|
+
host.append(panel);
|
|
55
|
+
function fillLegend() {
|
|
56
|
+
const head = el('div', 'stage-legend-head'); head.append(el('h3', 'plate', 'Legend'));
|
|
57
|
+
const x = el('button', 'icon-btn'); x.type = 'button'; x.setAttribute('aria-label', 'Close the legend'); x.append(icon('close')); x.addEventListener('click', () => legend(false));
|
|
58
|
+
head.append(x);
|
|
59
|
+
const body = [head];
|
|
60
|
+
for (const sec of ctl.legend()) {
|
|
61
|
+
const s = el('div', 'lg-sec'); s.append(el('h4', 'plate lg-title', sec.title));
|
|
62
|
+
for (const r of sec.rows) { const row = el('div', 'lg-row'); row.append(swatch(r.sw), el('span', 'lg-label', r.label)); s.append(row); }
|
|
63
|
+
if (sec.note) s.append(el('p', 'lg-note', sec.note));
|
|
64
|
+
body.push(s);
|
|
65
|
+
}
|
|
66
|
+
panel.replaceChildren(...body);
|
|
67
|
+
}
|
|
68
|
+
function swatch(sw = {}) {
|
|
69
|
+
const s = el('span', 'lg-sw');
|
|
70
|
+
if (sw.lamp) { s.classList.add('lg-lamp'); s.append(el('i', 'lamp ' + sw.lamp)); }
|
|
71
|
+
else if (sw.route) { s.classList.add('lg-lamp'); s.append(el('i', 'lamp lamp-route')); }
|
|
72
|
+
else if (sw.sev) { s.classList.add('lg-sev', 'sev-' + sw.sev); if (sw.pale) s.classList.add('pale'); }
|
|
73
|
+
else if (sw.cable) s.classList.add('lg-cable', 'cable-' + sw.cable);
|
|
74
|
+
else if (sw.bead) s.classList.add('lg-bead');
|
|
75
|
+
else if (sw.ring) s.classList.add('lg-ring');
|
|
76
|
+
else if (sw.color) { s.classList.add('lg-color'); s.style.setProperty('--sw', sw.color); }
|
|
77
|
+
else if (sw.scale) { s.classList.add('lg-scale'); s.append(el('span', 'lg-min', sw.scale[0]), el('span', 'lg-bar'), el('span', 'lg-max', sw.scale[1])); }
|
|
78
|
+
else if (sw.height) { s.classList.add('lg-height'); s.append(el('i'), el('i'), el('i')); }
|
|
79
|
+
return s;
|
|
80
|
+
}
|
|
81
|
+
function legend(open = panel.hidden) {
|
|
82
|
+
if (open) fillLegend();
|
|
83
|
+
panel.hidden = !open; leg.setAttribute('aria-expanded', String(open)); leg.setAttribute('aria-pressed', String(open));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// the projection in use is the bank's lit key
|
|
87
|
+
function sync() {
|
|
88
|
+
for (const b of projs.children) b.setAttribute('aria-pressed', String(b.dataset.ctl === 'proj-' + ctl.projection));
|
|
89
|
+
full.setAttribute('aria-pressed', String(document.fullscreenElement === host));
|
|
90
|
+
const v = ctl.view(); bank.hidden = v !== 'run' && v !== 'atlas';
|
|
91
|
+
if (bank.hidden && !panel.hidden) legend(false);
|
|
92
|
+
else if (!panel.hidden) fillLegend();
|
|
93
|
+
}
|
|
94
|
+
function cycle() {
|
|
95
|
+
const i = PROJECTIONS.findIndex(x => x[0] === ctl.projection);
|
|
96
|
+
ctl.setProjection(PROJECTIONS[(i + 1) % PROJECTIONS.length][0]); sync();
|
|
97
|
+
}
|
|
98
|
+
function fullscreen() {
|
|
99
|
+
if (document.fullscreenElement === host) document.exitFullscreen();
|
|
100
|
+
else if (host.requestFullscreen) host.requestFullscreen().catch(() => {});
|
|
101
|
+
}
|
|
102
|
+
const onFull = () => sync(); // the engine refits a framing the user has not moved
|
|
103
|
+
document.addEventListener('fullscreenchange', onFull);
|
|
104
|
+
|
|
105
|
+
// the sheet of every key, a dialog made the first time it opens
|
|
106
|
+
let dlg = null;
|
|
107
|
+
function sheet() {
|
|
108
|
+
if (!dlg) {
|
|
109
|
+
dlg = el('dialog', 'keys-sheet'); dlg.setAttribute('aria-label', 'Keys');
|
|
110
|
+
const head = el('div', 'keys-head'); head.append(el('span', 'plate', 'Keys'));
|
|
111
|
+
const x = el('button', 'icon-btn'); x.type = 'button'; x.setAttribute('aria-label', 'Close'); x.append(icon('close')); x.addEventListener('click', () => dlg.close());
|
|
112
|
+
head.append(x); dlg.append(head);
|
|
113
|
+
const grid = el('div', 'keys-grid');
|
|
114
|
+
for (const [title, rows] of SHEET) {
|
|
115
|
+
const g = el('section', 'keys-group'); g.append(el('h4', 'plate lg-title', title));
|
|
116
|
+
const dl = el('dl');
|
|
117
|
+
for (const [k, what] of rows) { const dt = el('dt'); for (const part of k.split(' ')) dt.append(el('kbd', null, part)); dl.append(dt, el('dd', null, what)); }
|
|
118
|
+
g.append(dl); grid.append(g);
|
|
119
|
+
}
|
|
120
|
+
dlg.append(grid);
|
|
121
|
+
dlg.addEventListener('click', e => { if (e.target === dlg) dlg.close(); }); // a click on the backdrop
|
|
122
|
+
document.body.append(dlg); // a modal dialog is drawn over a full-screen stage too
|
|
123
|
+
}
|
|
124
|
+
if (!dlg.open) dlg.showModal();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// the keys: anywhere on the page while the stage is on screen and nothing is being typed
|
|
128
|
+
const shown = () => host.isConnected && !host.hidden && host.getClientRects().length > 0 && !bank.hidden;
|
|
129
|
+
function onKey(e) {
|
|
130
|
+
if (e.defaultPrevented || e.metaKey || e.ctrlKey || e.altKey || typing(e.target)) return;
|
|
131
|
+
if (document.querySelector('dialog[open]') && !(dlg && dlg.open)) return;
|
|
132
|
+
if (!shown()) return;
|
|
133
|
+
const k = e.key;
|
|
134
|
+
if (dlg && dlg.open) return;
|
|
135
|
+
if (k === '?') { e.preventDefault(); sheet(); return; }
|
|
136
|
+
if (e.repeat && !['+', '=', '-', '_'].includes(k)) return;
|
|
137
|
+
if (k === 'F' && e.shiftKey) { e.preventDefault(); fullscreen(); return; }
|
|
138
|
+
const act = { f: () => ctl.fit(), '+': () => ctl.zoom(1.35), '=': () => ctl.zoom(1.35), '-': () => ctl.zoom(1 / 1.35), _: () => ctl.zoom(1 / 1.35),
|
|
139
|
+
v: () => cycle(), 0: () => { ctl.reset(); sync(); }, l: () => legend(), i: () => { ctl.toggleRoute(); if (!panel.hidden) fillLegend(); } }[k.length === 1 ? k.toLowerCase() : k];
|
|
140
|
+
if (!act) return;
|
|
141
|
+
e.preventDefault(); act();
|
|
142
|
+
}
|
|
143
|
+
document.addEventListener('keydown', onKey);
|
|
144
|
+
sync();
|
|
145
|
+
return {
|
|
146
|
+
sync, bank, legend: panel,
|
|
147
|
+
get legendOpen() { return !panel.hidden; },
|
|
148
|
+
dispose() { document.removeEventListener('keydown', onKey); document.removeEventListener('fullscreenchange', onFull); bank.remove(); panel.remove(); if (dlg) dlg.remove(); },
|
|
149
|
+
};
|
|
150
|
+
}
|