feat: add toast notification system

Co-Authored-By: Claude Opus 4.6 <[email protected]>
This commit is contained in:
Alex-GilbertandClaude Opus 4.6 committed 2026-02-16 17:59:40 -06:00
1 parent fe9eb289ac
commit 3d739b13f5
3 files changed
+97 -71

No files matched your search

+54 -19
View File
@@ -19,8 +19,6 @@ const rightPanel = document.getElementById("right-panel");
const docContainer = document.getElementById("doc-container");
const popover = document.getElementById("popover");
const popoverClose = document.getElementById("popover-close");
const popoverContext = document.getElementById("popover-context");
const popoverFontInfo = document.getElementById("popover-font-info");
const solveCharset = document.getElementById("solve-charset");
const solveTolerance = document.getElementById("solve-tolerance");
const solveTolValue = document.getElementById("solve-tol-value");
@@ -53,6 +51,15 @@ const leftTextInput = document.getElementById("left-text-input");
const rightTextInput = document.getElementById("right-text-input");
const redactionMarker = document.getElementById("redaction-marker");
const textReset = document.getElementById("text-reset");
const toastContainer = document.getElementById("toast-container");
function showToast(message, type = "info") {
const el = document.createElement("div");
el.className = `toast ${type}`;
el.textContent = message;
toastContainer.appendChild(el);
setTimeout(() => el.remove(), 3000);
}
// ── State ──
@@ -369,22 +376,6 @@ function openPopover(id) {
const r = state.redactions[id];
if (!r || !r.analysis) return;
const a = r.analysis;
// Context line
const segs = a.segments;
const leftText = segs.length > 0 ? segs[0].text : "";
const rightText = segs.length > 1 ? segs[1].text : "";
popoverContext.innerHTML = `<span class="ctx-left">${escapeHtml(leftText)}</span><span class="ctx-gap">\u2588\u2588\u2588</span><span class="ctx-right">${escapeHtml(rightText)}</span>`;
// Font info
const font = a.font;
popoverFontInfo.innerHTML = `
<span>${escapeHtml(font.name)} ${font.size}px</span>
<span class="font-score">score: ${font.score.toFixed(1)}</span>
<span class="gap-width">gap: ${Math.round(a.gap.w)}px</span>
`;
// Pre-fill prefix/suffix from context
solveFilterPrefix.value = "";
solveFilterSuffix.value = "";
@@ -1319,7 +1310,7 @@ function showAssocDetail(assocMatches, anchorEl) {
let drawDrag = null;
canvas.addEventListener("mousedown", (e) => {
if (!e.shiftKey || e.button !== 0) return;
if (!e.shiftKey || e.ctrlKey || e.button !== 0) return;
const rect = rightPanel.getBoundingClientRect();
const sx = e.clientX - rect.left;
@@ -1389,6 +1380,50 @@ window.addEventListener("mouseup", (e) => {
activateRedaction(id);
});
// ── Ctrl+drag offset / Ctrl+Shift+drag gap width ──
let ctrlDrag = null;
canvas.addEventListener("mousedown", (e) => {
if (!e.ctrlKey || e.button !== 0) return;
const r = state.redactions[state.activeRedaction];
if (!r?.overrides) return;
ctrlDrag = {
startX: e.clientX,
startY: e.clientY,
startOffsetX: r.overrides.offsetX,
startOffsetY: r.overrides.offsetY,
startGapWidth: r.overrides.gapWidth,
widthMode: e.shiftKey,
};
e.stopPropagation();
e.preventDefault();
}, { capture: true });
window.addEventListener("mousemove", (e) => {
if (!ctrlDrag) return;
const r = state.redactions[state.activeRedaction];
if (!r?.overrides) return;
const dx = (e.clientX - ctrlDrag.startX) / state.zoom;
const dy = (e.clientY - ctrlDrag.startY) / state.zoom;
if (ctrlDrag.widthMode) {
r.overrides.gapWidth = Math.max(1, ctrlDrag.startGapWidth + dx);
gapValue.textContent = Math.round(r.overrides.gapWidth);
} else {
r.overrides.offsetX = ctrlDrag.startOffsetX + dx;
r.overrides.offsetY = ctrlDrag.startOffsetY + dy;
updatePosDisplay();
}
renderCanvas();
});
window.addEventListener("mouseup", () => {
if (ctrlDrag) ctrlDrag = null;
});
// ── Utility ──
function escapeHtml(text) {
+1 -2
View File
@@ -78,8 +78,6 @@
<span class="popover-title">Analyze Redaction</span>
<button id="popover-close" class="popover-close-btn">X</button>
</div>
<div id="popover-context" class="popover-context"></div>
<div id="popover-font-info" class="popover-font-info"></div>
<div class="solve-controls">
<label>
Charset
@@ -148,6 +146,7 @@
</section>
</main>
<div id="toast-container"></div>
<script src="/static/app.js"></script>
</body>
</html>
+42 -50
View File
@@ -235,7 +235,7 @@ main {
position: absolute;
top: 12px;
right: 12px;
bottom: 12px;
bottom: 3rem;
width: 320px;
display: flex;
flex-direction: column;
@@ -244,7 +244,7 @@ main {
border-radius: 8px;
padding: 12px;
z-index: 30;
overflow: hidden;
overflow-y: auto;
backdrop-filter: blur(10px);
}
@@ -276,54 +276,6 @@ main {
justify-content: center;
}
.popover-context {
padding: 6px 8px;
background: rgba(255, 255, 255, 0.04);
border-radius: 4px;
margin-bottom: 8px;
font-family: monospace;
font-size: 0.8rem;
line-height: 1.4;
white-space: nowrap;
overflow-x: auto;
flex-shrink: 0;
}
.ctx-left {
color: #aaa;
}
.ctx-gap {
color: #d32f2f;
margin: 0 2px;
}
.ctx-right {
color: #aaa;
}
.popover-font-info {
display: flex;
align-items: center;
gap: 10px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.04);
border-radius: 4px;
margin-bottom: 10px;
font-size: 0.75rem;
color: #aaa;
flex-shrink: 0;
}
.font-score {
color: #666;
}
.gap-width {
color: #42a5f5;
font-weight: 600;
}
/* Solve controls (shared between old and new) */
.solve-controls {
display: flex;
@@ -686,3 +638,43 @@ main {
background: #b8860b;
color: #ffe082;
}
/* Toast notifications */
#toast-container {
position: fixed;
bottom: 2rem;
left: 50%;
transform: translateX(-50%);
z-index: 100;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.5rem;
pointer-events: none;
}
.toast {
background: rgba(30, 30, 50, 0.95);
color: #e0e0e0;
padding: 0.6rem 1.2rem;
border-radius: 6px;
border: 1px solid #333;
font-size: 0.85rem;
backdrop-filter: blur(6px);
animation: toast-in 0.2s ease-out, toast-out 0.3s ease-in 2.5s forwards;
}
.toast.error {
border-color: #d32f2f;
color: #ef9a9a;
}
@keyframes toast-in {
from { opacity: 0; transform: translateY(10px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes toast-out {
from { opacity: 1; }
to { opacity: 0; }
}