@vibelord/lanes 1.1.0 → 1.2.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/README.md +36 -5
- package/bin/lanes-install.mjs +166 -13
- package/lib/plan.mjs +113 -3
- package/package.json +1 -1
- package/payload/lanes/README.md +47 -22
- package/payload/lanes/VERSION +1 -1
- package/payload/lanes/bin/lanes +2459 -164
- package/payload/lanes/client/ghostty/lanes-palette-light.conf +19 -19
- package/payload/lanes/client/ghostty/lanes-palette.conf +19 -19
- package/payload/lanes/client/lanes-launch +289 -40
- package/payload/lanes/client/lanes-window +15 -3
- package/payload/lanes/container/hooks/claude-settings.json +1 -0
- package/payload/lanes/container/lane +106 -92
- package/payload/lanes/container/lane-edit +100 -0
- package/payload/lanes/container/lane-mcp +1091 -0
- package/payload/lanes/container/lane-signal +109 -29
- package/payload/lanes/container/lane_core.py +1140 -0
- package/payload/lanes/container/vimrc +31 -0
- package/payload/lanes/docs/ARCHITECTURE.md +625 -15
- package/payload/lanes/docs/DESIGN.md +122 -44
- package/payload/lanes/docs/INSTALL.md +157 -17
- package/payload/lanes/docs/RUNBOOK.md +653 -81
- package/payload/lanes/docs/SHARING.md +3 -2
- package/payload/lanes/docs/USERGUIDE.md +792 -140
- package/payload/lanes/install/enable-web.sh +2 -0
- package/payload/lanes/install/host-install.sh +163 -7
- package/payload/lanes/install/services.py +148 -18
- package/payload/lanes/install.sh +68 -7
- package/payload/lanes/libexec/agent_state.py +74 -1
- package/payload/lanes/libexec/cursor_sync.py +6 -0
- package/payload/lanes/libexec/device_sync.py +47 -10
- package/payload/lanes/libexec/economy_poll.py +1 -0
- package/payload/lanes/libexec/enrold.py +41 -22
- package/payload/lanes/libexec/keys.py +54 -23
- package/payload/lanes/libexec/lanes_lib.py +620 -22
- package/payload/lanes/libexec/members.py +16 -2
- package/payload/lanes/libexec/notify.py +104 -24
- package/payload/lanes/libexec/registrations.py +216 -18
- package/payload/lanes/libexec/session_actions.py +182 -14
- package/payload/lanes/libexec/sidebar +11 -3
- package/payload/lanes/libexec/sidebar.py +2135 -361
- package/payload/lanes/libexec/statusd.py +137 -10
- package/payload/lanes/libexec/studio_host.py +8 -1
- package/payload/lanes/libexec/tokens.py +121 -30
- package/payload/lanes/libexec/tokens.sh +86 -2
- package/payload/lanes/libexec/update.py +972 -0
- package/payload/lanes/libexec/users.py +614 -0
- package/payload/lanes/libexec/view.sh +34 -6
- package/payload/lanes/systemd/lanes-ensure.service +3 -0
- package/payload/lanes/systemd/lanes-gc.service +1 -1
- package/payload/lanes/systemd/lanes-update.service +17 -0
- package/payload/lanes/systemd/lanes-update.timer +17 -0
- package/payload/lanes/web/README.md +207 -25
- package/payload/lanes/web/browser.js +3 -5
- package/payload/lanes/web/mobile.css +17 -17
- package/payload/lanes/web/mobile.js +128 -76
- package/payload/lanes/workspaces.example/README.md +3 -1
- package/payload/lanes/zellij/config.kdl +106 -56
- package/payload/manifest.json +5 -3
|
@@ -4,12 +4,23 @@
|
|
|
4
4
|
# loops forever and shows a status screen instead of dying.
|
|
5
5
|
# usage: view.sh <device> <workspace> [slot]
|
|
6
6
|
# switch lane: write the lane id to $STATE/views/<device>/<workspace>.<slot>.target, then `kill -USR1` this pid.
|
|
7
|
+
# new tree (cockpit v3 U5): `kill -USR2` this pid after an update flipped ~/.local/share/lanes/current. The view
|
|
8
|
+
# re-executes itself (`exec "$0" …`: the same pid, the path it was started by — a `current` path runs the new tree) at
|
|
9
|
+
# the NEXT NATURAL EXIT of its child: an attached lane's viewer detaching or a USR1 switch, the end of a status
|
|
10
|
+
# screen's wait. USR2 itself never signals the child, so it never drops an attached tmux client.
|
|
7
11
|
set -u
|
|
12
|
+
# Trapped first, before this view's pid file names it: a USR2 must never find the default action (terminate).
|
|
13
|
+
reexec=0
|
|
14
|
+
on_reexec() { reexec=1; }
|
|
15
|
+
trap on_reexec USR2
|
|
8
16
|
device=${1:?device} ws=${2:?workspace} slot=${3:-main}
|
|
9
17
|
mobile=0; [ "${4:-}" != --mobile ] || mobile=1
|
|
10
18
|
case "$device$ws$slot" in *[!A-Za-z0-9_.-]*) echo 'invalid view name'; exit 64 ;; esac
|
|
11
19
|
state=${LANES_STATE:-$HOME/.local/state/lanes}
|
|
12
|
-
|
|
20
|
+
# -P: the PHYSICAL tree. A layout names this script through the `current` link (W9 U1); resolving it once pins every
|
|
21
|
+
# holder and connection.py this loop runs to the tree the view started from, so an update that flips the link never
|
|
22
|
+
# mixes an old view.sh with new holders. The view moves to the new tree when it is started again.
|
|
23
|
+
repo=$(cd "$(dirname "$0")/.." && pwd -P)
|
|
13
24
|
dir=$state/views/$device
|
|
14
25
|
mkdir -p "$dir"
|
|
15
26
|
target_file=$dir/$ws.$slot.target
|
|
@@ -50,6 +61,13 @@ run() {
|
|
|
50
61
|
# A trapped signal interrupts wait; make sure the child is really gone before moving on.
|
|
51
62
|
while kill -0 "$child" 2>/dev/null; do wait "$child" 2>/dev/null || :; done
|
|
52
63
|
child=
|
|
64
|
+
# A USR2 came while the child ran (or before it): it has ended by itself now, so run the tree `current` names. A
|
|
65
|
+
# failed exec ends a non-interactive shell, so a script that is gone (a pruned tree) is not exec'd: this one goes on.
|
|
66
|
+
if [ "$reexec" = 1 ]; then
|
|
67
|
+
reexec=0
|
|
68
|
+
if [ -x "$0" ] && [ "$mobile" = 1 ]; then exec "$0" "$device" "$ws" "$slot" --mobile; fi
|
|
69
|
+
if [ -x "$0" ]; then exec "$0" "$device" "$ws" "$slot"; fi
|
|
70
|
+
fi
|
|
53
71
|
return "$rc"
|
|
54
72
|
}
|
|
55
73
|
|
|
@@ -58,20 +76,30 @@ reset_terminal() {
|
|
|
58
76
|
printf '\033[?1049l\033[?1000l\033[?1002l\033[?1003l\033[?1006l\033[?2004l\033[<u\033[0m\033[?25h\033[2J\033[3J\033[H'
|
|
59
77
|
}
|
|
60
78
|
|
|
61
|
-
# The
|
|
79
|
+
# The SGR literals of the design tokens (libexec/tokens.py --sh): sourced once, printf only, no fork. A plain source
|
|
80
|
+
# assigns the 16-colour set; `lz_tones` re-points them to another set (assignments and a `case`, no fork either).
|
|
62
81
|
# They hold `\033[…m` as text, so they go into printf's FORMAT (which turns `\033` into ESC), never into a %s argument.
|
|
63
82
|
# shellcheck source=libexec/tokens.sh
|
|
64
83
|
. "$repo/libexec/tokens.sh"
|
|
65
84
|
|
|
66
85
|
# status_screen <title> <text> <dim line> [<lead>]: a lead, when given, is the bold headline above the title.
|
|
67
|
-
# The
|
|
68
|
-
#
|
|
69
|
-
# too slow for one, and `lanes attach DEVICE --ascii` against a
|
|
86
|
+
# The look follows the stamps the sidebar publishes for this device from its resolved modes (devices/<device>.json
|
|
87
|
+
# first): $STATE/clients/<device>.glyphs, .color and .theme, read on EVERY call with the builtin read, no fork: this
|
|
88
|
+
# runs exactly when the host may be too slow for one, and `lanes attach DEVICE --ascii|--color C|--theme T` against a
|
|
89
|
+
# live session must reach the next screen without a restart. The lead glyph is `!` in ascii. `.color` and `.theme`
|
|
90
|
+
# pick the colour set (cockpit v3 P3): truecolor or 256 of dark or light, none (NO_COLOR: bold only), and anything
|
|
91
|
+
# else — no stamp, a vivid mode with no known variant yet — the 16-colour set, the user's own palette, never a guess.
|
|
92
|
+
# In the vivid sets (LZ_VIVID: the colours Lanes defines) the lead takes the warning mark's tone, attention, as the
|
|
93
|
+
# sidebar's banner does; 16 and none keep v1's bytes, as the sidebar's rows do.
|
|
70
94
|
status_screen() {
|
|
71
95
|
reset_terminal
|
|
72
96
|
glyphs=unicode; { IFS= read -r glyphs < "$state/clients/$device.glyphs"; } 2>/dev/null || :
|
|
97
|
+
color=16; { IFS= read -r color < "$state/clients/$device.color"; } 2>/dev/null || :
|
|
98
|
+
theme=; { IFS= read -r theme < "$state/clients/$device.theme"; } 2>/dev/null || :
|
|
99
|
+
lz_tones "$color" "$theme"
|
|
73
100
|
lead_glyph='⚠'; [ "$glyphs" != ascii ] || lead_glyph='!'
|
|
74
|
-
|
|
101
|
+
lead_tone=; [ -z "${LZ_VIVID:-}" ] || lead_tone=$LZ_ATTENTION
|
|
102
|
+
if [ -n "${4:-}" ]; then printf "\n ${LZ_BOLD}${lead_tone}%s %s${LZ_RESET}\n\n %s\n" "$lead_glyph" "$4" "$1"
|
|
75
103
|
else printf "\n ${LZ_BOLD}%s${LZ_RESET}\n" "$1"; fi
|
|
76
104
|
printf "\n %s\n\n ${LZ_DIM}%s${LZ_RESET}\n" "$2" "$3"
|
|
77
105
|
}
|
|
@@ -3,5 +3,8 @@ Description=lanes: recreate any missing device view session (agents are never to
|
|
|
3
3
|
|
|
4
4
|
[Service]
|
|
5
5
|
Type=oneshot
|
|
6
|
+
# No After=/Conflicts= with lanes-update.service: `lanes ensure` holds the view-session lock (locks/sessions.lock,
|
|
7
|
+
# DEF-1792) itself, as the update's `ensure --if-layout-changed`, `lanes rebuild` and every view create do. A tick that
|
|
8
|
+
# finds it held waits 10 s, then skips (exit 0). Conflicts= would stop a running update; After= orders queued jobs only.
|
|
6
9
|
ExecStart="@REPO@/bin/lanes" ensure
|
|
7
10
|
Slice=lanes.slice
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=lanes: install the newest npm release when there is one (lanes update --timer)
|
|
3
|
+
Documentation=https://github.com/bwllaming/bw-tools/blob/main/lanes/docs/USERGUIDE.md#update
|
|
4
|
+
|
|
5
|
+
[Service]
|
|
6
|
+
Type=oneshot
|
|
7
|
+
# Asks the npm registry for the release @vibelord/lanes's dist-tag names (`latest`, or `lanes-stable` with channel=stable
|
|
8
|
+
# in ~/.config/lanes/update.conf) and, when it is newer than the installed one, runs the npx the npm wrapper recorded at
|
|
9
|
+
# install (~/.local/state/lanes/update/npx): `npx --yes @vibelord/lanes@<release> --server --update-only`. A last run
|
|
10
|
+
# that ended `partial` gets its post-commit again first (once per release from here). A checkout install, an update
|
|
11
|
+
# already running and an older or rolled-back release are a line in the journal and exit 0 (plan W9 U3, LNS-91).
|
|
12
|
+
ExecStart="@REPO@/bin/lanes" update --timer
|
|
13
|
+
# A oneshot has no start timeout by default. The update's own bounds are npx (50 min) and each post-commit step (300 s);
|
|
14
|
+
# this one stops a run wedged anywhere else. A stop is SIGTERM to every process of the run: the installer records it.
|
|
15
|
+
TimeoutStartSec=1h
|
|
16
|
+
Nice=5
|
|
17
|
+
Slice=lanes.slice
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
[Unit]
|
|
2
|
+
Description=lanes: look for a newer npm release 15 min after boot, then every 30 min (enabled by the server install)
|
|
3
|
+
|
|
4
|
+
[Timer]
|
|
5
|
+
# Enabled by every full server install (install/host-install.sh) with the other timers. OnBootSec= is already in the
|
|
6
|
+
# past on a running host, so enabling it starts one check at once — refused while that install still holds the run lock
|
|
7
|
+
# (~/.local/state/lanes/update/.lock), harmless after it (the release just installed is the newest, or the next one is).
|
|
8
|
+
OnBootSec=15min
|
|
9
|
+
OnUnitActiveSec=30min
|
|
10
|
+
RandomizedDelaySec=5min
|
|
11
|
+
# Persistent= acts only on OnCalendar= timers (systemd.timer(5)): a host that was off misses no check here because
|
|
12
|
+
# OnBootSec= checks 15 min after every boot. Kept as the plan sets it; `lanes update` itself skips a check made less than
|
|
13
|
+
# 20 min before (update.INTERVAL).
|
|
14
|
+
Persistent=true
|
|
15
|
+
|
|
16
|
+
[Install]
|
|
17
|
+
WantedBy=timers.target
|
|
@@ -17,9 +17,11 @@ undoes the client's per-load single-pane start once
|
|
|
17
17
|
(`laneFullRender`, the client's own `SetMobileRenderPreferences {single_pane: false, fit: true}`), so a desktop
|
|
18
18
|
view's sidebar shows beside its lanes. `tests/web/activation.test.mjs` (`node --test`) pins the activation matrix,
|
|
19
19
|
the picker model and that render preference; `tests/unit/test_web_view.py` pins the size budgets and runs that
|
|
20
|
-
suite when node is installed.
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
suite when node is installed; `tests/web/phone.test.mjs` boots `mobile.js` as a phone against a fake page and
|
|
21
|
+
client and records every frame it sends (input v2's bytes, keyboard, clipboard, dock and menu; cockpit v3 S5's filter
|
|
22
|
+
Esc and prompt quick row; `test_web_view.PhoneKeys` names S5's tests and counts the file's). After an upgrade,
|
|
23
|
+
check that `__zjMobileUi`, `__zjSendControl`, `__zjSoftKbdCapture` (with `sendFn` = `websockets.sendAnsiKey`),
|
|
24
|
+
`__zjSyncInactiveCursorStyle` and `window.term` still exist in the served `app.js`.
|
|
23
25
|
|
|
24
26
|
The phone keyboard opens through a native tap on an input field. The bottom **Type here** field contains
|
|
25
27
|
Zellij's original capture input in its closed shadow root, preserving its keyboard/IME handling. The client
|
|
@@ -30,7 +32,11 @@ The guard is installed before Zellij's first terminal or login modal: otherwise
|
|
|
30
32
|
touchstart, touchend, click and server `SetSoftKeyboard` messages. The **Type here** field keeps the original input
|
|
31
33
|
and its event handlers inside the closed shadow root (including its protection from password-manager extensions) and
|
|
32
34
|
makes it a real, tappable field instead of a 1 px invisible focus target; keystrokes appear in the terminal and its
|
|
33
|
-
internal padding/history stays invisible. Upstream
|
|
35
|
+
internal padding/history stays invisible. Upstream creates that input as `type="password" autocomplete="new-password"`
|
|
36
|
+
(to switch off prediction); shown to the user, that gave iOS a secure-entry keyboard and the strong-password bar
|
|
37
|
+
(DEF-1734), so the shell makes it `type="text" autocomplete="off" autocapitalize="none"` and keeps upstream's
|
|
38
|
+
`autocorrect="off"`, `spellcheck="false"`, `inputmode="text"` and the `data-*ignore` attributes. Upstream cancels
|
|
39
|
+
`pointerdown` on the extra keys, which also suppresses
|
|
34
40
|
their touch click in Safari, so the shell cancels `mousedown` instead: typing focus is kept while the native click
|
|
35
41
|
still activates CTRL, the arrows and the other keys. The shell attaches only once server state has arrived — resizing
|
|
36
42
|
before the control socket opens makes 0.45.1 throw `InvalidStateError` and leaves the server with the old size.
|
|
@@ -46,7 +52,9 @@ Run `lanes web <device> --token` on the server (it also adds that Tailscale Serv
|
|
|
46
52
|
`lanes-launch web phone|laptop` from an installed client, or `sh install/enable-phone.sh --host lanes` (`--local` on
|
|
47
53
|
the server).
|
|
48
54
|
The [installation guide](../docs/INSTALL.md) covers Caddy, Tailscale operator permission and the selected SSH key.
|
|
49
|
-
Service registration supplies `LANES_REPO`
|
|
55
|
+
Service registration supplies `LANES_REPO` — the `~/.local/share/lanes/current` link, which names the installed
|
|
56
|
+
tree (W9 U1) — so Caddy serves that tree from any installation path, and the files an update brings as soon as
|
|
57
|
+
the link moves; the routes of a new Caddyfile take a restart of `lanes-mobile` (the server installer does it).
|
|
50
58
|
|
|
51
59
|
`index.html` keeps the upstream asset integrity hashes. When upgrading Zellij, compare the HTML and
|
|
52
60
|
window hooks against the new version before changing the pinned binary. The mobile JS and CSS have
|
|
@@ -60,12 +68,112 @@ is stored locally. Minimum touch targets are 44 px (workspace cards are 74 px).
|
|
|
60
68
|
`tests/mobile_view.py` verifies the touch-to-lane path with isolated holders; sidebar unit tests cover the
|
|
61
69
|
complete touch target.
|
|
62
70
|
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
71
|
+
### Input, keys and clipboard (phone shell input v2, CAP-142)
|
|
72
|
+
|
|
73
|
+
Every byte the shell sends to a terminal goes through `send()` to `__zjSoftKbdCapture.sendFn`, which is upstream's
|
|
74
|
+
`websockets.sendAnsiKey`: a text frame on the terminal WebSocket, the path the client's own capture input, ESC/arrow
|
|
75
|
+
bar and xterm `onData` use (an armed CTRL/ALT on the modifier bar therefore applies to a one-character key, through
|
|
76
|
+
upstream's `__zjMobileMergeKey`). Until 2026-10-07 the shell posted `__zjSendControl({type: 'Write', bytes})` on the
|
|
77
|
+
control socket, whose message enum has no `Write` variant: the server logged "Ignoring unknown control message type"
|
|
78
|
+
and dropped every quick key and filter keystroke (DEF-1733). The quick keys send `\r`, `y`, `n`, `\x1b`, `\x03`,
|
|
79
|
+
`\x1b[A`, `\x1b[B` — upstream's own arrow bytes (CSI, never SS3; Zellij re-encodes a key for its pane). With no
|
|
80
|
+
`sendFn` the text is queued and sent by the next `refresh()` (no timer); the pinned client installs `sendFn` together
|
|
81
|
+
with the capture element the shell waits for, so the queue only covers an upstream reordering. A key the socket
|
|
82
|
+
refuses (it throws while not open) is dropped, never replayed later into whatever lane is focused then. There is no
|
|
83
|
+
focus-and-type fallback: the focus guard above would make it type nowhere (DEF-1529).
|
|
84
|
+
|
|
85
|
+
The lane filter bar never puts an Esc and other bytes in one frame. The server parses each text frame with termwiz,
|
|
86
|
+
which holds a leading ESC while more bytes follow: `\x1b/abc` became one Alt-`/` key whose raw bytes are the whole
|
|
87
|
+
frame, then `a`, `b`, `c` again as keys of their own, so the picker sidebar got `\x1b/abc` + `abc` and filtered on
|
|
88
|
+
`abcabc` — on every keystroke, since the one-frame filter re-sent the whole query each time (DEF-1751, INFERRED from
|
|
89
|
+
the independent review's reading of the Zellij 0.45.1 source; `tests/web/phone.test.mjs` models it). The filter now
|
|
90
|
+
sends only what changed. When the field goes from empty to text: a lone `Esc` frame (it leaves whatever the sidebar is
|
|
91
|
+
in — an open filter, a rename, a close prompt, the key sheet), then, no sooner than 60 ms later, `/` alone and the
|
|
92
|
+
text. After that, one DEL (`\x7f`, which the sidebar's filter takes as backspace, as it does `\x08`) for each
|
|
93
|
+
character of the old query after its common prefix with the new one, then the new query's characters after that
|
|
94
|
+
prefix; an emptied field or the Esc key sends a lone `Esc`. The text is the `TextEncoder` bytes with control bytes and
|
|
95
|
+
DEL dropped, decoded back to text. There is no `setTimeout`: `send()` keeps a pause marker in its queue after a lone
|
|
96
|
+
`Esc` and re-arms the one interval, whose next tick sends what follows once 60 ms have passed. The phone keeps the
|
|
97
|
+
last query it sent (`laneFilterPrev`) and the picker it was typed on (`filterTab`, cockpit v3 S5); once a refresh
|
|
98
|
+
finds another tab current — a lane, or another workspace's picker by the Workspaces sheet — it forgets the query,
|
|
99
|
+
empties the field and drops a filter tail still waiting in the queue, without sending anything: the lane now has the
|
|
100
|
+
focus, and an `Esc` there can interrupt an agent (DEF-1752 here, DEF-1817 on S5's line; the sidebar clears its own
|
|
101
|
+
filter when it opens a lane), and another workspace's picker starts with an empty bar instead of the first one's
|
|
102
|
+
query, whose next letter the deltas would have sent there as a bare key (DEF-1820). The lone `Esc` of an emptied
|
|
103
|
+
field or of the Esc key, and the one a picker left filtered is owed, go through one debt (`owed`, picker tab names):
|
|
104
|
+
`updateLaneFilterBar` sends it only while the picker it is owed to is the current tab, so a picker left filtered shows
|
|
105
|
+
its whole list again on its next visit, and an Escape that arrives once the client has already moved on is held,
|
|
106
|
+
never sent into the lane or another picker. Limits: the deltas assume the
|
|
107
|
+
sidebar holds what the phone sent, which each new query's leading `Esc` restores — another client typing into the same
|
|
108
|
+
picker pane, or a character the sidebar does not print (a no-break space, a zero-width joiner), puts them out of step
|
|
109
|
+
until the field is cleared; and an `Esc` while the sidebar's upload strip is armed (`U`) returns the focus to the
|
|
110
|
+
lane, so the `/` and text after it could reach the lane unless the tab change arrives within the pause.
|
|
111
|
+
|
|
112
|
+
Typing in **Type here**: one character at a time flows through upstream's `input` handler unchanged (a diff of the
|
|
113
|
+
field against its no-break-space padding, with an 8 ms duplicate filter for the two `deleteContentBackward` events one
|
|
114
|
+
backspace fires). A text field brings back QuickType, whose multi-character inserts would lose a repeated letter to
|
|
115
|
+
that filter (`hello` → `helo`). The shell therefore cancels two cancelable `beforeinput` types and sends their text
|
|
116
|
+
whole: `insertText` longer than one character, and `insertReplacementText` of any length. The text is `data`, or
|
|
117
|
+
`dataTransfer.getData('text')` where an engine leaves `data` null. A replacement first sends one DEL per character it
|
|
118
|
+
replaces: the length of `getTargetRanges()[0]` (its `endOffset − startOffset` in the field) — iOS accepts a QuickType
|
|
119
|
+
suggestion over a partial word with a caret, not a selection, and names the replaced range there — else the field's
|
|
120
|
+
selection, else nothing (DEF-1753; before, only the selection was read, so a caret replacement sent `hehello`). Every
|
|
121
|
+
other type stays upstream's or another handler's: a composition (`insertCompositionText`, which the Input Events spec
|
|
122
|
+
makes uncancelable, and `insertFromComposition`) is upstream's diff, a paste is the `paste` handler's, a delete is a
|
|
123
|
+
key. Caveat, unverified on a device: an engine that names neither the range nor a selection gives the terminal the
|
|
124
|
+
partial word and then the suggestion (`hehello`); delete by hand or type without the suggestion bar.
|
|
125
|
+
|
|
126
|
+
Paste: a `paste` on the field is cancelled and its `text/plain` sent as ONE bracketed paste — `\x1b[200~`, the text
|
|
127
|
+
with `\r?\n` as `\r` and every end marker neutralised, `\x1b[201~` — so upstream's per-character path, which collapsed
|
|
128
|
+
`ll` to `l` and stripped the newlines (DEF-1735), never sees it. Only the end marker is touched: each run of ESCs
|
|
129
|
+
directly before `[201~` is removed (a run, so `\x1b\x1b[201~` or `\x1b[20\x1b[201~1~` cannot reassemble one), so the
|
|
130
|
+
clipboard cannot end the bracket early and run the rest as typed keys, while a deliberate ANSI paste keeps its other
|
|
131
|
+
escape sequences (until DEF-1755 every ESC was removed). On a picker tab there is no paste: the picker sidebar turns
|
|
132
|
+
on bracketed paste to take file drops and reads any paste as one (text says `that was not a file drop`; a pasted path
|
|
133
|
+
becomes an upload attempt), so the header's **Paste** is hidden there and a paste into **Type here** is cancelled,
|
|
134
|
+
sends nothing and says `Open a lane to paste` (DEF-1754). Explicit markers rather than xterm's `term.paste` (also
|
|
135
|
+
reachable, as `window.term`): `term.paste` brackets only when the server has turned on xterm's bracketed-paste mode,
|
|
136
|
+
which nothing in this tree shows; Zellij reads the markers as a paste and removes them for a pane that has not asked
|
|
137
|
+
for bracketed paste.
|
|
138
|
+
|
|
139
|
+
**Copy** and **Paste** sit in the header beside **More** (nine 44 px quick keys do not fit a 390 px phone, so the row
|
|
140
|
+
keeps its seven). Copy writes the terminal's selection when xterm has one, else the visible rows of
|
|
141
|
+
`window.term.buffer.active` (`translateToString(true)` per row, trailing blank rows dropped), with
|
|
142
|
+
`navigator.clipboard.writeText` called inside the tap: Safari refuses a write outside a user gesture, which is why the
|
|
143
|
+
client's clipboard add-on (OSC 52) and copy-on-select never reached the phone — upstream cancels `touchstart`, a drag
|
|
144
|
+
scrolls and a long press is a right click (DEF-1736). The buffer is the only text source: `initTerminal` loads xterm's
|
|
145
|
+
WebGL renderer (`addon-webgl`), so the DOM holds no row text. The copy is the whole visible screen, Zellij's frames
|
|
146
|
+
and bars included. Paste calls `navigator.clipboard.readText()` inside the tap (iOS shows its own Paste confirmation)
|
|
147
|
+
and sends the text through the bracketed path above; it is hidden on a picker tab. Both cancel `mousedown`, so an open
|
|
148
|
+
keyboard stays open. The header subtitle says `Copied`, `Pasted` or `Clipboard blocked` (no clipboard API, or a
|
|
149
|
+
refusal).
|
|
150
|
+
|
|
151
|
+
Hints (`Hold to send` and the clipboard results) hold the header subtitle for eight ticks of the one 200 ms interval:
|
|
152
|
+
`hint()` re-arms it, `tick()` counts down, restores the subtitle through `refresh()` and clears the interval again,
|
|
153
|
+
and `refresh()` leaves the subtitle alone while a hint is up (before, the next `MobileState` refresh overwrote it at
|
|
154
|
+
once, DEF-1739). **More** toggles: upstream's capture-phase `pointerdown` hook closes an open menu before the button's
|
|
155
|
+
click, which then reopened it (DEF-1738), so a window capture-phase `pointerdown` listener records whether the menu was
|
|
156
|
+
open and the click then leaves it closed. On a picker tab the dock shows the filter bar and hides **Panes** (a picker
|
|
157
|
+
tab has one pane, and upstream's pane list duplicates the picker): four items for the four grid columns, where five
|
|
158
|
+
wrapped **Type here** below the fixed-height dock (DEF-1737).
|
|
159
|
+
|
|
160
|
+
The pinned `app.js` facts this section relies on (`sendFn` = `sendAnsiKey`, the capture input's attributes and
|
|
161
|
+
handlers, the menu dismiss hook, `window.term`, the WebGL addon) were read on 2026-10-07 from the asset embedded in
|
|
162
|
+
the zellij 0.45.1 release binary: the bytes between the `app.js` and `auth.js` names in its asset table hash to this
|
|
163
|
+
`index.html`'s SRI pin (sha384 `zEYVY/2Dhk…`). `tests/web/phone.test.mjs` runs `mobile.js` against a fake page shaped
|
|
164
|
+
on those facts (`node --test`, no jsdom).
|
|
165
|
+
|
|
66
166
|
Attention counts come from the picker sidebar's pane title (`WS · ◆1 ◇2 ▸6`, written by the sidebar as OSC 2):
|
|
67
167
|
`parseCounts` (also `window.__lanesParseCounts`) feeds the picker subtitle, `document.title`, the Lanes button's
|
|
68
168
|
`data-count` and the workspace cards, inside the existing `refresh()` / `renderCards()` — no fetch, file or timer.
|
|
169
|
+
The shell makes one request (W9 V3, LNS-85): once per page load, when it builds, `installFontControls()` asks
|
|
170
|
+
`/enrol/site` on the same origin (the `/enrol/*` route above; the enrolment service's `GET /site` answers
|
|
171
|
+
`{alias, host_name, host_key, version, request_mode, caller, source}`) and shows its `version` as the first row of
|
|
172
|
+
the client's **More** menu, **Lanes v1.2.0**, above **Text size** — the same `.lanes-font-controls` row, so no CSS.
|
|
173
|
+
Only a release is shown (`^\d[\w.+-]{0,31}$`, as text): `dev`, anything else, an error answer, a non-JSON 502 while
|
|
174
|
+
the service is down, or no network shows nothing and throws nothing. The version is the enrolment service's tree
|
|
175
|
+
(the server installer restarts it on every install). `tests/web/version.test.mjs` boots `mobile.js` against a fake
|
|
176
|
+
logged-in page to pin this; `test_web_view` pins one fetch, inside `installFontControls()`, which `build()` runs once.
|
|
69
177
|
Until plan Task 26 (2026-10-04) this reader was inert (DEF-1531): `bin/lanes` `mobile_workspace_tab` gave the
|
|
70
178
|
picker pane a fixed `name="Lanes"`, and Zellij reports a pane's layout name instead of its OSC 2 title, so
|
|
71
179
|
`pickerTitle()` read `Lanes`, `parseCounts` returned null and the shell showed no counts ("Choose a lane to continue").
|
|
@@ -74,9 +182,15 @@ phone device keeps the old picker pane until `lanes rebuild <device>` (`upgrade-
|
|
|
74
182
|
`lanes doctor` names such a device). Whether the OSC 2 title now reaches
|
|
75
183
|
`panes[].title` is still unchecked in a browser (no WebKit pass has run; fallback: the counts stay in the
|
|
76
184
|
sidebar header). The quick-key row (`⏎ y n Esc ^C ↑ ↓`, `.lanes-quick`) shows on view
|
|
77
|
-
tabs
|
|
78
|
-
|
|
79
|
-
|
|
185
|
+
tabs; on the picker tab it shows only while the sidebar asks for a key (a close, the Register confirmation or Add to
|
|
186
|
+
workspace, which the sidebar announces by ending its pane title with ` · › <verb>`, ` · > <verb>` under ASCII glyphs;
|
|
187
|
+
DM-LANES-SIDEBAR-TITLE), and then only `⏎ y Esc` (cockpit v3 S5) — `^C`, `n` and the arrows have no use in a prompt.
|
|
188
|
+
A name field (Rename, Register as) is never announced: there `y` is a letter of the name (DEF-1819). `refresh()` sets
|
|
189
|
+
`--lanes-quick` (48 px or 0), which the terminal height subtracts. `n` and `^C` send
|
|
190
|
+
only when `pointerup.timeStamp − pointerdown.timeStamp ≥ 400` (no `setTimeout`); `pointercancel` and `pointerleave`
|
|
191
|
+
disarm a key, and a `pointerup` without the key's own `pointerdown` sends nothing (before, a cancelled hold stayed
|
|
192
|
+
armed, so a pointer released over `^C` later sent it, DEF-1739). The buttons cancel `mousedown` so a tap never moves
|
|
193
|
+
focus.
|
|
80
194
|
|
|
81
195
|
Colour (spec §6.3; the operator legend, token table and phone mockup are in [`docs/DESIGN.md`](../docs/DESIGN.md)):
|
|
82
196
|
`mobile.css` uses only the generated `--lanes-*` tokens (legacy names such as `--lanes-bg` stay
|
|
@@ -90,46 +204,114 @@ chaining `window.__zjSyncInactiveCursorStyle`, which the pinned `app.js` calls i
|
|
|
90
204
|
assigning the server theme wholesale (`SetConfig` is sent on each config-file reload). The handler then runs
|
|
91
205
|
`applyTerminalBackground`, which writes the dark background inline on `<body>` and `#terminal`; the stylesheet's
|
|
92
206
|
`!important` `--lanes-surface` rule for those two elements outranks it in either variant. An SRI bump of `app.js`
|
|
93
|
-
re-opens this contract: re-read `startWsControl`
|
|
94
|
-
`
|
|
95
|
-
`
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
207
|
+
re-opens this contract: re-read `startWsControl`, `installSoftKeyboardCapture`, `initWebSockets` (`sendAnsiKey`) and
|
|
208
|
+
`installMenuDismissHook` before accepting it.
|
|
209
|
+
`index.html` carries one `theme-color` per colour scheme, each that variant's `surface` token, and the manifest's
|
|
210
|
+
`background_color` and `theme_color` are the dark `surface`; they are hand-written, so `test_tokens.GeneratedSurfaces`
|
|
211
|
+
holds them to the table (cockpit v3 P5). The icon (`icon.svg`, `icon-192.png`, a raster with anti-aliased edges) is
|
|
212
|
+
the brand mark, drawn before the token table and not generated: palette v2 did not recolour it, and its bars keep
|
|
213
|
+
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
|
|
215
|
+
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
|
+
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
|
+
fetches `browser.js` and `browser.css` (1 947 B); `test_web_view` pins that download at `LAPTOP_TABLET_JS_RATCHET` and
|
|
218
|
+
`LAPTOP_TABLET_CSS_CAP` (DEF-1569).
|
|
219
|
+
|
|
220
|
+
The two lines that met at the cockpit v3 main sync (D2, LNS-99, 2026-10-07). On `wave/lanes-cockpit-v3`: after
|
|
221
|
+
cockpit v3 S5 (the filter's Esc, the prompt quick row) `mobile.js` 22 164 B, and `browser.js` 3 334 B (3 641 B before
|
|
222
|
+
its comments moved to Code notes below, to keep that download inside the ratchet, then pinned at 25 498 B); after S5
|
|
223
|
+
fix cycle 1 (the Esc owed to the picker it was typed on) `mobile.js` 22 162 B and the ratchet 25 496 B. W9 V3's
|
|
224
|
+
release row (+257 B of code; `mobile.js`'s line-1 pointer comment moved here first) measured 22 164 B on its own
|
|
225
|
+
track; the fan-in of the two tracks measured 22 362 B, over §8's 22 174 B, and paid for it inside `mobile.js` with
|
|
226
|
+
equivalent code — the four viewport properties through one setter, one `fill` style shared by the typing field's
|
|
227
|
+
host and capture, the Workspaces icon's four squares drawn from their corners: 22 153 B, the download 25 487 B. On
|
|
228
|
+
`bwtools/main`: the JS cap was spec §8's 22 174 B until phone shell input v2 (2026-10-07, DEF-1733): the working send
|
|
229
|
+
path, the text keyboard, paste, Copy and Paste, the dock, More and hint fixes grew `mobile.js` to 23 456 B MEASURED
|
|
230
|
+
after the dead `Write` path, the redundant focus/blur `is-typing` listeners and the file's header comment were
|
|
231
|
+
removed and the new code was written compactly (`kbd()`, `aria()`), so the cap rose by exactly 1 282 B to that size
|
|
232
|
+
(`mobile.css` unchanged); the independent review's fixes the same day (the filter's Esc-free frames and deltas,
|
|
233
|
+
replacement ranges, no paste on a picker, only the paste end marker neutralised; DEF-1751..DEF-1755) grew it to
|
|
234
|
+
24 258 B MEASURED, and the cap and the ratchet (27 899 B: 25 605 B plus the same 1 492 + 802 B) rose by exactly those
|
|
235
|
+
802 B again. The composed file — input v2's frames, deltas, Copy/Paste and dock, plus S5's per-picker Esc debt and
|
|
236
|
+
prompt quick row and W9 V3's release row, the branch's equivalent rewrites applied to the code both kept —
|
|
237
|
+
MEASURED `mobile.js` 24 612 B (+354 B over input v2) and the download 24 612 + 3 334 = 27 946 B: the phone cap rose by
|
|
238
|
+
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
|
+
(`test_web_view.JS_COCKPIT_V3`, `LAPTOP_TABLET_JS_RATCHET`), and all four cap assertions red on one byte more.
|
|
100
240
|
|
|
101
241
|
## Code notes
|
|
102
242
|
|
|
103
243
|
`mobile.js` carries no explanations (the spec §8 budget above); each note is keyed to the code it explains, in file
|
|
104
244
|
order.
|
|
105
245
|
|
|
106
|
-
- The file: the Lanes browser shell (phone, tablet, desktop) over the pinned Zellij 0.45.1 client's window hooks.
|
|
246
|
+
- The file: the Lanes browser shell (phone, tablet, desktop) over the pinned Zellij 0.45.1 client's window hooks. Its
|
|
247
|
+
explanations are these notes (the file's line-1 pointer to them moved here too, W9 V3).
|
|
107
248
|
- `root.dataset.lanesShell = mode`: read by `browser.js` for the desktop bar and the tablet's full rendering.
|
|
108
249
|
- `if (mode !== 'phone')`: appends the laptop/tablet extras (`browser.css`, `browser.js`); a phone never fetches them.
|
|
109
250
|
- `if (mode === 'tablet')`: iPad Safari's user agent is a desktop one, so the shell asks for the client's mobile UI
|
|
110
251
|
(`zellij:mobile-ui` set to `on`); a stored `off` stays.
|
|
111
252
|
- The `HTMLElement.prototype.focus` guard: only a native tap on an input may open the keyboard; it is installed before
|
|
112
253
|
upstream's first terminal or modal.
|
|
254
|
+
- `kbd()` / `aria()`: `window.__zjSoftKbdCapture` and an `aria-label` setter, shared to keep the phone budget.
|
|
113
255
|
- `viewport()`: browser toolbars alone must not mean keyboard mode (`occluded` needs more than 120 px hidden). Zellij's
|
|
114
256
|
input is in a closed shadow root, so `document.activeElement` is its host; `__zjSoftKbdCapture.isFocused` is read
|
|
115
257
|
first.
|
|
116
|
-
- `
|
|
258
|
+
- `setKeyboard()`: also the one place **Type here** gets `is-typing`, driven by upstream's own focus/blur listeners
|
|
259
|
+
(they set `isFocused` and fire `zellij:soft-keyboard-visibility`); the field's own focus/blur listeners were redundant.
|
|
260
|
+
- `buildTypingField()`: upstream's capture input stays in its closed shadow root and becomes the real, tappable field:
|
|
261
|
+
a text input (DEF-1734); `beforeinput` sends a multi-character `insertText` or any `insertReplacementText` whole,
|
|
262
|
+
after one DEL per character of `getTargetRanges()[0]` or else the selection (DEF-1753); `paste` sends one bracketed
|
|
263
|
+
paste (DEF-1735). See Input, keys and clipboard above.
|
|
117
264
|
- `GLYPHS` / `parseCounts()`: counts from the picker sidebar's OSC 2 pane title (spec §6.4).
|
|
265
|
+
- `build()`, the header: the window capture-phase `pointerdown` listener records `menuWas` before upstream's dismiss
|
|
266
|
+
hook runs, so **More** closes an open menu instead of reopening it (DEF-1738); **Copy** and **Paste** (DEF-1736).
|
|
118
267
|
- `build()`, the `.zj-mobile-modbar` keys: cancel `mousedown`, not `pointerdown` (upstream's `pointerdown` cancel kills
|
|
119
268
|
Safari's touch click on the extra keys).
|
|
269
|
+
- `send()`: the terminal socket through `sendFn` (DEF-1733); queued only while `sendFn` is absent, never after a throw.
|
|
270
|
+
A `null` in the queue is a pause: what follows waits until 60 ms after the last frame sent (`sentAt`), and `send()`
|
|
271
|
+
re-arms the one interval to come back (DEF-1751).
|
|
272
|
+
- `paste`: the bracketed paste through `send()`, its end markers neutralised (DEF-1755); nothing on a picker tab, where
|
|
273
|
+
the sidebar reads a paste as a file drop (DEF-1754).
|
|
274
|
+
- `applyLaneFilter()`: the filter's UTF-8 query as deltas against `laneFilterPrev` — a lone Esc, a pause, `/` and the
|
|
275
|
+
text for a new query; DELs and the added text for an edit; a lone Esc and a pause to clear (DEF-1751).
|
|
276
|
+
- `clip()`: calls the clipboard API synchronously inside the tap (Safari's user-gesture rule) and hints the result.
|
|
277
|
+
- `screen()`: xterm's selection, else the visible rows of `term.buffer.active` (the WebGL renderer leaves no DOM text).
|
|
278
|
+
- `hint()`: holds the subtitle for eight ticks of the one interval (DEF-1739).
|
|
120
279
|
- `buildQuickRow()` (spec §6.5): `n` and `^C` send after a 400 ms hold, measured from event timestamps with no timer;
|
|
121
|
-
nothing takes focus.
|
|
122
|
-
- `clearLaneFilter()`: the lone `Esc` leaves the sidebar's
|
|
280
|
+
a cancelled or left key is disarmed and a release needs the key's own press (DEF-1739); nothing takes focus.
|
|
281
|
+
- `clearLaneFilter()`: forgets the query and owes its picker (`filterTab`) the lone `Esc` that leaves the sidebar's
|
|
282
|
+
filter mode (`owed`, a set of picker tab names). `updateLaneFilterBar(tab, isPicker)`, on every refresh: once the
|
|
283
|
+
current tab is not the one the query was typed on, a filter tail still waiting in the queue is dropped and the query
|
|
284
|
+
cleared, sending nothing (the lane may have the focus now, DEF-1752); the lone `Esc` and its pause are sent only when
|
|
285
|
+
the current tab is an owed picker; then `filterTab` becomes the current tab if it is a picker. Escape in the bar and
|
|
286
|
+
an emptied field clear and call `refresh()`, so the `Esc` goes out at once while that picker is still current.
|
|
123
287
|
- `lanesTheme()`: the shell's own terminal palette for both variants (spec §6.3, DEF-1413).
|
|
288
|
+
- `refresh()`: leaves the subtitle to a live hint, hides **Panes** (and **Paste**, DEF-1754) on a picker tab so the
|
|
289
|
+
dock's four columns hold its four items (DEF-1737), and flushes the `send()` queue after `updateLaneFilterBar()`, so a
|
|
290
|
+
filter tail is dropped before it could reach a lane; `quick` is `true` on a view tab (every key), `2` on the picker
|
|
291
|
+
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.
|
|
124
294
|
- `connect()`: waits for server state, because an earlier resize throws `InvalidStateError` in 0.45.1. `applyTheme` is
|
|
125
295
|
re-applied after every `SetConfig` (the client calls the chained `__zjSyncInactiveCursorStyle` hook) and on the theme
|
|
126
296
|
toggle.
|
|
127
297
|
- The `zellij:soft-keyboard-visibility` listener: rotation also shrinks the viewport, so the shell sends
|
|
128
298
|
`SoftKeyboardVisibilityChanged` itself to correct upstream's height-only keyboard heuristic.
|
|
129
|
-
- `
|
|
130
|
-
appending its mobile root
|
|
299
|
+
- `tick()` / `arm()` (the one `setInterval`, 200 ms): polls `connect()` until attached, because upstream installs its
|
|
300
|
+
window hooks just after appending its mobile root; `hint()` and a paused `send()` re-arm it; each tick flushes the
|
|
301
|
+
queue, and it clears itself once attached, with no hint up and no pause waiting.
|
|
302
|
+
|
|
303
|
+
`browser.js` (laptop/tablet extras) carries none either, since cockpit v3 S5:
|
|
304
|
+
|
|
305
|
+
- `laneFullRender()`: the client starts a tablet in single pane on every load; once, it sends the client's own
|
|
306
|
+
switch-to-desktop preference.
|
|
307
|
+
- `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.
|
|
309
|
+
- `ui()`: `window.__zjMobileUi` is set synchronously after the client appends its mobile root, which the
|
|
310
|
+
`MutationObserver` on `<body>` waits for.
|
|
131
311
|
|
|
132
312
|
Validate on Safari/WebKit at 440 × 763 (iPhone 17 Pro Max with browser chrome), 440 × 956 (Home Screen),
|
|
133
313
|
838 × 390 (landscape Safari), and smaller phone widths. Check login, workspace search and switching,
|
|
134
|
-
lane selection, pane navigation, font controls
|
|
314
|
+
lane selection, pane navigation, font controls and the release row in **More**, rotation and the visible viewport
|
|
315
|
+
while the keyboard opens, and the input checklist in the [runbook](../docs/RUNBOOK.md) (text keyboard, quick keys,
|
|
316
|
+
paste, Copy and Paste, the picker's filter and prompt row).
|
|
135
317
|
Use disposable view sessions and revoke any temporary test token afterward.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
/*
|
|
1
|
+
/* Lanes laptop/tablet extras; see web/README.md, Code notes */
|
|
2
2
|
function laneBarModel(d) {
|
|
3
3
|
const panes = (d?.panes || []).filter(p => !p.is_plugin), at = d?.active_pane || {}, ws = new Map();
|
|
4
4
|
const first = t => t && panes.find(p => p.tab_position === t.position);
|
|
@@ -14,7 +14,6 @@ function laneBarModel(d) {
|
|
|
14
14
|
.map(p => item(p.title || `Pane ${p.pane_id}`, p))].filter(Boolean),
|
|
15
15
|
};
|
|
16
16
|
}
|
|
17
|
-
// Tablet: upstream starts single pane on every load; once, send its own switch-to-desktop prefs.
|
|
18
17
|
function laneFullRender(api, send) {
|
|
19
18
|
let done;
|
|
20
19
|
return () => {
|
|
@@ -41,7 +40,6 @@ if (typeof module === 'object') module.exports = {laneBarModel, laneFullRender};
|
|
|
41
40
|
const full = laneFullRender(api, send);
|
|
42
41
|
return () => full() && dispatchEvent(new Event('zellij:rendering-resize'));
|
|
43
42
|
};
|
|
44
|
-
// Desktop: the chip toggles an overlay bar (no refit); picks send the phone picker's FocusPane.
|
|
45
43
|
const desktop = api => {
|
|
46
44
|
let last;
|
|
47
45
|
const picks = ['Workspace', 'Lane'].map(title => make('select', {title, onchange() {
|
|
@@ -61,13 +59,13 @@ if (typeof module === 'object') module.exports = {laneBarModel, laneFullRender};
|
|
|
61
59
|
render();
|
|
62
60
|
window.term?.focus();
|
|
63
61
|
}});
|
|
64
|
-
chip.addEventListener('mousedown', ev => ev.preventDefault());
|
|
62
|
+
chip.addEventListener('mousedown', ev => ev.preventDefault());
|
|
65
63
|
bar.addEventListener('keydown', ev => ev.key === 'Escape' && focus());
|
|
66
64
|
bar.append(...picks);
|
|
67
65
|
document.body.append(bar, chip);
|
|
68
66
|
return render;
|
|
69
67
|
};
|
|
70
|
-
const ui = () => window.__zjMobileUi;
|
|
68
|
+
const ui = () => window.__zjMobileUi;
|
|
71
69
|
if (ui()) return ready(ui());
|
|
72
70
|
new MutationObserver((_, o) => ui() && (o.disconnect(), ready(ui()))).observe(document.body, {childList: true});
|
|
73
71
|
})();
|
|
@@ -3,20 +3,20 @@ html.lanes-mobile {
|
|
|
3
3
|
--lanes-surface: #101318;
|
|
4
4
|
--lanes-surface-raised: #191e25;
|
|
5
5
|
--lanes-line: #2c333d;
|
|
6
|
-
--lanes-accent-wash: #
|
|
6
|
+
--lanes-accent-wash: #1c3326;
|
|
7
7
|
--lanes-accent-line: #4b6955;
|
|
8
8
|
--lanes-press: #26332c;
|
|
9
9
|
--lanes-chip: #ffffff0f;
|
|
10
|
-
--lanes-ink: #
|
|
11
|
-
--lanes-ink-dim: #
|
|
12
|
-
--lanes-ink-faint: #
|
|
13
|
-
--lanes-accent: #
|
|
10
|
+
--lanes-ink: #f2f5f8;
|
|
11
|
+
--lanes-ink-dim: #a6b1be;
|
|
12
|
+
--lanes-ink-faint: #6f7a87;
|
|
13
|
+
--lanes-accent: #8fe3a6;
|
|
14
14
|
--lanes-on-accent: #101318;
|
|
15
15
|
--lanes-on-accent-urgent: #7a1a12;
|
|
16
|
-
--lanes-tone-urgent: #
|
|
17
|
-
--lanes-tone-attention: #
|
|
18
|
-
--lanes-tone-working: #
|
|
19
|
-
--lanes-tone-done: #
|
|
16
|
+
--lanes-tone-urgent: #ff7b6e;
|
|
17
|
+
--lanes-tone-attention: #ffc233;
|
|
18
|
+
--lanes-tone-working: #6fb6ff;
|
|
19
|
+
--lanes-tone-done: #5fdc8a;
|
|
20
20
|
--lanes-scrim: #000000a6;
|
|
21
21
|
--lanes-shadow: 0 -16px 80px #0007;
|
|
22
22
|
color-scheme: dark;
|
|
@@ -29,16 +29,16 @@ html.lanes-mobile:root[data-zj-theme="light"] {
|
|
|
29
29
|
--lanes-accent-line: #acc9b4;
|
|
30
30
|
--lanes-press: #d8e9db;
|
|
31
31
|
--lanes-chip: #edf3ec;
|
|
32
|
-
--lanes-ink: #
|
|
33
|
-
--lanes-ink-dim: #
|
|
34
|
-
--lanes-ink-faint: #
|
|
35
|
-
--lanes-accent: #
|
|
32
|
+
--lanes-ink: #111a14;
|
|
33
|
+
--lanes-ink-dim: #4a5661;
|
|
34
|
+
--lanes-ink-faint: #6f7b87;
|
|
35
|
+
--lanes-accent: #1f6f3f;
|
|
36
36
|
--lanes-on-accent: #ffffff;
|
|
37
37
|
--lanes-on-accent-urgent: #ffd3cc;
|
|
38
|
-
--lanes-tone-urgent: #
|
|
39
|
-
--lanes-tone-attention: #
|
|
40
|
-
--lanes-tone-working: #
|
|
41
|
-
--lanes-tone-done: #
|
|
38
|
+
--lanes-tone-urgent: #c41c1c;
|
|
39
|
+
--lanes-tone-attention: #a04d00;
|
|
40
|
+
--lanes-tone-working: #1a56c4;
|
|
41
|
+
--lanes-tone-done: #0d7338;
|
|
42
42
|
--lanes-scrim: #1a222b80;
|
|
43
43
|
--lanes-shadow: 0 -16px 80px #0003;
|
|
44
44
|
color-scheme: light;
|