@leftoff/cli 0.9.17-beta.4 → 0.9.17-beta.6
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 +2 -0
- package/bin/leftoff.js +28 -28
- package/mod/.claude-plugin/plugin.json +1 -1
- package/mod/hooks/register.js +1 -1
- package/package.json +1 -1
- package/ui/app.css +121 -20
- package/ui/app.js +277 -22
- package/ui/icons.js +9 -0
- package/ui/index.html +16 -12
- package/ui/live-view.js +2 -0
- package/ui/navrail.js +99 -0
- package/ui/semdiff.js +73 -20
package/ui/app.js
CHANGED
|
@@ -71,6 +71,209 @@ import { recapLineHtml, beforeClearHtml } from './recap-line.js' // 0.9.15 Claud
|
|
|
71
71
|
import { Presence, cardRows, awayWords } from './away.js' // 0.9.15 a card shows more the longer you've been away
|
|
72
72
|
import { backLine, backLineHtml, BACK_PREFIX, BACK_AWAY_MS } from './back-line.js' // 0.9.15 the recap above the Live composer
|
|
73
73
|
import { glyphState, glyphHtml, glyphWords, lastReplyOf, seenAtFor, ensureSeenBase, markSeen, GLYPH_LABEL, offlineWords, offlineTip, waitingOnYou } from './sglyph.js' // 0.9.14 status glyphs
|
|
74
|
+
import { createNavStore, navState, toggleWords, isNavKey, navKeyLabel, safariOwnsEdge, edgeStart, swipeIntent, drawerHtml, RAIL_MIN, RAIL_W_SLIM } from './navrail.js'
|
|
75
|
+
|
|
76
|
+
// ------------------------------------------------------------ 0.9.18 the left nav on every screen (ui/navrail.js)
|
|
77
|
+
// From 700px the rail stays beside every page (full screen and the walkthrough
|
|
78
|
+
// too), expanded or collapsed by its toggle or [ and remembered; a short room
|
|
79
|
+
// collapses it for that view without touching the choice. Under 700px a menu
|
|
80
|
+
// button (or a swipe in from the left edge where Safari doesn't own it) opens
|
|
81
|
+
// the nav as a drawer.
|
|
82
|
+
const navStore = createNavStore(window.localStorage)
|
|
83
|
+
const navUi = { shown: null, drawer: false, opener: null, closeT: 0, animT: 0, tipT: 0, tipFor: null, sw: null }
|
|
84
|
+
const navRailQ = window.matchMedia('(min-width: ' + RAIL_MIN + 'px)')
|
|
85
|
+
const navReduced = () => !!(window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)').matches)
|
|
86
|
+
const navMac = /Mac|iP(hone|ad|od)/.test(navigator.platform || navigator.userAgent)
|
|
87
|
+
const navEdgeOk = !safariOwnsEdge(navigator.userAgent, navigator.standalone === true || window.matchMedia('(display-mode: standalone)').matches)
|
|
88
|
+
// the room the session panel works in: the window less the (at least slim) rail
|
|
89
|
+
function fsViewport() { return window.innerWidth - (navRailQ.matches ? RAIL_W_SLIM : 0) }
|
|
90
|
+
// the session panel open beside a full-screen chat, else 0
|
|
91
|
+
function navSideWidth() {
|
|
92
|
+
const de = document.documentElement, side = document.getElementById('fsSide')
|
|
93
|
+
if (!side || !de.classList.contains('live-full') || side.classList.contains('collapsed')) return 0
|
|
94
|
+
return Math.round(side.getBoundingClientRect().width) || 0
|
|
95
|
+
}
|
|
96
|
+
function applyNav() {
|
|
97
|
+
const st = navState({ width: window.innerWidth, saved: navStore.collapsed(), side: navSideWidth() })
|
|
98
|
+
const de = document.documentElement
|
|
99
|
+
de.classList.add('nav-ready')
|
|
100
|
+
de.classList.toggle('nav-slim', st.rail && st.collapsed)
|
|
101
|
+
de.classList.toggle('nav-auto', st.rail && st.auto)
|
|
102
|
+
const b = document.getElementById('navToggle')
|
|
103
|
+
if (b) {
|
|
104
|
+
const w = toggleWords(st, navKeyLabel(navMac))
|
|
105
|
+
b.setAttribute('aria-expanded', st.collapsed ? 'false' : 'true')
|
|
106
|
+
b.setAttribute('aria-label', w.label)
|
|
107
|
+
b.setAttribute('aria-disabled', st.auto ? 'true' : 'false')
|
|
108
|
+
b.dataset.tip = w.tip
|
|
109
|
+
const lbl = b.querySelector('.lbl')
|
|
110
|
+
if (lbl) lbl.textContent = st.collapsed ? 'Expand' : 'Collapse'
|
|
111
|
+
if (b.dataset.icon !== w.icon) { b.dataset.icon = w.icon; const svg = b.querySelector('svg'); if (svg) svg.outerHTML = icon(w.icon) }
|
|
112
|
+
if (navUi.tipFor === b) railTipShow(b, true)
|
|
113
|
+
}
|
|
114
|
+
const changed = !navUi.shown || navUi.shown.collapsed !== st.collapsed || navUi.shown.rail !== st.rail
|
|
115
|
+
navUi.shown = st
|
|
116
|
+
if (st.rail && navUi.drawer) closeDrawer(false)
|
|
117
|
+
return changed
|
|
118
|
+
}
|
|
119
|
+
function toggleNav() {
|
|
120
|
+
const st = navUi.shown || navState({ width: window.innerWidth, saved: navStore.collapsed(), side: navSideWidth() })
|
|
121
|
+
if (!st.rail) return false
|
|
122
|
+
if (st.auto) { const b = document.getElementById('navToggle'); if (b) railTipShow(b, true); return false } // no room: say so, change nothing
|
|
123
|
+
const de = document.documentElement
|
|
124
|
+
if (!navReduced()) { de.classList.add('nav-anim'); clearTimeout(navUi.animT); navUi.animT = setTimeout(() => de.classList.remove('nav-anim'), 260) }
|
|
125
|
+
navStore.set(!st.collapsed)
|
|
126
|
+
applyNav()
|
|
127
|
+
return true
|
|
128
|
+
}
|
|
129
|
+
// one floating tip for the rail (the rail scrolls, so a CSS ::after would be cut off at its edge)
|
|
130
|
+
function railTipShow(el, now) {
|
|
131
|
+
const tip = document.getElementById('railTip')
|
|
132
|
+
if (!tip || !el || !navRailQ.matches) return
|
|
133
|
+
const text = el.dataset.tip || ''
|
|
134
|
+
const lbl = el.querySelector('.lbl, .sb-lbl, .label')
|
|
135
|
+
const words = lbl ? lbl.textContent.trim() : ''
|
|
136
|
+
// expanded, the label's already there; the tip shows only when it says more
|
|
137
|
+
if (!text || (!document.documentElement.classList.contains('nav-slim') && text === words)) { railTipHide(); return }
|
|
138
|
+
clearTimeout(navUi.tipT)
|
|
139
|
+
const show = () => {
|
|
140
|
+
if (!el.isConnected) return
|
|
141
|
+
tip.textContent = text
|
|
142
|
+
tip.hidden = false
|
|
143
|
+
const r = el.getBoundingClientRect()
|
|
144
|
+
const rail = document.getElementById('navRail')
|
|
145
|
+
tip.style.left = Math.round((rail ? rail.getBoundingClientRect().right : r.right) + 6) + 'px'
|
|
146
|
+
tip.style.top = Math.round(r.top + r.height / 2) + 'px'
|
|
147
|
+
navUi.tipFor = el
|
|
148
|
+
}
|
|
149
|
+
if (now) show(); else navUi.tipT = setTimeout(show, 350)
|
|
150
|
+
}
|
|
151
|
+
function railTipHide() {
|
|
152
|
+
clearTimeout(navUi.tipT)
|
|
153
|
+
const tip = document.getElementById('railTip')
|
|
154
|
+
if (tip) tip.hidden = true
|
|
155
|
+
navUi.tipFor = null
|
|
156
|
+
}
|
|
157
|
+
const navTipTarget = (t) => t && t.closest && t.closest('#navRail [data-tip]:not(#needsTab)') // Needs you has Quick look on hover
|
|
158
|
+
document.addEventListener('pointerover', (e) => {
|
|
159
|
+
if (e.pointerType === 'touch') return
|
|
160
|
+
const el = navTipTarget(e.target)
|
|
161
|
+
if (!el) return
|
|
162
|
+
if (el !== navUi.tipFor) railTipShow(el, false)
|
|
163
|
+
})
|
|
164
|
+
document.addEventListener('pointerout', (e) => {
|
|
165
|
+
const el = navTipTarget(e.target)
|
|
166
|
+
if (el && !(e.relatedTarget && el.contains(e.relatedTarget)) && document.activeElement !== el) railTipHide()
|
|
167
|
+
})
|
|
168
|
+
document.addEventListener('focusin', (e) => {
|
|
169
|
+
const el = navTipTarget(e.target)
|
|
170
|
+
if (el && el.matches(':focus-visible')) railTipShow(el, true)
|
|
171
|
+
else if (navUi.tipFor && navUi.tipFor !== el) railTipHide()
|
|
172
|
+
})
|
|
173
|
+
document.addEventListener('focusout', (e) => { if (navTipTarget(e.target)) railTipHide() })
|
|
174
|
+
window.addEventListener('scroll', railTipHide, true)
|
|
175
|
+
document.addEventListener('click', (e) => {
|
|
176
|
+
const t = e.target
|
|
177
|
+
if (!t || !t.closest) return
|
|
178
|
+
if (t.closest('#navToggle')) { e.preventDefault(); toggleNav(); return }
|
|
179
|
+
const open = t.closest('[data-nav-drawer]')
|
|
180
|
+
if (open) { e.preventDefault(); if (navUi.drawer) closeDrawer(true); else openDrawer(open); return }
|
|
181
|
+
if (!navUi.drawer) return
|
|
182
|
+
if (t.closest('#navScrim') || t.closest('[data-nd-close]')) { e.preventDefault(); closeDrawer(true); return }
|
|
183
|
+
if (t.closest('#navDrawer .nd-item')) closeDrawer(false) // the link navigates; the page it opens takes focus
|
|
184
|
+
})
|
|
185
|
+
// the drawer (phones): the rail's destinations over the page, a scrim behind
|
|
186
|
+
function navDrawerItems() {
|
|
187
|
+
const items = []
|
|
188
|
+
const take = (a) => {
|
|
189
|
+
if (!a) return
|
|
190
|
+
const lbl = a.querySelector('.lbl')
|
|
191
|
+
const n = a.querySelector('.navcount')
|
|
192
|
+
items.push({ href: a.getAttribute('href'), route: a.dataset.route || '', label: lbl ? lbl.textContent.trim() : a.getAttribute('aria-label') || '', svg: (a.querySelector('svg') || {}).outerHTML || '', on: a.classList.contains('on'), count: n && !n.hidden ? n.textContent.trim() : '' })
|
|
193
|
+
}
|
|
194
|
+
for (const a of document.querySelectorAll('#navRail .tabs a[data-route]')) take(a)
|
|
195
|
+
for (const id of ['newTab', 'wnTab', 'setTab']) take(document.getElementById(id))
|
|
196
|
+
return items
|
|
197
|
+
}
|
|
198
|
+
function openDrawer(opener) {
|
|
199
|
+
if (navRailQ.matches || navUi.drawer) return
|
|
200
|
+
const d = document.getElementById('navDrawer'), s = document.getElementById('navScrim')
|
|
201
|
+
if (!d || !s) return
|
|
202
|
+
clearTimeout(navUi.closeT)
|
|
203
|
+
d.innerHTML = drawerHtml(navDrawerItems())
|
|
204
|
+
navUi.opener = opener || (document.activeElement !== document.body ? document.activeElement : null)
|
|
205
|
+
d.hidden = false; s.hidden = false
|
|
206
|
+
void d.offsetWidth
|
|
207
|
+
d.classList.add('open'); s.classList.add('open')
|
|
208
|
+
document.documentElement.classList.add('nav-drawer-open')
|
|
209
|
+
navUi.drawer = true
|
|
210
|
+
for (const b of document.querySelectorAll('[data-nav-drawer]')) b.setAttribute('aria-expanded', 'true')
|
|
211
|
+
const f = d.querySelector('.nd-item.on') || d.querySelector('.nd-item')
|
|
212
|
+
if (f) f.focus({ preventScroll: true })
|
|
213
|
+
}
|
|
214
|
+
function closeDrawer(restore) {
|
|
215
|
+
if (!navUi.drawer) return
|
|
216
|
+
const d = document.getElementById('navDrawer'), s = document.getElementById('navScrim')
|
|
217
|
+
navUi.drawer = false
|
|
218
|
+
d.classList.remove('open'); s.classList.remove('open')
|
|
219
|
+
document.documentElement.classList.remove('nav-drawer-open')
|
|
220
|
+
for (const b of document.querySelectorAll('[data-nav-drawer]')) b.setAttribute('aria-expanded', 'false')
|
|
221
|
+
const done = () => { if (navUi.drawer) return; d.hidden = true; s.hidden = true; d.innerHTML = '' }
|
|
222
|
+
clearTimeout(navUi.closeT)
|
|
223
|
+
if (navReduced()) done(); else navUi.closeT = setTimeout(done, 200)
|
|
224
|
+
const o = navUi.opener
|
|
225
|
+
navUi.opener = null
|
|
226
|
+
if (restore && o && o.isConnected) o.focus({ preventScroll: true })
|
|
227
|
+
else if (restore) { const m = document.querySelector('[data-nav-drawer]'); if (m && m.offsetParent) m.focus({ preventScroll: true }) }
|
|
228
|
+
}
|
|
229
|
+
document.addEventListener('keydown', (e) => {
|
|
230
|
+
if (navUi.drawer) {
|
|
231
|
+
const d = document.getElementById('navDrawer')
|
|
232
|
+
if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); closeDrawer(true); return }
|
|
233
|
+
if (e.key === 'Tab') { // focus stays inside while it's open
|
|
234
|
+
const f = [...d.querySelectorAll('a[href], button:not([disabled])')]
|
|
235
|
+
if (!f.length) return
|
|
236
|
+
const i = f.indexOf(document.activeElement)
|
|
237
|
+
if (e.shiftKey && (i <= 0)) { e.preventDefault(); f[f.length - 1].focus() }
|
|
238
|
+
else if (!e.shiftKey && (i === -1 || i === f.length - 1)) { e.preventDefault(); f[0].focus() }
|
|
239
|
+
}
|
|
240
|
+
return
|
|
241
|
+
}
|
|
242
|
+
if (e.defaultPrevented) return
|
|
243
|
+
const t = e.target
|
|
244
|
+
const term = !!(t && t.closest && t.closest('.xterm, .term-dock'))
|
|
245
|
+
const typing = term || !!(t && t.closest && t.closest('input, textarea, select, [contenteditable]'))
|
|
246
|
+
if (!isNavKey(e, { typing, term, mac: navMac }) || !navRailQ.matches) return
|
|
247
|
+
if (document.querySelector('.lo-menu, .cmdk:not([hidden]), .lightbox:not([hidden]), .pdfview:not([hidden])')) return
|
|
248
|
+
if ([...document.querySelectorAll('[role="dialog"][aria-modal="true"]')].some((d) => d.id !== 'navDrawer' && !d.closest('[hidden]') && d.getClientRects().length)) return
|
|
249
|
+
e.preventDefault()
|
|
250
|
+
toggleNav()
|
|
251
|
+
}, true)
|
|
252
|
+
// a swipe in from the left edge opens it, a swipe left on it closes it (phones; not where Safari's back-swipe owns the edge)
|
|
253
|
+
function navOwnsTouch(x) { return !navRailQ.matches && (navUi.drawer || (navEdgeOk && edgeStart(x))) }
|
|
254
|
+
document.addEventListener('touchstart', (e) => {
|
|
255
|
+
navUi.sw = null
|
|
256
|
+
if (navRailQ.matches || e.touches.length !== 1) return
|
|
257
|
+
const p = e.touches[0]
|
|
258
|
+
if (navUi.drawer) { navUi.sw = { x: p.clientX, y: p.clientY, open: true }; return }
|
|
259
|
+
if (navEdgeOk && edgeStart(p.clientX)) navUi.sw = { x: p.clientX, y: p.clientY, open: false }
|
|
260
|
+
}, { passive: true })
|
|
261
|
+
document.addEventListener('touchend', (e) => {
|
|
262
|
+
const sw = navUi.sw
|
|
263
|
+
navUi.sw = null
|
|
264
|
+
if (!sw || !e.changedTouches.length) return
|
|
265
|
+
const p = e.changedTouches[0]
|
|
266
|
+
const what = swipeIntent(p.clientX - sw.x, p.clientY - sw.y, sw.open)
|
|
267
|
+
if (what === 'open') openDrawer(null)
|
|
268
|
+
else if (what === 'close') closeDrawer(true)
|
|
269
|
+
}, { passive: true })
|
|
270
|
+
{
|
|
271
|
+
let raf = 0
|
|
272
|
+
window.addEventListener('resize', () => { if (raf) return; raf = requestAnimationFrame(() => { raf = 0; railTipHide(); applyNav() }) })
|
|
273
|
+
navRailQ.addEventListener('change', () => { if (navRailQ.matches) closeDrawer(false) })
|
|
274
|
+
}
|
|
275
|
+
applyNav()
|
|
276
|
+
|
|
74
277
|
const $app = document.getElementById('app')
|
|
75
278
|
// 0.9.17 Walk me through it: which session shows it, and its view (this page only; the open one survives a reload)
|
|
76
279
|
const wtUi = { sid: null, scope: new Map(), view: 'story', filter: 'all', data: new Map(), loading: new Set(), error: new Map(), cur: new Map(), code: new Set(), groups: new Set(), pending: [], ask: '', node: null, sig: '', restored: false, fellBack: new Set() }
|
|
@@ -968,7 +1171,11 @@ function wtFill(s) {
|
|
|
968
1171
|
const sig = k + '|' + (data ? (data.fingerprint || data.status) + (data.cached ? 'c' : '') : '-') + '|' + JSON.stringify([ui.view, ui.filter, ui.loading, ui.error, ui.fellBack, ui.deck, [...wtUi.code], (data && data.scopes || []).map((x) => x.key + x.available).join()])
|
|
969
1172
|
let node = wtUi.node
|
|
970
1173
|
if (!node || wtUi.sig !== sig) {
|
|
971
|
-
|
|
1174
|
+
// (the old node may be out of the page already, reading 0: what was recorded as you scrolled wins)
|
|
1175
|
+
const same = node && wtUi.sig.startsWith(k + '|')
|
|
1176
|
+
const was = (sel) => { const el = same && node.querySelector(sel); return el ? (wtScrolls.get(el) || [el.scrollTop])[0] || 0 : 0 }
|
|
1177
|
+
const keep = was('#wtScroll'), keepSide = was('.wt-side')
|
|
1178
|
+
wtScrolls.clear()
|
|
972
1179
|
node = document.createElement('div')
|
|
973
1180
|
node.className = 'wt-node'
|
|
974
1181
|
const when = data && data.generatedAt ? clock(data.generatedAt) : ''
|
|
@@ -979,12 +1186,42 @@ function wtFill(s) {
|
|
|
979
1186
|
wtStuck(document.querySelector('.wt-scroll')) // 0.9.18
|
|
980
1187
|
wtDeckPaint(node, { first: true }) // 0.9.18 the slideshow: its progress, its dots in view
|
|
981
1188
|
const sc = node.querySelector('#wtScroll'); if (sc && keep) sc.scrollTop = keep
|
|
1189
|
+
const sd = node.querySelector('.wt-side'); if (sd && keepSide) sd.scrollTop = keepSide
|
|
982
1190
|
} else {
|
|
983
|
-
if (node.parentNode !== mount)
|
|
1191
|
+
if (node.parentNode !== mount) {
|
|
1192
|
+
// 0.9.18 (Joel: "pause for a second and it jumps back to the top"): a live update redraws the
|
|
1193
|
+
// session page (paintSession sets $app.innerHTML), which takes this node out of the page;
|
|
1194
|
+
// putting it back resets every scroll box inside it to 0, so put back where you were, and focus
|
|
1195
|
+
mount.replaceChildren(node)
|
|
1196
|
+
wtRestoreView(node)
|
|
1197
|
+
}
|
|
984
1198
|
wtMarks(s)
|
|
985
1199
|
}
|
|
986
1200
|
if (!data && !ui.loading && !ui.error) setTimeout(() => wtLoad(s.id), 0) // opened by a reload: read it now
|
|
987
1201
|
}
|
|
1202
|
+
// 0.9.18 where each scroll box in the walkthrough was (the slide or long page, the file list, the
|
|
1203
|
+
// dots) and what had focus, kept as you go, since a node taken out of the page reads 0 and has no focus
|
|
1204
|
+
const wtScrolls = new Map() // element -> [top, left], for the node on screen (cleared when it's rebuilt)
|
|
1205
|
+
let wtFocus = null // what had focus in the walkthrough just before a redraw took it out (wtBeforePaint)
|
|
1206
|
+
document.addEventListener('scroll', (e) => { const t = e.target; if (t && t.nodeType === 1 && wtUi.node && t.isConnected && wtUi.node.contains(t)) wtScrolls.set(t, [t.scrollTop, t.scrollLeft]) }, { capture: true, passive: true })
|
|
1207
|
+
let wtQuietUntil = 0, wtRefocusing = false // the scroll and focus our own restore makes don't close a tip
|
|
1208
|
+
function wtBeforePaint() {
|
|
1209
|
+
const a = document.activeElement
|
|
1210
|
+
wtFocus = wtUi.node && a && a !== document.body && wtUi.node.contains(a) ? a : null
|
|
1211
|
+
}
|
|
1212
|
+
function wtRestoreView(node) {
|
|
1213
|
+
for (const [el, v] of wtScrolls) {
|
|
1214
|
+
if (!node.contains(el)) { wtScrolls.delete(el); continue }
|
|
1215
|
+
if (el.scrollTop !== v[0] || el.scrollLeft !== v[1]) wtQuietUntil = performance.now() + 150
|
|
1216
|
+
if (el.scrollTop !== v[0]) el.scrollTop = v[0]
|
|
1217
|
+
if (el.scrollLeft !== v[1]) el.scrollLeft = v[1]
|
|
1218
|
+
}
|
|
1219
|
+
// focus only goes back if it was in here when the redraw began and the redraw left it nowhere (never taken from elsewhere)
|
|
1220
|
+
const a = document.activeElement
|
|
1221
|
+
if (wtFocus && node.contains(wtFocus) && (!a || a === document.body)) { wtRefocusing = true; try { wtFocus.focus({ preventScroll: true }) } finally { wtRefocusing = false } }
|
|
1222
|
+
wtFocus = null
|
|
1223
|
+
wtStuck(node.querySelector('.wt-scroll'))
|
|
1224
|
+
}
|
|
988
1225
|
// The reviewed marks and the count, in place (no redraw).
|
|
989
1226
|
function wtMarks(s) {
|
|
990
1227
|
const node = wtUi.node; if (!node || !s) return
|
|
@@ -1853,7 +2090,7 @@ function paintNeedsCount(n) {
|
|
|
1853
2090
|
const c = document.getElementById('needsCount')
|
|
1854
2091
|
if (c) { c.hidden = !n; c.textContent = badgeText(n) }
|
|
1855
2092
|
const tab = document.getElementById('needsTab')
|
|
1856
|
-
if (tab) { tab.classList.toggle('has-needs', !!n); tab.
|
|
2093
|
+
if (tab) { tab.classList.toggle('has-needs', !!n); tab.dataset.tip = n ? 'Needs you · ' + n + ' waiting' : 'Needs you' }
|
|
1857
2094
|
if (quickLookOpen() && paintNeedsCount.count !== n) loadQuickLook()
|
|
1858
2095
|
paintNeedsCount.count = n
|
|
1859
2096
|
document.title = (n ? '(' + n + ') ' : '') + 'Leftoff'
|
|
@@ -2065,6 +2302,7 @@ function paintSession(s, data) {
|
|
|
2065
2302
|
// preview closed with Esc puts focus back on one, then a redraw replaced it)
|
|
2066
2303
|
const kb = !kf && fa && fa !== document.body && $app.contains(fa) ? focusKey(fa) : null
|
|
2067
2304
|
liveKeeper.before($app)
|
|
2305
|
+
wtBeforePaint() // 0.9.18 the walkthrough's focus, before this takes it out of the page
|
|
2068
2306
|
$app.innerHTML = sessionPageHtml(s, data)
|
|
2069
2307
|
liveAfterRender(s.id)
|
|
2070
2308
|
fitAskDetail($app)
|
|
@@ -2345,7 +2583,7 @@ function sessionPageHtml(s, { spv = null, live: liveV = null, lists = null } = {
|
|
|
2345
2583
|
side += answeredList(s)
|
|
2346
2584
|
const foot = '<div class="fs-foot"><button type="button" class="btn fs-term" data-term-toggle aria-label="Terminal" data-tip="Terminal (⌃`)">' + icon('terminal') + 'Terminal</button>' + termTabButtonHtml(s) + '</div>' // Open terminal (a window on the Mac): the head's ⋯
|
|
2347
2585
|
const label = fsPeekWords(s)
|
|
2348
|
-
return center + fsSideHtml({ id: s.id, side, foot, label, away: away ? shortAway(awayWords(awayMs)) : '', cpCount: s.checkpointCount || 0, cu: !!cu, wt: !s.agent, look: s.lookCount || 0, collapsed: fsSide.collapsed(), sheet: fsSheetUi.open === s.id, width: fsWidth.shown(
|
|
2586
|
+
return center + fsSideHtml({ id: s.id, side, foot, label, away: away ? shortAway(awayWords(awayMs)) : '', cpCount: s.checkpointCount || 0, cu: !!cu, wt: !s.agent, look: s.lookCount || 0, collapsed: fsSide.collapsed(), sheet: fsSheetUi.open === s.id, width: fsWidth.shown(fsViewport()), viewport: fsViewport() }, { esc, icon })
|
|
2349
2587
|
}
|
|
2350
2588
|
// The middle for a session with no Live thread: the same full-screen frame and bar, and what it can say there.
|
|
2351
2589
|
// "Away 3 hours" → "Away 3h", so Catch me up and Checkpoint now share one row
|
|
@@ -2571,26 +2809,27 @@ function exitFull(id) {
|
|
|
2571
2809
|
const fsResize = { drag: null, raf: 0 }
|
|
2572
2810
|
function applyFsWidth(w) {
|
|
2573
2811
|
const de = document.documentElement
|
|
2574
|
-
const n = clampSideWidth(w,
|
|
2812
|
+
const n = clampSideWidth(w, fsViewport())
|
|
2575
2813
|
if (n === SIDE_W_DEFAULT && fsWidth.stored() == null) de.style.removeProperty('--fs-uw')
|
|
2576
2814
|
else de.style.setProperty('--fs-uw', n + 'px')
|
|
2577
|
-
for (const h of document.querySelectorAll('.fs-resize')) { h.setAttribute('aria-valuenow', String(n)); h.setAttribute('aria-valuemax', String(sideMax(
|
|
2815
|
+
for (const h of document.querySelectorAll('.fs-resize')) { h.setAttribute('aria-valuenow', String(n)); h.setAttribute('aria-valuemax', String(sideMax(fsViewport()))) }
|
|
2816
|
+
applyNav()
|
|
2578
2817
|
return n
|
|
2579
2818
|
}
|
|
2580
|
-
applyFsWidth(fsWidth.shown(
|
|
2581
|
-
window.addEventListener('resize', () => { if (!fsResize.drag) applyFsWidth(fsWidth.shown(
|
|
2819
|
+
applyFsWidth(fsWidth.shown(fsViewport()))
|
|
2820
|
+
window.addEventListener('resize', () => { if (!fsResize.drag) applyFsWidth(fsWidth.shown(fsViewport())) })
|
|
2582
2821
|
document.addEventListener('pointerdown', (e) => {
|
|
2583
2822
|
const h = e.target.closest && e.target.closest('.fs-resize')
|
|
2584
2823
|
if (!h || e.button !== 0 || window.innerWidth < WIDE_MIN) return
|
|
2585
2824
|
e.preventDefault()
|
|
2586
2825
|
try { h.setPointerCapture(e.pointerId) } catch {}
|
|
2587
|
-
fsResize.drag = { id: e.pointerId, x: e.clientX, w: fsWidth.shown(
|
|
2826
|
+
fsResize.drag = { id: e.pointerId, x: e.clientX, w: fsWidth.shown(fsViewport()), now: null, h }
|
|
2588
2827
|
document.documentElement.classList.add('fs-resizing')
|
|
2589
2828
|
})
|
|
2590
2829
|
document.addEventListener('pointermove', (e) => {
|
|
2591
2830
|
const d = fsResize.drag
|
|
2592
2831
|
if (!d || e.pointerId !== d.id) return
|
|
2593
|
-
d.now = clampSideWidth(d.w + (d.x - e.clientX),
|
|
2832
|
+
d.now = clampSideWidth(d.w + (d.x - e.clientX), fsViewport()) // the handle is the panel's left edge: left is wider
|
|
2594
2833
|
if (!fsResize.raf) fsResize.raf = requestAnimationFrame(() => { fsResize.raf = 0; if (fsResize.drag && fsResize.drag.now != null) applyFsWidth(fsResize.drag.now) })
|
|
2595
2834
|
})
|
|
2596
2835
|
const fsResizeEnd = (e) => {
|
|
@@ -2599,7 +2838,7 @@ const fsResizeEnd = (e) => {
|
|
|
2599
2838
|
fsResize.drag = null
|
|
2600
2839
|
document.documentElement.classList.remove('fs-resizing')
|
|
2601
2840
|
try { d.h.releasePointerCapture(d.id) } catch {}
|
|
2602
|
-
if (d.now != null && d.now !== d.w) applyFsWidth(fsWidth.set(d.now,
|
|
2841
|
+
if (d.now != null && d.now !== d.w) applyFsWidth(fsWidth.set(d.now, fsViewport()))
|
|
2603
2842
|
}
|
|
2604
2843
|
document.addEventListener('pointerup', fsResizeEnd)
|
|
2605
2844
|
document.addEventListener('pointercancel', fsResizeEnd)
|
|
@@ -2607,7 +2846,7 @@ document.addEventListener('dblclick', (e) => { if (e.target.closest && e.target.
|
|
|
2607
2846
|
document.addEventListener('keydown', (e) => {
|
|
2608
2847
|
const h = e.target.closest && e.target.closest('.fs-resize')
|
|
2609
2848
|
if (!h || e.metaKey || e.ctrlKey || e.altKey) return
|
|
2610
|
-
const cur = fsWidth.shown(
|
|
2849
|
+
const cur = fsWidth.shown(fsViewport())
|
|
2611
2850
|
let next = null
|
|
2612
2851
|
if (e.key === 'ArrowLeft') next = cur + SIDE_W_STEP
|
|
2613
2852
|
else if (e.key === 'ArrowRight') next = cur - SIDE_W_STEP
|
|
@@ -2616,7 +2855,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
2616
2855
|
else if (e.key === 'Enter') { e.preventDefault(); fsWidth.reset(); applyFsWidth(SIDE_W_DEFAULT); return }
|
|
2617
2856
|
if (next == null) return
|
|
2618
2857
|
e.preventDefault()
|
|
2619
|
-
applyFsWidth(fsWidth.set(Number.isFinite(next) ? next : sideMax(
|
|
2858
|
+
applyFsWidth(fsWidth.set(Number.isFinite(next) ? next : sideMax(fsViewport()), fsViewport()))
|
|
2620
2859
|
})
|
|
2621
2860
|
// 0.9.16 the session panel: open, collapsed to its rail (⌘\, remembered), or on a phone a sheet
|
|
2622
2861
|
function fsSideClasses() {
|
|
@@ -2625,6 +2864,7 @@ function fsSideClasses() {
|
|
|
2625
2864
|
de.classList.toggle('fs-on', !!side)
|
|
2626
2865
|
de.classList.toggle('fs-collapsed', !!side && side.classList.contains('collapsed'))
|
|
2627
2866
|
de.classList.toggle('fs-sheet-up', !!side && side.classList.contains('sheet-up'))
|
|
2867
|
+
applyNav() // 0.9.18: a session panel beside the chat can leave too little room for the expanded rail
|
|
2628
2868
|
}
|
|
2629
2869
|
function toggleFsSide(force) {
|
|
2630
2870
|
const side = document.getElementById('fsSide')
|
|
@@ -2695,6 +2935,7 @@ document.addEventListener('keydown', (ev) => {
|
|
|
2695
2935
|
function setFullClass(on) {
|
|
2696
2936
|
document.documentElement.classList.toggle('live-full', !!on)
|
|
2697
2937
|
if (on) fitFullViewport()
|
|
2938
|
+
applyNav()
|
|
2698
2939
|
}
|
|
2699
2940
|
// The window as you see it: on a phone the keyboard takes the bottom of it,
|
|
2700
2941
|
// and the composer stays just above the keyboard.
|
|
@@ -6602,10 +6843,10 @@ async function refreshHealth() {
|
|
|
6602
6843
|
// the header shows this Mac's name; the rest is in the status popover (v0.5.9)
|
|
6603
6844
|
const summary = h.sessionsOpen + ' open · ' + h.machine + ' · ' + h.summarizer
|
|
6604
6845
|
el.querySelector('.label').textContent = h.machine
|
|
6605
|
-
el.
|
|
6846
|
+
el.dataset.tip = summary
|
|
6606
6847
|
el.setAttribute('aria-label', 'Status: ' + summary)
|
|
6607
6848
|
const brand = document.getElementById('brand')
|
|
6608
|
-
if (brand) brand.
|
|
6849
|
+
if (brand) brand.dataset.tip = 'Leftoff v' + h.version + (h.update && h.update.available ? ' · update available: v' + (h.update.remoteVersion || '?') : '')
|
|
6609
6850
|
paintTopDot()
|
|
6610
6851
|
if (statusOpen()) paintStatus()
|
|
6611
6852
|
paintNeedsCount(needsCountOf(h))
|
|
@@ -6621,8 +6862,8 @@ async function refreshHealth() {
|
|
|
6621
6862
|
if (updateBar) updateBar.check(null) // a failed check changes nothing
|
|
6622
6863
|
el.className = 'health down'
|
|
6623
6864
|
el.querySelector('.label').textContent = e && e.code === 'login_required' ? 'not signed in' : 'daemon offline'
|
|
6624
|
-
el.
|
|
6625
|
-
el.setAttribute('aria-label', 'Status: ' + el.
|
|
6865
|
+
el.dataset.tip = el.querySelector('.label').textContent
|
|
6866
|
+
el.setAttribute('aria-label', 'Status: ' + el.dataset.tip)
|
|
6626
6867
|
}
|
|
6627
6868
|
}
|
|
6628
6869
|
|
|
@@ -6632,7 +6873,7 @@ async function refreshHealth() {
|
|
|
6632
6873
|
// Settings as icons. Nothing wraps or is cut off from 900px up.
|
|
6633
6874
|
|
|
6634
6875
|
const isMac = /Mac|iP(hone|ad|od)/.test(navigator.platform || navigator.userAgent)
|
|
6635
|
-
{ const k = document.getElementById('searchKbd'); if (k) k.textContent = isMac ? '⌘K' : 'Ctrl K'; const b = document.getElementById('searchBtn'); if (b) b.
|
|
6876
|
+
{ const k = document.getElementById('searchKbd'); if (k) k.textContent = isMac ? '⌘K' : 'Ctrl K'; const b = document.getElementById('searchBtn'); if (b) b.dataset.tip = 'Search (' + (isMac ? '⌘K' : 'Ctrl+K') + ' or /)' }
|
|
6636
6877
|
|
|
6637
6878
|
// The gear's dot: anything a Settings section would flag (an update, a failed digest, a feed that can't be pulled, sync trouble).
|
|
6638
6879
|
function paintTopDot() {
|
|
@@ -6646,7 +6887,7 @@ function paintTopDot() {
|
|
|
6646
6887
|
el.hidden = !all.length || (!warn && !(h.update && h.update.available))
|
|
6647
6888
|
el.className = 'topdot' + (warn ? ' warn' : '')
|
|
6648
6889
|
const why = all.flatMap((x) => x.why)
|
|
6649
|
-
document.getElementById('setTab').
|
|
6890
|
+
document.getElementById('setTab').dataset.tip = 'Settings' + (why.length && !el.hidden ? ' · ' + why.join(' · ') : '')
|
|
6650
6891
|
paintMoreDots()
|
|
6651
6892
|
}
|
|
6652
6893
|
|
|
@@ -7125,12 +7366,13 @@ document.addEventListener('keydown', (e) => { const b = e.target.closest && e.ta
|
|
|
7125
7366
|
tip.classList.add('on')
|
|
7126
7367
|
}, delay)
|
|
7127
7368
|
}
|
|
7128
|
-
const tipped = (e) => e.target.closest && e.target.closest('#tabStrip [data-tip], #tabPager [data-tip], #lfTabs [data-tip], .sg[data-tip], .lo-act-x[data-tip], .repos.projects [data-tip], .lo-sheet-x[data-tip], .proj-edit[data-tip], .feed-why[data-tip], .term-menu [data-tip], #wt
|
|
7369
|
+
const tipped = (e) => e.target.closest && e.target.closest('#tabStrip [data-tip], #tabPager [data-tip], #lfTabs [data-tip], .sg[data-tip], .lo-act-x[data-tip], .repos.projects [data-tip], .lo-sheet-x[data-tip], .proj-edit[data-tip], .feed-why[data-tip], .term-menu [data-tip], #wt [data-tip]') // 0.9.18 everything in the walkthrough (its header's icon buttons, the slide nav, file status glyphs); before: the file map's eye, step number and long folders; a step header's files and (cut short) title the file map's eye, step number and long folders; a step header's files and (cut short) title // 0.9.12: the project chips, the sheet's ×, a project page's Edit
|
|
7129
7370
|
document.addEventListener('pointerover', (e) => { const el = tipped(e); if (!el) { if (cur) hide() } else if (e.pointerType === 'mouse' && el !== cur) show(el, 350) })
|
|
7130
7371
|
document.addEventListener('pointerout', (e) => { const el = tipped(e); if (el && el === cur && !el.contains(e.relatedTarget)) hide() })
|
|
7131
|
-
document.addEventListener('focusin', (e) => { const el = tipped(e); if (el && el.matches(':focus-visible')) show(el, 0); else if (cur) hide() })
|
|
7372
|
+
document.addEventListener('focusin', (e) => { if (wtRefocusing) return; const el = tipped(e); if (el && el.matches(':focus-visible')) show(el, 0); else if (cur) hide() })
|
|
7132
7373
|
document.addEventListener('focusout', (e) => { if (tipped(e) === cur) hide() })
|
|
7133
|
-
for (const ev of ['pointerdown', '
|
|
7374
|
+
for (const ev of ['pointerdown', 'hashchange', 'resize']) window.addEventListener(ev, hide, true)
|
|
7375
|
+
window.addEventListener('scroll', () => { if (performance.now() >= wtQuietUntil) hide() }, true) // 0.9.18 not for a redraw putting the walkthrough's scroll back
|
|
7134
7376
|
document.addEventListener('keydown', (e) => { if (e.key === 'Escape' && cur) hide() })
|
|
7135
7377
|
// 0.9.18 touch: a long press shows the tip (the tap it ends with doesn't follow the link)
|
|
7136
7378
|
let lp = 0, lpd = false
|
|
@@ -7170,6 +7412,7 @@ document.addEventListener('click', (e) => { if (e.target.closest && $app.contain
|
|
|
7170
7412
|
document.addEventListener('touchstart', (e) => {
|
|
7171
7413
|
sw = null
|
|
7172
7414
|
if (!narrowQ.matches || tabsUi.route !== 's' || e.touches.length !== 1) return
|
|
7415
|
+
if (navOwnsTouch(e.touches[0].clientX)) return // 0.9.18: the left edge (and the open drawer) belong to the nav drawer
|
|
7173
7416
|
const t = e.target
|
|
7174
7417
|
// a table (even one that fits) or a code block is for reading and scrolling sideways, not for changing sessions
|
|
7175
7418
|
if (t.closest('input, textarea, select, [contenteditable], .lo-menu, .cmdk, .doc-viewer, .lightbox, .pdfview, .tabs, .lf-tabs, .moremenu, table, .doc-table, pre, #wtDeck, .wt-dnav') || scrollsSideways(t)) return
|
|
@@ -7797,6 +8040,18 @@ document.addEventListener('click', async (ev) => {
|
|
|
7797
8040
|
fmToggle((d && d.fingerprint) || '', btn.dataset.key, shut)
|
|
7798
8041
|
return
|
|
7799
8042
|
}
|
|
8043
|
+
if (action === 'wt-file') { // 0.9.18 the Files view's list: open that file's diff and bring it to the top of the diff pane
|
|
8044
|
+
ev.preventDefault()
|
|
8045
|
+
const node = wtUi.node, d = node && node.querySelector('#wt-f-' + CSS.escape(btn.dataset.i || ''))
|
|
8046
|
+
if (!d) return
|
|
8047
|
+
d.open = true
|
|
8048
|
+
for (const a of node.querySelectorAll('.wt-fl-i')) a.classList.toggle('cur', a.dataset.i === btn.dataset.i)
|
|
8049
|
+
const m = btn.closest('details.wt-fl-m'); if (m) m.open = false // a phone: the folded list closes again
|
|
8050
|
+
const sc = node.querySelector('#wtScroll')
|
|
8051
|
+
if (sc) sc.scrollTo({ top: Math.max(0, sc.scrollTop + d.getBoundingClientRect().top - sc.getBoundingClientRect().top - 4), behavior: reducedMotion() ? 'auto' : 'smooth' })
|
|
8052
|
+
const sm = d.querySelector('summary'); if (sm) sm.focus({ preventScroll: true })
|
|
8053
|
+
return
|
|
8054
|
+
}
|
|
7800
8055
|
if (action === 'wt-go') { ev.preventDefault(); return wtGo(wtUi.sid, btn.dataset.step || null, { group: btn.dataset.group || null }) }
|
|
7801
8056
|
if (action === 'wt-step') return wtStep(Number(btn.dataset.dir) || 1)
|
|
7802
8057
|
if (action === 'wt-export-html') return wtExport('html')
|
package/ui/icons.js
CHANGED
|
@@ -3,6 +3,9 @@
|
|
|
3
3
|
// so they follow light and dark; always aria-hidden next to a text label.
|
|
4
4
|
// index.html carries the nav's copies inline (no script needed to draw them).
|
|
5
5
|
export const ICONS = {
|
|
6
|
+
"panel-left-close": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <path d=\"M9 3v18\"/> <path d=\"m16 15-3-3 3-3\"/>",
|
|
7
|
+
"panel-left-open": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <path d=\"M9 3v18\"/> <path d=\"m14 9 3 3-3 3\"/>",
|
|
8
|
+
"menu": "<path d=\"M4 5h16\"/> <path d=\"M4 12h16\"/> <path d=\"M4 19h16\"/>",
|
|
6
9
|
"arrow-up": "<path d=\"m5 12 7-7 7 7\"/> <path d=\"M12 19V5\"/>",
|
|
7
10
|
"eye": "<path d=\"M2 12s3.6-7 10-7 10 7 10 7-3.6 7-10 7S2 12 2 12z\"/> <circle cx=\"12\" cy=\"12\" r=\"3\"/>",
|
|
8
11
|
"circle-help": "<circle cx=\"12\" cy=\"12\" r=\"10\"/> <path d=\"M9.1 9a3 3 0 0 1 5.8 1c0 2-3 3-3 3\"/> <path d=\"M12 17h.01\"/>",
|
|
@@ -37,6 +40,12 @@ export const ICONS = {
|
|
|
37
40
|
"maximize-2": "<path d=\"M15 3h6v6\"/> <path d=\"m21 3-7 7\"/> <path d=\"m3 21 7-7\"/> <path d=\"M9 21H3v-6\"/>",
|
|
38
41
|
"minimize-2": "<path d=\"m14 10 7-7\"/> <path d=\"M20 10h-6V4\"/> <path d=\"m3 21 7-7\"/> <path d=\"M4 14h6v6\"/>",
|
|
39
42
|
"rotate-ccw": "<path d=\"M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8\"/> <path d=\"M3 3v5h5\"/>",
|
|
43
|
+
"square-plus": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <path d=\"M8 12h8\"/> <path d=\"M12 8v8\"/>",
|
|
44
|
+
"square-minus": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <path d=\"M8 12h8\"/>",
|
|
45
|
+
"square-dot": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <circle cx=\"12\" cy=\"12\" r=\"2.6\" fill=\"currentColor\" stroke=\"none\"/>",
|
|
46
|
+
"square-arrow-right": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <path d=\"M8 12h8\"/> <path d=\"m12 16 4-4-4-4\"/>",
|
|
47
|
+
"rows-3": "<rect width=\"18\" height=\"18\" x=\"3\" y=\"3\" rx=\"2\"/> <path d=\"M21 9H3\"/> <path d=\"M21 15H3\"/>",
|
|
48
|
+
"gallery-horizontal": "<path d=\"M2 3v18\"/> <rect width=\"12\" height=\"18\" x=\"6\" y=\"3\" rx=\"2\"/> <path d=\"M22 3v18\"/>",
|
|
40
49
|
"list-checks": "<path d=\"M13 5h8\"/> <path d=\"M13 12h8\"/> <path d=\"M13 19h8\"/> <path d=\"m3 17 2 2 4-4\"/> <path d=\"m3 7 2 2 4-4\"/>",
|
|
41
50
|
"plus": "<path d=\"M5 12h14\"/> <path d=\"M12 5v14\"/>",
|
|
42
51
|
"play": "<path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/>",
|
package/ui/index.html
CHANGED
|
@@ -20,29 +20,33 @@
|
|
|
20
20
|
<link rel="stylesheet" href="app.css">
|
|
21
21
|
</head>
|
|
22
22
|
<body>
|
|
23
|
-
<header class="top">
|
|
24
|
-
<a class="brand" href="#/" id="brand"
|
|
23
|
+
<header class="top" id="navRail">
|
|
24
|
+
<a class="brand" href="#/" id="brand" data-tip="Leftoff" aria-label="Leftoff">
|
|
25
25
|
<img src="favicon.svg" class="logo" alt="" width="22" height="22">
|
|
26
26
|
<span>Leftoff</span>
|
|
27
27
|
</a>
|
|
28
28
|
<nav class="tabs" aria-label="Sections">
|
|
29
|
-
<a href="#/" data-route="board"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect width="7" height="9" x="3" y="3" rx="1"/> <rect width="7" height="5" x="14" y="3" rx="1"/> <rect width="7" height="9" x="14" y="12" rx="1"/> <rect width="7" height="5" x="3" y="16" rx="1"/></svg><span class="lbl">Board</span></a>
|
|
30
|
-
<a href="#/waiting" data-route="waiting" id="needsTab" aria-haspopup="dialog" aria-expanded="false" aria-controls="quickLook"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.268 21a2 2 0 0 0 3.464 0"/> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></svg><span class="lbl">Needs you</span><span class="lbl-short">Needs</span><span class="navcount" id="needsCount" hidden></span></a>
|
|
31
|
-
<a href="#/lists" data-route="lists" id="listsTab"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M13 5h8"/> <path d="M13 12h8"/> <path d="M13 19h8"/> <path d="m3 17 2 2 4-4"/> <path d="m3 7 2 2 4-4"/></svg><span class="lbl">Lists</span><span class="navcount" id="listsCount" hidden></span></a>
|
|
32
|
-
<a href="#/prs" data-route="prs" id="prsTab"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="18" cy="18" r="3"/> <circle cx="6" cy="6" r="3"/> <path d="M13 6h3a2 2 0 0 1 2 2v7"/> <line x1="6" x2="6" y1="9" y2="21"/></svg><span class="lbl">PRs</span></a>
|
|
33
|
-
<a href="#/timeline" data-route="timeline" class="tab-extra"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/> <path d="M3 3v5h5"/> <path d="M12 7v5l4 2"/></svg><span class="lbl">Timeline</span></a>
|
|
34
|
-
<a href="#/digest" data-route="digest" class="tab-extra"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M15 18h-5"/> <path d="M18 14h-8"/> <path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-4 0v-9a2 2 0 0 1 2-2h2"/> <rect width="8" height="4" x="10" y="6" rx="1"/></svg><span class="lbl">Digest</span></a>
|
|
29
|
+
<a href="#/" data-route="board" data-tip="Board"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect width="7" height="9" x="3" y="3" rx="1"/> <rect width="7" height="5" x="14" y="3" rx="1"/> <rect width="7" height="9" x="14" y="12" rx="1"/> <rect width="7" height="5" x="3" y="16" rx="1"/></svg><span class="lbl">Board</span></a>
|
|
30
|
+
<a href="#/waiting" data-route="waiting" id="needsTab" data-tip="Needs you" aria-haspopup="dialog" aria-expanded="false" aria-controls="quickLook"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M10.268 21a2 2 0 0 0 3.464 0"/> <path d="M3.262 15.326A1 1 0 0 0 4 17h16a1 1 0 0 0 .74-1.673C19.41 13.956 18 12.499 18 8A6 6 0 0 0 6 8c0 4.499-1.411 5.956-2.738 7.326"/></svg><span class="lbl">Needs you</span><span class="lbl-short">Needs</span><span class="navcount" id="needsCount" hidden></span></a>
|
|
31
|
+
<a href="#/lists" data-route="lists" id="listsTab" data-tip="Lists"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M13 5h8"/> <path d="M13 12h8"/> <path d="M13 19h8"/> <path d="m3 17 2 2 4-4"/> <path d="m3 7 2 2 4-4"/></svg><span class="lbl">Lists</span><span class="navcount" id="listsCount" hidden></span></a>
|
|
32
|
+
<a href="#/prs" data-route="prs" id="prsTab" data-tip="PRs"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="18" cy="18" r="3"/> <circle cx="6" cy="6" r="3"/> <path d="M13 6h3a2 2 0 0 1 2 2v7"/> <line x1="6" x2="6" y1="9" y2="21"/></svg><span class="lbl">PRs</span></a>
|
|
33
|
+
<a href="#/timeline" data-route="timeline" class="tab-extra" data-tip="Timeline"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/> <path d="M3 3v5h5"/> <path d="M12 7v5l4 2"/></svg><span class="lbl">Timeline</span></a>
|
|
34
|
+
<a href="#/digest" data-route="digest" class="tab-extra" data-tip="Digest"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M15 18h-5"/> <path d="M18 14h-8"/> <path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-4 0v-9a2 2 0 0 1 2-2h2"/> <rect width="8" height="4" x="10" y="6" rx="1"/></svg><span class="lbl">Digest</span></a>
|
|
35
35
|
<button type="button" class="tab-more" id="moreBtn" aria-haspopup="menu" aria-expanded="false" aria-controls="moreMenu"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="1"/> <circle cx="19" cy="12" r="1"/> <circle cx="5" cy="12" r="1"/></svg><span class="lbl">More</span><span class="moredot" hidden></span></button>
|
|
36
36
|
</nav>
|
|
37
37
|
<div class="topright">
|
|
38
|
-
<button type="button" class="searchbtn" id="searchBtn"
|
|
38
|
+
<button type="button" class="searchbtn" id="searchBtn" data-tip="Search (⌘K)" aria-label="Search" aria-haspopup="dialog" aria-keyshortcuts="Meta+K Control+K /"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><circle cx="11" cy="11" r="8"/> <path d="m21 21-4.3-4.3"/></svg><span class="sb-lbl">Search</span><kbd class="sb-kbd" id="searchKbd">⌘K</kbd></button>
|
|
39
39
|
<a href="#/new" data-route="new" id="newTab" class="iconbtn newbtn" data-tip="New session" aria-label="New session"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M5 12h14"/> <path d="M12 5v14"/></svg><span class="lbl">New session</span></a>
|
|
40
40
|
<button type="button" id="termBtn" class="iconbtn termbtn" data-term-toggle data-tip="Terminal (⌃`)" aria-label="Terminal" aria-expanded="false" aria-controls="termDock" aria-keyshortcuts="Control+`"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M12 19h8"/> <path d="m4 17 6-6-6-6"/></svg><span class="lbl">Terminal</span></button>
|
|
41
|
-
<button type="button" class="health" id="health"
|
|
42
|
-
<a href="#/whats-new" data-route="whats-new" id="wnTab" class="iconbtn"
|
|
43
|
-
<a href="#/settings" data-route="settings" id="setTab" class="iconbtn"
|
|
41
|
+
<button type="button" class="health" id="health" data-tip="Status" aria-haspopup="dialog" aria-expanded="false" aria-controls="statusPop"><span class="dot"></span><span class="label">connecting…</span></button>
|
|
42
|
+
<a href="#/whats-new" data-route="whats-new" id="wnTab" class="iconbtn" data-tip="What’s new" aria-label="What’s new"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M11.017 2.814a1 1 0 0 1 1.966 0l1.051 5.558a2 2 0 0 0 1.594 1.594l5.558 1.051a1 1 0 0 1 0 1.966l-5.558 1.051a2 2 0 0 0-1.594 1.594l-1.051 5.558a1 1 0 0 1-1.966 0l-1.051-5.558a2 2 0 0 0-1.594-1.594l-5.558-1.051a1 1 0 0 1 0-1.966l5.558-1.051a2 2 0 0 0 1.594-1.594z"/> <path d="M20 2v4"/> <path d="M22 4h-4"/> <circle cx="4" cy="20" r="2"/></svg><span class="lbl">What’s new</span><span class="newdot" hidden></span></a>
|
|
43
|
+
<a href="#/settings" data-route="settings" id="setTab" class="iconbtn" data-tip="Settings" aria-label="Settings"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M9.671 4.136a2.34 2.34 0 0 1 4.659 0 2.34 2.34 0 0 0 3.319 1.915 2.34 2.34 0 0 1 2.33 4.033 2.34 2.34 0 0 0 0 3.831 2.34 2.34 0 0 1-2.33 4.033 2.34 2.34 0 0 0-3.319 1.915 2.34 2.34 0 0 1-4.659 0 2.34 2.34 0 0 0-3.32-1.915 2.34 2.34 0 0 1-2.33-4.033 2.34 2.34 0 0 0 0-3.831A2.34 2.34 0 0 1 6.35 6.051a2.34 2.34 0 0 0 3.319-1.915"/> <circle cx="12" cy="12" r="3"/></svg><span class="lbl">Settings</span><span class="topdot" hidden></span></a>
|
|
44
|
+
<button type="button" class="iconbtn navtoggle" id="navToggle" aria-controls="navRail" aria-expanded="true" aria-label="Collapse the sidebar" aria-keyshortcuts="Meta+B Control+B [" data-tip="Collapse sidebar (⌘B)"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><rect width="18" height="18" x="3" y="3" rx="2"/> <path d="M9 3v18"/> <path d="m16 15-3-3 3-3"/></svg><span class="lbl">Collapse</span></button>
|
|
44
45
|
</div>
|
|
45
46
|
</header>
|
|
47
|
+
<div class="navscrim" id="navScrim" hidden></div>
|
|
48
|
+
<div class="navdrawer" id="navDrawer" role="dialog" aria-modal="true" aria-label="Menu" hidden></div>
|
|
49
|
+
<div class="railtip" id="railTip" role="tooltip" hidden></div>
|
|
46
50
|
<div class="moremenu" id="moreMenu" role="menu" aria-label="More" hidden>
|
|
47
51
|
<a href="#/timeline" data-route="timeline" role="menuitem"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M3 12a9 9 0 1 0 9-9 9.75 9.75 0 0 0-6.74 2.74L3 8"/> <path d="M3 3v5h5"/> <path d="M12 7v5l4 2"/></svg><span>Timeline</span></a>
|
|
48
52
|
<a href="#/digest" data-route="digest" role="menuitem"><svg class="ic" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false"><path d="M15 18h-5"/> <path d="M18 14h-8"/> <path d="M4 22h16a2 2 0 0 0 2-2V4a2 2 0 0 0-2-2H8a2 2 0 0 0-2 2v16a2 2 0 0 1-4 0v-9a2 2 0 0 1 2-2h2"/> <rect width="8" height="4" x="10" y="6" rx="1"/></svg><span>Digest</span></a>
|
package/ui/live-view.js
CHANGED
|
@@ -242,6 +242,8 @@ export function fullButtonHtml(id, { esc, icon }) {
|
|
|
242
242
|
// name goes in as text (the caller fills .lf-title with textContent).
|
|
243
243
|
export function fullBarHtml(id, { esc, icon, pills = '', stop = '', retitle = '', glyph = '' }) {
|
|
244
244
|
return '<div class="live-fbar">' +
|
|
245
|
+
// 0.9.18 phones: the nav as a drawer (ui/navrail.js); from 700px the rail is beside full screen and this hides
|
|
246
|
+
'<button type="button" class="btn lf-menu" data-nav-drawer aria-label="Menu" aria-expanded="false" aria-controls="navDrawer" data-tip="Menu">' + icon('menu') + '</button>' +
|
|
245
247
|
'<button type="button" class="btn live-fs-x" data-action="live-full-exit" data-id="' + esc(id) + '" aria-label="Back to the board" data-tip="Board">' + icon('chevron-left') + '</button>' + // 0.9.16 one layout: back is the board
|
|
246
248
|
glyph + '<h2 class="lf-title"></h2>' + retitle + '<div class="lf-pills">' + pills + '</div>' + stop +
|
|
247
249
|
// 0.9.15 the embedded terminal (ui/term.js): docked under full screen, which gets shorter
|