diff options
| author | Adam Soltys <[email protected]> | 2026-09-05 19:46:48 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-09-05 19:46:48 -0700 |
| commit | c8c18d838573859b61c47875a4320b9ad0ad7514 (patch) | |
| tree | bd0e28438a2fa6bb52dffcd55cc8490b0ad0f8b8 /src | |
| parent | c99efea0ab7a724221d439bbc227058223dfe3c4 (diff) | |
| download | coinosv3-c8c18d838573859b61c47875a4320b9ad0ad7514.tar.gz coinosv3-c8c18d838573859b61c47875a4320b9ad0ad7514.zip | |
morph: leave <details open> to the user; key the names settings sections
Every background repaint stripped the browser-set `open` attribute (the
fresh render never carries it), collapsing expanded sections, and the
positional child match handed one section's open state to its neighbour
whenever the conditional zap-code section came or went. `open` on a
details is now user-owned unless a render sets it, and nodes carrying
data-key only patch into the same key.
Co-Authored-By: Claude Fable 5.1 <[email protected]>
Claude-Session: https://claude.ai/code/session_01VMi2Y1oKPZ3TeeFmLej47y
Diffstat (limited to 'src')
| -rw-r--r-- | src/app.js | 14 | ||||
| -rw-r--r-- | src/features/names.js | 12 |
2 files changed, 19 insertions, 7 deletions
@@ -180,8 +180,20 @@ function morph(a, b) { // would visibly crossfade the old style into the new one (a dark filled // button melting into a text link read as a black flash). Repaints aren't // style animations: snap it, restore the transition after the paint. + // A node with a data-key is only ever patched into a node with the SAME + // key: siblings line up by position, so a conditional sibling appearing or + // vanishing would otherwise hand one section's live state (an expanded + // <details>) to its neighbour. + if (a.getAttribute('data-key') !== b.getAttribute('data-key')) { + a.replaceWith(b); + return; + } const snap = a.nodeName === 'BUTTON' && a.getAttribute('class') !== b.getAttribute('class'); - for (const at of [...a.attributes]) if (!b.hasAttribute(at.name)) a.removeAttribute(at.name); + // <details open> is the user's doing (the browser toggles the attribute on + // click), like a field's value: a render that doesn't set `open` leaves it + // alone instead of collapsing the section on every background repaint. + const userOwned = (at) => a.nodeName === 'DETAILS' && at.name === 'open'; + for (const at of [...a.attributes]) if (!b.hasAttribute(at.name) && !userOwned(at)) a.removeAttribute(at.name); for (const at of [...b.attributes]) if (a.getAttribute(at.name) !== at.value) a.setAttribute(at.name, at.value); if (snap) { a.style.transition = 'none'; diff --git a/src/features/names.js b/src/features/names.js index fa5bda1..06bb678 100644 --- a/src/features/names.js +++ b/src/features/names.js @@ -425,27 +425,27 @@ export function namesFeature(ctx) { h('button', { class: 'btn-ghost btn-sm', onClick: async () => { await release(); toast(t('namesReleased')); render(); } }, t('namesRelease'))), - h('details', { class: 'small faint' }, + h('details', { class: 'small faint', 'data-key': 'custom' }, h('summary', {}, t('namesCustom')), h('p', { style: 'margin:4px 0' }, t('namesCustomHow')), claimForm(false)), (() => { const code = hook('nwcOfferString'); - return code ? h('details', { class: 'small faint' }, + return code ? h('details', { class: 'small faint', 'data-key': 'zapcode' }, h('summary', {}, t('namesZapCode')), h('p', { style: 'margin:4px 0' }, t('namesZapCodeHow')), h('div', { class: 'addr-box break', style: 'font-size:10px' }, code), copyBtn(code, t('namesZapCodeCopy'))) : null; })(), - h('details', { class: 'small faint' }, + h('details', { class: 'small faint', 'data-key': 'pos' }, h('summary', {}, t('namesPos')), h('p', { style: 'margin:4px 0' }, t('namesPosHow')), posSection(st)), - h('details', { class: 'small faint' }, + h('details', { class: 'small faint', 'data-key': 'ownnode' }, h('summary', {}, t('namesOwnNode')), h('p', { style: 'margin:4px 0' }, t('namesOwnNodeMenuHow')), ownNodeSection()), - h('details', { class: 'small faint' }, + h('details', { class: 'small faint', 'data-key': 'owndomain' }, h('summary', {}, t('namesOwnDomain')), h('p', { style: 'margin:4px 0' }, t('namesOwnDomainHow')), h('div', { class: 'addr-box break', style: 'font-size:11px' }, @@ -483,7 +483,7 @@ export function namesFeature(ctx) { try { await post('/pos/token', 'DELETE', body()); ui.posToken = null; toast(t('namesPosRevoked')); render(); } catch (e) { toast(e.message); } } }, t('namesPosRevoke'))), - h('details', { class: 'small faint', style: 'margin-top:6px' }, + h('details', { class: 'small faint', style: 'margin-top:6px', 'data-key': 'poslink' }, h('summary', {}, t('namesPosLink')), h('p', { style: 'margin:4px 0' }, t('namesPosLinkHow')), h('div', { style: 'align-self:center;max-width:220px', html: qrSvg(lnurl, { ec: 'L', mode: 'Alphanumeric' }) }), |
