@leftoff/cli 0.9.7 → 0.9.8-beta.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +39 -0
- package/bin/leftoff.js +414 -379
- package/docs/SECURITY.md +10 -0
- package/docs/peer-protocol.md +2 -0
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/core.js +14 -4
- package/mod/hooks/register.js +1 -1
- package/package.json +1 -1
- package/ui/app.css +425 -1
- package/ui/app.js +952 -40
- package/ui/index.html +18 -6
- package/ui/live-status.js +97 -0
- package/ui/live-view.js +374 -0
- package/ui/live-wake.js +96 -0
- package/ui/nav.js +88 -0
- package/ui/navhide.js +35 -0
- package/ui/needs.js +33 -2
- package/ui/pull.js +31 -0
- package/ui/push.js +140 -0
- package/ui/relay-client.js +70 -5
- package/ui/restore-gate.js +26 -2
- package/ui/sw.js +66 -6
- package/ui/tabs.js +216 -0
- package/ui/tips.js +117 -0
package/docs/SECURITY.md
CHANGED
|
@@ -96,6 +96,16 @@ When your hosted board is open on a Mac that runs Leftoff, the page sends the sa
|
|
|
96
96
|
|
|
97
97
|
Since 0.9.7 the page also tries `https://leftoff.localhost/v1/relay/direct` (Leftoff's own Caddy from `leftoff setup`, bound to loopback; it rewrites Host, drops X-Forwarded-*, and passes the page's Origin through), since Safari won't let an https page call plain `http://127.0.0.1`. A request that came in through this door came from a browser on this Mac, from your own board's origin, sealed with a paired key, so it may also do the one thing only this Mac's own browser may do: **Restore** (`POST /v1/restore`, which opens Ghostty tabs here). The daemon marks such requests with a second in-memory secret that only the door sets; a sealed request can't carry it, and every other endpoint treats a direct request exactly like the relay (a paired remote device). Through the relay, from a phone or another Mac, Restore stays off.
|
|
98
98
|
|
|
99
|
+
### Live link (0.9.8 beta)
|
|
100
|
+
|
|
101
|
+
Live link is opt-in, per session: only sessions started with `leftoff live` have a channel. It adds:
|
|
102
|
+
|
|
103
|
+
- **The channel** is a stdio MCP server that Claude Code starts. It opens no port. It talks to the daemon on 127.0.0.1 with the board's token, and the daemon accepts its calls (hello, next message, ack, reply, permission) from loopback only. A channel is tied to the session whose process started it, so the daemon only writes into sessions on its own Mac. Other Macs go through the peer link.
|
|
104
|
+
- **Prompts from the board or a phone** reach Claude exactly like typed input, so they follow the same rule as answering a permission: a phone needs *Trust my paired devices*, and they're rate-limited and written to the session's audit trail. Anyone who can send prompts can make Claude act, so treat trusting your paired devices as giving them your keyboard.
|
|
105
|
+
- **Permission relay** never allows anything on its own. A prompt waits for Allow once or Deny. With no answer the terminal dialog stays and decides, and an answer in the terminal wins. Live link answers one call at a time and never adds session-wide rules. **Always allow here** (0.9.8) is a click that writes the exact rule its ⓘ tooltip and label show into that project's `.claude/settings.local.json` (the file Claude Code's own option writes), only for shapes an exact rule can pin down. A broken settings file is never overwritten.
|
|
106
|
+
- **Outside events** need this Mac's event key (`~/.config/checkpoints/secrets/live-events`, mode 600, made by `leftoff live key`, replaced with `--new`). Locally that's a Bearer key. Through the relay only an HMAC signature is accepted (5-minute window, replays refused), so the relay can pass an event along but can't read the key, forge an event or change one. Both the relay and the Mac rate-limit, and bodies are capped at 16 KB. Claude's instructions call an event information, not a command, but it is still text Claude reads: anyone with the key can put words in front of your session. Keep the key in your CI's secrets.
|
|
107
|
+
- **Remote Restore** opens Ghostty tabs, so a phone or another Mac can only ask. The Mac shows a notification and a card, and only that Mac's own browser can confirm. Requests expire after 5 minutes, at most 3 wait at once, and they're rate-limited.
|
|
108
|
+
|
|
99
109
|
### Logging
|
|
100
110
|
|
|
101
111
|
The Worker never logs request or response bodies (it can't read them anyway) and stores only what's listed above, in your Durable Object. `wrangler.toml` turns Workers observability (request logs) off. Cloudflare's edge keeps its usual metadata for any site (IP, hostname, path such as `/_relay/api`, status), under its own policies.
|
package/docs/peer-protocol.md
CHANGED
|
@@ -111,6 +111,8 @@ Only Macs registered for that user can connect: the first with `leftoff relay re
|
|
|
111
111
|
|
|
112
112
|
Every few seconds (`peers.pollMs`, default 4 s) each daemon asks every machine that accepts for `needs.list`, so another Mac's permission prompts and questions show on this board with a ⌂ machine chip. Answer, reply and Go on another machine's session go out as envelopes. When that isn't possible, the button is grey and says why: not allowed on that machine, unreachable, key changed, no key yet (that Mac needs 0.5.2), or no route. Answering from a phone keeps its own rules (Settings → Privacy & security), and Go stays a local-browser action.
|
|
113
113
|
|
|
114
|
+
Phone notifications (0.9.8) ride the same envelopes: `push.sync` carries the Web Push subscriptions a browser turned on (endpoint, keys, a device label) with the VAPID keys they were made with, plus 30-day removal tombstones; each side merges (newer word per endpoint wins) and answers with its own, so every Mac can push for its own sessions and only those. It is answered even when a Mac doesn't allow incoming commands, because it runs nothing there. Kept on each Mac in `~/.config/checkpoints/push/` (0600), never in the synced folder. Shared when a device subscribes or unsubscribes, at start, and every 20 minutes.
|
|
115
|
+
|
|
114
116
|
## Pairing through Syncthing (0.5.3)
|
|
115
117
|
|
|
116
118
|
Before two Macs share a folder they have no channel to each other, so pairing rides on Syncthing itself. Leftoff never opens a port for it.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leftoff",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8-beta.10",
|
|
4
4
|
"description": "Leftoff (formerly Checkpoints): WTF was I working on? Tracks every Claude Code session on this machine, writes a 3-line checkpoint when one goes quiet, and adds /cp (checkpoint now), /cp-name and a /leftoff pane. Talks to the local Leftoff daemon on 127.0.0.1:7429 and falls back to writing markdown in ~/checkpoints.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Joel"
|
package/mod/hooks/core.js
CHANGED
|
@@ -299,9 +299,19 @@ export function resumeCommand(session) {
|
|
|
299
299
|
const id = session.id || session.sessionId
|
|
300
300
|
const cwd = resumeDir(session)
|
|
301
301
|
const wt = resumeWorktreeName(session)
|
|
302
|
-
|
|
302
|
+
const args = [...(wt ? ['--worktree', wt] : []), '--resume', id]
|
|
303
|
+
return (cwd ? 'cd ' + shArg(cwd) + ' && ' : '') + (isCodex(session) ? codexInvocation(id) : wasLiveSession(session) ? liveInvocation(args) : claudeInvocation(session, args))
|
|
303
304
|
}
|
|
304
305
|
|
|
306
|
+
// 0.9.8: a session started with `leftoff live` (it had Leftoff's Live channel)
|
|
307
|
+
// resumes as one, `leftoff live -- --resume <id>`, so it shows up Live again.
|
|
308
|
+
// Codex and Ollama sessions resume their own way (Live is Claude Code only).
|
|
309
|
+
export function wasLiveSession(session) {
|
|
310
|
+
if (!session || isCodex(session) || (session.backend && session.backend.kind === 'ollama')) return false
|
|
311
|
+
return !!(session.wasLive || (session.liveLink && session.liveLink.on))
|
|
312
|
+
}
|
|
313
|
+
export function liveInvocation(args = []) { return 'leftoff live --' + (args.length ? ' ' + args.map(shArg).join(' ') : '') }
|
|
314
|
+
|
|
305
315
|
// 0.8.0: a Codex session (its rollout under ~/.codex/sessions) resumes with `codex resume <id>`.
|
|
306
316
|
export function isCodex(session) { return !!session && session.codingAgent === 'codex' }
|
|
307
317
|
export function codexInvocation(id) { return 'codex resume ' + shArg(id) }
|
|
@@ -547,7 +557,7 @@ const SESSION_ORDER = ['id', 'title', 'title_source', 'machine', 'cwd', 'repo',
|
|
|
547
557
|
export const SESSION_EXTRA = ['inTurn', 'needsInput', 'backgroundTasks', 'turnsSinceCheckpoint', 'dormantStretch',
|
|
548
558
|
'finalCheckpointDone', 'firstPrompt', 'lastPreview', 'lastNotification', 'lastEventAt', 'statusChanges', 'folder',
|
|
549
559
|
'pid', 'tty', 'pidStartedAt', 'pidSource', 'term', 'closedAt', 'answers', 'lastNotificationMessage', 'needsInputAt', 'question', 'questionAt',
|
|
550
|
-
'startSource', 'historyTurns', 'healedAt', 'healedVia', 'archivedReason', 'backend', 'prLinks', 'prUnlinked', 'todos', 'files', 'startRepo', 'editedRepos', 'parked', 'nonInteractive', 'transcriptCwd', 'doubtedCheckpoints']
|
|
560
|
+
'startSource', 'historyTurns', 'healedAt', 'healedVia', 'archivedReason', 'backend', 'prLinks', 'prUnlinked', 'todos', 'files', 'startRepo', 'editedRepos', 'parked', 'nonInteractive', 'transcriptCwd', 'doubtedCheckpoints', 'liveLink', 'transcriptTurns', 'wasLive']
|
|
551
561
|
|
|
552
562
|
export const SESSION_FIELDS = ['id', 'title', 'titleSource', 'machine', 'cwd', 'repo', 'transcriptPath', 'model', 'surface', 'capture',
|
|
553
563
|
'status', 'startedAt', 'lastActivityAt', 'lastHeartbeatAt', 'endedAt', 'endReason', 'turns', 'lastCheckpointId',
|
|
@@ -800,7 +810,7 @@ export function checkpointDue(s, now, cfg = DEFAULT_CONFIG) {
|
|
|
800
810
|
export function isEmptySession(s, checkpointCount = 0) {
|
|
801
811
|
if (!s) return true
|
|
802
812
|
// 0.7.1: a prompt counts. A long first turn (a worktree bug fix) has no completed turn yet.
|
|
803
|
-
return !(Number(s.turns) > 0) && !s.title && !s.firstPrompt && !(Number(checkpointCount) > 0) && !s.lastCheckpointId &&
|
|
813
|
+
return !(Number(s.turns) > 0) && !(Number(s.transcriptTurns) > 0) && !s.title && !s.firstPrompt && !(Number(checkpointCount) > 0) && !s.lastCheckpointId &&
|
|
804
814
|
!(Number(s.historyTurns) > 0) && s.startSource !== 'resume'
|
|
805
815
|
}
|
|
806
816
|
|
|
@@ -876,7 +886,7 @@ export function applyEvent(s, ev) {
|
|
|
876
886
|
s.needsInput = false
|
|
877
887
|
s.question = null
|
|
878
888
|
s.lastNotification = null
|
|
879
|
-
if (d.prompt && !s.firstPrompt) s.firstPrompt = clip(d.prompt, 300)
|
|
889
|
+
if (d.prompt && (!s.firstPrompt || /^\s*<channel\b/.test(s.firstPrompt))) s.firstPrompt = clip(String(d.prompt).replace(/^\s*<channel\b[^>]*>/, '').replace(/<\/channel>\s*$/, '').trim(), 300) || s.firstPrompt || null // 0.9.8: a Live link message, without its wrapper (even one the 300-character clip cut short)
|
|
880
890
|
if (d.countTurn === true) s.turns = (s.turns || 0) + 1 // 0.8.1: Codex counts a turn at its prompt when no task_started came first
|
|
881
891
|
if (s.capture === 'hooks') { s.inTurn = true; s.activeSince = s.activeSince || at }
|
|
882
892
|
}
|
package/mod/hooks/register.js
CHANGED
|
@@ -28,7 +28,7 @@ const END_TIMEOUT_MS = 1000
|
|
|
28
28
|
const MODEL_TIMEOUT_MS = 20000
|
|
29
29
|
|
|
30
30
|
// Module state. $.state is not used: the pane redraws with $.ui.invalidate.
|
|
31
|
-
const MOD_VERSION = '0.9.
|
|
31
|
+
const MOD_VERSION = '0.9.8-beta.10' // keep in step with .claude-plugin/plugin.json
|
|
32
32
|
|
|
33
33
|
const st = {
|
|
34
34
|
internal: false,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@leftoff/cli",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.8-beta.10",
|
|
4
4
|
"description": "Leftoff: WTF was I working on? Tracks every Claude Code session, writes a 3-line checkpoint when one goes quiet, and gives you a daily digest and a board, on your Mac and (optionally) your phone.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude",
|
package/ui/app.css
CHANGED
|
@@ -120,7 +120,7 @@ h2 .count { color: var(--faint); font-weight: 500; }
|
|
|
120
120
|
.card:hover { border-color: var(--line-strong); }
|
|
121
121
|
.card.needs_input { border-color: color-mix(in srgb, var(--red) 45%, transparent); box-shadow: 0 0 0 1px var(--tint-red) inset, var(--card-shadow); }
|
|
122
122
|
.card .row1 { display: flex; align-items: flex-start; gap: 10px; }
|
|
123
|
-
.card .title { font-size: 15px; font-weight: 600; line-height: 1.35; flex: 1; }
|
|
123
|
+
.card .title { font-size: 15px; font-weight: 600; line-height: 1.35; flex: 1; min-width: 0; overflow-wrap: break-word; } /* breaks at the <wbr> after a "/" and at spaces first; inside a word only when one word is wider than the card */
|
|
124
124
|
.card .title a:hover { text-decoration: underline; text-decoration-color: var(--faint); }
|
|
125
125
|
.s-title { display: flex; align-items: center; gap: 6px; min-width: 0; }
|
|
126
126
|
.s-title h1 { min-width: 0; }
|
|
@@ -507,6 +507,22 @@ h2 .count { color: var(--faint); font-weight: 500; }
|
|
|
507
507
|
.btn.allow:hover { filter: brightness(1.08); }
|
|
508
508
|
.btn.deny { color: var(--red); border-color: color-mix(in srgb, var(--red) 40%, var(--line)); }
|
|
509
509
|
.btn.deny:hover { background: var(--tint-red); }
|
|
510
|
+
/* Always allow here (0.9.8): one line like the rest, green outline, an ⓘ inside it with the rule in a tooltip */
|
|
511
|
+
.always-wrap { position: relative; display: inline-flex; max-width: 100%; }
|
|
512
|
+
.btn.always { color: var(--green); border-color: color-mix(in srgb, var(--green) 45%, var(--btn-line)); font-weight: 600; white-space: nowrap; padding-right: 34px; }
|
|
513
|
+
.btn.always:hover { background: var(--tint-green, color-mix(in srgb, var(--green) 10%, transparent)); color: var(--green); }
|
|
514
|
+
.tip-btn { position: absolute; top: 0; right: 0; bottom: 0; width: 32px; display: grid; place-items: center; padding: 0; margin: 0; border: 0; background: none; color: color-mix(in srgb, var(--green) 75%, var(--muted)); cursor: help; border-radius: 0 8px 8px 0; }
|
|
515
|
+
.tip-btn .ic { width: 15px; height: 15px; }
|
|
516
|
+
.tip-btn:hover, .tip-btn[aria-expanded="true"] { color: var(--green); background: var(--tint-green, color-mix(in srgb, var(--green) 10%, transparent)); }
|
|
517
|
+
.tip-btn:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
518
|
+
.btn.always:disabled + .tip-btn { color: var(--muted); }
|
|
519
|
+
@media (pointer: coarse) { .btn.always { padding-right: 46px; } .tip-btn { width: 44px; } } /* the ⓘ is its own 44px target on touch */
|
|
520
|
+
.tip { position: fixed; z-index: 70; max-width: 320px; padding: 9px 11px; border-radius: 10px; background: var(--card2); border: 1px solid var(--line-strong); box-shadow: var(--toast-shadow); color: var(--text2); font-size: 12.5px; line-height: 1.45; font-weight: 400; text-align: left; white-space: normal; overflow-wrap: anywhere; }
|
|
521
|
+
.tip[hidden] { display: none; }
|
|
522
|
+
.tip code { font: 11.5px/1.4 var(--mono); color: var(--text); background: var(--bg2); border: 1px solid var(--line); border-radius: 5px; padding: 0 4px; }
|
|
523
|
+
.tip b { color: var(--text); font-weight: 600; }
|
|
524
|
+
.need-actions .btn { white-space: nowrap; }
|
|
525
|
+
.need.k-permission .need-actions .btn, .need.k-permission .deny-form .btn { min-height: 44px; }
|
|
510
526
|
.deny-form { display: flex; gap: 6px; flex-wrap: wrap; }
|
|
511
527
|
.deny-form[hidden] { display: none; }
|
|
512
528
|
.deny-form input, .need .qother, .reply-text { flex: 1 1 220px; min-width: 0; background: var(--field); border: 1px solid var(--line); border-radius: 8px; color: var(--text); padding: 7px 9px; font: inherit; font-size: 13px; }
|
|
@@ -541,6 +557,10 @@ h2 .count { color: var(--faint); font-weight: 500; }
|
|
|
541
557
|
@media (max-width: 640px) {
|
|
542
558
|
.needs-strip { padding: 10px; margin-left: -4px; margin-right: -4px; }
|
|
543
559
|
.need-actions .btn, .deny-form .btn { flex: 1 1 auto; justify-content: center; }
|
|
560
|
+
.need.k-permission .need-actions { flex-direction: column; align-items: stretch; } /* Allow / Always / Deny stack on a phone */
|
|
561
|
+
.need.k-permission .need-actions .btn { width: 100%; }
|
|
562
|
+
.need.k-permission .need-actions .always-wrap { width: 100%; }
|
|
563
|
+
.need.k-permission .need-actions .btn.always { padding-left: 46px; padding-right: 46px; } /* the label stays centred beside the ⓘ */
|
|
544
564
|
.need-wait { margin-left: 0; }
|
|
545
565
|
.answered .where, .recent .where { margin-left: 0; width: 100%; }
|
|
546
566
|
}
|
|
@@ -2015,3 +2035,407 @@ mark.tx, .co-snip mark, .tx-snip mark, .tx-msg-text mark { background: var(--tx-
|
|
|
2015
2035
|
.wz-cmd { width: 100%; }
|
|
2016
2036
|
.wz-nav .btn { padding: 8px 16px; }
|
|
2017
2037
|
}
|
|
2038
|
+
|
|
2039
|
+
/* 0.9.8 Live link: the Live badge, and the session page's message box with its delivery states */
|
|
2040
|
+
.pill.live-pill { color: var(--green); background: var(--tint-green); font-weight: 600; } /* not .live: that class is the open/closed dot */
|
|
2041
|
+
/* Its dot pulses while Claude is running a turn (a soft ring growing out of it); idle, it stays still. The text doesn't blink. */
|
|
2042
|
+
.pill.live-pill.working::before { animation: live-pulse 1.6s ease-in-out infinite; }
|
|
2043
|
+
@keyframes live-pulse {
|
|
2044
|
+
0% { box-shadow: 0 0 0 0 color-mix(in srgb, var(--green) 55%, transparent); }
|
|
2045
|
+
80%, 100% { box-shadow: 0 0 0 5px color-mix(in srgb, var(--green) 0%, transparent); }
|
|
2046
|
+
}
|
|
2047
|
+
@media (prefers-reduced-motion: reduce) { .pill.live-pill.working::before, .s-active::before { animation: none; } }
|
|
2048
|
+
.live-box { margin: 16px 0 18px; border: 1px solid color-mix(in srgb, var(--green) 35%, var(--line)); background: linear-gradient(180deg, var(--tint-green), transparent 70%), var(--card); border-radius: var(--radius); padding: 12px 14px 14px; }
|
|
2049
|
+
.live-box.off { border-color: var(--line); background: var(--card); }
|
|
2050
|
+
.live-box.off p { margin: 4px 0 0; font-size: 13px; line-height: 1.5; }
|
|
2051
|
+
.live-head { display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; margin-bottom: 6px; }
|
|
2052
|
+
.live-head h2 { margin: 0; font-size: 15px; display: inline-flex; align-items: center; gap: 8px; }
|
|
2053
|
+
.live-head .faint { font-size: 12.5px; }
|
|
2054
|
+
/* 0.9.8 polish: a plain block list (a flex column squeezed its bubbles and clipped long ones), scrolling inside */
|
|
2055
|
+
.live-thread { position: relative; }
|
|
2056
|
+
.live-log { list-style: none; margin: 8px 0 10px; padding: 0 2px 0 0; display: block; max-height: min(62vh, 560px); overflow-y: auto; overscroll-behavior: contain; -webkit-overflow-scrolling: touch; scroll-padding-bottom: 8px; }
|
|
2057
|
+
.live-log:empty { display: none; }
|
|
2058
|
+
.lv { display: block; border: 1px solid var(--line); border-radius: 10px; padding: 8px 11px; background: var(--card); margin: 0 0 8px; min-width: 0; }
|
|
2059
|
+
.lv:last-child { margin-bottom: 0; }
|
|
2060
|
+
.lv-you, .lv-event { width: fit-content; max-width: 88%; margin-left: auto; }
|
|
2061
|
+
.lv-you { background: var(--tint-accent); border-color: color-mix(in srgb, var(--accent) 30%, var(--line)); }
|
|
2062
|
+
.lv-event { background: var(--tint-yellow); border-color: color-mix(in srgb, var(--yellow) 35%, var(--line)); }
|
|
2063
|
+
.lv-claude { width: auto; max-width: 100%; }
|
|
2064
|
+
.lv-who { font-size: 11.5px; color: var(--muted); display: flex; gap: 6px; align-items: baseline; flex-wrap: wrap; }
|
|
2065
|
+
.lv-text { white-space: pre-wrap; overflow-wrap: anywhere; font-size: 13.5px; line-height: 1.5; color: var(--text); margin-top: 2px; }
|
|
2066
|
+
.lv-text.lv-md { white-space: normal; }
|
|
2067
|
+
.lv-md > :first-child { margin-top: 0; }
|
|
2068
|
+
.lv-md > :last-child { margin-bottom: 0; }
|
|
2069
|
+
.lv-md p { margin: 0 0 8px; }
|
|
2070
|
+
.lv-md ul, .lv-md ol { margin: 4px 0 8px; padding-left: 22px; }
|
|
2071
|
+
.lv-md li { margin: 2px 0; }
|
|
2072
|
+
.lv-md h2, .lv-md h3, .lv-md h4, .lv-md h5, .lv-md h6 { font-size: 14px; font-weight: 700; margin: 10px 0 4px; color: var(--text); text-transform: none; letter-spacing: normal; border: 0; padding: 0; }
|
|
2073
|
+
.lv-md b, .lv-md strong { font-weight: 650; color: var(--text); }
|
|
2074
|
+
.lv-md code { font-family: var(--mono); font-size: .88em; background: var(--card2); border: 1px solid var(--line); border-radius: 5px; padding: 0 4px; }
|
|
2075
|
+
.lv-md pre { background: var(--card2); border: 1px solid var(--line); border-radius: 8px; padding: 8px 10px; margin: 6px 0 8px; overflow-x: auto; white-space: pre; font-size: 12.5px; }
|
|
2076
|
+
.lv-md pre code { background: none; border: 0; padding: 0; }
|
|
2077
|
+
.lv-md a { color: var(--accent); text-decoration: underline; }
|
|
2078
|
+
.lv-md blockquote { margin: 6px 0; padding-left: 10px; border-left: 3px solid var(--line); color: var(--muted); }
|
|
2079
|
+
.lv-note { margin-top: 6px; font-size: 12px; color: var(--muted); }
|
|
2080
|
+
/* the status line under a message: Sent → Received → Working → Replied, with a spinner and the time */
|
|
2081
|
+
.lv-status { margin-top: 6px; font-size: 12px; color: var(--muted); }
|
|
2082
|
+
.lvs-steps { display: flex; flex-wrap: wrap; align-items: center; gap: 4px; }
|
|
2083
|
+
.lvs-step { color: var(--faint); }
|
|
2084
|
+
.lvs-step.done { color: var(--green); }
|
|
2085
|
+
.lvs-step.now { color: var(--text); font-weight: 650; }
|
|
2086
|
+
.lvs-step.bad { color: var(--red); font-weight: 650; }
|
|
2087
|
+
.lvs-arrow { color: var(--faint); }
|
|
2088
|
+
.lvs-line { display: flex; align-items: center; gap: 6px; margin-top: 2px; color: var(--text2); }
|
|
2089
|
+
.lv-status.warn .lvs-line { color: var(--yellow); }
|
|
2090
|
+
.lv-status.bad .lvs-line { color: var(--red); }
|
|
2091
|
+
.lv-status.good .lvs-line { color: var(--green); }
|
|
2092
|
+
.lvs-steps .lvs-time { padding-left: 8px; }
|
|
2093
|
+
.lvs-time { margin-left: auto; font-variant-numeric: tabular-nums; color: var(--muted); }
|
|
2094
|
+
.lv-spin { width: 12px; height: 12px; flex: none; border-radius: 50%; border: 2px solid color-mix(in srgb, var(--accent) 25%, transparent); border-top-color: var(--accent); animation: spin .8s linear infinite; }
|
|
2095
|
+
@media (prefers-reduced-motion: reduce) { .lv-spin { animation: none; border-top-color: color-mix(in srgb, var(--accent) 25%, transparent); background: var(--accent); transform: scale(.6); } }
|
|
2096
|
+
.lv-earlier { list-style: none; display: flex; justify-content: center; margin: 2px 0 10px; }
|
|
2097
|
+
.live-earlier { min-height: 44px; font-size: 13px; }
|
|
2098
|
+
.lv-terminal { width: fit-content; max-width: 88%; margin-left: auto; background: color-mix(in srgb, var(--tint-accent) 50%, var(--card)); border-color: color-mix(in srgb, var(--accent) 18%, var(--line)); }
|
|
2099
|
+
.lv-tick { margin-top: 4px; font-size: 11.5px; color: var(--muted); text-align: right; }
|
|
2100
|
+
.lv-tick.got span { color: var(--accent); letter-spacing: -2px; padding-right: 2px; }
|
|
2101
|
+
.lv-tick.bad { color: var(--red); text-align: left; }
|
|
2102
|
+
.lv-working .lvs-line { margin-top: 0; }
|
|
2103
|
+
.lv-pending .lv-working { margin-top: 4px; }
|
|
2104
|
+
.lvs-sep { color: var(--faint); }
|
|
2105
|
+
.lv-working .lvs-time { margin-left: 0; }
|
|
2106
|
+
.lv-tool { font-size: 12px; line-height: 1.45; color: var(--text2); margin-top: 3px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 100%; }
|
|
2107
|
+
.lv-tool.done { color: var(--faint); }
|
|
2108
|
+
.lv-tool.now { color: var(--text); }
|
|
2109
|
+
.lv-tools { margin-top: 6px; font-size: 12px; color: var(--muted); }
|
|
2110
|
+
.lv-tools > summary { cursor: pointer; min-height: 32px; display: inline-flex; align-items: center; gap: 4px; list-style: none; }
|
|
2111
|
+
.lv-tools > summary::-webkit-details-marker { display: none; }
|
|
2112
|
+
.lv-tools > summary::before { content: '▸'; font-size: 10px; color: var(--faint); transition: transform .15s; }
|
|
2113
|
+
.lv-tools[open] > summary::before { transform: rotate(90deg); }
|
|
2114
|
+
@media (prefers-reduced-motion: reduce) { .lv-tools > summary::before { transition: none; } }
|
|
2115
|
+
.lv-tools > summary:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 4px; }
|
|
2116
|
+
.lv-tools[open] > summary { margin-bottom: 2px; }
|
|
2117
|
+
.lv-none { color: var(--muted); font-style: italic; }
|
|
2118
|
+
.lv-pending { width: fit-content; min-width: min(260px, 100%); }
|
|
2119
|
+
.live-jump { position: absolute; left: 50%; bottom: 10px; transform: translateX(-50%); min-height: 44px; padding: 0 16px; border-radius: 22px; border: 1px solid var(--btn-line); background: var(--card); color: var(--text); font: inherit; font-size: 13px; font-weight: 600; box-shadow: 0 4px 14px rgba(0,0,0,.18); cursor: pointer; z-index: 2; }
|
|
2120
|
+
.live-jump:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
2121
|
+
.live-jump-n { font-weight: 500; color: var(--muted); }
|
|
2122
|
+
.live-empty { margin: 6px 0 10px; font-size: 13px; }
|
|
2123
|
+
.live-err { color: var(--red); font-size: 13px; }
|
|
2124
|
+
/* 0.9.8: another Mac's session whose Mac is offline */
|
|
2125
|
+
.owner-off { display: flex; gap: 10px; align-items: flex-start; margin: 12px 0; padding: 10px 12px; border: 1px solid var(--line); border-radius: var(--radius); background: var(--card); color: var(--text); font-size: 14px; line-height: 1.45; }
|
|
2126
|
+
.owner-off .ic { flex: none; width: 18px; height: 18px; margin-top: 1px; color: var(--muted); }
|
|
2127
|
+
.live-row .live-text { min-height: 44px; }
|
|
2128
|
+
/* Chat on a live card: straight to the Live box */
|
|
2129
|
+
.btn.chat-btn { position: relative; color: var(--green); border-color: color-mix(in srgb, var(--green) 40%, var(--btn-line)); font-weight: 600; }
|
|
2130
|
+
.chat-btn .unread-dot { width: 8px; height: 8px; border-radius: 50%; background: var(--accent); position: absolute; top: -3px; right: -3px; box-shadow: 0 0 0 2px var(--card); }
|
|
2131
|
+
.restore-ask .rs-m { color: var(--muted); font-size: 12.5px; }
|
|
2132
|
+
.rs-note.rs-ask.ok { color: var(--green); }
|
|
2133
|
+
.rs-note.rs-ask.warn { color: var(--red); }
|
|
2134
|
+
@media (pointer: coarse), (max-width: 600px) {
|
|
2135
|
+
.live-row .btn, .restore-ask .btn, .btn.chat-btn { min-height: 44px; min-width: 44px; }
|
|
2136
|
+
.need-actions .btn, .deny-form .btn { min-height: 44px; } /* Allow / Deny from the phone */
|
|
2137
|
+
.lv-tools > summary { min-height: 44px; }
|
|
2138
|
+
.live-box { padding: 10px; margin-left: -4px; margin-right: -4px; }
|
|
2139
|
+
.lv-you, .lv-event, .lv-terminal { max-width: 92%; }
|
|
2140
|
+
.live-log { max-height: 60vh; max-height: 60dvh; }
|
|
2141
|
+
}
|
|
2142
|
+
|
|
2143
|
+
/* ------------------------------------------------------------ the sidebar rail (0.9.8)
|
|
2144
|
+
From 700px up (0.9.8 tabs: was 820): a narrow rail on the left. The Leftoff mark at the top, then
|
|
2145
|
+
the sections as an icon over a small label, the current one in a soft pill,
|
|
2146
|
+
a red count on Needs you; search, What's new, Settings and this Mac's status
|
|
2147
|
+
(the Mac switcher) at the bottom; 700–1099px it shows icons only. Under 700px: the mark, search and status in
|
|
2148
|
+
a slim top row, and a bottom tab bar (Board, Needs you, Lists, PRs, More). */
|
|
2149
|
+
:root { --rail-w: 84px; --rail-on: color-mix(in srgb, var(--accent) 13%, transparent); --bar-h: 58px; }
|
|
2150
|
+
.tab-more { display: none; }
|
|
2151
|
+
.moredot { position: absolute; width: 7px; height: 7px; border-radius: 50%; background: var(--accent); box-shadow: 0 0 0 2px var(--header-bg); }
|
|
2152
|
+
.moredot.warn { background: var(--yellow); }
|
|
2153
|
+
.moredot[hidden] { display: none; }
|
|
2154
|
+
.iconbtn .lbl { display: none; }
|
|
2155
|
+
.tabs a, .tab-more { -webkit-touch-callout: none; -webkit-user-select: none; user-select: none; }
|
|
2156
|
+
@media (min-width: 700px) {
|
|
2157
|
+
body { padding-left: calc(var(--rail-w) + env(safe-area-inset-left)); }
|
|
2158
|
+
.top { position: fixed !important; top: 0; bottom: 0; left: 0; width: calc(var(--rail-w) + env(safe-area-inset-left)); z-index: 20; /* !important: the rail never joins the page flow */
|
|
2159
|
+
flex-direction: column; flex-wrap: nowrap; align-items: stretch; justify-content: flex-start; gap: 6px; min-height: 0;
|
|
2160
|
+
padding: max(14px, env(safe-area-inset-top)) 8px max(12px, env(safe-area-inset-bottom)) max(8px, env(safe-area-inset-left));
|
|
2161
|
+
border-bottom: 0; border-right: 1px solid var(--line); overflow-x: hidden; overflow-y: auto; scrollbar-width: none; }
|
|
2162
|
+
.brand { order: 0; justify-content: center; padding: 2px 0 10px; border-radius: 12px; }
|
|
2163
|
+
.brand .logo { width: 30px; height: 30px; }
|
|
2164
|
+
.brand span { display: none; }
|
|
2165
|
+
.tabs { order: 0; flex: none; display: flex; flex-direction: column; align-items: stretch; gap: 4px; margin: 0; }
|
|
2166
|
+
.tabs a, .topright .searchbtn, .topright .iconbtn, .topright .health {
|
|
2167
|
+
position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 4px;
|
|
2168
|
+
width: auto; min-width: 0; height: auto; min-height: 54px; margin: 0; padding: 7px 2px 6px; border: 0; border-radius: 12px; background: none;
|
|
2169
|
+
color: var(--muted); font-size: 11px; font-weight: 500; line-height: 1.15; text-align: center; white-space: nowrap; box-shadow: none; }
|
|
2170
|
+
.tabs a .ic, .topright .ic { width: 20px; height: 20px; opacity: 1; }
|
|
2171
|
+
.tabs a .lbl, .topright .lbl, .topright .sb-lbl, .topright .health .label { display: block; max-width: 100%; overflow: hidden; text-overflow: ellipsis; flex: none; text-align: center; }
|
|
2172
|
+
.tabs .lbl-short, .topright .sb-kbd { display: none; }
|
|
2173
|
+
.tabs a:hover, .topright .searchbtn:hover, .topright .iconbtn:hover, .topright .health:hover, .topright .health[aria-expanded="true"] { color: var(--text); background: var(--bg2); }
|
|
2174
|
+
.tabs a.on, .topright .iconbtn.on { color: var(--text); background: var(--rail-on); box-shadow: none; }
|
|
2175
|
+
.tabs a.on .ic, .topright .iconbtn.on .ic { color: var(--accent); }
|
|
2176
|
+
.tabs a#needsTab.has-needs .ic { color: var(--red); }
|
|
2177
|
+
.navcount { position: absolute; top: 3px; left: calc(50% + 3px); min-width: 18px; height: 18px; line-height: 18px; font-size: 10.5px; padding: 0 5px; box-shadow: 0 0 0 2px var(--bg); }
|
|
2178
|
+
.topright { order: 0; margin: auto 0 0; padding-top: 8px; border-top: 1px solid var(--line); display: flex; flex-direction: column; align-items: stretch; gap: 4px; }
|
|
2179
|
+
.topright .health { order: 9; max-width: none; gap: 6px; }
|
|
2180
|
+
.topright .health .dot { width: 9px; height: 9px; margin: 5px 0 1px; }
|
|
2181
|
+
.newdot, .topdot { top: 8px; right: calc(50% - 15px); box-shadow: 0 0 0 2px var(--bg); }
|
|
2182
|
+
.set-nav { top: 24px; }
|
|
2183
|
+
}
|
|
2184
|
+
@media (min-width: 700px) and (max-height: 640px) {
|
|
2185
|
+
.tabs a, .topright .searchbtn, .topright .iconbtn, .topright .health { min-height: 46px; padding: 4px 2px; gap: 2px; }
|
|
2186
|
+
.brand { padding-bottom: 4px; }
|
|
2187
|
+
}
|
|
2188
|
+
@media (max-width: 699.98px) {
|
|
2189
|
+
.top { flex-wrap: nowrap; gap: 8px; min-height: 52px; padding-top: max(6px, env(safe-area-inset-top)); padding-bottom: 6px; background: var(--bg); -webkit-backdrop-filter: none; backdrop-filter: none; }
|
|
2190
|
+
.brand { order: 1; }
|
|
2191
|
+
.topright { order: 2; margin-left: auto; gap: 2px; }
|
|
2192
|
+
.topright #wnTab, .topright #setTab { display: none; }
|
|
2193
|
+
.searchbtn { width: 44px; height: 44px; }
|
|
2194
|
+
.health { height: 44px; }
|
|
2195
|
+
.tabs { position: fixed; z-index: 30; left: 0; right: 0; bottom: 0; order: 3; margin: 0; display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 2px;
|
|
2196
|
+
padding: 4px max(6px, env(safe-area-inset-right)) max(4px, env(safe-area-inset-bottom)) max(6px, env(safe-area-inset-left));
|
|
2197
|
+
background: var(--header-bg); -webkit-backdrop-filter: blur(14px); backdrop-filter: blur(14px); border-top: 1px solid var(--line); }
|
|
2198
|
+
.tabs .tab-extra { display: none; }
|
|
2199
|
+
.tabs a, .tab-more { position: relative; display: flex; flex-direction: column; align-items: center; justify-content: center; gap: 3px; height: auto; min-height: 50px; padding: 5px 2px 4px;
|
|
2200
|
+
border: 0; border-radius: 12px; background: none; color: var(--muted); font: inherit; font-size: 11px; font-weight: 500; line-height: 1.1; text-align: center; white-space: nowrap; box-shadow: none; cursor: pointer; }
|
|
2201
|
+
.tabs a .ic, .tab-more .ic { width: 22px; height: 22px; opacity: 1; }
|
|
2202
|
+
.tabs #needsTab .lbl { display: block; }
|
|
2203
|
+
.tabs #needsTab .lbl-short { display: none; }
|
|
2204
|
+
.tabs a.on, .tab-more.on { color: var(--text); background: none; box-shadow: none; }
|
|
2205
|
+
.tabs a:hover, .tab-more:hover { background: none; color: var(--text); }
|
|
2206
|
+
.tabs a.on .ic, .tab-more.on .ic { color: var(--accent); }
|
|
2207
|
+
.tabs a.on::before, .tab-more.on::before { content: ''; position: absolute; top: 4px; left: 50%; width: 46px; height: 28px; transform: translateX(-50%); border-radius: 14px; background: var(--rail-on); z-index: -1; }
|
|
2208
|
+
.tabs a, .tab-more { isolation: isolate; }
|
|
2209
|
+
.tab-more:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
2210
|
+
.navcount { position: absolute; top: 2px; left: calc(50% + 4px); right: auto; min-width: 17px; height: 17px; line-height: 17px; font-size: 10.5px; padding: 0 4px; box-shadow: 0 0 0 2px var(--bg); }
|
|
2211
|
+
.moredot { top: 6px; left: calc(50% + 8px); }
|
|
2212
|
+
main { padding-bottom: calc(var(--bar-h) + 40px + env(safe-area-inset-bottom)); }
|
|
2213
|
+
.toast { bottom: calc(var(--bar-h) + 14px + env(safe-area-inset-bottom)); }
|
|
2214
|
+
}
|
|
2215
|
+
/* Hide the nav on scroll (phones, 0.9.8): body.nav-hidden slides the top row up
|
|
2216
|
+
(its strip under the status bar stays, so nothing scrolls beneath the clock) and
|
|
2217
|
+
the bottom bar down. The top row keeps its place in the flow and main keeps the
|
|
2218
|
+
bar's padding, so nothing reflows. The top row is moved with `top`, not a
|
|
2219
|
+
transform: a transform would become the fixed bar's containing block. */
|
|
2220
|
+
@media (max-width: 699.98px) {
|
|
2221
|
+
.top { transition: top .2s ease; }
|
|
2222
|
+
.top .tabs { transition: transform .2s ease, visibility 0s linear 0s; }
|
|
2223
|
+
body.nav-hidden .top { top: calc(env(safe-area-inset-top) - var(--top-h, 60px)); }
|
|
2224
|
+
body.nav-hidden .top .tabs { transform: translateY(calc(100% + 2px)); visibility: hidden; transition: transform .2s ease, visibility 0s linear .2s; }
|
|
2225
|
+
}
|
|
2226
|
+
@media (max-width: 699.98px) and (prefers-reduced-motion: reduce) {
|
|
2227
|
+
.top, .top .tabs, body.nav-hidden .top .tabs { transition: none; }
|
|
2228
|
+
}
|
|
2229
|
+
/* More (phones): the rest of the sections, above the bar */
|
|
2230
|
+
.moremenu { position: fixed; z-index: 45; right: max(8px, env(safe-area-inset-right)); bottom: calc(var(--bar-h) + 8px + env(safe-area-inset-bottom)); width: 230px; max-width: calc(100vw - 16px);
|
|
2231
|
+
display: flex; flex-direction: column; padding: 6px; background: var(--card); color: var(--text); border: 1px solid var(--line); border-radius: 14px; box-shadow: 0 16px 40px rgba(15, 20, 35, .18), 0 2px 6px rgba(15, 20, 35, .08); animation: lo-in .12s ease-out; }
|
|
2232
|
+
[data-theme="dark"] .moremenu { background: var(--card2); box-shadow: 0 16px 40px rgba(0, 0, 0, .5); }
|
|
2233
|
+
.moremenu[hidden] { display: none; }
|
|
2234
|
+
.moremenu a { position: relative; display: flex; align-items: center; gap: 12px; min-height: 48px; padding: 0 12px; border-radius: 10px; color: var(--text); text-decoration: none; font-size: 15px; }
|
|
2235
|
+
.moremenu a .ic { width: 20px; height: 20px; color: var(--muted); flex: none; }
|
|
2236
|
+
.moremenu a:hover, .moremenu a:focus-visible { background: var(--bg2); outline: none; }
|
|
2237
|
+
.moremenu a:focus-visible { box-shadow: inset 0 0 0 2px var(--accent); }
|
|
2238
|
+
.moremenu a.on { background: var(--rail-on); }
|
|
2239
|
+
.moremenu a.on .ic { color: var(--accent); }
|
|
2240
|
+
.moremenu .newdot, .moremenu .topdot { position: static; margin-left: auto; box-shadow: none; }
|
|
2241
|
+
@media (min-width: 700px) { .moremenu { display: none !important; } }
|
|
2242
|
+
/* Quick look on Needs you (0.9.8) */
|
|
2243
|
+
.quicklook { position: fixed; z-index: 45; width: 360px; max-width: calc(100vw - 16px); max-height: calc(100vh - 16px); overflow-y: auto; overscroll-behavior: contain;
|
|
2244
|
+
background: var(--card); color: var(--text); border: 1px solid var(--line); border-radius: 14px; box-shadow: 0 16px 40px rgba(15, 20, 35, .18), 0 2px 6px rgba(15, 20, 35, .08); padding: 12px 12px 8px; font-size: 13.5px; animation: lo-in .12s ease-out; }
|
|
2245
|
+
[data-theme="dark"] .quicklook { background: var(--card2); box-shadow: 0 16px 40px rgba(0, 0, 0, .5); }
|
|
2246
|
+
.quicklook[hidden] { display: none; }
|
|
2247
|
+
.ql-head { display: flex; align-items: center; gap: 8px; padding: 2px 4px 10px; border-bottom: 1px solid var(--line); }
|
|
2248
|
+
.ql-head h2 { margin: 0; font-size: 15px; font-weight: 650; text-transform: none; letter-spacing: 0; color: var(--text); }
|
|
2249
|
+
.ql-list { list-style: none; margin: 0; padding: 0; }
|
|
2250
|
+
.ql-item { display: flex; align-items: center; gap: 8px; padding: 9px 4px; border-bottom: 1px solid var(--line); min-width: 0; }
|
|
2251
|
+
.ql-main { flex: 1; min-width: 0; display: flex; flex-direction: column; gap: 2px; color: var(--text); text-decoration: none; border-radius: 8px; }
|
|
2252
|
+
.ql-main:hover .ql-t { color: var(--accent); }
|
|
2253
|
+
.ql-main:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
2254
|
+
.ql-t { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2255
|
+
.ql-what { color: var(--muted); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2256
|
+
.ql-meta { color: var(--faint); font: 11.5px/1.3 var(--mono); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2257
|
+
.ql-item.blocking .ql-t::before { content: ''; display: inline-block; width: 7px; height: 7px; border-radius: 50%; background: var(--red); margin: 0 6px 1px 0; vertical-align: middle; }
|
|
2258
|
+
.ql-actions { display: flex; gap: 6px; flex: none; }
|
|
2259
|
+
.ql-actions .btn { min-height: 44px; padding: 0 11px; font-size: 13px; justify-content: center; }
|
|
2260
|
+
.ql-item.has-always { flex-wrap: wrap; } /* three buttons: their own row under the item */
|
|
2261
|
+
.ql-item.has-always .ql-main { flex: 1 1 100%; }
|
|
2262
|
+
.ql-item.has-always .ql-actions { flex: 1 1 100%; }
|
|
2263
|
+
.ql-item.has-always .ql-actions .btn, .ql-item.has-always .ql-actions .always-wrap { flex: 1 1 0; min-width: 0; }
|
|
2264
|
+
.ql-actions .always-wrap .btn.always { width: 100%; padding-right: 34px; }
|
|
2265
|
+
@media (pointer: coarse) { .ql-actions .always-wrap .btn.always { padding-right: 46px; } }
|
|
2266
|
+
.ql-empty, .ql-more { color: var(--muted); margin: 0; padding: 12px 4px; font-size: 13px; }
|
|
2267
|
+
.ql-more { padding: 8px 4px 0; }
|
|
2268
|
+
.ql-foot { display: flex; align-items: center; justify-content: flex-end; gap: 6px; min-height: 44px; padding: 0 6px; margin-top: 2px; color: var(--accent); font-weight: 600; text-decoration: none; border-radius: 8px; }
|
|
2269
|
+
.ql-foot:hover { text-decoration: underline; }
|
|
2270
|
+
.ql-foot:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
2271
|
+
@media (pointer: coarse), (max-width: 699.98px) { .ql-actions .btn { min-height: 44px; min-width: 64px; } .ql-main { min-height: 44px; justify-content: center; } }
|
|
2272
|
+
@media (max-width: 699.98px) { .quicklook { left: max(8px, env(safe-area-inset-left)); right: max(8px, env(safe-area-inset-right)); width: auto; max-width: none; bottom: calc(var(--bar-h) + 8px + env(safe-area-inset-bottom)); max-height: calc(100vh - var(--bar-h) - 80px); } }
|
|
2273
|
+
@media (prefers-reduced-motion: reduce) { .quicklook, .moremenu { animation: none; } }
|
|
2274
|
+
|
|
2275
|
+
/* Pull to refresh (phones, 0.9.8): the spinner comes down from under the top row */
|
|
2276
|
+
.ptr { position: fixed; z-index: 19; left: 50%; top: calc(env(safe-area-inset-top) + 8px); width: 38px; height: 38px; margin-top: -8px; transform: translate(-50%, 0); display: grid; place-items: center;
|
|
2277
|
+
border-radius: 50%; background: var(--card); border: 1px solid var(--line); box-shadow: 0 6px 18px rgba(15, 20, 35, .16); color: var(--muted); pointer-events: none; }
|
|
2278
|
+
[data-theme="dark"] .ptr { background: var(--card2); box-shadow: 0 6px 18px rgba(0, 0, 0, .45); }
|
|
2279
|
+
.ptr[hidden] { display: none; }
|
|
2280
|
+
.ptr-ring { display: grid; place-items: center; transform: rotate(var(--ptr-turn, 0deg)); }
|
|
2281
|
+
.ptr-ring svg { width: 19px; height: 19px; }
|
|
2282
|
+
.ptr.armed { color: var(--accent); }
|
|
2283
|
+
.ptr.busy .ptr-ring { animation: spin .8s linear infinite; }
|
|
2284
|
+
@media (pointer: coarse) and (max-width: 699.98px) { html, body { overscroll-behavior-y: contain; } }
|
|
2285
|
+
@media (prefers-reduced-motion: reduce) { .ptr-ring { transform: none; } .ptr.busy .ptr-ring { animation: lo-pulse 1.2s ease-in-out infinite; } }
|
|
2286
|
+
@keyframes lo-pulse { 50% { opacity: .35; } }
|
|
2287
|
+
|
|
2288
|
+
/* 0.9.8: the session page's header. The title takes the room that's left and
|
|
2289
|
+
breaks at " · " and after "/" (titleHtml's <wbr>), never mid-word unless one
|
|
2290
|
+
word is wider than the page; the buttons are one group that drops under the
|
|
2291
|
+
title as a whole when it doesn't fit beside it. Every button: one line, 44px,
|
|
2292
|
+
an icon and a short label; the primary ones (Checkpoint, Resume) are tinted,
|
|
2293
|
+
not filled, so they don't shout over the rest. */
|
|
2294
|
+
.pagehead.s-head { flex-wrap: wrap; align-items: flex-start; column-gap: 20px; row-gap: 12px; }
|
|
2295
|
+
.pagehead.s-head > .s-head-main { flex: 1 1 28rem; min-width: 0; } /* under ~28rem beside the buttons, the buttons move down instead */
|
|
2296
|
+
.s-head h1 { overflow-wrap: break-word; line-height: 1.3; }
|
|
2297
|
+
.pagehead.s-head > .s-actions { margin-left: 0; flex: 0 1 auto; display: flex; flex-wrap: wrap; align-items: center; gap: 8px; max-width: 100%; }
|
|
2298
|
+
.s-actions .btn { height: 44px; min-height: 44px; padding: 0 14px; gap: 7px; font-size: 13.5px; line-height: 1; white-space: nowrap; flex: none; justify-content: center; }
|
|
2299
|
+
.s-actions .btn .ic { width: 16px; height: 16px; flex: none; margin: 0; }
|
|
2300
|
+
.s-actions .btn.icon-btn { width: 44px; padding: 0; }
|
|
2301
|
+
.s-actions .more-wrap { display: inline-flex; }
|
|
2302
|
+
.s-actions .btn.primary { background: var(--tint-accent); color: var(--accent); border-color: color-mix(in srgb, var(--accent) 45%, var(--btn-line)); }
|
|
2303
|
+
.s-actions .btn.primary:hover { filter: none; background: color-mix(in srgb, var(--accent) 16%, transparent); color: var(--accent); border-color: var(--accent); }
|
|
2304
|
+
.s-actions .btn.primary:active { filter: none; background: color-mix(in srgb, var(--accent) 22%, transparent); }
|
|
2305
|
+
.s-actions .split { flex: none; display: inline-flex; align-items: stretch; height: 44px; }
|
|
2306
|
+
.s-actions .split .btn { height: 44px; }
|
|
2307
|
+
.s-actions .split .btn.split-more { width: 38px; min-width: 38px; padding: 0; margin-left: 0; border-left: 1px solid color-mix(in srgb, var(--accent) 30%, var(--btn-line)); }
|
|
2308
|
+
.s-actions .split .btn.split-main { border-right-width: 0; }
|
|
2309
|
+
.s-actions .split.is-writing .btn:disabled { opacity: .85; border-style: solid; background: var(--tint-accent); color: var(--accent); border-color: color-mix(in srgb, var(--accent) 35%, var(--btn-line)); }
|
|
2310
|
+
.s-actions .split.is-writing .spin { border-color: color-mix(in srgb, var(--accent) 30%, transparent); border-top-color: var(--accent); }
|
|
2311
|
+
@media (max-width: 640px) {
|
|
2312
|
+
/* a phone: the buttons are a row under the title that wraps, each part of a row sharing its width */
|
|
2313
|
+
.pagehead.s-head > .s-actions { flex: 1 1 100%; }
|
|
2314
|
+
.s-actions > .btn, .s-actions > .split { flex: 1 1 auto; }
|
|
2315
|
+
.s-actions .split .split-main { flex: 1 1 auto; }
|
|
2316
|
+
.s-actions > .more-wrap { flex: none; }
|
|
2317
|
+
}
|
|
2318
|
+
|
|
2319
|
+
/* 0.9.8: a card's row (Go, Chat, copy, ⋯) and Catch me up's row: one line each,
|
|
2320
|
+
all the same height (44px on touch). */
|
|
2321
|
+
.card > .actions-row > .btn, .card > .actions-row > .go-off > .btn, .cu-s .actions-row > .btn { white-space: nowrap; height: 30px; min-height: 30px; padding-top: 0; padding-bottom: 0; line-height: 28px; }
|
|
2322
|
+
.card > .actions-row > .btn.icon-btn { width: 32px; line-height: 1; }
|
|
2323
|
+
.card > .actions-row > .btn.chat-btn { min-width: 0; display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box; padding-top: 0; padding-bottom: 0; overflow: visible; }
|
|
2324
|
+
.cu-s .actions-row > .btn { display: inline-flex; align-items: center; }
|
|
2325
|
+
@media (pointer: coarse), (max-width: 600px) {
|
|
2326
|
+
.card > .actions-row > .btn, .card > .actions-row > .go-off > .btn, .cu-s .actions-row > .btn { height: 44px; min-height: 44px; line-height: 42px; }
|
|
2327
|
+
.card > .actions-row > .btn.icon-btn { width: 44px; }
|
|
2328
|
+
.card > .actions-row > .btn:not(.icon-btn) { min-width: 44px; }
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
/* Phone notifications (0.9.8): the one per-device button, in More and Settings → General */
|
|
2332
|
+
.push-slot[hidden] { display: none; }
|
|
2333
|
+
.push-box { display: flex; gap: 10px; align-items: flex-start; }
|
|
2334
|
+
.push-box > .ic { width: 20px; height: 20px; flex: none; margin-top: 2px; color: var(--muted); }
|
|
2335
|
+
.push-main { min-width: 0; flex: 1; }
|
|
2336
|
+
.push-line { margin: 0; font-size: 14px; line-height: 1.4; color: var(--text2); }
|
|
2337
|
+
.push-line strong { color: var(--text); font-weight: 600; }
|
|
2338
|
+
.push-acts { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 8px; }
|
|
2339
|
+
.push-acts .btn { min-height: 44px; padding: 0 14px; font-size: 14px; white-space: nowrap; justify-content: center; }
|
|
2340
|
+
.push-note { margin: 8px 0 0; font-size: 13px; line-height: 1.4; color: var(--muted); }
|
|
2341
|
+
.push-note.bad { color: var(--red, #c0392b); }
|
|
2342
|
+
.moremenu .push-slot { border-top: 1px solid var(--line); margin-top: 6px; padding: 12px 8px 6px; }
|
|
2343
|
+
.moremenu:has(.push-slot:not([hidden])) { width: 270px; }
|
|
2344
|
+
.form .push-slot { grid-column: 1 / -1; padding: 12px 14px; border: 1px solid var(--line); border-radius: 10px; background: var(--card2, transparent); margin-top: 4px; }
|
|
2345
|
+
|
|
2346
|
+
/* ------------------------------------------------------------ tabs for sessions (0.9.8)
|
|
2347
|
+
By width, never by device. Under 700px: no strip; an Open row on the board
|
|
2348
|
+
and "‹ 2 of 4 ›" on a session page (swipe works too). 700–1099px: the strip,
|
|
2349
|
+
the rail as icons. From 1100px: the full rail and the strip. */
|
|
2350
|
+
.tabstrip, .openrow, .tabpager { display: none; }
|
|
2351
|
+
.tabstrip[hidden], .openrow[hidden], .tabpager[hidden] { display: none !important; }
|
|
2352
|
+
.tab-t { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
2353
|
+
.preview .tab-t, .tab-s.preview .tab-t, .ochip.preview .tab-t { font-style: italic; }
|
|
2354
|
+
.tab-live, .tab-need { flex: none; width: 8px; height: 8px; border-radius: 50%; position: relative; }
|
|
2355
|
+
.tab-live { background: var(--green); animation: live-pulse 1.6s ease-in-out infinite; }
|
|
2356
|
+
.tab-need { background: var(--red); }
|
|
2357
|
+
@media (prefers-reduced-motion: reduce) { .tab-live { animation: none; } }
|
|
2358
|
+
@media (min-width: 700px) {
|
|
2359
|
+
.tabstrip { display: flex; position: sticky; top: 0; z-index: 15; align-items: stretch; height: 44px; background: var(--bg); border-bottom: 1px solid var(--line); padding-top: env(safe-area-inset-top); }
|
|
2360
|
+
.tabstrip-in { flex: 1; min-width: 0; display: flex; align-items: stretch; }
|
|
2361
|
+
.tab-scroll { flex: 1; min-width: 0; display: flex; align-items: stretch; overflow-x: auto; overflow-y: hidden; scrollbar-width: none; overscroll-behavior-x: contain; }
|
|
2362
|
+
.tab-scroll::-webkit-scrollbar { display: none; }
|
|
2363
|
+
.tab { display: flex; align-items: center; gap: 7px; min-height: 44px; padding: 0 12px; color: var(--muted); text-decoration: none; font-size: 13px; font-weight: 500; white-space: nowrap; }
|
|
2364
|
+
.tab .ic { width: 16px; height: 16px; flex: none; }
|
|
2365
|
+
.tab:hover { color: var(--text); }
|
|
2366
|
+
.tab:focus-visible, .tab-x:focus-visible, .tab-all:focus-visible { outline: 2px solid var(--accent); outline-offset: -2px; }
|
|
2367
|
+
.tab-board { flex: none; border-right: 1px solid var(--line); }
|
|
2368
|
+
.tab-cell { position: relative; flex: 0 1 200px; min-width: 120px; display: flex; align-items: stretch; border-right: 1px solid var(--line); }
|
|
2369
|
+
.tab-s { flex: 1; min-width: 0; padding-right: 0; }
|
|
2370
|
+
.tab.on, .tab-cell.on { color: var(--text); background: var(--card); }
|
|
2371
|
+
.tab-board.on, .tab-cell.on { box-shadow: inset 0 -2px 0 var(--accent); }
|
|
2372
|
+
.tab-x { flex: none; width: 44px; min-height: 44px; display: flex; align-items: center; justify-content: center; border: 0; background: none; color: var(--muted); cursor: pointer; padding: 0; border-radius: 8px; }
|
|
2373
|
+
.tab-x .ic { width: 14px; height: 14px; }
|
|
2374
|
+
.tab-x { position: relative; }
|
|
2375
|
+
.tab-x::before { content: ''; position: absolute; left: 50%; top: 50%; width: 24px; height: 24px; margin: -12px 0 0 -12px; border-radius: 50%; background: transparent; transition: background .12s; }
|
|
2376
|
+
.tab-x .ic { position: relative; }
|
|
2377
|
+
.tab-x:hover { color: var(--text); background: none; }
|
|
2378
|
+
.tab-x:hover::before { background: color-mix(in srgb, var(--text) 9%, transparent); }
|
|
2379
|
+
.tab-keep .ic { width: 12px; height: 12px; }
|
|
2380
|
+
.tab-keep { opacity: 0; }
|
|
2381
|
+
.tab-cell:hover .tab-keep, .tab-keep:focus-visible, .tab-cell.on .tab-keep { opacity: 1; }
|
|
2382
|
+
.tab-cell.dragging { opacity: .5; }
|
|
2383
|
+
.tab-cell.drop-before { box-shadow: inset 2px 0 0 var(--accent); }
|
|
2384
|
+
.tab-all { flex: none; width: 44px; min-height: 44px; display: flex; align-items: center; justify-content: center; border: 0; border-left: 1px solid var(--line); background: var(--bg); color: var(--muted); cursor: pointer; }
|
|
2385
|
+
.tab-all .ic { width: 18px; height: 18px; }
|
|
2386
|
+
.tab-all:hover, .tab-all[aria-expanded="true"] { color: var(--text); background: var(--bg2); }
|
|
2387
|
+
.tab-all[hidden] { display: none; }
|
|
2388
|
+
}
|
|
2389
|
+
@media (min-width: 700px) and (pointer: coarse) { .tab-keep { opacity: 1; } }
|
|
2390
|
+
/* the rail as icons (700–1099px): the labels stay for screen readers */
|
|
2391
|
+
@media (min-width: 700px) and (max-width: 1099.98px) {
|
|
2392
|
+
:root { --rail-w: 64px; }
|
|
2393
|
+
.tabs a .lbl, .topright .lbl, .topright .sb-lbl, .topright .health .label { position: absolute !important; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); clip-path: inset(50%); white-space: nowrap; }
|
|
2394
|
+
.tabs a, .topright .searchbtn, .topright .iconbtn, .topright .health { min-height: 48px; padding: 6px 2px; }
|
|
2395
|
+
.navcount { left: calc(50% + 2px); }
|
|
2396
|
+
}
|
|
2397
|
+
/* phones (under 700px): the Open row on the board, ‹ N of M › on a session page */
|
|
2398
|
+
@media (max-width: 699.98px) {
|
|
2399
|
+
body[data-route="board"] .openrow { display: block; }
|
|
2400
|
+
body[data-route="s"] .tabpager { display: block; }
|
|
2401
|
+
.openrow { padding: 8px 0 0; }
|
|
2402
|
+
.openrow-h { display: flex; gap: 8px; align-items: baseline; padding: 0 max(16px, env(safe-area-inset-left)); font-size: 12px; font-weight: 600; letter-spacing: .02em; text-transform: uppercase; color: var(--muted); }
|
|
2403
|
+
.openrow-chips { display: flex; gap: 8px; overflow-x: auto; scrollbar-width: none; padding: 8px max(16px, env(safe-area-inset-left)) 6px; scroll-snap-type: x proximity; overscroll-behavior-x: contain; }
|
|
2404
|
+
.openrow-chips::-webkit-scrollbar { display: none; }
|
|
2405
|
+
.ochip-cell { flex: none; display: flex; align-items: stretch; max-width: 62vw; border: 1px solid var(--line); border-radius: 22px; background: var(--card); scroll-snap-align: start; }
|
|
2406
|
+
.ochip-cell.preview { border-style: dashed; }
|
|
2407
|
+
.ochip { display: flex; align-items: center; gap: 7px; min-height: 44px; min-width: 44px; padding: 0 14px; color: var(--text); text-decoration: none; font-size: 15px; max-width: 100%; min-width: 0; }
|
|
2408
|
+
.ochip-cell:not(.preview) .ochip { padding-right: 2px; }
|
|
2409
|
+
.ochip-x { flex: none; width: 44px; min-height: 44px; border: 0; background: none; color: var(--muted); display: flex; align-items: center; justify-content: center; padding: 0; border-radius: 22px; }
|
|
2410
|
+
.ochip-x .ic { width: 15px; height: 15px; }
|
|
2411
|
+
.tabpager { padding: 4px max(8px, env(safe-area-inset-left)) 0; }
|
|
2412
|
+
.pager-in { display: flex; align-items: center; gap: 6px; }
|
|
2413
|
+
.pager-b { flex: none; width: 44px; height: 44px; display: flex; align-items: center; justify-content: center; border-radius: 22px; color: var(--text); text-decoration: none; }
|
|
2414
|
+
.pager-b .ic { width: 22px; height: 22px; }
|
|
2415
|
+
.pager-b.off { opacity: .25; }
|
|
2416
|
+
.pager-b:not(.off):active { background: var(--bg2); }
|
|
2417
|
+
.pager-n { flex: 1; text-align: center; font-size: 14px; color: var(--muted); font-variant-numeric: tabular-nums; }
|
|
2418
|
+
.pager-keep { flex: none; min-height: 44px; }
|
|
2419
|
+
.swipe-next main { animation: lo-page-fwd .18s ease-out; }
|
|
2420
|
+
.swipe-prev main { animation: lo-page-back .18s ease-out; }
|
|
2421
|
+
}
|
|
2422
|
+
@media (prefers-reduced-motion: reduce) { .swipe-next main, .swipe-prev main { animation: none !important; } }
|
|
2423
|
+
|
|
2424
|
+
/* 0.9.8: a tapped card shows it's opening at once; the session page's shell
|
|
2425
|
+
(title from the card) stands in while its data loads. */
|
|
2426
|
+
.card[data-href]:active { transform: scale(.985); }
|
|
2427
|
+
.card.opening { border-color: color-mix(in srgb, var(--accent) 60%, transparent); box-shadow: 0 0 0 1px color-mix(in srgb, var(--accent) 35%, transparent) inset, var(--card-shadow); transform: scale(.985); }
|
|
2428
|
+
.card.opening::after { content: ''; position: absolute; top: 12px; right: 12px; width: 14px; height: 14px; border-radius: 50%; border: 2px solid color-mix(in srgb, var(--accent) 25%, transparent); border-top-color: var(--accent); animation: spin .8s linear infinite; }
|
|
2429
|
+
.s-skel { display: flex; flex-direction: column; gap: 12px; margin-top: 18px; }
|
|
2430
|
+
.s-skel-note { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 14px; }
|
|
2431
|
+
.skel-line, .skel-block { border-radius: 8px; background: linear-gradient(90deg, var(--line) 0%, color-mix(in srgb, var(--line) 40%, transparent) 50%, var(--line) 100%); background-size: 200% 100%; animation: skel 1.2s ease-in-out infinite; }
|
|
2432
|
+
.skel-line { height: 14px; width: 100%; }
|
|
2433
|
+
.skel-line.short { width: 60%; }
|
|
2434
|
+
.skel-block { height: 160px; }
|
|
2435
|
+
.skel-title { height: 26px; width: 70%; margin: 4px 0; }
|
|
2436
|
+
@keyframes skel { 0% { background-position: 100% 0; } 100% { background-position: -100% 0; } }
|
|
2437
|
+
@media (prefers-reduced-motion: reduce) { .skel-line, .skel-block, .card.opening::after { animation: none; } .card.opening, .card[data-href]:active { transform: none; } }
|
|
2438
|
+
/* 0.9.8: a running bubble whose data is stale says so instead of counting. */
|
|
2439
|
+
.lvs-hint { color: var(--muted); font-style: italic; }
|
|
2440
|
+
.lv-reconnecting .lvs-time, .lv-reconnecting .lvs-sep, .lv-reconnecting .lvs-label { display: none; }
|
|
2441
|
+
.lv-reconnecting .lv-spin { animation-duration: 2.4s; opacity: .5; }
|