koneck 2.55.0 → 2.57.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.
@@ -168,7 +168,14 @@ function setView(name) {
168
168
  if (name === 'changes') loadChanges();
169
169
  if (name === 'timing') loadTiming();
170
170
  if (name === 'files') loadTree('');
171
- if (name === 'terminal') { loadJobs(); $('tcmd').focus(); }
171
+ if (name === 'terminal') {
172
+ // Built on first sight and kept, so a pane's scrollback survives leaving the tab and coming
173
+ // back — a terminal that forgets what it printed when you look away is not a terminal.
174
+ if (term.panes.length === 0 || !term.panes[0].el) buildTerminal();
175
+ watchTerminal();
176
+ const here = term.panes[term.active];
177
+ if (here && here.el) here.el.input.focus();
178
+ }
172
179
  if (name === 'search') $('q').focus();
173
180
  if (name === 'review') loadReview();
174
181
  if (name === 'refactor') loadRefactor();
@@ -411,6 +418,20 @@ function onEvent(e) {
411
418
  // Follow it: not just which file, but which lines and which pattern.
412
419
  watchTool(e, false);
413
420
  break;
421
+ case 'tool-args': {
422
+ // Only a write has anything to show mid-flight; a read has nothing to watch. The fields were
423
+ // pulled out of the still-arriving JSON on the server, where that can be tested.
424
+ if (!follow.on || !toolWrites(e.name) || !e.path || !e.content) break;
425
+ const path = e.path;
426
+ const content = e.content;
427
+ $('watch').hidden = false;
428
+ // Whatever it was showing, the thing happening now is this file being written.
429
+ if (follow.mode !== 'work') setPanelMode('work');
430
+ watchHead('writing', path);
431
+ follow.kind = 'write';
432
+ drawWriteInProgress(path, content);
433
+ break;
434
+ }
414
435
  case 'tool-output':
415
436
  if (state.lastTool) {
416
437
  state.lastTool.body.textContent =
@@ -2606,8 +2627,17 @@ addEventListener('keydown', (ev) => {
2606
2627
  */
2607
2628
  function ansiToHtml(text) {
2608
2629
  const escChar = String.fromCharCode(27);
2630
+ /*
2631
+ * Colour is kept; everything else a terminal emits is swallowed.
2632
+ *
2633
+ * The last alternative is the one that was missing: an escape followed by a single character —
2634
+ * ESC = for keypad mode, ESC > , ESC 7 to save the cursor — has no bracket, so the CSI pattern
2635
+ * never matched it and it reached the page as a control glyph. That is what appeared at the top
2636
+ * of a pane running git.
2637
+ */
2609
2638
  const pattern = new RegExp(escChar + '\\[([0-9;]*)m|' + escChar + '\\[[0-9;?]*[A-Za-z]|'
2610
- + escChar + '\\][^\\u0007]*\\u0007', 'g');
2639
+ + escChar + '\\][^\\u0007]*(?:\\u0007|' + escChar + '\\\\)|'
2640
+ + escChar + '[=>78MDEHcZ]|[\\u0000-\\u0008\\u000b\\u000c\\u000e-\\u001f]', 'g');
2611
2641
  let out = '';
2612
2642
  let open = 0;
2613
2643
  let last = 0;
@@ -2645,96 +2675,303 @@ function ansiToHtml(text) {
2645
2675
  * matters is not a setting but a fact about the command: a test run finishes and a dev server
2646
2676
  * does not, and a runner that waits for the second one looks broken.
2647
2677
  */
2648
- const term = { selected: null, polling: null };
2678
+ /*
2679
+ * A terminal, not a form with an output box.
2680
+ *
2681
+ * What was here was a job list beside a pane of text: you typed into a field at the top, picked a
2682
+ * row, and read the result — which arrived in whole-buffer snapshots a few times a second, because
2683
+ * the view polled while the runner had been emitting a line-by-line event all along. Everything
2684
+ * about it said "web page".
2685
+ *
2686
+ * This is a scrollback: the command you typed stays above what it printed, the cursor waits at the
2687
+ * bottom, and the next thing you type continues the same column of text. Up to three panes, each
2688
+ * its own shell with its own history, because two is the common case — watch the server, run the
2689
+ * tests — and four on a laptop is four things too narrow to read.
2690
+ */
2691
+ const term = {
2692
+ panes: [],
2693
+ /** Which pane the keyboard belongs to. */
2694
+ active: 0,
2695
+ stream: null,
2696
+ /** Pane index by job id, so a line can be routed to the pane that asked for it. */
2697
+ ownerOf: {},
2698
+ fontPx: 12.5,
2699
+ };
2649
2700
 
2650
- async function loadJobs(select) {
2651
- let out;
2652
- try { out = await api('/api/jobs'); } catch (e) { return; }
2653
- const host = $('tjobs');
2701
+ function newPane() {
2702
+ return { job: null, history: [], at: -1, lines: [], running: false, el: null,
2703
+ /** Where this command's output begins, so a catch-up can replace exactly that. */
2704
+ outputStart: 0,
2705
+ /** How many output lines have arrived on the stream, for the same reason. */
2706
+ received: 0 };
2707
+ }
2708
+
2709
+ /** The pane a job's output belongs to, or null when nothing here started it. */
2710
+ function paneForJob(id) {
2711
+ const idx = term.ownerOf[id];
2712
+ return idx === undefined ? null : term.panes[idx] || null;
2713
+ }
2714
+
2715
+ function buildTerminal() {
2716
+ if (term.panes.length === 0) term.panes.push(newPane());
2717
+ const host = $('termpanes');
2654
2718
  host.innerHTML = '';
2655
- if ((out.jobs || []).length === 0) {
2656
- host.appendChild(el('div', 'wempty', 'Nothing run yet.'));
2657
- }
2658
- const pick = select || term.selected;
2659
- let anyRunning = false;
2660
- for (const j of out.jobs || []) {
2661
- if (j.state === 'running') anyRunning = true;
2662
- const row = el('div', 'jrow' + (j.id === pick ? ' on' : ''));
2663
- row.appendChild(el('div', 'jc', j.command));
2664
- const meta = el('div', 'jm');
2665
- meta.appendChild(el('span', 'js ' + j.state, j.state
2666
- + (j.exitCode !== null && j.state !== 'running' ? ' ' + j.exitCode : '')));
2667
- // Who ran it is a column, not a separate screen.
2668
- if (j.by === 'agent') meta.appendChild(el('span', null, 'by the agent'));
2669
- if (j.background) meta.appendChild(el('span', null, 'background'));
2670
- if (j.pty) meta.appendChild(el('span', null, 'tty'));
2671
- meta.appendChild(el('span', null, j.lines + (j.lines === 1 ? ' line' : ' lines')));
2672
- if (j.state === 'running' && j.by !== 'agent') {
2673
- const stop = el('button', 'jk', 'kill');
2674
- stop.onclick = async (ev) => {
2675
- ev.stopPropagation();
2676
- try { await api('/api/kill', { method: 'POST', body: JSON.stringify({ id: j.id }) }); }
2677
- catch (e) {}
2678
- loadJobs(j.id);
2679
- };
2680
- meta.appendChild(stop);
2719
+ term.panes.forEach((pane, index) => {
2720
+ if (index > 0) {
2721
+ // A draggable divider, so a pane can be made wide enough for the thing being read.
2722
+ const grip = el('div', 'termgrip');
2723
+ grip.onmousedown = (ev) => startPaneDrag(ev, index);
2724
+ host.appendChild(grip);
2681
2725
  }
2682
- row.appendChild(meta);
2683
- row.onclick = () => { term.selected = j.id; loadJobs(j.id); showJob(j.id); };
2684
- host.appendChild(row);
2685
- }
2686
- if (pick) showJob(pick);
2687
- // Polled only while something is running, and stopped when nothing is: a terminal tab left open
2688
- // should not be a request every second for the rest of the day.
2689
- if ((anyRunning || state.busy) && !term.polling) {
2690
- term.polling = setInterval(() => {
2691
- if (state.view === 'terminal') loadJobs();
2692
- else { clearInterval(term.polling); term.polling = null; }
2693
- }, 1000);
2694
- } else if (!anyRunning && !state.busy && term.polling) {
2695
- clearInterval(term.polling); term.polling = null;
2696
- }
2726
+ const box = el('div', 'termpane' + (index === term.active ? ' on' : ''));
2727
+ box.style.flex = String(pane.flex || 1);
2728
+ box.onmousedown = () => { term.active = index; markActivePane(); };
2729
+
2730
+ const scroll = el('div', 'termscroll');
2731
+ const body = el('pre', 'termbody');
2732
+ scroll.appendChild(body);
2733
+
2734
+ const row = el('div', 'termrow');
2735
+ row.appendChild(el('span', 'termps', '$'));
2736
+ const input = document.createElement('input');
2737
+ input.className = 'termin';
2738
+ input.spellcheck = false;
2739
+ input.autocomplete = 'off';
2740
+ input.placeholder = index === 0 ? 'npm test, git status, npm run dev…' : '';
2741
+ input.onkeydown = (ev) => paneKey(ev, index);
2742
+ input.onfocus = () => { term.active = index; markActivePane(); };
2743
+ row.appendChild(input);
2744
+ const stop = el('button', 'termstop', 'stop');
2745
+ stop.title = 'Kill what is running in this pane';
2746
+ stop.onclick = () => void killPane(index);
2747
+ row.appendChild(stop);
2748
+
2749
+ box.appendChild(scroll);
2750
+ box.appendChild(row);
2751
+ host.appendChild(box);
2752
+ pane.el = { box: box, scroll: scroll, body: body, input: input, stop: stop };
2753
+ paintPane(index);
2754
+ });
2755
+ applyTerminalLook();
2756
+ markActivePane();
2757
+ const here = term.panes[term.active];
2758
+ if (here && here.el) here.el.input.focus();
2697
2759
  }
2698
2760
 
2699
- async function showJob(id) {
2700
- let job;
2701
- try { job = await api('/api/jobs?id=' + encodeURIComponent(id)); } catch (e) { return; }
2702
- const host = $('tout');
2703
- const wasAtEnd = host.scrollHeight - host.scrollTop - host.clientHeight < 60;
2704
- host.innerHTML = '';
2705
- if (job.trimmed) host.appendChild(el('div', 'oe', 'earlier output dropped\n'));
2706
- // The escape codes are why a pty was allocated; rendering them is the point.
2707
- const body = el('div');
2708
- body.innerHTML = ansiToHtml((job.lines || []).join('\n'));
2709
- host.appendChild(body);
2710
- for (const line of job.lines || []) noticeServedUrl(line);
2711
- if (job.state !== 'running') {
2712
- host.appendChild(el('div', job.state === 'exited' ? null : 'oe',
2713
- '\n[' + job.state + (job.exitCode !== null ? ' ' + job.exitCode : '') + ' after '
2714
- + ms((job.endedAt || Date.now()) - job.startedAt) + ']'));
2761
+ function markActivePane() {
2762
+ term.panes.forEach((pane, i) => {
2763
+ if (pane.el) pane.el.box.className = 'termpane' + (i === term.active ? ' on' : '');
2764
+ });
2765
+ }
2766
+
2767
+ /** Everything this pane has printed, as one column of text. */
2768
+ function paintPane(index) {
2769
+ const pane = term.panes[index];
2770
+ if (!pane || !pane.el) return;
2771
+ const atEnd = pane.el.scroll.scrollHeight - pane.el.scroll.scrollTop
2772
+ - pane.el.scroll.clientHeight < 60;
2773
+ pane.el.body.innerHTML = ansiToHtml(pane.lines.join('\n'));
2774
+ pane.el.stop.hidden = !pane.running;
2775
+ if (atEnd) pane.el.scroll.scrollTop = pane.el.scroll.scrollHeight;
2776
+ }
2777
+
2778
+ function pushPane(index, line, redraw) {
2779
+ const pane = term.panes[index];
2780
+ if (!pane) return;
2781
+ if (redraw && pane.lines.length > 0) pane.lines[pane.lines.length - 1] = line;
2782
+ else pane.lines.push(line);
2783
+ while (pane.lines.length > 4000) pane.lines.shift();
2784
+ paintPane(index);
2785
+ }
2786
+
2787
+ async function paneKey(ev, index) {
2788
+ const pane = term.panes[index];
2789
+ if (!pane || !pane.el) return;
2790
+ const input = pane.el.input;
2791
+ if (ev.key === 'Enter') {
2792
+ const command = input.value.trim();
2793
+ input.value = '';
2794
+ if (command === '') { pushPane(index, '$ '); return; }
2795
+ if (command === 'clear') { pane.lines = []; paintPane(index); return; }
2796
+ pane.history.push(command);
2797
+ pane.at = pane.history.length;
2798
+ // Echoed above its own output, which is the whole difference from a form.
2799
+ pushPane(index, '$ ' + command);
2800
+ pane.outputStart = pane.lines.length;
2801
+ pane.received = 0;
2802
+ await runInPane(index, command);
2803
+ return;
2715
2804
  }
2716
- if (wasAtEnd) host.scrollTop = host.scrollHeight;
2805
+ // A history nobody has to think about: up and down, as everywhere else.
2806
+ if (ev.key === 'ArrowUp') {
2807
+ ev.preventDefault();
2808
+ if (pane.at > 0) pane.at--;
2809
+ input.value = pane.history[pane.at] || '';
2810
+ return;
2811
+ }
2812
+ if (ev.key === 'ArrowDown') {
2813
+ ev.preventDefault();
2814
+ if (pane.at < pane.history.length) pane.at++;
2815
+ input.value = pane.history[pane.at] || '';
2816
+ return;
2817
+ }
2818
+ if (ev.key === 'c' && ev.ctrlKey) { void killPane(index); }
2717
2819
  }
2718
2820
 
2719
- async function runCommand() {
2720
- const command = $('tcmd').value.trim();
2721
- if (!command) return;
2821
+ async function runInPane(index, command) {
2822
+ const pane = term.panes[index];
2823
+ if (!pane) return;
2824
+ // Anything that does not exit on its own is a background job; a runner that waits for a dev
2825
+ // server looks broken, and the fact is about the command rather than a box somebody ticked.
2826
+ const looksLikeServer = /\b(dev|serve|start|watch|nodemon|vite|next\s+dev)\b/.test(command);
2722
2827
  let out;
2723
2828
  try {
2724
- out = await api('/api/run', { method: 'POST', body: JSON.stringify({
2725
- command: command, background: $('tbackground').checked }) });
2829
+ out = await api('/api/run', { method: 'POST',
2830
+ body: JSON.stringify({ command: command, background: looksLikeServer }) });
2726
2831
  } catch (e) {
2727
- $('tout').textContent = e.message;
2832
+ pushPane(index, e.message);
2728
2833
  return;
2729
2834
  }
2730
- $('tcmd').value = '';
2731
- term.selected = out.id;
2732
- loadJobs(out.id);
2835
+ pane.job = out.id;
2836
+ pane.running = true;
2837
+ term.ownerOf[out.id] = index;
2838
+ paintPane(index);
2839
+
2840
+ /*
2841
+ * One catch-up read, because a command can outrun its own stream.
2842
+ *
2843
+ * The event stream is opened when the tab is first shown and takes a moment to establish. A
2844
+ * command typed inside that moment printed its first lines before anything was listening, and
2845
+ * they were gone: the pane showed the echoed command and nothing under it. Measured — the stream
2846
+ * was still in readyState 0 when the lines were emitted.
2847
+ *
2848
+ * So the runner is asked once, shortly after, what the job has actually produced, and anything
2849
+ * the stream missed is filled in. Counted rather than merged, so a line never appears twice.
2850
+ */
2851
+ setTimeout(() => { void catchUpPane(index, out.id); }, 400);
2852
+ }
2853
+
2854
+ async function catchUpPane(index, jobId) {
2855
+ const pane = term.panes[index];
2856
+ if (!pane || pane.job !== jobId) return;
2857
+ let job;
2858
+ try { job = await api('/api/jobs?id=' + encodeURIComponent(jobId)); } catch (e) { return; }
2859
+ const produced = (job.lines || []).length;
2860
+ if (produced <= pane.received) return;
2861
+ pane.lines = pane.lines.slice(0, pane.outputStart).concat(job.lines || []);
2862
+ pane.received = produced;
2863
+ if (job.state && job.state !== 'running') pane.running = false;
2864
+ paintPane(index);
2865
+ }
2866
+
2867
+ async function killPane(index) {
2868
+ const pane = term.panes[index];
2869
+ if (!pane || !pane.job || !pane.running) return;
2870
+ try { await api('/api/kill', { method: 'POST', body: JSON.stringify({ id: pane.job }) }); }
2871
+ catch (e) { pushPane(index, e.message); }
2872
+ }
2873
+
2874
+ /*
2875
+ * Live, from the runner's own event stream.
2876
+ *
2877
+ * Its own stream rather than the session's: a command belongs to the workspace, not to whichever
2878
+ * conversation was open when it was typed.
2879
+ */
2880
+ function watchTerminal() {
2881
+ if (term.stream) return;
2882
+ const url = '/api/terminal/events' + (state.token ? '?token=' + encodeURIComponent(state.token) : '');
2883
+ const source = new EventSource(url);
2884
+ term.stream = source;
2885
+ source.onmessage = (ev) => {
2886
+ let e;
2887
+ try { e = JSON.parse(ev.data); } catch (err) { return; }
2888
+ if (e.t === 'job-line') {
2889
+ const idx = term.ownerOf[e.id];
2890
+ if (idx === undefined) return;
2891
+ const pane = term.panes[idx];
2892
+ if (pane && !e.redraw) pane.received++;
2893
+ pushPane(idx, e.line, e.redraw === true);
2894
+ noticeServedUrl(e.line);
2895
+ return;
2896
+ }
2897
+ if (e.t === 'job' && e.job) {
2898
+ const idx = term.ownerOf[e.job.id];
2899
+ if (idx === undefined) return;
2900
+ const pane = term.panes[idx];
2901
+ if (!pane) return;
2902
+ if (e.job.state !== 'running') {
2903
+ pane.running = false;
2904
+ // Said the way a shell says it, rather than as a status badge.
2905
+ pushPane(idx, e.job.exitCode === 0 || e.job.exitCode === null
2906
+ ? ''
2907
+ : '[' + e.job.state + ' ' + e.job.exitCode + ']');
2908
+ }
2909
+ paintPane(idx);
2910
+ }
2911
+ };
2912
+ source.onerror = () => { /* the browser reconnects on its own */ };
2913
+ }
2914
+
2915
+ /** Font size and backdrop, applied to every pane at once. */
2916
+ function applyTerminalLook() {
2917
+ const host = $('termpanes');
2918
+ host.style.setProperty('--term-font', term.fontPx + 'px');
2919
+ const on = $('tbgimg').checked;
2920
+ const strength = Number($('tbgop').value) / 100;
2921
+ host.classList.toggle('backdrop', on);
2922
+ host.style.setProperty('--term-bg-strength', on ? String(strength) : '0');
2923
+ if (on && !host.style.getPropertyValue('--term-bg-image')) {
2924
+ const url = '/api/terminal/background'
2925
+ + (state.token ? '?token=' + encodeURIComponent(state.token) : '');
2926
+ host.style.setProperty('--term-bg-image', 'url("' + url + '")');
2927
+ }
2733
2928
  }
2734
- $('trun').onclick = () => void runCommand();
2735
- $('tcmd').addEventListener('keydown', (ev) => {
2736
- if (ev.key === 'Enter') { ev.preventDefault(); void runCommand(); }
2737
- });
2929
+
2930
+ function startPaneDrag(ev, dividerIndex) {
2931
+ ev.preventDefault();
2932
+ const left = term.panes[dividerIndex - 1];
2933
+ const right = term.panes[dividerIndex];
2934
+ if (!left || !right || !left.el || !right.el) return;
2935
+ const startX = ev.clientX;
2936
+ const leftWas = left.el.box.getBoundingClientRect().width;
2937
+ const rightWas = right.el.box.getBoundingClientRect().width;
2938
+ const total = leftWas + rightWas;
2939
+ const move = (m) => {
2940
+ const wanted = Math.min(Math.max(leftWas + (m.clientX - startX), 120), total - 120);
2941
+ left.flex = wanted / total;
2942
+ right.flex = (total - wanted) / total;
2943
+ left.el.box.style.flex = String(left.flex);
2944
+ right.el.box.style.flex = String(right.flex);
2945
+ };
2946
+ const up = () => {
2947
+ window.removeEventListener('mousemove', move);
2948
+ window.removeEventListener('mouseup', up);
2949
+ };
2950
+ window.addEventListener('mousemove', move);
2951
+ window.addEventListener('mouseup', up);
2952
+ }
2953
+
2954
+ $('tsplit').onclick = () => {
2955
+ // Three, deliberately. Four on a laptop is four things too narrow to read.
2956
+ if (term.panes.length >= 3) return;
2957
+ term.panes.push(newPane());
2958
+ term.active = term.panes.length - 1;
2959
+ buildTerminal();
2960
+ };
2961
+
2962
+ $('tunsplit').onclick = () => {
2963
+ if (term.panes.length <= 1) return;
2964
+ const gone = term.panes.pop();
2965
+ if (gone && gone.job) delete term.ownerOf[gone.job];
2966
+ term.active = Math.min(term.active, term.panes.length - 1);
2967
+ buildTerminal();
2968
+ };
2969
+
2970
+ $('tbgimg').onchange = applyTerminalLook;
2971
+ $('tbgop').oninput = applyTerminalLook;
2972
+ $('tfontup').onclick = () => { term.fontPx = Math.min(20, term.fontPx + 1); applyTerminalLook(); };
2973
+ $('tfontdown').onclick = () => { term.fontPx = Math.max(9, term.fontPx - 1); applyTerminalLook(); };
2974
+
2738
2975
 
2739
2976
  /* ── the page, as it really renders ───────────────────────────────────────────────────────── */
2740
2977
 
@@ -3110,6 +3347,16 @@ function watchTool(e, done) {
3110
3347
  return;
3111
3348
  }
3112
3349
 
3350
+ /*
3351
+ * Whatever the panel was showing, it follows the work.
3352
+ *
3353
+ * Page mode was entered by a browser call and never left, so a session that opened a page and
3354
+ * then went back to editing code sat on a stale screenshot while the interesting thing happened
3355
+ * out of sight. The browser is still one click away — and any browser call switches straight back
3356
+ * to it — but the panel follows what is being done now.
3357
+ */
3358
+ if (follow.mode === 'page') setPanelMode('work');
3359
+
3113
3360
  if (t.command) {
3114
3361
  // A command is not a file, and its output IS the thing to watch.
3115
3362
  follow.kind = 'term';
@@ -3220,6 +3467,32 @@ function prettyArgs(args) {
3220
3467
  return String(args).slice(0, 8000);
3221
3468
  }
3222
3469
 
3470
+ /*
3471
+ * The write in progress, repainted as its content arrives.
3472
+ *
3473
+ * Plain while it streams and coloured the moment it lands: highlighting needs a round trip per
3474
+ * repaint, and four of those a second to colour text that is about to change again is a cost with
3475
+ * no reader. The finished render replaces it with the coloured version a fraction of a second
3476
+ * later, which is what the eye actually follows.
3477
+ */
3478
+ function drawWriteInProgress(path, content) {
3479
+ const host = $('wbody');
3480
+ let head = host.querySelector('.wwrite');
3481
+ if (!head) {
3482
+ host.innerHTML = '';
3483
+ head = el('div', 'wwrite');
3484
+ host.appendChild(head);
3485
+ host.appendChild(el('div', 'wwbody'));
3486
+ }
3487
+ const lines = content.split('\n');
3488
+ head.textContent = 'writing ' + path + ' — ' + lines.length
3489
+ + (lines.length === 1 ? ' line so far' : ' lines so far');
3490
+ const body = host.querySelector('.wwbody');
3491
+ body.innerHTML = '';
3492
+ body.appendChild(drawCode(content, [], null, null));
3493
+ host.scrollTop = host.scrollHeight;
3494
+ }
3495
+
3223
3496
  /** Paints the content a write is carrying, coloured for the file it is going into. */
3224
3497
  async function drawWriting(path, content, before) {
3225
3498
  const host = $('wbody');
@@ -1 +1 @@
1
- {"version":3,"file":"ui-client.js","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAkxHlB,CAAC;AACF,CAAC"}
1
+ {"version":3,"file":"ui-client.js","sourceRoot":"","sources":["../../src/web/ui-client.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,MAAM,UAAU,YAAY;IAC1B,OAAO,MAAM,CAAC,GAAG,CAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAmiIlB,CAAC;AACF,CAAC"}
@@ -1,15 +1,2 @@
1
- /**
2
- * The stylesheet, and the three themes.
3
- *
4
- * Every colour is a token defined once on `:root`, so a theme is a list of token values rather
5
- * than a second stylesheet. Three states, which is what "light, dark and system" actually needs:
6
- * an explicit choice writes `data-theme` on the root and wins; with nothing written, the browser's
7
- * own preference decides.
8
- *
9
- * Contrast was asked for specifically — "font visible well in dark theme". The dark palette's body
10
- * text is #e6f1f5 on #0d1418, which is about 14:1, and the muted tone is #93aab5 at about 7:1;
11
- * both clear WCAG AA for body text with room to spare. The first version used #7f9aa6 for anything
12
- * secondary, which measures 4.6:1 — passable on paper and genuinely hard to read on a dim screen.
13
- */
14
1
  export declare function css(markUrl: string): string;
15
2
  //# sourceMappingURL=ui-css.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"ui-css.d.ts","sourceRoot":"","sources":["../../src/web/ui-css.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAy7B3C"}
1
+ {"version":3,"file":"ui-css.d.ts","sourceRoot":"","sources":["../../src/web/ui-css.ts"],"names":[],"mappings":"AA8CA,wBAAgB,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,CAy8B3C"}