@vibelord/lanes 1.2.0 → 1.3.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.
Files changed (35) hide show
  1. package/package.json +1 -1
  2. package/payload/lanes/README.md +4 -3
  3. package/payload/lanes/VERSION +1 -1
  4. package/payload/lanes/bin/lanes +301 -45
  5. package/payload/lanes/container/lane +42 -1
  6. package/payload/lanes/container/lane-mcp +65 -6
  7. package/payload/lanes/container/lane_core.py +178 -0
  8. package/payload/lanes/docs/ARCHITECTURE.md +192 -20
  9. package/payload/lanes/docs/DESIGN.md +6 -3
  10. package/payload/lanes/docs/INSTALL.md +25 -8
  11. package/payload/lanes/docs/RUNBOOK.md +369 -8
  12. package/payload/lanes/docs/USERGUIDE.md +201 -25
  13. package/payload/lanes/holders.lock +5 -0
  14. package/payload/lanes/install/enable-docs.sh +78 -0
  15. package/payload/lanes/install/host-install.sh +39 -5
  16. package/payload/lanes/libexec/docs_store.py +995 -0
  17. package/payload/lanes/libexec/docsd.py +2010 -0
  18. package/payload/lanes/libexec/mdlite.py +575 -0
  19. package/payload/lanes/libexec/members.py +5 -0
  20. package/payload/lanes/libexec/notify.py +65 -5
  21. package/payload/lanes/libexec/sidebar.py +106 -13
  22. package/payload/lanes/libexec/statusd.py +97 -5
  23. package/payload/lanes/libexec/tokens.py +31 -9
  24. package/payload/lanes/libexec/update.py +94 -22
  25. package/payload/lanes/libexec/uploads.py +538 -0
  26. package/payload/lanes/systemd/lanes-docsd.service +29 -0
  27. package/payload/lanes/web/Caddyfile +24 -1
  28. package/payload/lanes/web/README.md +86 -8
  29. package/payload/lanes/web/browser.js +2 -4
  30. package/payload/lanes/web/docs.css +52 -0
  31. package/payload/lanes/web/docs.js +36 -0
  32. package/payload/lanes/web/mobile.js +3 -2
  33. package/payload/lanes/web/upload.css +17 -0
  34. package/payload/lanes/web/upload.js +84 -0
  35. package/payload/manifest.json +4 -3
@@ -43,11 +43,15 @@ before the control socket opens makes 0.45.1 throw `InvalidStateError` and leave
43
43
 
44
44
  `lanes-mobile.service` runs a separate, loopback-only Caddy on port 8083 with its admin API disabled.
45
45
  It does not change the system Caddy. Tailscale Serve forwards private HTTPS on 8443 to this frontend;
