1 Commits
Author SHA1 Message Date
fen 7cc6a9c706 Add line wrap toggle for editor and paste viewer (#130)
CI / docker (pull_request) Skipped
CI / test (pull_request) Successful in 27s
Shared client-side preference in localStorage 'palette-wrap', default
off. Toggle button on /new editor head and paste title bar, plus a Line
wrap checkbox in the settings menu; all read/write the same preference.
Active state uses the accent treatment like other toggles. No em dashes
in UI text.
2026-09-09 22:35:49 -05:00
5 changed files with 40 additions and 59 deletions
+5 -5
View File
@@ -237,6 +237,11 @@ body {
.theme-card .swatch { width: 26px; height: 26px; border-radius: 6px; border: 1px solid rgba(255,255,255,.15); } .theme-card .swatch { width: 26px; height: 26px; border-radius: 6px; border: 1px solid rgba(255,255,255,.15); }
.theme-name { font-weight: 600; } .theme-name { font-weight: 600; }
.iconbtn:hover { color: var(--fg); border-color: var(--muted); } .iconbtn:hover { color: var(--fg); border-color: var(--muted); }
/* #130: line wrap toggle, accent when active like other toggles */
.iconbtn.wrap-toggle[aria-pressed="true"],
.btn-icon.wrap-toggle[aria-pressed="true"] { background: var(--accent); color: var(--bg); border-color: var(--accent); }
html[data-wrap] .codebody { white-space: pre-wrap; }
html[data-wrap] .editor { white-space: pre-wrap; }
.iconbtn.danger:hover { color: #ff8fa3; border-color: #ff8fa3; } .iconbtn.danger:hover { color: #ff8fa3; border-color: #ff8fa3; }
.code-head { .code-head {
display: flex; align-items: center; gap: 10px; padding: 8px 16px; display: flex; align-items: center; gap: 10px; padding: 8px 16px;
@@ -691,8 +696,3 @@ button[type="submit"]:focus-visible,
.attachment-chip:hover { border-color: var(--accent); } .attachment-chip:hover { border-color: var(--accent); }
.attachment-chip .attachment-size { color: var(--muted-fg); font-size: 19px; } .attachment-chip .attachment-size { color: var(--muted-fg); font-size: 19px; }
.attachment-preview img { max-width: 480px; max-height: 360px; border-radius: var(--radius); border: 1px solid var(--border); } .attachment-preview img { max-width: 480px; max-height: 360px; border-radius: var(--radius); border: 1px solid var(--border); }
/* line wrap toggle (#130): body wrap variants + toggle button */
.editor.wrap, .codebody.wrap { white-space: pre-wrap; word-break: break-word; }
.wrap-toggle[aria-pressed="true"] { background: var(--accent); color: var(--bg); border-color: var(--accent); }
.editor-tools { display: flex; justify-content: flex-end; }
+31
View File
@@ -46,6 +46,37 @@
// do not persist anything here: URL theme is a one-off override // do not persist anything here: URL theme is a one-off override
document.documentElement.dataset.preset = dark ? PAIRS[t].dark : PAIRS[t].light; document.documentElement.dataset.preset = dark ? PAIRS[t].dark : PAIRS[t].light;
})(); })();
// #130: shared line wrap preference (localStorage 'palette-wrap', default off)
(function () {
function wrapOn() {
try { return localStorage.getItem('palette-wrap') === '1'; } catch (e) { return false; }
}
function wrapApply(on) {
if (on) document.documentElement.setAttribute('data-wrap', '1');
else document.documentElement.removeAttribute('data-wrap');
document.querySelectorAll('.wrap-toggle').forEach(function (b) {
b.setAttribute('aria-pressed', on ? 'true' : 'false');
});
var s = document.getElementById('wrap-setting');
if (s) s.checked = on;
}
window.paletteWrapToggle = function () {
var on = !wrapOn();
try { localStorage.setItem('palette-wrap', on ? '1' : '0'); } catch (e) {}
wrapApply(on);
return on;
};
document.addEventListener('DOMContentLoaded', function () {
wrapApply(wrapOn());
document.querySelectorAll('.wrap-toggle').forEach(function (b) {
b.addEventListener('click', function () { window.paletteWrapToggle(); });
});
var s = document.getElementById('wrap-setting');
if (s) s.addEventListener('change', function () {
if (s.checked !== wrapOn()) window.paletteWrapToggle();
});
});
})();
</script> </script>
{{end}} {{end}}
+1 -23
View File
@@ -16,15 +16,13 @@
<option>markdown</option><option>text</option> <option>markdown</option><option>text</option>
</select> </select>
<button class="btn btn-icon" id="reguess" title="Re-detect language" type="button"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><polyline points="21 3 21 9 15 9"/></svg></button> <button class="btn btn-icon" id="reguess" title="Re-detect language" type="button"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-2.64-6.36"/><polyline points="21 3 21 9 15 9"/></svg></button>
<button type="button" class="btn btn-icon wrap-toggle" id="wrap-toggle" title="Toggle line wrap" aria-pressed="false">wrap</button>
</div> </div>
</div> </div>
<div class="float editor-wrap"> <div class="float editor-wrap">
<div class="gutter" id="gutter">1</div> <div class="gutter" id="gutter">1</div>
<textarea class="editor" id="content" placeholder="Paste your code, text, or notes here…" spellcheck="false"></textarea> <textarea class="editor" id="content" placeholder="Paste your code, text, or notes here…" spellcheck="false"></textarea>
</div> </div>
<div class="editor-tools">
<button class="iconbtn wrap-toggle" id="wrap-toggle" type="button" title="Toggle line wrap" aria-label="Toggle line wrap" aria-pressed="false">wrap</button>
</div>
<div class="created-banner" id="created"></div> <div class="created-banner" id="created"></div>
<div class="actionbar"> <div class="actionbar">
<span class="hint">Ctrl+Enter to create</span> <span class="hint">Ctrl+Enter to create</span>
@@ -294,25 +292,5 @@ window.addEventListener('pageshow', e => {
document.addEventListener('keydown', e => { document.addEventListener('keydown', e => {
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); create(); } if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') { e.preventDefault(); create(); }
}); });
// #130: line wrap toggle, shared preference with the viewer (palette-wrap)
(function () {
var el = document.getElementById('content');
var btn = document.getElementById('wrap-toggle');
function apply(on) {
el.classList.toggle('wrap', on);
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
}
var on = false;
try { on = localStorage.getItem('palette-wrap') === 'true'; } catch (e) {}
apply(on);
btn.addEventListener('click', function () {
on = !on;
try { localStorage.setItem('palette-wrap', on ? 'true' : 'false'); } catch (e) {}
apply(on);
});
// follow changes made in settings on other tabs/pages
document.addEventListener('palette-wrapchange', function (e) { apply(e.detail); });
})();
</script> </script>
{{template "foot" .}} {{template "foot" .}}
+1 -20
View File
@@ -6,7 +6,7 @@
<h1>{{if .Title}}{{.Title}}{{else}}Untitled paste{{end}}</h1> <h1>{{if .Title}}{{.Title}}{{else}}Untitled paste{{end}}</h1>
{{if .CustomSlug}}<span class="slug">/{{.CustomSlug}}</span>{{end}} {{if .CustomSlug}}<span class="slug">/{{.CustomSlug}}</span>{{end}}
<div class="spacer"></div> <div class="spacer"></div>
<button class="iconbtn wrap-toggle" id="wrap-toggle" type="button" title="Toggle line wrap" aria-label="Toggle line wrap" aria-pressed="false">wrap</button> <button type="button" class="iconbtn wrap-toggle" title="Toggle line wrap" aria-pressed="false">wrap</button>
<a class="iconbtn" href="/raw/{{.ID}}">raw</a> <a class="iconbtn" href="/raw/{{.ID}}">raw</a>
<a class="iconbtn" href="#" id="copy-btn" onclick="copyContent(this); return false;">copy</a> <a class="iconbtn" href="#" id="copy-btn" onclick="copyContent(this); return false;">copy</a>
{{if .DeletionToken}}<a class="iconbtn danger" href="#" onclick="redeem('{{.DeletionToken}}'); return false;">delete</a>{{end}} {{if .DeletionToken}}<a class="iconbtn danger" href="#" onclick="redeem('{{.DeletionToken}}'); return false;">delete</a>{{end}}
@@ -81,24 +81,5 @@ function redeem(token) {
fetch('/api/pastes/{{.ID}}/redeem?token=' + encodeURIComponent(token), {method: 'DELETE'}) fetch('/api/pastes/{{.ID}}/redeem?token=' + encodeURIComponent(token), {method: 'DELETE'})
.then(r => { if (r.ok) location.href = '/history'; else alert('delete failed'); }); .then(r => { if (r.ok) location.href = '/history'; else alert('delete failed'); });
} }
// #130: line wrap toggle, shared preference with the editor (palette-wrap)
(function () {
var el = document.getElementById('codebody');
var btn = document.getElementById('wrap-toggle');
function apply(on) {
el.classList.toggle('wrap', on);
btn.setAttribute('aria-pressed', on ? 'true' : 'false');
}
var on = false;
try { on = localStorage.getItem('palette-wrap') === 'true'; } catch (e) {}
apply(on);
btn.addEventListener('click', function () {
on = !on;
try { localStorage.setItem('palette-wrap', on ? 'true' : 'false'); } catch (e) {}
apply(on);
});
document.addEventListener('palette-wrapchange', function (e) { apply(e.detail); });
})();
</script> </script>
{{template "foot" .}} {{template "foot" .}}
+2 -11
View File
@@ -13,8 +13,8 @@
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg> <svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"/></svg>
<span>Dark mode</span> <span>Dark mode</span>
</button> </button>
<h3>Line wrap</h3> <h3 style="margin-top:18px">Editor</h3>
<label class="toggle"><input type="checkbox" id="settings-wrap-toggle"> Wrap lines in the editor and paste view</label> <label class="toggle" for="wrap-setting" style="display:inline-flex"><input type="checkbox" id="wrap-setting"> Line wrap</label>
<p class="admin-link-row"><a class="admin-link" href="/admin">Admin</a></p> <p class="admin-link-row"><a class="admin-link" href="/admin">Admin</a></p>
</div> </div>
</div> </div>
@@ -97,15 +97,6 @@
if (btns.length) btns[0].click(); else document.dispatchEvent(new CustomEvent('palette-darkchange')); if (btns.length) btns[0].click(); else document.dispatchEvent(new CustomEvent('palette-darkchange'));
}); });
document.addEventListener('palette-darkchange', sync); document.addEventListener('palette-darkchange', sync);
// #130: line wrap setting shares the palette-wrap preference with the
// editor and paste view toggles.
var wt = document.getElementById('settings-wrap-toggle');
try { wt.checked = localStorage.getItem('palette-wrap') === 'true'; } catch (e) {}
wt.addEventListener('change', function () {
var on = wt.checked;
try { localStorage.setItem('palette-wrap', on ? 'true' : 'false'); } catch (e) {}
document.dispatchEvent(new CustomEvent('palette-wrapchange', { detail: on }));
});
})(); })();
</script> </script>
{{template "foot" .}} {{template "foot" .}}