@leftoff/cli 0.9.9-beta.6 → 0.9.9-beta.8
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 +12 -1
- package/bin/leftoff.js +466 -407
- package/docs/SECURITY.md +2 -1
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/core.js +37 -2
- package/mod/hooks/register.js +1 -1
- package/package.json +1 -1
- package/ui/app.css +139 -0
- package/ui/app.js +229 -36
- package/ui/composer.js +12 -0
- package/ui/facts.js +34 -0
- package/ui/icons.js +1 -0
- package/ui/live-status.js +12 -1
- package/ui/live-view.js +65 -9
- package/ui/live-wake.js +1 -1
- package/ui/needs.js +158 -2
- package/ui/start.js +128 -12
package/docs/SECURITY.md
CHANGED
|
@@ -113,7 +113,8 @@ Live link is opt-in, per session: only sessions started with `leftoff live` have
|
|
|
113
113
|
- **This Mac's own board** starts it at once. **A phone or another Mac** may start one only when that Mac has *Allow starting sessions from other devices* on (Settings → Privacy & security, off by default) **and** *Trust my paired devices* is on. The switch is kept on that Mac (`~/.config/checkpoints/local/`, not the synced folder, so turning it on one Mac never turns it on another) and only that Mac's own browser can change it (loopback; a phone, the relay or a peer gets 403). Every start from another device shows a notification on the Mac. With the switch off, the request waits up to 5 minutes for a yes on that Mac's board, like Remote Restore. At most 3 wait at once; 3 starts at once, then 1 a minute.
|
|
114
114
|
- **The repo list is folders that Mac has already run Claude Code in** (from its session records; no typed paths). A headless run loads a repo's hooks and `.mcp.json` without the trust dialog, so a folder Claude Code never ran in is treated as a first time:
|
|
115
115
|
- **Other folder…** browses that Mac's folders: names only (never a file or what's in one), inside your home by real path (a symlink out of home is refused or left out), never the synced Leftoff folder or anything in it, no hidden folders, `node_modules` or `Library`, at most 500 per listing. Another Mac's are read over the peer link (`fs.dirs`, not an open action, the same as any read), a phone's through the relay. Nothing is written.
|
|
116
|
-
- **A first time starts at once only from that Mac's own board** (loopback, or your hosted board open on it): you are the trust prompt. From a phone or another Mac it
|
|
116
|
+
- **A first time starts at once only from that Mac's own board** (loopback, or your hosted board open on it): you are the trust prompt. From a phone or another Mac it waits for a yes on that Mac's board, even with *Allow starting sessions from other devices* on (unless it's inside an approved folder, below), and the ask and the notification say "first time in <folder>; this runs its hooks". After one run it's in the list like the rest. Home itself is refused; a folder that isn't a git repo can't get a new worktree.
|
|
117
|
+
- **Approved folders** (per Mac, 0.9.9): with the switch on, a first time inside a folder you approved on that Mac starts from another device without the ask (and notifies that Mac, like any remote start). They're kept in that Mac's own local file (`~/.config/checkpoints/local/`, not `config.json`, never synced), stored as real paths inside your home; home itself, the synced Leftoff folder (or anything inside it), hidden folders and links out of home are refused. A start counts by the folder's real path, so a link that only looks inside an approved folder doesn't. They're added and removed only at the Mac itself: Settings (locked elsewhere), "Always allow in <folder>" on the first-time ask (the folder above it that holds the repos that Mac knows, or its parent; never home), or `leftoff starts allow|disallow <folder>`.
|
|
117
118
|
- **Permission mode `default`, always passed.** Reads in the repo run; every edit and command becomes a Needs-you request (here, the phone, any board; first answer wins) and waits for it, with no 9.5-minute cap since nobody is at a terminal. A request settled any other way is a no. Never `bypassPermissions` or `auto`. AskUserQuestion is answered "ask in words", so Claude asks in plain text and you reply in Live.
|
|
118
119
|
- **Other Macs** reach it over the signed, encrypted peer link (`session.start`, `session.start.get`, `session.headless`, `fs.dirs`; not open actions, so the target Mac's *Allow other machines* applies too).
|
|
119
120
|
- **Nothing new in the folder.** The card, its title and checkpoints as for any session, plus `startedFrom` (device and how it came, not the prompt) and the run's state. Stream output, stdin and stderr stay in the daemon's memory; Claude's transcript stays in `~/.claude` on that Mac; the Live thread and attachments stay where Live keeps them (`~/.config/checkpoints`).
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "leftoff",
|
|
3
|
-
"version": "0.9.9-beta.
|
|
3
|
+
"version": "0.9.9-beta.8",
|
|
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
|
@@ -1509,13 +1509,48 @@ export function fallbackTitle(session) {
|
|
|
1509
1509
|
return repoName(session) + (branch ? '@' + branch : '') + (words ? ' · ' + words : '')
|
|
1510
1510
|
}
|
|
1511
1511
|
|
|
1512
|
-
|
|
1512
|
+
// 0.9.9: the title is in the language the user writes in (English when unsure),
|
|
1513
|
+
// one language, one script: a small local model once named an English session
|
|
1514
|
+
// "Ants README笑话" ("joke" in Chinese).
|
|
1515
|
+
export const TITLE_LANGUAGE = 'Write it in the same language as the user\'s own messages (English if unsure), in one language only: never mix in words or characters from another language.'
|
|
1516
|
+
export const TITLE_SYSTEM = 'Write a title of at most 6 words for a coding session, based on its first prompt. ' + TITLE_LANGUAGE + ' Reply with the title only, no quotes, no punctuation at the end.'
|
|
1513
1517
|
|
|
1514
1518
|
export function cleanTitle(text) {
|
|
1515
|
-
const t = String(text || '').split('\n').map((l) => l.trim()).find(Boolean) || ''
|
|
1519
|
+
const t = String(text || '').replace(/<think>[\s\S]*?<\/think>/gi, '').split('\n').map((l) => l.trim()).find(Boolean) || ''
|
|
1516
1520
|
return clip(t.replace(/^title\s*:\s*/i, '').replace(/^["'`*]+|["'`*.]+$/g, ''), 60)
|
|
1517
1521
|
}
|
|
1518
1522
|
|
|
1523
|
+
// The writing systems in a text's letters (digits, punctuation and emoji have none).
|
|
1524
|
+
// Chinese and Japanese characters count as one ("han").
|
|
1525
|
+
const SCRIPTS = [
|
|
1526
|
+
['latin', /\p{Script=Latin}/u], ['han', /[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Bopomofo}]/u], ['hangul', /\p{Script=Hangul}/u],
|
|
1527
|
+
['cyrillic', /\p{Script=Cyrillic}/u], ['greek', /\p{Script=Greek}/u], ['arabic', /\p{Script=Arabic}/u], ['hebrew', /\p{Script=Hebrew}/u],
|
|
1528
|
+
['thai', /\p{Script=Thai}/u], ['devanagari', /\p{Script=Devanagari}/u],
|
|
1529
|
+
]
|
|
1530
|
+
export function scriptsOf(text) {
|
|
1531
|
+
const out = new Set()
|
|
1532
|
+
for (const ch of String(text || '')) {
|
|
1533
|
+
if (!/\p{L}/u.test(ch)) continue
|
|
1534
|
+
const hit = SCRIPTS.find(([, re]) => re.test(ch))
|
|
1535
|
+
out.add(hit ? hit[0] : 'other')
|
|
1536
|
+
}
|
|
1537
|
+
return out
|
|
1538
|
+
}
|
|
1539
|
+
// Does a generated title keep to the user's writing? Every script in it must
|
|
1540
|
+
// be one the user wrote in; Latin letters are always fine (file names, code,
|
|
1541
|
+
// "README"). A title that brings in another script is dropped for the fallback.
|
|
1542
|
+
export function titleFitsUser(title, userText) {
|
|
1543
|
+
const mine = scriptsOf(userText)
|
|
1544
|
+
for (const sc of scriptsOf(title)) if (sc !== 'latin' && !mine.has(sc)) return false
|
|
1545
|
+
return true
|
|
1546
|
+
}
|
|
1547
|
+
// The line under a title prompt that pins the language: written in Latin
|
|
1548
|
+
// letters only, the title is too (what small models drift from).
|
|
1549
|
+
export function titleLanguageLine(userText) {
|
|
1550
|
+
const mine = scriptsOf(userText)
|
|
1551
|
+
return [...mine].some((sc) => sc !== 'latin') ? 'Title language: the language of the messages above.' : 'Title language: the language of the messages above (English if unsure), Latin letters only.'
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1519
1554
|
// ---------------------------------------------------------------- catch me up (v0.5.0)
|
|
1520
1555
|
|
|
1521
1556
|
// A model reply -> its bullet lines (at most max), or null when fewer than two.
|
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.9-beta.
|
|
31
|
+
const MOD_VERSION = '0.9.9-beta.8' // 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.9-beta.
|
|
3
|
+
"version": "0.9.9-beta.8",
|
|
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
|
@@ -182,6 +182,20 @@ h2 .count { color: var(--faint); font-weight: 500; }
|
|
|
182
182
|
.fact { background: var(--card); box-shadow: var(--card-shadow); border: 1px solid var(--line); border-radius: 10px; padding: 10px 12px; min-width: 0; }
|
|
183
183
|
.fact .k { color: var(--faint); font-size: 11.5px; text-transform: uppercase; letter-spacing: .6px; }
|
|
184
184
|
.fact .v { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
185
|
+
.fact .k, .fact .v { display: block; }
|
|
186
|
+
/* 0.9.9: a card with an id or a path is a button: tap copies the whole value, which is its tip */
|
|
187
|
+
.fact.copyable { position: relative; width: 100%; min-height: 44px; text-align: left; cursor: pointer; font: inherit; color: inherit; outline: none; -webkit-tap-highlight-color: transparent; transition: border-color .12s, background .12s; }
|
|
188
|
+
.fact.copyable .k { display: flex; align-items: center; gap: 6px; }
|
|
189
|
+
.fact-ic { display: inline-grid; margin-left: auto; color: var(--faint); opacity: .55; transition: opacity .12s, color .12s; }
|
|
190
|
+
.fact-ic svg { width: 13px; height: 13px; }
|
|
191
|
+
@media (hover: hover) { .fact.copyable:hover { border-color: var(--line-strong); } .fact.copyable:hover .fact-ic { opacity: 1; color: var(--text2); } }
|
|
192
|
+
.fact.copyable:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; }
|
|
193
|
+
.fact.copyable:focus-visible .fact-ic { opacity: 1; }
|
|
194
|
+
.fact.copyable.copied { border-color: var(--accent); }
|
|
195
|
+
.fact.copyable.copied .fact-ic { opacity: 1; color: var(--accent); }
|
|
196
|
+
.fact.copyable[data-tip]::after { content: attr(data-tip); display: none; position: absolute; top: calc(100% + 4px); left: 0; right: 0; z-index: 20; padding: 4px 8px; border-radius: 7px; background: var(--card2); border: 1px solid var(--line-strong); box-shadow: var(--toast-shadow); color: var(--text2); font: 500 12px/1.35 var(--mono); text-transform: none; letter-spacing: normal; white-space: normal; overflow-wrap: anywhere; pointer-events: none; }
|
|
197
|
+
@media (hover: hover) { .fact.copyable[data-tip]:hover::after { display: block; } }
|
|
198
|
+
.fact.copyable[data-tip]:focus-visible::after, .fact.copyable.copied[data-tip]::after { display: block; }
|
|
185
199
|
.strip { display: flex; gap: 3px; margin: 8px 0 4px; flex-wrap: wrap; }
|
|
186
200
|
.strip span { font-size: 11px; padding: 2px 8px; border-radius: 6px; }
|
|
187
201
|
.timeline { position: relative; margin-left: 8px; padding-left: 22px; border-left: 2px solid var(--line); }
|
|
@@ -2100,6 +2114,16 @@ mark.tx, .co-snip mark, .tx-snip mark, .tx-msg-text mark { background: var(--tx-
|
|
|
2100
2114
|
.lv-tick { margin-top: 4px; font-size: 11.5px; color: var(--muted); text-align: right; }
|
|
2101
2115
|
.lv-tick.got span { color: var(--accent); letter-spacing: -2px; padding-right: 2px; }
|
|
2102
2116
|
.lv-tick.bad { color: var(--red); text-align: left; }
|
|
2117
|
+
/* 0.9.9 Queued: sent while Claude works, not taken in yet: dimmed, a dashed edge, Queued (not Working) */
|
|
2118
|
+
.lv-you.lv-queued { background: transparent; border-style: dashed; border-color: color-mix(in srgb, var(--accent) 35%, var(--line)); }
|
|
2119
|
+
.lv-you.lv-queued .lv-text, .lv-you.lv-queued .lv-who, .lv-you.lv-queued .att-bubble { opacity: .62; }
|
|
2120
|
+
.lv-q { display: flex; flex-wrap: wrap; align-items: center; justify-content: flex-end; gap: 6px 8px; margin-top: 6px; font-size: 12px; color: var(--muted); }
|
|
2121
|
+
.lv-q-tag { display: inline-flex; align-items: center; gap: 5px; font-weight: 600; letter-spacing: .01em; color: var(--muted); }
|
|
2122
|
+
.lv-q-tag::before { content: ""; width: 7px; height: 7px; border-radius: 50%; border: 1.5px solid currentColor; }
|
|
2123
|
+
.lv-q-hint { color: var(--muted); }
|
|
2124
|
+
.lv-q-acts { display: inline-flex; gap: 6px; }
|
|
2125
|
+
.lv-q-btn { min-height: 44px; min-width: 64px; padding: 0 14px; font-size: 14px; }
|
|
2126
|
+
.lv-q-btn:focus:not(:focus-visible) { outline: none; }
|
|
2103
2127
|
.lv-working .lvs-line { margin-top: 0; }
|
|
2104
2128
|
.lv-pending .lv-working { margin-top: 4px; }
|
|
2105
2129
|
.lvs-sep { color: var(--faint); }
|
|
@@ -2874,6 +2898,29 @@ del.gd-x { background: var(--gd-del-word); }
|
|
|
2874
2898
|
.headless-bar.running { border-color: var(--green); background: var(--tint-green); }
|
|
2875
2899
|
.headless-bar.paused { border-color: var(--yellow); background: var(--tint-yellow); }
|
|
2876
2900
|
.headless-bar .btn { min-height: 44px; }
|
|
2901
|
+
/* 0.9.9 Permissions: the New session choice, the session page's switch and its pill beside Live */
|
|
2902
|
+
.start-mode { grid-template-columns: repeat(3, minmax(0, 1fr)); }
|
|
2903
|
+
.start-mode .start-opt-sub { white-space: normal; }
|
|
2904
|
+
.headless-mode { display: flex; flex-wrap: wrap; align-items: center; gap: 6px 10px; margin: -4px 0 10px; }
|
|
2905
|
+
.headless-mode .hm-l { font-size: 12.5px; font-weight: 600; color: var(--muted); }
|
|
2906
|
+
.hm-seg { display: inline-flex; border: 1px solid var(--line); border-radius: 10px; overflow: visible; background: var(--card); }
|
|
2907
|
+
.hm-opt { position: relative; min-height: 44px; min-width: 44px; padding: 0 14px; border: 0; border-left: 1px solid var(--line); background: transparent; color: var(--text2); font: inherit; font-size: 13.5px; cursor: pointer; }
|
|
2908
|
+
.hm-opt:first-child { border-left: 0; border-radius: 10px 0 0 10px; }
|
|
2909
|
+
.hm-opt:last-child { border-radius: 0 10px 10px 0; }
|
|
2910
|
+
.hm-opt[aria-checked="true"] { background: var(--tint-accent); color: var(--text); font-weight: 600; box-shadow: inset 0 0 0 1px var(--accent); }
|
|
2911
|
+
.hm-opt:disabled { opacity: .55; cursor: default; }
|
|
2912
|
+
.hm-opt:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; z-index: 1; }
|
|
2913
|
+
.hm-opt[data-tip]::after, .mode-pill[data-tip]::after { content: attr(data-tip); display: none; position: absolute; top: calc(100% + 4px); left: 0; width: max-content; max-width: min(280px, 80vw); padding: 4px 8px; border-radius: 7px; background: var(--card2); border: 1px solid var(--line-strong); box-shadow: var(--toast-shadow); color: var(--text2); font-size: 12px; line-height: 1.35; font-weight: 500; text-align: left; white-space: normal; pointer-events: none; z-index: 20; }
|
|
2914
|
+
@media (hover: hover) { .hm-opt[data-tip]:hover::after, .mode-pill[data-tip]:hover::after { display: block; animation: tip-in .12s ease-out .35s both; } }
|
|
2915
|
+
.hm-opt[data-tip]:focus-visible::after, .mode-pill[data-tip]:focus-visible::after { display: block; }
|
|
2916
|
+
.hm-hint { flex-basis: 100%; margin: 0; font-size: 12px; color: var(--muted); }
|
|
2917
|
+
.mode-pill { position: relative; display: inline-block; padding: 1px 8px; border-radius: 999px; border: 1px solid var(--line-strong); background: var(--bg2); color: var(--text2); font-size: 11.5px; font-weight: 600; line-height: 1.5; white-space: nowrap; }
|
|
2918
|
+
.mode-pill.m-auto { border-color: var(--accent); background: var(--tint-accent); color: var(--text); }
|
|
2919
|
+
.mode-pill.m-acceptEdits { border-color: var(--yellow); background: var(--tint-yellow); color: var(--text); }
|
|
2920
|
+
@media (max-width: 640px) {
|
|
2921
|
+
.hm-seg { display: flex; width: 100%; }
|
|
2922
|
+
.hm-opt { flex: 1 1 0; padding: 0 6px; }
|
|
2923
|
+
}
|
|
2877
2924
|
.headless-err { margin: -4px 0 10px; padding: 8px 12px; border-radius: 8px; background: var(--tint-red); color: var(--text2); font-size: 12px; white-space: pre-wrap; overflow-wrap: anywhere; max-height: 9em; overflow: auto; }
|
|
2878
2925
|
@media (max-width: 640px) {
|
|
2879
2926
|
.start-opts { grid-template-columns: 1fr; }
|
|
@@ -2951,3 +2998,95 @@ del.gd-x { background: var(--gd-del-word); }
|
|
|
2951
2998
|
/* 0.9.9 asked at pairing time: may other devices start sessions here? */
|
|
2952
2999
|
.pair-ask .grow { min-width: 0; }
|
|
2953
3000
|
@media (pointer: coarse) { .pair-ask > .btn { min-height: 44px; } }
|
|
3001
|
+
|
|
3002
|
+
/* 0.9.9 a setting that saves on change says so beside it, then fades */
|
|
3003
|
+
.saved-mark { display: inline-block; margin-left: 10px; font-size: 13px; font-weight: 600; white-space: nowrap; color: var(--green, var(--accent)); transition: opacity .35s ease; }
|
|
3004
|
+
.saved-mark.bad { color: var(--red); font-weight: 400; white-space: normal; }
|
|
3005
|
+
.saved-mark.gone { opacity: 0; }
|
|
3006
|
+
@media (prefers-reduced-motion: reduce) { .saved-mark { transition: none; } }
|
|
3007
|
+
|
|
3008
|
+
/* 0.9.9 settings kept on one Mac only: "🖥️ studio only" beside the label (a styled tip,
|
|
3009
|
+
never title=), and a locked control (a lock, aria-disabled) when you're not at that Mac */
|
|
3010
|
+
.local-pill { position: relative; display: inline-block; margin-left: 6px; padding: 1px 7px; border-radius: 999px; border: 1px solid var(--line-strong); background: var(--bg2); color: var(--text2); font-size: 11.5px; font-weight: 500; line-height: 1.5; white-space: nowrap; vertical-align: 1px; cursor: default; }
|
|
3011
|
+
.local-pill::after { content: attr(data-tip); display: none; position: absolute; top: calc(100% + 4px); left: 0; width: max-content; max-width: min(240px, 70vw); padding: 4px 8px; border-radius: 7px; background: var(--card2); border: 1px solid var(--line-strong); box-shadow: var(--toast-shadow); color: var(--text2); font-size: 12px; line-height: 1.3; font-weight: 500; white-space: normal; pointer-events: none; z-index: 20; }
|
|
3012
|
+
@media (hover: hover) { .local-pill:hover::after { display: block; animation: tip-in .12s ease-out .35s both; } }
|
|
3013
|
+
.local-pill.show::after { display: block; }
|
|
3014
|
+
@media (prefers-reduced-motion: reduce) { .local-pill::after { animation: none !important; } }
|
|
3015
|
+
.local-pill .sr-only { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
|
|
3016
|
+
.form label.check.locked { color: var(--muted); cursor: not-allowed; }
|
|
3017
|
+
.form label.check.locked input { cursor: not-allowed; }
|
|
3018
|
+
.form label.check .lock { display: inline-flex; color: var(--muted); }
|
|
3019
|
+
.form label.check .lock .ic { width: 14px; height: 14px; }
|
|
3020
|
+
/* 0.9.9 Settings → Approved folders: a list with Remove, and an add field (16px) */
|
|
3021
|
+
.form label[for="f-start-approve"] { align-self: start; padding-top: 12px; }
|
|
3022
|
+
.approved { display: flex; flex-direction: column; gap: 8px; min-width: 0; }
|
|
3023
|
+
.approved-list { list-style: none; margin: 0; padding: 0; display: flex; flex-direction: column; gap: 6px; }
|
|
3024
|
+
.approved-list li { display: flex; align-items: center; gap: 8px; min-width: 0; padding: 4px 4px 4px 10px; border: 1px solid var(--line); border-radius: 8px; background: var(--bg2); }
|
|
3025
|
+
.approved-path { flex: 1; min-width: 0; overflow-wrap: anywhere; color: var(--text); font-size: 13px; }
|
|
3026
|
+
.approved .mono, .start-ask-banner .mono { font-family: var(--mono); }
|
|
3027
|
+
.approved-x { flex: none; min-height: 44px; min-width: 44px; justify-content: center; gap: 4px; }
|
|
3028
|
+
.approved-x .ic { width: 14px; height: 14px; }
|
|
3029
|
+
.approved-none { color: var(--faint); font-size: 13px; }
|
|
3030
|
+
.approved-add { display: flex; gap: 8px; align-items: center; }
|
|
3031
|
+
.form .approved-add input { flex: 1; min-width: 0; height: 44px; font-size: 16px; }
|
|
3032
|
+
.approved-add .btn { min-height: 44px; flex: none; }
|
|
3033
|
+
.approved.locked .approved-add input, .approved.locked .approved-add .btn { cursor: not-allowed; }
|
|
3034
|
+
.approved .lock { display: inline-flex; color: var(--muted); }
|
|
3035
|
+
.approved .lock .ic { width: 14px; height: 14px; }
|
|
3036
|
+
.approved-err { color: var(--red); font-size: 12.5px; overflow-wrap: anywhere; }
|
|
3037
|
+
.start-ask-banner .btn.approve { max-width: 100%; }
|
|
3038
|
+
.start-ask-banner .btn.approve .mono { overflow-wrap: anywhere; white-space: normal; text-align: left; }
|
|
3039
|
+
@media (max-width: 640px) {
|
|
3040
|
+
.start-ask-banner > .btn.approve { order: 1; flex: 1 1 100%; }
|
|
3041
|
+
}
|
|
3042
|
+
|
|
3043
|
+
/* 0.9.9 the ask in Live: a waiting permission prompt (or question) takes the
|
|
3044
|
+
composer's place at the bottom of Live, and stays in reach while you read the
|
|
3045
|
+
thread (sticky inside the Live box, above the phone's tab bar and the home
|
|
3046
|
+
indicator); the thread keeps a short row for each ask where it happened. */
|
|
3047
|
+
.ask-dock-wrap { position: sticky; bottom: max(12px, calc(env(safe-area-inset-bottom) + 8px)); z-index: 6; margin-top: 10px; }
|
|
3048
|
+
.need.ask-dock { gap: 10px; padding: 12px 14px; border-radius: 16px; border-color: color-mix(in srgb, var(--red) 50%, var(--line)); background: var(--card); box-shadow: 0 8px 28px rgba(15, 20, 35, .14), 0 1px 3px rgba(15, 20, 35, .08); }
|
|
3049
|
+
[data-theme="dark"] .need.ask-dock { background: var(--card2); box-shadow: 0 8px 28px rgba(0, 0, 0, .45); }
|
|
3050
|
+
.ask-dock .need-head { font-size: 13px; }
|
|
3051
|
+
.ask-count { font-size: 12px; font-weight: 600; color: var(--text2); background: var(--field); border: 1px solid var(--line); border-radius: 10px; padding: 1px 8px; font-variant-numeric: tabular-nums; }
|
|
3052
|
+
.ask-what { display: grid; gap: 6px; min-width: 0; }
|
|
3053
|
+
.ask-verb { font-weight: 600; font-size: 15px; color: var(--text); }
|
|
3054
|
+
.ask-detail { display: block; font-family: var(--mono); font-size: 13px; line-height: 1.5; background: var(--field); border: 1px solid var(--line); border-radius: 8px; padding: 6px 9px; white-space: pre-wrap; overflow-wrap: anywhere; min-width: 0; max-height: 40vh; overflow: auto; }
|
|
3055
|
+
.ask-detail.long { max-height: calc(3 * 1.5em + 12px); overflow: hidden; }
|
|
3056
|
+
.ask-detail.long.open { max-height: 40vh; overflow: auto; }
|
|
3057
|
+
.ask-more, .ask-reply-instead { justify-self: start; min-height: 44px; padding: 0 2px; margin: -6px 0 -8px; border: 0; background: none; color: var(--accent); font: inherit; font-size: 14px; font-weight: 500; cursor: pointer; -webkit-tap-highlight-color: transparent; }
|
|
3058
|
+
.ask-reply-instead { color: var(--muted); }
|
|
3059
|
+
.ask-more:hover, .ask-reply-instead:hover { text-decoration: underline; }
|
|
3060
|
+
.ask-more:focus-visible, .ask-reply-instead:focus-visible { outline: 2px solid var(--accent); outline-offset: 2px; border-radius: 6px; }
|
|
3061
|
+
.ask-actions { display: flex; flex-wrap: wrap; gap: 8px; align-items: stretch; }
|
|
3062
|
+
.ask-actions .btn, .ask-dock .deny-form .btn { min-height: 44px; font-size: 14px; padding: 8px 16px; justify-content: center; white-space: nowrap; }
|
|
3063
|
+
.ask-actions .btn.ask-allow { min-width: 120px; font-size: 15px; }
|
|
3064
|
+
.ask-dock .deny-form .deny-note { flex: 1 1 220px; min-width: 0; min-height: 44px; box-sizing: border-box; font-size: 16px; }
|
|
3065
|
+
@media (max-width: 699.98px) {
|
|
3066
|
+
.ask-dock-wrap { bottom: calc(var(--bar-h) + 8px + env(safe-area-inset-bottom)); }
|
|
3067
|
+
body.nav-hidden .ask-dock-wrap { bottom: max(10px, calc(env(safe-area-inset-bottom) + 6px)); }
|
|
3068
|
+
.need.ask-dock { padding: 12px; }
|
|
3069
|
+
.ask-actions .btn { flex: 1 1 calc(50% - 4px); font-size: 15px; min-height: 48px; }
|
|
3070
|
+
.ask-actions .btn.ask-allow, .ask-actions .btn.ask-deny { flex-basis: 100%; }
|
|
3071
|
+
.ask-actions .always-wrap { flex: 1 1 calc(50% - 4px); }
|
|
3072
|
+
.ask-actions .always-wrap .btn { flex: 1 1 auto; width: 100%; }
|
|
3073
|
+
.ask-dock .deny-form .btn { flex: 1 1 calc(50% - 4px); min-height: 48px; }
|
|
3074
|
+
.ask-dock .deny-form .deny-note { flex-basis: 100%; }
|
|
3075
|
+
}
|
|
3076
|
+
/* after "Reply instead": one line above the composer, Answer brings the card back */
|
|
3077
|
+
.ask-slim { display: flex; align-items: center; gap: 8px; margin-top: 10px; padding: 4px 4px 4px 12px; border: 1px solid color-mix(in srgb, var(--red) 40%, var(--line)); border-radius: 12px; background: var(--tint-red); font-size: 13px; min-width: 0; }
|
|
3078
|
+
.ask-slim .need-dot { background: var(--red); }
|
|
3079
|
+
.ask-slim-t { flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; color: var(--text2); }
|
|
3080
|
+
.ask-slim .btn { flex: none; min-height: 44px; min-width: 44px; justify-content: center; }
|
|
3081
|
+
/* the row in the thread: what it asked, and how it was answered */
|
|
3082
|
+
.lv-ask { display: flex; flex-wrap: wrap; align-items: baseline; column-gap: 8px; row-gap: 2px; margin: 0 0 8px; padding: 6px 10px; border-left: 3px solid var(--line-strong); border-radius: 0 8px 8px 0; background: color-mix(in srgb, var(--muted) 7%, transparent); font-size: 12.5px; color: var(--text2); min-width: 0; }
|
|
3083
|
+
.lv-ask .lva-what { flex: 1 1 200px; min-width: 0; overflow-wrap: anywhere; }
|
|
3084
|
+
.lv-ask .lva-verb { font-weight: 600; }
|
|
3085
|
+
.lv-ask code { font-family: var(--mono); font-size: 12px; overflow-wrap: anywhere; }
|
|
3086
|
+
.lv-ask .lva-state { flex: none; max-width: 100%; font-weight: 600; font-size: 12px; overflow-wrap: anywhere; }
|
|
3087
|
+
.lv-ask.t-ok { border-left-color: var(--green); } .lv-ask.t-ok .lva-state { color: var(--green); }
|
|
3088
|
+
.lv-ask.t-auto { border-left-color: var(--accent); } .lv-ask.t-auto .lva-state { color: var(--accent); }
|
|
3089
|
+
.lv-ask.t-no { border-left-color: var(--red); } .lv-ask.t-no .lva-state { color: var(--red); }
|
|
3090
|
+
.lv-ask.t-wait { border-left-color: var(--red); background: var(--tint-red); } .lv-ask.t-wait .lva-state { color: var(--red); }
|
|
3091
|
+
.ask-note { color: var(--muted); font-size: 13px; margin-top: -4px; }
|
|
3092
|
+
.lv-ask .lva-what { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 2; line-clamp: 2; overflow: hidden; }
|