46
- the original Zellij service stays on 8082. Caddy (2.5 or later) must already be installed on the host. `/enrol/*` is
47
- the one route that is neither these assets nor Zellij: `handle_path` strips `/enrol` and forwards to the enrolment
48
- service on 127.0.0.1:8184 (`lanes-enrold.service`, [runbook](../docs/RUNBOOK.md#enrolment-service-lanes-enrold-w8-a3));
46
+ the original Zellij service stays on 8082. Caddy (2.5 or later) must already be installed on the host. `/enrol/*` and
47
+ `/lanes-docs/*` are the routes that are neither these assets nor Zellij: `handle_path` strips `/enrol` and forwards to
48
+ the enrolment service on 127.0.0.1:8184 (`lanes-enrold.service`, [runbook](../docs/RUNBOOK.md#enrolment-service-lanes-enrold-w8-a3));
49
49
  it must stay before the Zellij routes, and its `trusted_proxies 127.0.0.0/8` keeps the `X-Forwarded-For` Tailscale
50
- Serve sets (the client's tailnet address), so the redeem rate limit counts per device.
50
+ Serve sets (the client's tailnet address), so the redeem rate limit counts per device. `/lanes-docs/*` (prefix
51
+ stripped) goes to the docs service on 127.0.0.1:8185 (`lanes-docsd.service`,
52
+ [runbook](../docs/RUNBOOK.md#docs-service-lanes-docsd-w10-w3)): it is the FIRST handle of the site, keeps Serve's
53
+ `X-Forwarded-For` the same way, and the site-wide `header` block skips it (`header @notdocs`, `@notdocs not path
54
+ /lanes-docs/*`) so docsd's own per-route security headers are the only ones a document gets.
51
55
  Run `lanes web <device> --token` on the server (it also adds that Tailscale Serve mapping when it is missing),
52
56
  `lanes-launch web phone|laptop` from an installed client, or `sh install/enable-phone.sh --host lanes` (`--local` on
53
57
  the server).
@@ -211,7 +215,7 @@ re-opens this contract: re-read `startWsControl`, `installSoftKeyboardCapture`,
211
215
  holds them to the table (cockpit v3 P5). The icon (`icon.svg`, `icon-192.png`, a raster with anti-aliased edges) is
212
216
  the brand mark, drawn before the token table and not generated: palette v2 did not recolour it, and its bars keep
213
217
  v1's dark accent. Bundle budget (spec §8, `test_web.test_bundle_growth`):
214
- `mobile.css` ≤ 18 982 B and `mobile.js` ≤ 24 612 B; the rules sit one per line and the explanations live here rather
218
+ `mobile.css` ≤ 18 982 B and `mobile.js` ≤ 24 747 B; the rules sit one per line and the explanations live here rather
215
219
  than in the shipped files. MEASURED 2026-10-04 with `wc -c` after LNS-31 (plan Task 31) met Tasks 13/28: `mobile.js`
216
220
  21 964 B (23 268 B before its comments moved to Code notes below) and `mobile.css` 18 945 B. A laptop or tablet also
217
221
  fetches `browser.js` and `browser.css` (1 947 B); `test_web_view` pins that download at `LAPTOP_TABLET_JS_RATCHET` and
@@ -238,6 +242,72 @@ MEASURED `mobile.js` 24 612 B (+354 B over input v2) and the download 24 612 + 3
238
242
  those 354 B and the ratchet by 47 B (354 less the 307 B `browser.js` had already shed), each to the measured size
239
243
  (`test_web_view.JS_COCKPIT_V3`, `LAPTOP_TABLET_JS_RATCHET`), and all four cap assertions red on one byte more.
240
244
 
245
+ The docs cockpit (plan W10 W7, LNS-108, 2026-10-08): **More** gains a **Docs** row, a link to `/lanes-docs/` (the docs
246
+ page, `lanes-docsd` behind Caddy's `/lanes-docs/*`) in a new tab, and the desktop bar a **Docs** link beside its two
247
+ pickers. The operator raised the phone cap by the measured amount, pinned exact: `mobile.js` MEASURED 24 747 B with
248
+ `wc -c`, +135 B (`test_web_view.JS_DOCS`; `menuRow()` takes a tag and an extra class, and the row borrows
249
+ `.lanes-session-link` for its 48 px touch target, so `mobile.css` stays 18 945 B). `browser.js` paid for its link
250
+ inside its own bytes — its line-1 pointer comment moved to Code notes below and the bar's Escape listener became an
251
+ `onkeydown` property — 3 334 -> 3 318 B MEASURED, so the laptop/tablet download is 24 747 + 3 318 = 28 065 B and the
252
+ DEF-1569 ratchet rose by the 135 `mobile.js` bytes net of those 16, to that measured size.
253
+
254
+ | File | Cap | MEASURED 2026-10-08 (`wc -c`) | Bound by |
255
+ | --- | --- | --- | --- |
256
+ | `mobile.js` | 24 747 B (exact) | 24 747 B | spec §8 phone row, `test_web.test_bundle_growth`, `test_web_view` `JS_DOCS` |
257
+ | `mobile.css` | 18 982 B | 18 945 B | spec §8 phone row |
258
+ | `browser.js` | 4 096 B | 3 318 B | `test_web_view.BROWSER_JS_CAP` |
259
+ | `browser.css` | 2 048 B | 1 947 B | `test_web_view.BROWSER_CSS_CAP` |
260
+ | `mobile.js` + `browser.js` | 28 065 B (exact) | 28 065 B | DEF-1569, `LAPTOP_TABLET_JS_RATCHET` |
261
+ | `mobile.css` + `browser.css` | 20 993 B | 20 892 B | DEF-1569's CSS twin, `LAPTOP_TABLET_CSS_CAP` |
262
+
263
+ ## Docs page (plan W10 W6, LNS-107)
264
+
265
+ The docs cockpit's pages (Markdown, Mermaid, files; served by `lanes-docsd` behind `/lanes-docs/*`, plan W3) are their
266
+ own HTML documents with their own assets, outside the phone budget above: `docs.css` (cap 2 560 B, MEASURED 2 351 B)
267
+ and `docs.js` (cap 1 536 B, MEASURED 1 530 B), both from `/lanes-assets/` with `no-cache` like the shell's. A page links
268
+ `/lanes-assets/docs.css` and loads `<script src="/lanes-assets/docs.js" defer></script>`, and renders a ```` ```mermaid ````
269
+ fence as `<pre class="mermaid">` holding the escaped source (`libexec/mdlite.py`, plan W2).
270
+
271
+ - `docs.css` is phone first: a 16 px gutter (safe-area aware), a 46 rem measure, tables and code blocks that scroll
272
+ sideways instead of the page, `pre.mermaid` centred, system fonts only (the docs CSP allows no font source). Its
273
+ colours are the block between the `BEGIN`/`END lanes tokens` markers, generated by `libexec/tokens.py --docs-css`:
274
+ the six roles it paints (`DOCS_ROLES`) on `:root`, dark by default and light under `prefers-color-scheme` — not the
275
+ phone's `html.lanes-mobile` / `data-zj-theme` block. `tokens.py --check` covers it; no colour lies outside it.
276
+ - `docs.js` is a classic script that does nothing on a page without `pre.mermaid`. With one, it appends one
277
+ `<script>` for `/lanes-assets/vendor/mermaid-<version>.min.js` with `integrity="sha256-…"` (base64 of the raw
278
+ sha256 of the pinned member, `holders.lock`) and `crossorigin="anonymous"`; on load it calls
279
+ `mermaid.initialize({startOnLoad: false, securityLevel: 'strict', suppressErrorRendering: true, theme})` (`dark`
280
+ when the system scheme is dark, else `default`) and `mermaid.run({querySelector: 'pre.mermaid'})`. Mermaid empties a
281
+ fence before it draws, so each fence left without an `<svg>` gets its source text back (`textContent`, never markup;
282
+ DEF-1873). A missing file (`install/enable-docs.sh` not run yet: 404) or an integrity mismatch fires no load, and every
283
+ fence stays a `<pre>` with its source. It never evaluates strings, writes markup or names the unversioned link.
284
+ - The vendor route: `handle_path /lanes-assets/vendor/*` serves `$LANES_SHARE/vendor` (`install/services.py` puts
285
+ `LANES_SHARE` in `lanes-mobile.service`, as it does `LANES_REPO`) with `Cache-Control: public, max-age=31536000,
286
+ immutable` and no listing. Immutable is safe only because a page names the VERSIONED file: under the stable
287
+ `mermaid.min.js` link a browser would keep the old bytes after a pin bump and refuse them under the new integrity
288
+ (DEF-1870). The block sits first among the `handle` routes: Caddy sorts it ahead of `/lanes-assets/*` (longest path
289
+ first) only past routes that also have exactly one path (Caddy issue #5037), so after `@backend` it would never be
290
+ reached (DEF-1872). `test_docs_assets.VendorRoute` proves the order on a model of Caddy's sort; no Caddy runs in the
291
+ tests (DEF-1871).
292
+ - CSP needs, MEASURED 2026-10-08 in headless Chromium 153 with Mermaid 11.17.2: `script-src 'self'` suffices — no
293
+ `'unsafe-eval'` (a flowchart and a sequence diagram rendered with no violation; a control page's `eval` was refused
294
+ and reported) — but the SVG Mermaid draws carries inline `<style>` and `style` attributes, so `style-src` needs
295
+ `'unsafe-inline'` (without it: 88 violations, unstyled diagrams). The docs page's CSP (plan W3) has both.
296
+ - Tests: `tests/unit/test_docs_assets.py` (the pin, the enable script against a fake curl, the route model, the unit's
297
+ environment, both files' caps, tokens and SRI) and `tests/web/docs.test.mjs` (docs.js in a vm against a fake page and
298
+ a fake Mermaid that empties fences as the real one does). How to bump the pin: [runbook](../docs/RUNBOOK.md#mermaid-pin).
299
+ - The drop area (plan W10 W5, LNS-106): the index's upload form is a plain form that works without a script — a
300
+ `label.docs-drop` wrapping a multi-file input, so a tap opens the picker and a drop onto the input takes the files.
301
+ `upload.js` (cap 2 892 B, MEASURED 2 892 B, exact) and `upload.css` (cap 1 033 B, MEASURED 1 033 B, exact) make it
302
+ better: a drop anywhere on the area (`input.files` = the dropped files), a size check against the form's `data-max`
303
+ before anything is sent, an `XMLHttpRequest` `POST up` asking for JSON with a `<progress>`, then one line per receipt
304
+ — a link to `d/<id>` named with `textContent`, its size, sha256 and the in-container path it `landed` at — and a
305
+ refusal or a network error in words. They are their own files because `docs.js` had 6 B of its cap left and
306
+ `docs.css` 209 B; docsd serves them like `docs.css`/`docs.js`, and only the index (the page with the form) loads them.
307
+ The page CSP has `connect-src 'self'` for that request. The styles use `docs.css`'s token roles only (no colour of
308
+ their own), 44 px touch targets, phone first. Tests: `test_docs_assets.UploadAssets` (caps, classic script, no
309
+ markup, roles) and `tests/web/upload.test.mjs` (upload.js in a vm against a fake page and XHR).
310
+
241
311
  ## Code notes
242
312
 
243
313
  `mobile.js` carries no explanations (the spec §8 budget above); each note is keyed to the code it explains, in file
@@ -289,8 +359,12 @@ order.
289
359
  dock's four columns hold its four items (DEF-1737), and flushes the `send()` queue after `updateLaneFilterBar()`, so a
290
360
  filter tail is dropped before it could reach a lane; `quick` is `true` on a view tab (every key), `2` on the picker
291
361
  while its title ends ` · › <verb>` (only `⏎ y Esc`), else none.
292
- - `menuRow()` / `installFontControls()`: a row of the client's **More** menu, prepended (Text size, then the release
293
- above it once `/enrol/site` answers). The one `fetch` is there because `build()` runs once per page load.
362
+ - `menuRow()` / `installFontControls()`: a row of the client's **More** menu, prepended (Text size, then **Docs** above
363
+ it, then the release at the top once `/enrol/site` answers). The one `fetch` is there because `build()` runs once per
364
+ page load. `menuRow(label, tag, more)`: the **Docs** row is an `<a>` (plan W10 W7) to `/lanes-docs/` with
365
+ `target="_blank"` — a new tab, so the terminal and its socket stay where they are, and a Home Screen app, which has
366
+ no back button, is never navigated away — carrying `lanes-session-link` as well, the existing rule that gives it a
367
+ 48 px touch target with no CSS of its own.
294
368
  - `connect()`: waits for server state, because an earlier resize throws `InvalidStateError` in 0.45.1. `applyTheme` is
295
369
  re-applied after every `SetConfig` (the client calls the chained `__zjSyncInactiveCursorStyle` hook) and on the theme
296
370
  toggle.
@@ -302,10 +376,14 @@ order.
302
376
 
303
377
  `browser.js` (laptop/tablet extras) carries none either, since cockpit v3 S5:
304
378
 
379
+ - The file: the Lanes laptop/tablet extras (its line-1 pointer to these notes moved here in plan W10 W7, to pay for the
380
+ bar's Docs link).
381
+
305
382
  - `laneFullRender()`: the client starts a tablet in single pane on every load; once, it sends the client's own
306
383
  switch-to-desktop preference.
307
384
  - `desktop()`: the chip toggles an overlay bar (no refit); a pick sends the phone picker's `FocusPane`. The chip
308
- cancels `mousedown`, so the terminal keeps the keys.
385
+ cancels `mousedown`, so the terminal keeps the keys. The bar ends in a **Docs** link to `/lanes-docs/` in a new tab
386
+ (plan W10 W7); Escape in the bar gives the terminal its keys back (`onkeydown`, one handler, as the listener was).
309
387
  - `ui()`: `window.__zjMobileUi` is set synchronously after the client appends its mobile root, which the
310
388
  `MutationObserver` on `<body>` waits for.
311
389
 
@@ -1,4 +1,3 @@
1
- /* Lanes laptop/tablet extras; see web/README.md, Code notes */
2
1
  function laneBarModel(d) {
3
2
  const panes = (d?.panes || []).filter(p => !p.is_plugin), at = d?.active_pane || {}, ws = new Map();
4
3
  const first = t => t && panes.find(p => p.tab_position === t.position);
@@ -46,7 +45,7 @@ if (typeof module === 'object') module.exports = {laneBarModel, laneFullRender};
46
45
  send({type: 'FocusPane', pane_id: +this.value, is_plugin: false});
47
46
  focus();
48
47
  }}));
49
- const bar = make('nav', {className: 'lanes-bar', hidden: true});
48
+ const bar = make('nav', {className: 'lanes-bar', hidden: true, onkeydown: ev => ev.key === 'Escape' && focus()});
50
49
  const render = () => {
51
50
  const m = !bar.hidden && laneBarModel(api.getState()?.data), sig = JSON.stringify(m);
52
51
  if (!m || sig === last) return;
@@ -60,8 +59,7 @@ if (typeof module === 'object') module.exports = {laneBarModel, laneFullRender};
60
59
  window.term?.focus();
61
60
  }});
62
61
  chip.addEventListener('mousedown', ev => ev.preventDefault());
63
- bar.addEventListener('keydown', ev => ev.key === 'Escape' && focus());
64
- bar.append(...picks);
62
+ bar.append(...picks, make('a', {href: '/lanes-docs/', target: '_blank', textContent: 'Docs'}));
65
63
  document.body.append(bar, chip);
66
64
  return render;
67
65
  };
@@ -0,0 +1,52 @@
1
+ /* Lanes docs page (plan W10 W6, LNS-107), phone first. Colours: the generated block below; system fonts only.
2
+ Notes: web/README.md "Docs page". */
3
+ /* BEGIN lanes tokens (generated by libexec/tokens.py --docs-css; do not edit by hand) */
4
+ :root {
5
+ --lanes-surface: #101318;
6
+ --lanes-surface-raised: #191e25;
7
+ --lanes-line: #2c333d;
8
+ --lanes-ink: #f2f5f8;
9
+ --lanes-ink-dim: #a6b1be;
10
+ --lanes-accent: #8fe3a6;
11
+ color-scheme: dark;
12
+ }
13
+ @media (prefers-color-scheme: light) {
14
+ :root {
15
+ --lanes-surface: #f3f5f2;
16
+ --lanes-surface-raised: #ffffff;
17
+ --lanes-line: #d5ded6;
18
+ --lanes-ink: #111a14;
19
+ --lanes-ink-dim: #4a5661;
20
+ --lanes-accent: #1f6f3f;
21
+ color-scheme: light;
22
+ }
23
+ }
24
+ /* END lanes tokens */
25
+ *, *::before, *::after { box-sizing: border-box; }
26
+ html { -webkit-text-size-adjust: 100%; text-size-adjust: 100%; }
27
+ body {
28
+ max-width: 46rem; margin: 0 auto;
29
+ padding: 16px max(16px, env(safe-area-inset-right)) 32px max(16px, env(safe-area-inset-left));
30
+ background: var(--lanes-surface); color: var(--lanes-ink);
31
+ font: 16px/1.55 system-ui, -apple-system, "Segoe UI", sans-serif; overflow-wrap: break-word;
32
+ }
33
+ h1, h2, h3 { line-height: 1.25; margin: 1.4em 0 .5em; }
34
+ h1 { font-size: 1.6em; margin-top: .4em; }
35
+ h2 { font-size: 1.3em; padding-bottom: .2em; border-bottom: 1px solid var(--lanes-line); }
36
+ a { color: var(--lanes-accent); }
37
+ small { color: var(--lanes-ink-dim); }
38
+ img { max-width: 100%; height: auto; }
39
+ hr { border: 0; border-top: 1px solid var(--lanes-line); }
40
+ blockquote { margin: 1em 0; padding-left: 12px; border-left: 3px solid var(--lanes-line); color: var(--lanes-ink-dim); }
41
+ code, pre { font: .875em/1.45 ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
42
+ code { padding: .1em .3em; border-radius: 4px; background: var(--lanes-surface-raised); }
43
+ pre {
44
+ margin: 1em 0; padding: 12px; overflow-x: auto; tab-size: 4;
45
+ background: var(--lanes-surface-raised); border: 1px solid var(--lanes-line); border-radius: 8px;
46
+ }
47
+ pre code { padding: 0; font-size: 1em; background: none; }
48
+ pre.mermaid { text-align: center; }
49
+ pre.mermaid svg { max-width: 100%; height: auto; }
50
+ table { display: block; max-width: 100%; overflow-x: auto; border-collapse: collapse; margin: 1em 0; }
51
+ th, td { padding: 6px 10px; border: 1px solid var(--lanes-line); text-align: left; }
52
+ th { background: var(--lanes-surface-raised); }
@@ -0,0 +1,36 @@
1
+ // Lanes docs page (plan W10 W6, LNS-107): render ```mermaid fences with the pinned Mermaid, only on a page that has
2
+ // one: its versioned file (DEF-1870), its sha256 as SRI. Without it every fence keeps its source. web/README.md.
3
+ (function () {
4
+ 'use strict';
5
+ var SRC = '/lanes-assets/vendor/mermaid-11.17.2.min.js';
6
+ var SRI = 'sha256-WB7X10vZBI0OOpE2OSfXLvIpQtdyJUayf3zCnjU5Drg=';
7
+
8
+ function render() {
9
+ var pres = document.querySelectorAll('pre.mermaid'), text = [], i;
10
+ for (i = 0; i < pres.length; i++) text[i] = pres[i].textContent;
11
+ // Mermaid empties each fence first: one left without a diagram gets its source back (DEF-1873).
12
+ function restore() {
13
+ for (i = 0; i < pres.length; i++) if (!pres[i].querySelector('svg')) pres[i].textContent = text[i];
14
+ }
15
+ try {
16
+ mermaid.initialize({startOnLoad: false, securityLevel: 'strict', suppressErrorRendering: true,
17
+ theme: matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'default'});
18
+ mermaid.run({querySelector: 'pre.mermaid'}).then(restore, restore);
19
+ } catch (e) {
20
+ restore();
21
+ }
22
+ }
23
+
24
+ function start() {
25
+ if (!document.querySelector('pre.mermaid')) return;
26
+ var s = document.createElement('script');
27
+ s.setAttribute('integrity', SRI);
28
+ s.setAttribute('crossorigin', 'anonymous');
29
+ s.setAttribute('src', SRC);
30
+ s.onload = render;
31
+ document.head.appendChild(s);
32
+ }
33
+
34
+ if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', start);
35
+ else start();
36
+ }());
@@ -381,8 +381,8 @@ if (typeof module === 'object') module.exports = {laneShellMode};
381
381
  return t;
382
382
  }
383
383
  const applyTheme = () => { if (term) term.options.theme = lanesTheme(); };
384
- function menuRow(label) {
385
- const row = make('div', 'lanes-font-controls');
384
+ function menuRow(label, tag = 'div', more = '') {
385
+ const row = make(tag, 'lanes-font-controls' + more);
386
386
  row.append(make('span', '', label));
387
387
  document.querySelector('.zj-mobile-menu')?.prepend(row);
388
388
  return row;
@@ -396,6 +396,7 @@ if (typeof module === 'object') module.exports = {laneShellMode};
396
396
  smaller.addEventListener('click', () => {fontSize = Math.max(14, fontSize - 1); applyFont();});
397
397
  larger.addEventListener('click', () => {fontSize = Math.min(24, fontSize + 1); applyFont();});
398
398
  row.append(smaller, value, larger);
399
+ Object.assign(menuRow('Docs', 'a', ' lanes-session-link'), {href: '/lanes-docs/', target: '_blank'});
399
400
  fetch('/enrol/site', {credentials: 'same-origin'}).then(r => r.json())
400
401
  .then(({version: v}) => /^\d[\w.+-]{0,31}$/.test(v) && menuRow(`Lanes v${v}`)).catch(() => {});
401
402
  }
@@ -0,0 +1,17 @@
1
+ /* Lanes docs page: the drop area (plan W10 W5, LNS-106), phone first; colours: docs.css's token block. */
2
+ .docs-upload { margin: 1em 0; }
3
+ .docs-upload label, .docs-upload select, .docs-upload button { display: block; width: 100%; }
4
+ .docs-upload select, .docs-upload button, .docs-upload input { min-height: 44px; font: inherit; }
5
+ .docs-upload select, .docs-upload button {
6
+ margin: .4em 0; padding: 8px; border: 1px solid var(--lanes-line); border-radius: 8px;
7
+ background: var(--lanes-surface-raised); color: var(--lanes-ink);
8
+ }
9
+ .docs-drop {
10
+ margin: .6em 0; padding: 20px 12px; text-align: center; color: var(--lanes-ink-dim);
11
+ border: 2px dashed var(--lanes-line); border-radius: 8px;
12
+ }
13
+ .docs-drop.over { border-color: var(--lanes-accent); color: var(--lanes-ink); }
14
+ .docs-drop input { display: block; width: 100%; margin-top: 8px; }
15
+ .docs-drop::after { content: attr(data-count); display: block; color: var(--lanes-accent); }
16
+ .docs-sent { padding-left: 1.2em; overflow-wrap: anywhere; }
17
+ .docs-sent progress { width: 100%; }
@@ -0,0 +1,84 @@
1
+ // Lanes docs page: the drop area (plan W10 W5, LNS-106). The form works without this; here a drop lands anywhere on
2
+ // the area, the size is checked before sending, progress shows and each file's receipt is listed. web/README.md.
3
+ (function () {
4
+ 'use strict';
5
+ var form = document.querySelector('form.docs-upload');
6
+ if (!form) return;
7
+ var area = form.querySelector('.docs-drop'), input = form.querySelector('input[type=file]'),
8
+ list = form.querySelector('.docs-sent'), button = form.querySelector('button'),
9
+ max = +form.getAttribute('data-max') || 0;
10
+
11
+ function add(text, href, name) {
12
+ var li = document.createElement('li'), a;
13
+ if (href) {
14
+ a = li.appendChild(document.createElement('a'));
15
+ a.setAttribute('href', href);
16
+ a.textContent = name;
17
+ }
18
+ li.appendChild(document.createTextNode(text));
19
+ return list.appendChild(li);
20
+ }
21
+
22
+ function count() {
23
+ area.setAttribute('data-count', input.files.length ? input.files.length + ' chosen' : '');
24
+ }
25
+
26
+ function over(on) {
27
+ return function (e) {
28
+ e.preventDefault();
29
+ area.classList.toggle('over', on);
30
+ };
31
+ }
32
+
33
+ area.addEventListener('dragenter', over(true));
34
+ area.addEventListener('dragover', over(true));
35
+ area.addEventListener('dragleave', over(false));
36
+ area.addEventListener('drop', function (e) {
37
+ over(false)(e);
38
+ input.files = e.dataTransfer.files;
39
+ count();
40
+ });
41
+ input.addEventListener('change', count);
42
+
43
+ form.addEventListener('submit', function (e) {
44
+ var files = input.files, total = 0, i, x, row, bar;
45
+ e.preventDefault();
46
+ if (!files.length) return;
47
+ for (i = 0; i < files.length; i++) total += files[i].size + 1024;
48
+ if (max && total > max) {
49
+ add('Too large: ' + Math.ceil(total / 1048576) + ' MiB; an upload may be ' + max / 1048576 + ' MiB.');
50
+ return;
51
+ }
52
+ x = new XMLHttpRequest();
53
+ row = add('');
54
+ bar = row.appendChild(document.createElement('progress'));
55
+ function done(said) {
56
+ list.removeChild(row);
57
+ button.disabled = false;
58
+ if (said) add(said);
59
+ }
60
+ x.upload.onprogress = function (p) {
61
+ if (p.lengthComputable) bar.value = p.loaded / p.total;
62
+ };
63
+ x.onload = function () {
64
+ var r = {};
65
+ try {
66
+ r = JSON.parse(x.responseText);
67
+ } catch (err) { /* not JSON: said below */ }
68
+ done('');
69
+ (r.receipts || []).forEach(function (t) {
70
+ add(' · ' + t.bytes + ' B · sha256 ' + t.sha256 + ' · ' + t.landed, 'd/' + t.id, t.name);
71
+ });
72
+ if (x.status !== 200) add('Refused: ' + (r.error || x.status));
73
+ input.value = '';
74
+ count();
75
+ };
76
+ x.onerror = function () {
77
+ done('The upload did not reach the server; try again.');
78
+ };
79
+ x.open('POST', form.getAttribute('action'));
80
+ x.setRequestHeader('Accept', 'application/json');
81
+ button.disabled = true;
82
+ x.send(new FormData(form));
83
+ });
84
+ }());
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "name": "@vibelord/lanes",
3
- "version": "1.2.0",
4
- "commit": "d7718161405f7bd8a7ab6144b61a9edb550f4500",
5
- "digest": "sha256:e67fcdfbd2a64b636d204f007e453989e601231d0c06348a66b066df534d9fee",
3
+ "version": "1.3.0",
4
+ "commit": "cf8bfb3df12fb6210350adb6b3e481f29051ffae",
5
+ "digest": "sha256:7e3cce373458d09b77c588dfb98b00777ceda08df504b319bcdf7d26cc3cc206",
6
6
  "executables": [
7
7
  "bin/lanes",
8
8
  "client/install.sh",
@@ -20,6 +20,7 @@
20
20
  "holders/tmux.sh",
21
21
  "holders/zmx.sh",
22
22
  "install.sh",
23
+ "install/enable-docs.sh",
23
24
  "install/enable-studio-host.sh",
24
25
  "install/enable-web.sh",
25
26
  "install/host-install.sh",