@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.
- package/package.json +1 -1
- package/payload/lanes/README.md +4 -3
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +301 -45
- package/payload/lanes/container/lane +42 -1
- package/payload/lanes/container/lane-mcp +65 -6
- package/payload/lanes/container/lane_core.py +178 -0
- package/payload/lanes/docs/ARCHITECTURE.md +192 -20
- package/payload/lanes/docs/DESIGN.md +6 -3
- package/payload/lanes/docs/INSTALL.md +25 -8
- package/payload/lanes/docs/RUNBOOK.md +369 -8
- package/payload/lanes/docs/USERGUIDE.md +201 -25
- package/payload/lanes/holders.lock +5 -0
- package/payload/lanes/install/enable-docs.sh +78 -0
- package/payload/lanes/install/host-install.sh +39 -5
- package/payload/lanes/libexec/docs_store.py +995 -0
- package/payload/lanes/libexec/docsd.py +2010 -0
- package/payload/lanes/libexec/mdlite.py +575 -0
- package/payload/lanes/libexec/members.py +5 -0
- package/payload/lanes/libexec/notify.py +65 -5
- package/payload/lanes/libexec/sidebar.py +106 -13
- package/payload/lanes/libexec/statusd.py +97 -5
- package/payload/lanes/libexec/tokens.py +31 -9
- package/payload/lanes/libexec/update.py +94 -22
- package/payload/lanes/libexec/uploads.py +538 -0
- package/payload/lanes/systemd/lanes-docsd.service +29 -0
- package/payload/lanes/web/Caddyfile +24 -1
- package/payload/lanes/web/README.md +86 -8
- package/payload/lanes/web/browser.js +2 -4
- package/payload/lanes/web/docs.css +52 -0
- package/payload/lanes/web/docs.js +36 -0
- package/payload/lanes/web/mobile.js +3 -2
- package/payload/lanes/web/upload.css +17 -0
- package/payload/lanes/web/upload.js +84 -0
- 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/*`
|
|
47
|
-
the
|
|
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
|
|
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
|
|
293
|
-
|
|
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.
|
|
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(
|
|
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
|
+
}());
|
package/payload/manifest.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vibelord/lanes",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"commit": "
|
|
5
|
-
"digest": "sha256:
|
|
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",
|