feat(web): make the MR view edge to edge and touch friendly on mobile
Narrow screens drop side margins, force the unified diff without changing the saved preference, and open menus as bottom sheets. Coarse pointers get tall line-number targets and a tap-to-select line range for comments.
- commit
- d92c6dc3247ee58d9be537f6224e1cd009f97236
- author
- brice · · 2026-10-20 05:11 UTC
- parent
- cc7ba64
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
15 files · +480 -211
M internal/adapters/web/diff.templ +6 -2
| @@ -117,7 +117,7 @@ templ diffViewMenu(p DiffView) { | |||
| 117 | 117 | <details class="menu vmenu"> | |
| 118 | 118 | <summary class="btn" aria-label="Diff view"> | |
| 119 | 119 | if p.Both { | |
| 120 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">{ viewLabel(p.View) }</span> | |
| 120 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">{ viewLabel(p.View) }</span> | |
| 121 | 121 | } else { | |
| 122 | 122 | { viewLabel(p.View) } | |
| 123 | 123 | } | |
| @@ -139,12 +139,16 @@ templ viewLink(p DiffView, view, label, key, href string) { | |||
| 139 | 139 | class="btn vbtn" | |
| 140 | 140 | data-view={ view } | |
| 141 | 141 | aria-keyshortcuts={ key } | |
| 142 | + | if view == "split" { | |
| 143 | + | :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" | |
| 144 | + | :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" | |
| 145 | + | } | |
| 142 | 146 | if p.View == view { | |
| 143 | 147 | aria-current="true" | |
| 144 | 148 | } | |
| 145 | 149 | if p.Both { | |
| 146 | 150 | data-fire={ view } | |
| 147 | - | :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false" | |
| 151 | + | :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false" | |
| 148 | 152 | } | |
| 149 | 153 | >{ label }<kbd aria-hidden="true">{ key }</kbd></a> | |
| 150 | 154 | } | |
| @@ -117,7 +117,7 @@ templ diffViewMenu(p DiffView) { | |||||
| 117 | <details class="menu vmenu"> | 117 | <details class="menu vmenu"> | ||
| 118 | <summary class="btn" aria-label="Diff view"> | 118 | <summary class="btn" aria-label="Diff view"> | ||
| 119 | if p.Both { | 119 | if p.Both { | ||
| 120 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">{ viewLabel(p.View) }</span> | 120 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">{ viewLabel(p.View) }</span> |
| 121 | } else { | 121 | } else { | ||
| 122 | { viewLabel(p.View) } | 122 | { viewLabel(p.View) } | ||
| 123 | } | 123 | } | ||
| @@ -139,12 +139,16 @@ templ viewLink(p DiffView, view, label, key, href string) { | |||||
| 139 | class="btn vbtn" | 139 | class="btn vbtn" | ||
| 140 | data-view={ view } | 140 | data-view={ view } | ||
| 141 | aria-keyshortcuts={ key } | 141 | aria-keyshortcuts={ key } | ||
| 142 | + | if view == "split" { | |||
| 143 | + | :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" | |||
| 144 | + | :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" | |||
| 145 | + | } | |||
| 142 | if p.View == view { | 146 | if p.View == view { | ||
| 143 | aria-current="true" | 147 | aria-current="true" | ||
| 144 | } | 148 | } | ||
| 145 | if p.Both { | 149 | if p.Both { | ||
| 146 | data-fire={ view } | 150 | data-fire={ view } | ||
| 147 | - | :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false" | 151 | + | :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false" |
| 148 | } | 152 | } | ||
| 149 | >{ label }<kbd aria-hidden="true">{ key }</kbd></a> | 153 | >{ label }<kbd aria-hidden="true">{ key }</kbd></a> | ||
| 150 | } | 154 | } | ||
M internal/adapters/web/diff_templ.go +190 -184 generated file: collapsed
M internal/adapters/web/jstest/a11y.test.mjs +23 -0
| @@ -358,3 +358,26 @@ test('activity moves a home row up only in newest activity order, preserving foc | |||
| 358 | 358 | assert.ok(fresh.hasAttribute('data-cursor')); | |
| 359 | 359 | } | |
| 360 | 360 | }); | |
| 361 | + | ||
| 362 | + | test('mobile MR layout shortcuts preserve the saved preference until back on desktop', () => { | |
| 363 | + | const diffs = el('div', { id: 'diffs', attrs: { 'data-mr': '', 'data-diff': 'split', 'data-mobile': 'true' } }); | |
| 364 | + | dom.body.replaceChildren(diffs); | |
| 365 | + | const oldQuery = doc.querySelector; | |
| 366 | + | doc.querySelector = query.one; | |
| 367 | + | const previous = globalThis.sessionStorage; | |
| 368 | + | const writes = []; | |
| 369 | + | globalThis.sessionStorage = { setItem: (...args) => writes.push(args) }; | |
| 370 | + | try { | |
| 371 | + | window.ermite.layout('unified'); | |
| 372 | + | window.ermite.layout('split'); | |
| 373 | + | assert.equal(diffs.dataset.diff, 'split'); | |
| 374 | + | assert.deepEqual(writes, []); | |
| 375 | + | diffs.dataset.mobile = 'false'; | |
| 376 | + | window.ermite.layout('unified'); | |
| 377 | + | assert.equal(diffs.dataset.diff, 'unified'); | |
| 378 | + | assert.deepEqual(writes, [['diff', 'unified']]); | |
| 379 | + | } finally { | |
| 380 | + | globalThis.sessionStorage = previous; | |
| 381 | + | doc.querySelector = oldQuery; | |
| 382 | + | } | |
| 383 | + | }); | |
| @@ -358,3 +358,26 @@ test('activity moves a home row up only in newest activity order, preserving foc | |||||
| 358 | assert.ok(fresh.hasAttribute('data-cursor')); | 358 | assert.ok(fresh.hasAttribute('data-cursor')); | ||
| 359 | } | 359 | } | ||
| 360 | }); | 360 | }); | ||
| 361 | + | ||||
| 362 | + | test('mobile MR layout shortcuts preserve the saved preference until back on desktop', () => { | |||
| 363 | + | const diffs = el('div', { id: 'diffs', attrs: { 'data-mr': '', 'data-diff': 'split', 'data-mobile': 'true' } }); | |||
| 364 | + | dom.body.replaceChildren(diffs); | |||
| 365 | + | const oldQuery = doc.querySelector; | |||
| 366 | + | doc.querySelector = query.one; | |||
| 367 | + | const previous = globalThis.sessionStorage; | |||
| 368 | + | const writes = []; | |||
| 369 | + | globalThis.sessionStorage = { setItem: (...args) => writes.push(args) }; | |||
| 370 | + | try { | |||
| 371 | + | window.ermite.layout('unified'); | |||
| 372 | + | window.ermite.layout('split'); | |||
| 373 | + | assert.equal(diffs.dataset.diff, 'split'); | |||
| 374 | + | assert.deepEqual(writes, []); | |||
| 375 | + | diffs.dataset.mobile = 'false'; | |||
| 376 | + | window.ermite.layout('unified'); | |||
| 377 | + | assert.equal(diffs.dataset.diff, 'unified'); | |||
| 378 | + | assert.deepEqual(writes, [['diff', 'unified']]); | |||
| 379 | + | } finally { | |||
| 380 | + | globalThis.sessionStorage = previous; | |||
| 381 | + | doc.querySelector = oldQuery; | |||
| 382 | + | } | |||
| 383 | + | }); | |||
M internal/adapters/web/jstest/mr.test.mjs +106 -0
| @@ -92,3 +92,109 @@ for (const dataset of [{diff:'split'}, {view:'split'}]) test(`reveal selects the | |||
| 92 | 92 | globalThis.requestAnimationFrame=savedRAF; | |
| 93 | 93 | } | |
| 94 | 94 | }); | |
| 95 | + | ||
| 96 | + | test('touch range: first tap anchors, second tap opens the sorted range', () => { | |
| 97 | + | const base = 'https://forge.test/demo/merge-requests/1'; | |
| 98 | + | const first = base + '?on=new:24:a.go#f1'; | |
| 99 | + | const last = base + '?on=new:10:a.go#f1'; | |
| 100 | + | assert.deepEqual(C.touchRange(null, first, base), { start: first, url: null }); | |
| 101 | + | const result = C.touchRange(first, last, base); | |
| 102 | + | assert.equal(result.start, null); | |
| 103 | + | assert.equal(new URL(result.url).searchParams.get('on'), 'new:10-24:a.go'); | |
| 104 | + | assert.deepEqual(C.touchRange(first, first, base), { start: null, url: first }); | |
| 105 | + | }); | |
| 106 | + | ||
| 107 | + | test('touch range: a different file, side or request becomes a new anchor', () => { | |
| 108 | + | const base = 'https://forge.test/demo/merge-requests/1'; | |
| 109 | + | const first = base + '?on=new:24:a.go#f1'; | |
| 110 | + | for (const next of [first.replace('new:', 'old:'), first.replace('a.go', 'b.go'), first.replace('/1?', '/2?')]) { | |
| 111 | + | assert.deepEqual(C.touchRange(first, next, base), { start: next, url: null }); | |
| 112 | + | } | |
| 113 | + | }); | |
| 114 | + | ||
| 115 | + | test('mobile unified fallback only redirects single-layout split pages and preserves query/hash', () => { | |
| 116 | + | const href = 'https://forge.test/demo/merge-requests/1?view=split&from=25&on=new:3:a.go#f26'; | |
| 117 | + | assert.equal(C.mobileURL(href, 'split', false), null); | |
| 118 | + | assert.equal(C.mobileURL(href, undefined, true), null, 'both layouts are switched by CSS'); | |
| 119 | + | assert.equal(C.mobileURL(href, 'unified', true), null, 'no redirect loop'); | |
| 120 | + | const url = new URL(C.mobileURL(href, 'split', true)); | |
| 121 | + | assert.equal(url.searchParams.get('view'), 'unified'); | |
| 122 | + | assert.equal(url.searchParams.get('from'), '25'); | |
| 123 | + | assert.equal(url.searchParams.get('on'), 'new:3:a.go'); | |
| 124 | + | assert.equal(url.hash, '#f26'); | |
| 125 | + | }); | |
| 126 | + | ||
| 127 | + | test('touch control: lazy links enable toolbar; ordinary taps pass through; ranges navigate and cancel', async () => { | |
| 128 | + | const saved = Object.fromEntries(['document', 'location', 'addEventListener', 'localStorage', 'sessionStorage', 'fetch', 'matchMedia'].map((k) => [k, globalThis[k]])); | |
| 129 | + | const make = () => ({ | |
| 130 | + | attrs: {}, events: {}, children: [], textContent: '', | |
| 131 | + | setAttribute(k, v) { this.attrs[k] = v; }, | |
| 132 | + | removeAttribute(k) { delete this.attrs[k]; }, | |
| 133 | + | addEventListener(k, fn) { this.events[k] = fn; }, | |
| 134 | + | append(...nodes) { this.children.push(...nodes); }, | |
| 135 | + | prepend(node) { this.children.unshift(node); }, | |
| 136 | + | querySelector() { return null; }, querySelectorAll() { return []; }, | |
| 137 | + | }); | |
| 138 | + | const diffs = make(); | |
| 139 | + | diffs.dataset = { diff: 'split' }; | |
| 140 | + | const base = 'https://forge.test/demo/merge-requests/1'; | |
| 141 | + | const line = (n) => Object.assign(make(), { href: `${base}?on=new:${n}:a.go#f1`, textContent: String(n) }); | |
| 142 | + | const a = line(20), b = line(10); | |
| 143 | + | let loaded = false; | |
| 144 | + | diffs.querySelector = () => loaded ? a : null; | |
| 145 | + | let assigned = null; | |
| 146 | + | globalThis.document = { | |
| 147 | + | querySelector: (sel) => sel === '#diffs[data-mr]' || sel === '#diffs' ? diffs : null, | |
| 148 | + | querySelectorAll: () => [], createElement: (tag) => tag === 'template' ? { content: { querySelectorAll: () => [] } } : make(), | |
| 149 | + | }; | |
| 150 | + | globalThis.location = { href: base, pathname: '/demo/merge-requests/1', hash: '', assign(url) { assigned = url; } }; | |
| 151 | + | globalThis.addEventListener = () => {}; | |
| 152 | + | let resize; | |
| 153 | + | const media = { matches: true, addEventListener(type, fn) { resize = fn; } }; | |
| 154 | + | globalThis.matchMedia = () => media; | |
| 155 | + | globalThis.fetch = async () => { loaded = true; return { ok: true, status: 200, text: async () => '' }; }; | |
| 156 | + | globalThis.localStorage = globalThis.sessionStorage = { getItem: () => null }; | |
| 157 | + | const click = (link, modifiers = {}) => { | |
| 158 | + | const e = { target: { closest: () => link }, prevented: false, stopped: false, | |
| 159 | + | preventDefault() { this.prevented = true; }, stopPropagation() { this.stopped = true; }, ...modifiers }; | |
| 160 | + | diffs.events.click(e); | |
| 161 | + | return e; | |
| 162 | + | }; | |
| 163 | + | try { | |
| 164 | + | window.ermiteMR.start(); | |
| 165 | + | const bar = diffs.children[0]; | |
| 166 | + | const [button, status] = bar.children; | |
| 167 | + | assert.equal(bar.hidden, true, 'no commentable links in the initial window'); | |
| 168 | + | assert.equal(typeof diffs.events.click, 'function', 'delegation installed before lazy load'); | |
| 169 | + | await window.ermiteMR.load({ dataset: { src: '/files?from=25' } }); | |
| 170 | + | assert.equal(bar.hidden, false, 'lazy window refresh reveals range control'); | |
| 171 | + | assert.equal(diffs.dataset.mobile, 'true'); | |
| 172 | + | assert.equal(diffs.dataset.diff, 'split', 'preferred layout is retained'); | |
| 173 | + | media.matches = false; | |
| 174 | + | resize(); | |
| 175 | + | assert.equal(diffs.dataset.mobile, 'false', 'desktop menu returns to preferred layout'); | |
| 176 | + | assert.equal(diffs.dataset.diff, 'split'); | |
| 177 | + | assert.equal(click(a).prevented, false, 'normal single-line navigation is untouched'); | |
| 178 | + | button.events.click(); | |
| 179 | + | assert.equal(button.attrs['aria-pressed'], 'true'); | |
| 180 | + | assert.equal(click(a, { shiftKey: true }).prevented, false, 'desktop range selection is untouched'); | |
| 181 | + | const event = click(a); | |
| 182 | + | assert.equal(event.prevented, true); | |
| 183 | + | assert.equal(event.stopped, true); | |
| 184 | + | assert.ok('data-range-start' in a.attrs); | |
| 185 | + | assert.match(status.textContent, /20/); | |
| 186 | + | click(b); | |
| 187 | + | assert.equal(new URL(assigned).searchParams.get('on'), 'new:10-20:a.go'); | |
| 188 | + | assert.equal('data-range-start' in a.attrs, false); | |
| 189 | + | click(a); | |
| 190 | + | button.events.click(); | |
| 191 | + | assert.equal(button.attrs['aria-pressed'], 'false'); | |
| 192 | + | assert.equal(status.textContent, ''); | |
| 193 | + | assert.equal('data-range-start' in a.attrs, false); | |
| 194 | + | assert.equal(click(b).prevented, false); | |
| 195 | + | } finally { | |
| 196 | + | for (const [k, v] of Object.entries(saved)) { | |
| 197 | + | if (v === undefined) delete globalThis[k]; else globalThis[k] = v; | |
| 198 | + | } | |
| 199 | + | } | |
| 200 | + | }); | |
| @@ -92,3 +92,109 @@ for (const dataset of [{diff:'split'}, {view:'split'}]) test(`reveal selects the | |||||
| 92 | globalThis.requestAnimationFrame=savedRAF; | 92 | globalThis.requestAnimationFrame=savedRAF; | ||
| 93 | } | 93 | } | ||
| 94 | }); | 94 | }); | ||
| 95 | + | ||||
| 96 | + | test('touch range: first tap anchors, second tap opens the sorted range', () => { | |||
| 97 | + | const base = 'https://forge.test/demo/merge-requests/1'; | |||
| 98 | + | const first = base + '?on=new:24:a.go#f1'; | |||
| 99 | + | const last = base + '?on=new:10:a.go#f1'; | |||
| 100 | + | assert.deepEqual(C.touchRange(null, first, base), { start: first, url: null }); | |||
| 101 | + | const result = C.touchRange(first, last, base); | |||
| 102 | + | assert.equal(result.start, null); | |||
| 103 | + | assert.equal(new URL(result.url).searchParams.get('on'), 'new:10-24:a.go'); | |||
| 104 | + | assert.deepEqual(C.touchRange(first, first, base), { start: null, url: first }); | |||
| 105 | + | }); | |||
| 106 | + | ||||
| 107 | + | test('touch range: a different file, side or request becomes a new anchor', () => { | |||
| 108 | + | const base = 'https://forge.test/demo/merge-requests/1'; | |||
| 109 | + | const first = base + '?on=new:24:a.go#f1'; | |||
| 110 | + | for (const next of [first.replace('new:', 'old:'), first.replace('a.go', 'b.go'), first.replace('/1?', '/2?')]) { | |||
| 111 | + | assert.deepEqual(C.touchRange(first, next, base), { start: next, url: null }); | |||
| 112 | + | } | |||
| 113 | + | }); | |||
| 114 | + | ||||
| 115 | + | test('mobile unified fallback only redirects single-layout split pages and preserves query/hash', () => { | |||
| 116 | + | const href = 'https://forge.test/demo/merge-requests/1?view=split&from=25&on=new:3:a.go#f26'; | |||
| 117 | + | assert.equal(C.mobileURL(href, 'split', false), null); | |||
| 118 | + | assert.equal(C.mobileURL(href, undefined, true), null, 'both layouts are switched by CSS'); | |||
| 119 | + | assert.equal(C.mobileURL(href, 'unified', true), null, 'no redirect loop'); | |||
| 120 | + | const url = new URL(C.mobileURL(href, 'split', true)); | |||
| 121 | + | assert.equal(url.searchParams.get('view'), 'unified'); | |||
| 122 | + | assert.equal(url.searchParams.get('from'), '25'); | |||
| 123 | + | assert.equal(url.searchParams.get('on'), 'new:3:a.go'); | |||
| 124 | + | assert.equal(url.hash, '#f26'); | |||
| 125 | + | }); | |||
| 126 | + | ||||
| 127 | + | test('touch control: lazy links enable toolbar; ordinary taps pass through; ranges navigate and cancel', async () => { | |||
| 128 | + | const saved = Object.fromEntries(['document', 'location', 'addEventListener', 'localStorage', 'sessionStorage', 'fetch', 'matchMedia'].map((k) => [k, globalThis[k]])); | |||
| 129 | + | const make = () => ({ | |||
| 130 | + | attrs: {}, events: {}, children: [], textContent: '', | |||
| 131 | + | setAttribute(k, v) { this.attrs[k] = v; }, | |||
| 132 | + | removeAttribute(k) { delete this.attrs[k]; }, | |||
| 133 | + | addEventListener(k, fn) { this.events[k] = fn; }, | |||
| 134 | + | append(...nodes) { this.children.push(...nodes); }, | |||
| 135 | + | prepend(node) { this.children.unshift(node); }, | |||
| 136 | + | querySelector() { return null; }, querySelectorAll() { return []; }, | |||
| 137 | + | }); | |||
| 138 | + | const diffs = make(); | |||
| 139 | + | diffs.dataset = { diff: 'split' }; | |||
| 140 | + | const base = 'https://forge.test/demo/merge-requests/1'; | |||
| 141 | + | const line = (n) => Object.assign(make(), { href: `${base}?on=new:${n}:a.go#f1`, textContent: String(n) }); | |||
| 142 | + | const a = line(20), b = line(10); | |||
| 143 | + | let loaded = false; | |||
| 144 | + | diffs.querySelector = () => loaded ? a : null; | |||
| 145 | + | let assigned = null; | |||
| 146 | + | globalThis.document = { | |||
| 147 | + | querySelector: (sel) => sel === '#diffs[data-mr]' || sel === '#diffs' ? diffs : null, | |||
| 148 | + | querySelectorAll: () => [], createElement: (tag) => tag === 'template' ? { content: { querySelectorAll: () => [] } } : make(), | |||
| 149 | + | }; | |||
| 150 | + | globalThis.location = { href: base, pathname: '/demo/merge-requests/1', hash: '', assign(url) { assigned = url; } }; | |||
| 151 | + | globalThis.addEventListener = () => {}; | |||
| 152 | + | let resize; | |||
| 153 | + | const media = { matches: true, addEventListener(type, fn) { resize = fn; } }; | |||
| 154 | + | globalThis.matchMedia = () => media; | |||
| 155 | + | globalThis.fetch = async () => { loaded = true; return { ok: true, status: 200, text: async () => '' }; }; | |||
| 156 | + | globalThis.localStorage = globalThis.sessionStorage = { getItem: () => null }; | |||
| 157 | + | const click = (link, modifiers = {}) => { | |||
| 158 | + | const e = { target: { closest: () => link }, prevented: false, stopped: false, | |||
| 159 | + | preventDefault() { this.prevented = true; }, stopPropagation() { this.stopped = true; }, ...modifiers }; | |||
| 160 | + | diffs.events.click(e); | |||
| 161 | + | return e; | |||
| 162 | + | }; | |||
| 163 | + | try { | |||
| 164 | + | window.ermiteMR.start(); | |||
| 165 | + | const bar = diffs.children[0]; | |||
| 166 | + | const [button, status] = bar.children; | |||
| 167 | + | assert.equal(bar.hidden, true, 'no commentable links in the initial window'); | |||
| 168 | + | assert.equal(typeof diffs.events.click, 'function', 'delegation installed before lazy load'); | |||
| 169 | + | await window.ermiteMR.load({ dataset: { src: '/files?from=25' } }); | |||
| 170 | + | assert.equal(bar.hidden, false, 'lazy window refresh reveals range control'); | |||
| 171 | + | assert.equal(diffs.dataset.mobile, 'true'); | |||
| 172 | + | assert.equal(diffs.dataset.diff, 'split', 'preferred layout is retained'); | |||
| 173 | + | media.matches = false; | |||
| 174 | + | resize(); | |||
| 175 | + | assert.equal(diffs.dataset.mobile, 'false', 'desktop menu returns to preferred layout'); | |||
| 176 | + | assert.equal(diffs.dataset.diff, 'split'); | |||
| 177 | + | assert.equal(click(a).prevented, false, 'normal single-line navigation is untouched'); | |||
| 178 | + | button.events.click(); | |||
| 179 | + | assert.equal(button.attrs['aria-pressed'], 'true'); | |||
| 180 | + | assert.equal(click(a, { shiftKey: true }).prevented, false, 'desktop range selection is untouched'); | |||
| 181 | + | const event = click(a); | |||
| 182 | + | assert.equal(event.prevented, true); | |||
| 183 | + | assert.equal(event.stopped, true); | |||
| 184 | + | assert.ok('data-range-start' in a.attrs); | |||
| 185 | + | assert.match(status.textContent, /20/); | |||
| 186 | + | click(b); | |||
| 187 | + | assert.equal(new URL(assigned).searchParams.get('on'), 'new:10-20:a.go'); | |||
| 188 | + | assert.equal('data-range-start' in a.attrs, false); | |||
| 189 | + | click(a); | |||
| 190 | + | button.events.click(); | |||
| 191 | + | assert.equal(button.attrs['aria-pressed'], 'false'); | |||
| 192 | + | assert.equal(status.textContent, ''); | |||
| 193 | + | assert.equal('data-range-start' in a.attrs, false); | |||
| 194 | + | assert.equal(click(b).prevented, false); | |||
| 195 | + | } finally { | |||
| 196 | + | for (const [k, v] of Object.entries(saved)) { | |||
| 197 | + | if (v === undefined) delete globalThis[k]; else globalThis[k] = v; | |||
| 198 | + | } | |||
| 199 | + | } | |||
| 200 | + | }); | |||
A internal/adapters/web/static/css/85-mobile.css +51 -0
| @@ -0,0 +1,51 @@ | |||
| 1 | + | @layer components { | |
| 2 | + | .mr-touch-range { display: none; } | |
| 3 | + | ||
| 4 | + | /* MR only: retain the shell's desktop layout and other pages' spacing. */ | |
| 5 | + | @media (max-width: 720px) { | |
| 6 | + | body:has(#diffs[data-mr]) { | |
| 7 | + | --pad: 0px; | |
| 8 | + | --hdr-h: 97px; | |
| 9 | + | overflow-x: clip; | |
| 10 | + | } | |
| 11 | + | body:has(#diffs[data-mr]) .work { padding-inline: 0; margin-inline: 0; gap: var(--s3); } | |
| 12 | + | body:has(#diffs[data-mr]) main { min-width: 0; padding-inline: 0; } | |
| 13 | + | body:has(#diffs[data-mr]) :is(.card, .mr-card, .mr-desc, .dfile, .dscroll, .code, .rail section) { | |
| 14 | + | margin-inline: 0; border-inline: 0; border-radius: 0; min-width: 0; max-width: 100%; | |
| 15 | + | } | |
| 16 | + | body:has(#diffs[data-mr]) :is(.mr-h, .mr-desc.prose, .card-h) { padding-inline: 4px; } | |
| 17 | + | body:has(#diffs[data-mr]) .mr-desc.prose > pre { margin-inline: -4px; padding-inline: 0; max-width: calc(100% + 8px); } | |
| 18 | + | #diffs[data-mr] .dfile > summary { padding-inline: 0; border-inline: 0; border-radius: 0; } | |
| 19 | + | #diffs[data-mr] :is(.dscroll, .code) { overflow-x: auto; overscroll-behavior-x: contain; padding-inline: 0; } | |
| 20 | + | #diffs[data-mr] .dt .ln { width: calc(var(--lnw) + .5ch); min-width: calc(var(--lnw) + .5ch); padding-inline: .25ch; } | |
| 21 | + | #diffs[data-mr] .dt .m { width: 1ch; } | |
| 22 | + | #diffs[data-mr][data-diff] .uni { display: table; } | |
| 23 | + | #diffs[data-mr][data-diff] .spl { display: none; } | |
| 24 | + | #diffs[data-mr] .fnote td { padding-inline: 0; } | |
| 25 | + | #diffs[data-mr] :is(.cform, .cmt) { width: min(65ch, calc(100vw - 5rem)); max-width: calc(100vw - 5rem); overflow-wrap: anywhere; } | |
| 26 | + | body:has(#diffs[data-mr]) :is(input, textarea, select) { font-size: 16px; } | |
| 27 | + | body:has(#diffs[data-mr]) .f-btns { flex-wrap: wrap; } | |
| 28 | + | body:has(#diffs[data-mr]) .menu .menu-pop { | |
| 29 | + | position: fixed; inset: auto 0 0; width: auto; max-width: 100%; | |
| 30 | + | max-height: 75dvh; overflow-y: auto; border-radius: var(--r) var(--r) 0 0; | |
| 31 | + | padding-bottom: max(var(--s4), env(safe-area-inset-bottom)); | |
| 32 | + | } | |
| 33 | + | body:has(#diffs[data-mr]) .mr-bar { position: static; } | |
| 34 | + | /* A wrapping toolbar has no reliable fixed sticky height. */ | |
| 35 | + | body:has(#diffs[data-mr]) { --bar-h: 0px; } | |
| 36 | + | } | |
| 37 | + | ||
| 38 | + | @media (pointer: coarse), (hover: none) { | |
| 39 | + | .mr-touch-range { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); } | |
| 40 | + | /* Real, disjoint cells: never expand invisible targets into other lines | |
| 41 | + | or into the neighbouring old/new gutter. */ | |
| 42 | + | #diffs[data-mr] .dt { line-height: max(var(--lh-code), 40px); } | |
| 43 | + | #diffs[data-mr] .dt .ln { width: max(calc(var(--lnw) + .5ch), 40px); min-width: 40px; padding: 0; } | |
| 44 | + | #diffs[data-mr] .spl .c-ln { width: max(calc(var(--lnw) + .5ch), 40px); } | |
| 45 | + | #diffs[data-mr] .ln a { display: block; width: 100%; min-height: 40px; text-align: center; text-decoration: underline; } | |
| 46 | + | .mr-touch-range[hidden] { display: none; } | |
| 47 | + | #diffs[data-mr] .ln a[data-range-start] { outline: 2px solid var(--focus); background: var(--sel); } | |
| 48 | + | body:has(#diffs[data-mr]) :is(.cform .btn, .menu .btn, .mr-touch-range .btn, .c-act button) { min-height: 40px; height: auto; padding-block: var(--s2); } | |
| 49 | + | #diffs[data-mr] .cform { scroll-margin-block: calc(var(--hdr-h) + 12px); } | |
| 50 | + | } | |
| 51 | + | } | |
| @@ -0,0 +1,51 @@ | |||||
| 1 | + | @layer components { | |||
| 2 | + | .mr-touch-range { display: none; } | |||
| 3 | + | ||||
| 4 | + | /* MR only: retain the shell's desktop layout and other pages' spacing. */ | |||
| 5 | + | @media (max-width: 720px) { | |||
| 6 | + | body:has(#diffs[data-mr]) { | |||
| 7 | + | --pad: 0px; | |||
| 8 | + | --hdr-h: 97px; | |||
| 9 | + | overflow-x: clip; | |||
| 10 | + | } | |||
| 11 | + | body:has(#diffs[data-mr]) .work { padding-inline: 0; margin-inline: 0; gap: var(--s3); } | |||
| 12 | + | body:has(#diffs[data-mr]) main { min-width: 0; padding-inline: 0; } | |||
| 13 | + | body:has(#diffs[data-mr]) :is(.card, .mr-card, .mr-desc, .dfile, .dscroll, .code, .rail section) { | |||
| 14 | + | margin-inline: 0; border-inline: 0; border-radius: 0; min-width: 0; max-width: 100%; | |||
| 15 | + | } | |||
| 16 | + | body:has(#diffs[data-mr]) :is(.mr-h, .mr-desc.prose, .card-h) { padding-inline: 4px; } | |||
| 17 | + | body:has(#diffs[data-mr]) .mr-desc.prose > pre { margin-inline: -4px; padding-inline: 0; max-width: calc(100% + 8px); } | |||
| 18 | + | #diffs[data-mr] .dfile > summary { padding-inline: 0; border-inline: 0; border-radius: 0; } | |||
| 19 | + | #diffs[data-mr] :is(.dscroll, .code) { overflow-x: auto; overscroll-behavior-x: contain; padding-inline: 0; } | |||
| 20 | + | #diffs[data-mr] .dt .ln { width: calc(var(--lnw) + .5ch); min-width: calc(var(--lnw) + .5ch); padding-inline: .25ch; } | |||
| 21 | + | #diffs[data-mr] .dt .m { width: 1ch; } | |||
| 22 | + | #diffs[data-mr][data-diff] .uni { display: table; } | |||
| 23 | + | #diffs[data-mr][data-diff] .spl { display: none; } | |||
| 24 | + | #diffs[data-mr] .fnote td { padding-inline: 0; } | |||
| 25 | + | #diffs[data-mr] :is(.cform, .cmt) { width: min(65ch, calc(100vw - 5rem)); max-width: calc(100vw - 5rem); overflow-wrap: anywhere; } | |||
| 26 | + | body:has(#diffs[data-mr]) :is(input, textarea, select) { font-size: 16px; } | |||
| 27 | + | body:has(#diffs[data-mr]) .f-btns { flex-wrap: wrap; } | |||
| 28 | + | body:has(#diffs[data-mr]) .menu .menu-pop { | |||
| 29 | + | position: fixed; inset: auto 0 0; width: auto; max-width: 100%; | |||
| 30 | + | max-height: 75dvh; overflow-y: auto; border-radius: var(--r) var(--r) 0 0; | |||
| 31 | + | padding-bottom: max(var(--s4), env(safe-area-inset-bottom)); | |||
| 32 | + | } | |||
| 33 | + | body:has(#diffs[data-mr]) .mr-bar { position: static; } | |||
| 34 | + | /* A wrapping toolbar has no reliable fixed sticky height. */ | |||
| 35 | + | body:has(#diffs[data-mr]) { --bar-h: 0px; } | |||
| 36 | + | } | |||
| 37 | + | ||||
| 38 | + | @media (pointer: coarse), (hover: none) { | |||
| 39 | + | .mr-touch-range { display: flex; flex-wrap: wrap; align-items: center; gap: var(--s2); } | |||
| 40 | + | /* Real, disjoint cells: never expand invisible targets into other lines | |||
| 41 | + | or into the neighbouring old/new gutter. */ | |||
| 42 | + | #diffs[data-mr] .dt { line-height: max(var(--lh-code), 40px); } | |||
| 43 | + | #diffs[data-mr] .dt .ln { width: max(calc(var(--lnw) + .5ch), 40px); min-width: 40px; padding: 0; } | |||
| 44 | + | #diffs[data-mr] .spl .c-ln { width: max(calc(var(--lnw) + .5ch), 40px); } | |||
| 45 | + | #diffs[data-mr] .ln a { display: block; width: 100%; min-height: 40px; text-align: center; text-decoration: underline; } | |||
| 46 | + | .mr-touch-range[hidden] { display: none; } | |||
| 47 | + | #diffs[data-mr] .ln a[data-range-start] { outline: 2px solid var(--focus); background: var(--sel); } | |||
| 48 | + | body:has(#diffs[data-mr]) :is(.cform .btn, .menu .btn, .mr-touch-range .btn, .c-act button) { min-height: 40px; height: auto; padding-block: var(--s2); } | |||
| 49 | + | #diffs[data-mr] .cform { scroll-margin-block: calc(var(--hdr-h) + 12px); } | |||
| 50 | + | } | |||
| 51 | + | } | |||
M internal/adapters/web/static/js/ermite.js +1 -0
| @@ -183,6 +183,7 @@ | |||
| 183 | 183 | layout(mode) { | |
| 184 | 184 | const el = $('[data-diff]'); | |
| 185 | 185 | if (!el) return; | |
| 186 | + | if (el.hasAttribute('data-mr') && el.dataset.mobile === 'true') return; | |
| 186 | 187 | el.dataset.diff = mode; | |
| 187 | 188 | try { sessionStorage.setItem('diff', mode); } catch { /* storage refused: the layout holds for this page */ } | |
| 188 | 189 | for (const d of $$('[data-file]')) if ($('.dt', d)) d.open = true; | |
| @@ -183,6 +183,7 @@ | |||||
| 183 | layout(mode) { | 183 | layout(mode) { | ||
| 184 | const el = $('[data-diff]'); | 184 | const el = $('[data-diff]'); | ||
| 185 | if (!el) return; | 185 | if (!el) return; | ||
| 186 | + | if (el.hasAttribute('data-mr') && el.dataset.mobile === 'true') return; | |||
| 186 | el.dataset.diff = mode; | 187 | el.dataset.diff = mode; | ||
| 187 | try { sessionStorage.setItem('diff', mode); } catch { /* storage refused: the layout holds for this page */ } | 188 | try { sessionStorage.setItem('diff', mode); } catch { /* storage refused: the layout holds for this page */ } | ||
| 188 | for (const d of $$('[data-file]')) if ($('.dt', d)) d.open = true; | 189 | for (const d of $$('[data-file]')) if ($('.dt', d)) d.open = true; | ||
M internal/adapters/web/static/js/mr.js +79 -1
| @@ -27,6 +27,20 @@ window.ermiteMR = (() => { | |||
| 27 | 27 | b.searchParams.set('on', `${y[1]}:${nums[0]}-${nums[1]}:${y[3]}`); | |
| 28 | 28 | return b.href; | |
| 29 | 29 | }, | |
| 30 | + | // Range mode is explicit: ordinary taps still open single-line forms. | |
| 31 | + | touchRange(previous, current, base) { | |
| 32 | + | if (!previous) return { start: current, url: null }; | |
| 33 | + | const url = core.rangeURL(previous, current, base); | |
| 34 | + | if (url) return { start: null, url }; | |
| 35 | + | if (previous === current) return { start: null, url: current }; | |
| 36 | + | return { start: current, url: null }; // another file/side starts afresh | |
| 37 | + | }, | |
| 38 | + | mobileURL(href, view, narrow) { | |
| 39 | + | if (!narrow || view !== 'split') return null; | |
| 40 | + | const url = new URL(href); | |
| 41 | + | url.searchParams.set('view', 'unified'); | |
| 42 | + | return url.href; | |
| 43 | + | }, | |
| 30 | 44 | quote(button) { | |
| 31 | 45 | const thread = button.closest('.cmt').closest('[data-thread]'); | |
| 32 | 46 | const details = thread?.querySelector(':scope > details[data-reply]'); | |
| @@ -60,6 +74,7 @@ window.ermiteMR = (() => { | |||
| 60 | 74 | const $ = (sel, root = document) => root.querySelector(sel); | |
| 61 | 75 | const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | |
| 62 | 76 | const loading = new Map(); // src -> promise of the swap | |
| 77 | + | let refreshTouchComments = () => {}; | |
| 63 | 78 | let moved = false; | |
| 64 | 79 | let lazyIO = null; // the observer of the placeholders (watch) | |
| 65 | 80 | let revealGen = 0; // the last reveal wins the scroll | |
| @@ -108,6 +123,7 @@ window.ermiteMR = (() => { | |||
| 108 | 123 | window.htmx?.process(f); | |
| 109 | 124 | window.ermite?.watchFile?.(f, old); | |
| 110 | 125 | } | |
| 126 | + | refreshTouchComments(); | |
| 111 | 127 | } | |
| 112 | 128 | ||
| 113 | 129 | // stale: the diff changed under the page. The placeholders say so; no file | |
| @@ -130,7 +146,7 @@ window.ermiteMR = (() => { | |||
| 130 | 146 | // A page with both layouts says which shows in data-diff; a large diff | |
| 131 | 147 | // rendered in one layout only says it in data-view. | |
| 132 | 148 | const diffs = $('#diffs')?.dataset; | |
| 133 | - | if (/^c\d+$/.test(id) && (diffs?.diff || diffs?.view) === 'split') { | |
| 149 | + | if (/^c\d+$/.test(id) && !window.matchMedia?.('(max-width: 720px)').matches && (diffs?.diff || diffs?.view) === 'split') { | |
| 134 | 150 | const split = document.getElementById(id + '-s'); | |
| 135 | 151 | if (split) return split; | |
| 136 | 152 | } | |
| @@ -285,8 +301,70 @@ window.ermiteMR = (() => { | |||
| 285 | 301 | reveal(id); | |
| 286 | 302 | } | |
| 287 | 303 | ||
| 304 | + | function touchComments() { | |
| 305 | + | const diffs = $('#diffs'); | |
| 306 | + | const bar = document.createElement('div'); | |
| 307 | + | bar.className = 'mr-touch-range'; | |
| 308 | + | const button = document.createElement('button'); | |
| 309 | + | button.type = 'button'; | |
| 310 | + | button.className = 'btn'; | |
| 311 | + | button.textContent = 'Select line range'; | |
| 312 | + | button.setAttribute('aria-pressed', 'false'); | |
| 313 | + | const status = document.createElement('span'); | |
| 314 | + | status.setAttribute('role', 'status'); | |
| 315 | + | bar.append(button, status); | |
| 316 | + | diffs.prepend(bar); | |
| 317 | + | refreshTouchComments = () => { bar.hidden = !$('.ln a[href*="on="]', diffs); }; | |
| 318 | + | refreshTouchComments(); | |
| 319 | + | let enabled = false, start = null, marker = null; | |
| 320 | + | const clear = () => { marker?.removeAttribute('data-range-start'); marker = null; start = null; }; | |
| 321 | + | button.addEventListener('click', () => { | |
| 322 | + | enabled = !enabled; | |
| 323 | + | clear(); | |
| 324 | + | button.setAttribute('aria-pressed', String(enabled)); | |
| 325 | + | status.textContent = enabled ? 'Tap the first line number, then the last. Tap the same line for one line.' : ''; | |
| 326 | + | }); | |
| 327 | + | diffs.addEventListener('click', (e) => { | |
| 328 | + | const link = e.target.closest?.('.ln a[href*="on="]'); | |
| 329 | + | if (!enabled || !link || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return; | |
| 330 | + | e.preventDefault(); | |
| 331 | + | e.stopPropagation(); // do not alter the desktop shift-click anchor | |
| 332 | + | const next = core.touchRange(start, link.href, location.href); | |
| 333 | + | clear(); | |
| 334 | + | if (next.url) { location.assign(next.url); return; } | |
| 335 | + | start = next.start; | |
| 336 | + | marker = link; | |
| 337 | + | marker.setAttribute('data-range-start', ''); | |
| 338 | + | status.textContent = `Range starts at line ${link.textContent.trim()}. Tap the last line number (same file and side).`; | |
| 339 | + | }); | |
| 340 | + | // The server opens the line form. Scroll after layout without stealing | |
| 341 | + | // focus or forcing a mobile keyboard to appear. | |
| 342 | + | if (new URL(location.href).searchParams.has('on')) requestAnimationFrame(() => { | |
| 343 | + | const form = $$('.fnote .cform', diffs).find((f) => f.getClientRects().length); | |
| 344 | + | form?.scrollIntoView({ block: 'center' }); | |
| 345 | + | }); | |
| 346 | + | } | |
| 347 | + | ||
| 288 | 348 | function start() { | |
| 289 | 349 | if (!$('#diffs[data-mr]')) return; | |
| 350 | + | // Large diffs may contain only the split table. Ask the existing server | |
| 351 | + | // route for unified rather than writing the saved session preference. | |
| 352 | + | const narrow = window.matchMedia?.('(max-width: 720px)'); | |
| 353 | + | const unify = () => { | |
| 354 | + | $('#diffs').dataset.mobile = String(!!narrow?.matches); | |
| 355 | + | const url = core.mobileURL(location.href, $('#diffs')?.dataset.view, narrow?.matches); | |
| 356 | + | if (url) { location.replace(url); return true; } | |
| 357 | + | return false; | |
| 358 | + | }; | |
| 359 | + | if (unify()) return; | |
| 360 | + | narrow?.addEventListener('change', unify); | |
| 361 | + | addEventListener('click', (e) => { | |
| 362 | + | if (narrow?.matches && e.target.closest?.('.vbtn[data-view="split"]')) { | |
| 363 | + | e.preventDefault(); | |
| 364 | + | e.stopImmediatePropagation(); | |
| 365 | + | } | |
| 366 | + | }, true); | |
| 367 | + | touchComments(); | |
| 290 | 368 | let folded = false; | |
| 291 | 369 | try { folded = localStorage.getItem('mr-files-folded') === '1'; } catch { /* no storage: open */ } | |
| 292 | 370 | if (folded) foldFiles(true); | |
| @@ -27,6 +27,20 @@ window.ermiteMR = (() => { | |||||
| 27 | b.searchParams.set('on', `${y[1]}:${nums[0]}-${nums[1]}:${y[3]}`); | 27 | b.searchParams.set('on', `${y[1]}:${nums[0]}-${nums[1]}:${y[3]}`); | ||
| 28 | return b.href; | 28 | return b.href; | ||
| 29 | }, | 29 | }, | ||
| 30 | + | // Range mode is explicit: ordinary taps still open single-line forms. | |||
| 31 | + | touchRange(previous, current, base) { | |||
| 32 | + | if (!previous) return { start: current, url: null }; | |||
| 33 | + | const url = core.rangeURL(previous, current, base); | |||
| 34 | + | if (url) return { start: null, url }; | |||
| 35 | + | if (previous === current) return { start: null, url: current }; | |||
| 36 | + | return { start: current, url: null }; // another file/side starts afresh | |||
| 37 | + | }, | |||
| 38 | + | mobileURL(href, view, narrow) { | |||
| 39 | + | if (!narrow || view !== 'split') return null; | |||
| 40 | + | const url = new URL(href); | |||
| 41 | + | url.searchParams.set('view', 'unified'); | |||
| 42 | + | return url.href; | |||
| 43 | + | }, | |||
| 30 | quote(button) { | 44 | quote(button) { | ||
| 31 | const thread = button.closest('.cmt').closest('[data-thread]'); | 45 | const thread = button.closest('.cmt').closest('[data-thread]'); | ||
| 32 | const details = thread?.querySelector(':scope > details[data-reply]'); | 46 | const details = thread?.querySelector(':scope > details[data-reply]'); | ||
| @@ -60,6 +74,7 @@ window.ermiteMR = (() => { | |||||
| 60 | const $ = (sel, root = document) => root.querySelector(sel); | 74 | const $ = (sel, root = document) => root.querySelector(sel); | ||
| 61 | const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | 75 | const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; | ||
| 62 | const loading = new Map(); // src -> promise of the swap | 76 | const loading = new Map(); // src -> promise of the swap | ||
| 77 | + | let refreshTouchComments = () => {}; | |||
| 63 | let moved = false; | 78 | let moved = false; | ||
| 64 | let lazyIO = null; // the observer of the placeholders (watch) | 79 | let lazyIO = null; // the observer of the placeholders (watch) | ||
| 65 | let revealGen = 0; // the last reveal wins the scroll | 80 | let revealGen = 0; // the last reveal wins the scroll | ||
| @@ -108,6 +123,7 @@ window.ermiteMR = (() => { | |||||
| 108 | window.htmx?.process(f); | 123 | window.htmx?.process(f); | ||
| 109 | window.ermite?.watchFile?.(f, old); | 124 | window.ermite?.watchFile?.(f, old); | ||
| 110 | } | 125 | } | ||
| 126 | + | refreshTouchComments(); | |||
| 111 | } | 127 | } | ||
| 112 | 128 | ||||
| 113 | // stale: the diff changed under the page. The placeholders say so; no file | 129 | // stale: the diff changed under the page. The placeholders say so; no file | ||
| @@ -130,7 +146,7 @@ window.ermiteMR = (() => { | |||||
| 130 | // A page with both layouts says which shows in data-diff; a large diff | 146 | // A page with both layouts says which shows in data-diff; a large diff | ||
| 131 | // rendered in one layout only says it in data-view. | 147 | // rendered in one layout only says it in data-view. | ||
| 132 | const diffs = $('#diffs')?.dataset; | 148 | const diffs = $('#diffs')?.dataset; | ||
| 133 | - | if (/^c\d+$/.test(id) && (diffs?.diff || diffs?.view) === 'split') { | 149 | + | if (/^c\d+$/.test(id) && !window.matchMedia?.('(max-width: 720px)').matches && (diffs?.diff || diffs?.view) === 'split') { |
| 134 | const split = document.getElementById(id + '-s'); | 150 | const split = document.getElementById(id + '-s'); | ||
| 135 | if (split) return split; | 151 | if (split) return split; | ||
| 136 | } | 152 | } | ||
| @@ -285,8 +301,70 @@ window.ermiteMR = (() => { | |||||
| 285 | reveal(id); | 301 | reveal(id); | ||
| 286 | } | 302 | } | ||
| 287 | 303 | ||||
| 304 | + | function touchComments() { | |||
| 305 | + | const diffs = $('#diffs'); | |||
| 306 | + | const bar = document.createElement('div'); | |||
| 307 | + | bar.className = 'mr-touch-range'; | |||
| 308 | + | const button = document.createElement('button'); | |||
| 309 | + | button.type = 'button'; | |||
| 310 | + | button.className = 'btn'; | |||
| 311 | + | button.textContent = 'Select line range'; | |||
| 312 | + | button.setAttribute('aria-pressed', 'false'); | |||
| 313 | + | const status = document.createElement('span'); | |||
| 314 | + | status.setAttribute('role', 'status'); | |||
| 315 | + | bar.append(button, status); | |||
| 316 | + | diffs.prepend(bar); | |||
| 317 | + | refreshTouchComments = () => { bar.hidden = !$('.ln a[href*="on="]', diffs); }; | |||
| 318 | + | refreshTouchComments(); | |||
| 319 | + | let enabled = false, start = null, marker = null; | |||
| 320 | + | const clear = () => { marker?.removeAttribute('data-range-start'); marker = null; start = null; }; | |||
| 321 | + | button.addEventListener('click', () => { | |||
| 322 | + | enabled = !enabled; | |||
| 323 | + | clear(); | |||
| 324 | + | button.setAttribute('aria-pressed', String(enabled)); | |||
| 325 | + | status.textContent = enabled ? 'Tap the first line number, then the last. Tap the same line for one line.' : ''; | |||
| 326 | + | }); | |||
| 327 | + | diffs.addEventListener('click', (e) => { | |||
| 328 | + | const link = e.target.closest?.('.ln a[href*="on="]'); | |||
| 329 | + | if (!enabled || !link || e.shiftKey || e.ctrlKey || e.metaKey || e.altKey) return; | |||
| 330 | + | e.preventDefault(); | |||
| 331 | + | e.stopPropagation(); // do not alter the desktop shift-click anchor | |||
| 332 | + | const next = core.touchRange(start, link.href, location.href); | |||
| 333 | + | clear(); | |||
| 334 | + | if (next.url) { location.assign(next.url); return; } | |||
| 335 | + | start = next.start; | |||
| 336 | + | marker = link; | |||
| 337 | + | marker.setAttribute('data-range-start', ''); | |||
| 338 | + | status.textContent = `Range starts at line ${link.textContent.trim()}. Tap the last line number (same file and side).`; | |||
| 339 | + | }); | |||
| 340 | + | // The server opens the line form. Scroll after layout without stealing | |||
| 341 | + | // focus or forcing a mobile keyboard to appear. | |||
| 342 | + | if (new URL(location.href).searchParams.has('on')) requestAnimationFrame(() => { | |||
| 343 | + | const form = $$('.fnote .cform', diffs).find((f) => f.getClientRects().length); | |||
| 344 | + | form?.scrollIntoView({ block: 'center' }); | |||
| 345 | + | }); | |||
| 346 | + | } | |||
| 347 | + | ||||
| 288 | function start() { | 348 | function start() { | ||
| 289 | if (!$('#diffs[data-mr]')) return; | 349 | if (!$('#diffs[data-mr]')) return; | ||
| 350 | + | // Large diffs may contain only the split table. Ask the existing server | |||
| 351 | + | // route for unified rather than writing the saved session preference. | |||
| 352 | + | const narrow = window.matchMedia?.('(max-width: 720px)'); | |||
| 353 | + | const unify = () => { | |||
| 354 | + | $('#diffs').dataset.mobile = String(!!narrow?.matches); | |||
| 355 | + | const url = core.mobileURL(location.href, $('#diffs')?.dataset.view, narrow?.matches); | |||
| 356 | + | if (url) { location.replace(url); return true; } | |||
| 357 | + | return false; | |||
| 358 | + | }; | |||
| 359 | + | if (unify()) return; | |||
| 360 | + | narrow?.addEventListener('change', unify); | |||
| 361 | + | addEventListener('click', (e) => { | |||
| 362 | + | if (narrow?.matches && e.target.closest?.('.vbtn[data-view="split"]')) { | |||
| 363 | + | e.preventDefault(); | |||
| 364 | + | e.stopImmediatePropagation(); | |||
| 365 | + | } | |||
| 366 | + | }, true); | |||
| 367 | + | touchComments(); | |||
| 290 | let folded = false; | 368 | let folded = false; | ||
| 291 | try { folded = localStorage.getItem('mr-files-folded') === '1'; } catch { /* no storage: open */ } | 369 | try { folded = localStorage.getItem('mr-files-folded') === '1'; } catch { /* no storage: open */ } | ||
| 292 | if (folded) foldFiles(true); | 370 | if (folded) foldFiles(true); | ||
M internal/app/testdata/web/commit-big.html +3 -3
| @@ -127,12 +127,12 @@ | |||
| 127 | 127 | </span> | |
| 128 | 128 | <details class="menu vmenu"> | |
| 129 | 129 | <summary class="btn" aria-label="Diff view"> | |
| 130 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 130 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 131 | 131 | </summary> | |
| 132 | 132 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 133 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 133 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 134 | 134 | </a> | |
| 135 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 135 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 136 | 136 | </a> | |
| 137 | 137 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 138 | 138 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -127,12 +127,12 @@ | |||||
| 127 | </span> | 127 | </span> | ||
| 128 | <details class="menu vmenu"> | 128 | <details class="menu vmenu"> | ||
| 129 | <summary class="btn" aria-label="Diff view"> | 129 | <summary class="btn" aria-label="Diff view"> | ||
| 130 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | 130 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> |
| 131 | </summary> | 131 | </summary> | ||
| 132 | <div class="menu-pop" role="group" aria-label="Diff view"> | 132 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 133 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 133 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 134 | </a> | 134 | </a> | ||
| 135 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 135 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 136 | </a> | 136 | </a> | ||
| 137 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 137 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 138 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 138 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||
M internal/app/testdata/web/commit-binary.html +3 -3
| @@ -123,12 +123,12 @@ | |||
| 123 | 123 | </span> | |
| 124 | 124 | <details class="menu vmenu"> | |
| 125 | 125 | <summary class="btn" aria-label="Diff view"> | |
| 126 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 126 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 127 | 127 | </summary> | |
| 128 | 128 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 129 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 129 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 130 | 130 | </a> | |
| 131 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 131 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 132 | 132 | </a> | |
| 133 | 133 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 134 | 134 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -123,12 +123,12 @@ | |||||
| 123 | </span> | 123 | </span> | ||
| 124 | <details class="menu vmenu"> | 124 | <details class="menu vmenu"> | ||
| 125 | <summary class="btn" aria-label="Diff view"> | 125 | <summary class="btn" aria-label="Diff view"> | ||
| 126 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | 126 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> |
| 127 | </summary> | 127 | </summary> | ||
| 128 | <div class="menu-pop" role="group" aria-label="Diff view"> | 128 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 129 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 129 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 130 | </a> | 130 | </a> | ||
| 131 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 131 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 132 | </a> | 132 | </a> | ||
| 133 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 133 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 134 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 134 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||
M internal/app/testdata/web/commit-merge.html +3 -3
| @@ -122,12 +122,12 @@ | |||
| 122 | 122 | </span> | |
| 123 | 123 | <details class="menu vmenu"> | |
| 124 | 124 | <summary class="btn" aria-label="Diff view"> | |
| 125 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 125 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 126 | 126 | </summary> | |
| 127 | 127 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 128 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 128 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 129 | 129 | </a> | |
| 130 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 130 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 131 | 131 | </a> | |
| 132 | 132 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 133 | 133 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -122,12 +122,12 @@ | |||||
| 122 | </span> | 122 | </span> | ||
| 123 | <details class="menu vmenu"> | 123 | <details class="menu vmenu"> | ||
| 124 | <summary class="btn" aria-label="Diff view"> | 124 | <summary class="btn" aria-label="Diff view"> | ||
| 125 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | 125 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> |
| 126 | </summary> | 126 | </summary> | ||
| 127 | <div class="menu-pop" role="group" aria-label="Diff view"> | 127 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 128 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 128 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 129 | </a> | 129 | </a> | ||
| 130 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 130 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 131 | </a> | 131 | </a> | ||
| 132 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 132 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 133 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 133 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||
M internal/app/testdata/web/commit-modify-split.html +3 -3
| @@ -129,12 +129,12 @@ And <b>html</b> & "quotes".</pre> | |||
| 129 | 129 | </span> | |
| 130 | 130 | <details class="menu vmenu"> | |
| 131 | 131 | <summary class="btn" aria-label="Diff view"> | |
| 132 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Split</span> | |
| 132 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Split</span> | |
| 133 | 133 | </summary> | |
| 134 | 134 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 135 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 135 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 136 | 136 | </a> | |
| 137 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" aria-current="true" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 137 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" aria-current="true" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 138 | 138 | </a> | |
| 139 | 139 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 140 | 140 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -129,12 +129,12 @@ And <b>html</b> & "quotes".</pre> | |||||
| 129 | </span> | 129 | </span> | ||
| 130 | <details class="menu vmenu"> | 130 | <details class="menu vmenu"> | ||
| 131 | <summary class="btn" aria-label="Diff view"> | 131 | <summary class="btn" aria-label="Diff view"> | ||
| 132 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Split</span> | 132 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Split</span> |
| 133 | </summary> | 133 | </summary> | ||
| 134 | <div class="menu-pop" role="group" aria-label="Diff view"> | 134 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 135 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 135 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 136 | </a> | 136 | </a> | ||
| 137 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" aria-current="true" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 137 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" aria-current="true" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 138 | </a> | 138 | </a> | ||
| 139 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 139 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 140 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 140 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||
M internal/app/testdata/web/commit-modify.html +3 -3
| @@ -129,12 +129,12 @@ And <b>html</b> & "quotes".</pre> | |||
| 129 | 129 | </span> | |
| 130 | 130 | <details class="menu vmenu"> | |
| 131 | 131 | <summary class="btn" aria-label="Diff view"> | |
| 132 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 132 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 133 | 133 | </summary> | |
| 134 | 134 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 135 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 135 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 136 | 136 | </a> | |
| 137 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 137 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 138 | 138 | </a> | |
| 139 | 139 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 140 | 140 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -129,12 +129,12 @@ And <b>html</b> & "quotes".</pre> | |||||
| 129 | </span> | 129 | </span> | ||
| 130 | <details class="menu vmenu"> | 130 | <details class="menu vmenu"> | ||
| 131 | <summary class="btn" aria-label="Diff view"> | 131 | <summary class="btn" aria-label="Diff view"> | ||
| 132 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | 132 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> |
| 133 | </summary> | 133 | </summary> | ||
| 134 | <div class="menu-pop" role="group" aria-label="Diff view"> | 134 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 135 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 135 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 136 | </a> | 136 | </a> | ||
| 137 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 137 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 138 | </a> | 138 | </a> | ||
| 139 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 139 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 140 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 140 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||
M internal/app/testdata/web/commit-rename.html +3 -3
| @@ -123,12 +123,12 @@ | |||
| 123 | 123 | </span> | |
| 124 | 124 | <details class="menu vmenu"> | |
| 125 | 125 | <summary class="btn" aria-label="Diff view"> | |
| 126 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 126 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 127 | 127 | </summary> | |
| 128 | 128 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 129 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 129 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 130 | 130 | </a> | |
| 131 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 131 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 132 | 132 | </a> | |
| 133 | 133 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 134 | 134 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -123,12 +123,12 @@ | |||||
| 123 | </span> | 123 | </span> | ||
| 124 | <details class="menu vmenu"> | 124 | <details class="menu vmenu"> | ||
| 125 | <summary class="btn" aria-label="Diff view"> | 125 | <summary class="btn" aria-label="Diff view"> | ||
| 126 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | 126 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> |
| 127 | </summary> | 127 | </summary> | ||
| 128 | <div class="menu-pop" role="group" aria-label="Diff view"> | 128 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 129 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 129 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 130 | </a> | 130 | </a> | ||
| 131 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 131 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 132 | </a> | 132 | </a> | ||
| 133 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 133 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 134 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 134 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||
M internal/app/testdata/web/commit-root.html +3 -3
| @@ -121,12 +121,12 @@ | |||
| 121 | 121 | </span> | |
| 122 | 122 | <details class="menu vmenu"> | |
| 123 | 123 | <summary class="btn" aria-label="Diff view"> | |
| 124 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 124 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 125 | 125 | </summary> | |
| 126 | 126 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 127 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 127 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 128 | 128 | </a> | |
| 129 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 129 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 130 | 130 | </a> | |
| 131 | 131 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 132 | 132 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -121,12 +121,12 @@ | |||||
| 121 | </span> | 121 | </span> | ||
| 122 | <details class="menu vmenu"> | 122 | <details class="menu vmenu"> | ||
| 123 | <summary class="btn" aria-label="Diff view"> | 123 | <summary class="btn" aria-label="Diff view"> | ||
| 124 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | 124 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> |
| 125 | </summary> | 125 | </summary> | ||
| 126 | <div class="menu-pop" role="group" aria-label="Diff view"> | 126 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 127 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 127 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 128 | </a> | 128 | </a> | ||
| 129 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 129 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 130 | </a> | 130 | </a> | ||
| 131 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 131 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 132 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 132 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||
M internal/app/testdata/web/commit-trojan.html +3 -3
| @@ -124,12 +124,12 @@ | |||
| 124 | 124 | </span> | |
| 125 | 125 | <details class="menu vmenu"> | |
| 126 | 126 | <summary class="btn" aria-label="Diff view"> | |
| 127 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 127 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | |
| 128 | 128 | </summary> | |
| 129 | 129 | <div class="menu-pop" role="group" aria-label="Diff view"> | |
| 130 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 130 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | |
| 131 | 131 | </a> | |
| 132 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 132 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | |
| 133 | 133 | </a> | |
| 134 | 134 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | |
| 135 | 135 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | |
| @@ -124,12 +124,12 @@ | |||||
| 124 | </span> | 124 | </span> | ||
| 125 | <details class="menu vmenu"> | 125 | <details class="menu vmenu"> | ||
| 126 | <summary class="btn" aria-label="Diff view"> | 126 | <summary class="btn" aria-label="Diff view"> | ||
| 127 | - | <span hx-live="this.textContent = q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> | 127 | + | <span hx-live="this.textContent = q('#diffs').attr['data-mobile'] !== 'true' && q('#diffs').attr['data-diff'] === 'split' ? 'Split' : 'Unified'">Unified</span> |
| 128 | </summary> | 128 | </summary> | ||
| 129 | <div class="menu-pop" role="group" aria-label="Diff view"> | 129 | <div class="menu-pop" role="group" aria-label="Diff view"> | ||
| 130 | - | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> | 130 | + | <a href="/diffs/commit/SHA40?view=unified" class="btn vbtn" data-view="unified" aria-keyshortcuts="u" aria-current="true" data-fire="unified" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Unified<kbd aria-hidden="true">u</kbd> |
| 131 | </a> | 131 | </a> | ||
| 132 | - | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" data-fire="split" :aria-current="q('#diffs').attr['data-diff'] === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> | 132 | + | <a href="/diffs/commit/SHA40?view=split" class="btn vbtn" data-view="split" aria-keyshortcuts="d" :aria-disabled="q('#diffs').attr['data-mobile'] === 'true' ? 'true' : false" :title="q('#diffs').attr['data-mobile'] === 'true' ? 'Split view requires a wider screen' : 'Split view'" data-fire="split" :aria-current="(q('#diffs').attr['data-mobile'] === 'true' ? 'unified' : q('#diffs').attr['data-diff']) === this.dataset.view ? 'true' : false">Split<kbd aria-hidden="true">d</kbd> |
| 133 | </a> | 133 | </a> | ||
| 134 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | 134 | <button type="button" aria-keyshortcuts="x" data-js data-fire="collapse" class="btn"> | ||
| 135 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | 135 | <span hx-live="this.textContent = q('[data-file][open]').count || !q('[data-file]').count ? 'Collapse all' : 'Expand all'">Collapse all</span> | ||