annotation system
This commit is contained in:
parent
1210314cc8
commit
b38e9359d5
5
spec.md
5
spec.md
|
|
@ -412,7 +412,7 @@ levineuwirth.org/
|
||||||
- [x] Selection popup — `selection-popup.js` / `selection-popup.css`; context-aware toolbar appears 450 ms after text selection; see Implementation Notes
|
- [x] Selection popup — `selection-popup.js` / `selection-popup.css`; context-aware toolbar appears 450 ms after text selection; see Implementation Notes
|
||||||
- [x] Print stylesheet — `print.css` (media="print"); single-column, light colors, sidenotes as indented blocks, external URLs shown
|
- [x] Print stylesheet — `print.css` (media="print"); single-column, light colors, sidenotes as indented blocks, external URLs shown
|
||||||
- [x] Current page (`/current.html`) — now-page; added to primary nav
|
- [x] Current page (`/current.html`) — now-page; added to primary nav
|
||||||
- [~] Annotations — `annotations.js` / `annotations.css`; localStorage infrastructure + highlight re-anchoring written; UI (button in selection popup) deferred
|
- [x] Annotations — `annotations.js` / `annotations.css`; localStorage storage, text re-anchoring, highlight marks, tooltip with delete; color-picker UI in selection popup (four swatches + optional note field)
|
||||||
|
|
||||||
### Phase 4: Creative Content & Polish
|
### Phase 4: Creative Content & Polish
|
||||||
- [x] Image handling (lazy load, lightbox, figures, WebP `<picture>` wrapper for local raster images)
|
- [x] Image handling (lazy load, lightbox, figures, WebP `<picture>` wrapper for local raster images)
|
||||||
|
|
@ -432,9 +432,10 @@ levineuwirth.org/
|
||||||
- [ ] **Link archiving** — For all external links in `data/bibliography.bib` and in page bodies, check availability and save snapshots (Wayback Machine `save` API or local archivebox instance). Store archive URLs in `data/link-archive.json`; `Filters.Links` injects `data-archive-url` attributes; `popups.js` falls back to the archive if the live URL returns 404.
|
- [ ] **Link archiving** — For all external links in `data/bibliography.bib` and in page bodies, check availability and save snapshots (Wayback Machine `save` API or local archivebox instance). Store archive URLs in `data/link-archive.json`; `Filters.Links` injects `data-archive-url` attributes; `popups.js` falls back to the archive if the live URL returns 404.
|
||||||
- [ ] **Self-hosted git (Forgejo)** — Run Forgejo on the VPS. Mirror the build repo. Link from the colophon. Not essential; can remain on GitHub indefinitely.
|
- [ ] **Self-hosted git (Forgejo)** — Run Forgejo on the VPS. Mirror the build repo. Link from the colophon. Not essential; can remain on GitHub indefinitely.
|
||||||
- [ ] **Reader mode** — Distraction-free reading overlay: hides nav, TOC, sidenotes; widens the body column to ~70ch; activated via a keyboard shortcut or settings panel toggle. Distinct from focus mode (which affects the nav) — reader mode affects the content layout.
|
- [ ] **Reader mode** — Distraction-free reading overlay: hides nav, TOC, sidenotes; widens the body column to ~70ch; activated via a keyboard shortcut or settings panel toggle. Distinct from focus mode (which affects the nav) — reader mode affects the content layout.
|
||||||
|
- [ ] **HTTP/3 + QUIC** — nginx 1.25+ supports HTTP/3 via `listen 443 quic reuseport` + `http3 on` + `Alt-Svc` header. Requires UDP 443 open in Hetzner's Cloud Firewall. Deferred: latency is currently geographic RTT, not server processing; gains would be modest for a static site from a single DC. CDN alternatives (Bunny.net, multi-region Hetzner with GeoDNS) would address the root cause but raise ethical or operational complexity concerns. Revisit if latency becomes a real user complaint. Server-side improvements (brotli pre-compression, `open_file_cache`) are a lower-cost step first.
|
||||||
|
|
||||||
### Phase 6: Deferred Features
|
### Phase 6: Deferred Features
|
||||||
- [ ] **Annotation UI** — The `annotations.js` / `annotations.css` infrastructure exists (localStorage storage, re-anchoring on load, four highlight colors, hover tooltip). The selection popup "Annotate" button was removed pending a design decision on the color-picker and note-entry UX. Revisit: a popover with four color swatches and an optional text field, triggered from the selection popup.
|
- [x] **Annotation UI** — `annotations.js` / `annotations.css`: localStorage storage, text-stream re-anchoring, four highlight colors (amber/sage/steel/rose), hover tooltip with delete. Selection popup "Annotate" button triggers a color-swatch + optional note picker; Enter or "Highlight" button commits; Escape cancels. Picker positioned above the selection, same inverted style as the tooltip. Settings panel includes a "Clear Annotations" button (with confirmation) that wipes all annotations site-wide via `Annotations.clearAll()`.
|
||||||
- [~] **Visualization pipeline** — Implemented as a Pandoc IO filter (`Filters.Viz`), not a per-slug Hakyll rule. See Phase 4 entry and Implementation Notes. Infrastructure complete; production content pending.
|
- [~] **Visualization pipeline** — Implemented as a Pandoc IO filter (`Filters.Viz`), not a per-slug Hakyll rule. See Phase 4 entry and Implementation Notes. Infrastructure complete; production content pending.
|
||||||
- [x] **Music catalog page** — `/music/` index listing all compositions grouped by instrumentation category (orchestral → chamber → solo → vocal → choral → electronic → other), with an optional Featured section. Auto-generated from composition frontmatter by `build/Catalog.hs`; renders HTML in Haskell (same pattern as backlinks). Category, year, duration, instrumentation, and ◼/♫ indicators for score/recording availability. `content/music/index.md` provides prose intro + abstract. Template: `templates/music-catalog.html`. CSS: `static/css/catalog.css`. Context: `musicCatalogCtx` (provides `catalog: true` flag, `featured-works`, `has-featured`, `catalog-by-category`).
|
- [x] **Music catalog page** — `/music/` index listing all compositions grouped by instrumentation category (orchestral → chamber → solo → vocal → choral → electronic → other), with an optional Featured section. Auto-generated from composition frontmatter by `build/Catalog.hs`; renders HTML in Haskell (same pattern as backlinks). Category, year, duration, instrumentation, and ◼/♫ indicators for score/recording availability. `content/music/index.md` provides prose intro + abstract. Template: `templates/music-catalog.html`. CSS: `static/css/catalog.css`. Context: `musicCatalogCtx` (provides `catalog: true` flag, `featured-works`, `has-featured`, `catalog-by-category`).
|
||||||
- [x] **Score reader swipe gestures** — `touchstart`/`touchend` listeners on `#score-reader-stage` with passive: true. Threshold: ≥ 50 px horizontal, < 30 px vertical drift. Left swipe → next page; right swipe → previous page.
|
- [x] **Score reader swipe gestures** — `touchstart`/`touchend` listeners on `#score-reader-stage` with passive: true. Threshold: ≥ 50 px horizontal, < 30 px vertical drift. Left swipe → next page; right swipe → previous page.
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,14 @@ mark.user-annotation:hover { filter: brightness(0.80); }
|
||||||
[data-theme="dark"] mark.user-annotation.user-annotation--steel { background-color: rgba(112, 150, 184, 0.42); }
|
[data-theme="dark"] mark.user-annotation.user-annotation--steel { background-color: rgba(112, 150, 184, 0.42); }
|
||||||
[data-theme="dark"] mark.user-annotation.user-annotation--rose { background-color: rgba(200, 116, 116, 0.42); }
|
[data-theme="dark"] mark.user-annotation.user-annotation--rose { background-color: rgba(200, 116, 116, 0.42); }
|
||||||
|
|
||||||
|
/* System dark mode fallback (for prefers-color-scheme without explicit data-theme) */
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) mark.user-annotation.user-annotation--amber { background-color: rgba(245, 158, 11, 0.40); }
|
||||||
|
:root:not([data-theme="light"]) mark.user-annotation.user-annotation--sage { background-color: rgba(107, 158, 120, 0.42); }
|
||||||
|
:root:not([data-theme="light"]) mark.user-annotation.user-annotation--steel { background-color: rgba(112, 150, 184, 0.42); }
|
||||||
|
:root:not([data-theme="light"]) mark.user-annotation.user-annotation--rose { background-color: rgba(200, 116, 116, 0.42); }
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
ANNOTATION TOOLTIP
|
ANNOTATION TOOLTIP
|
||||||
Appears on hover over a mark. Inverted colours like the
|
Appears on hover over a mark. Inverted colours like the
|
||||||
|
|
@ -95,9 +103,113 @@ mark.user-annotation:hover { filter: brightness(0.80); }
|
||||||
|
|
||||||
.ann-tooltip-delete:hover { opacity: 1; }
|
.ann-tooltip-delete:hover { opacity: 1; }
|
||||||
|
|
||||||
|
/* ============================================================
|
||||||
|
ANNOTATE PICKER
|
||||||
|
Appears above the selection when "Annotate" is clicked.
|
||||||
|
Inverted colors (dark bg, light text) — same as the tooltip.
|
||||||
|
============================================================ */
|
||||||
|
|
||||||
|
.ann-picker {
|
||||||
|
position: absolute;
|
||||||
|
z-index: 760;
|
||||||
|
background: var(--text);
|
||||||
|
color: var(--bg);
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.28);
|
||||||
|
padding: 0.55rem 0.6rem;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
width: 13rem;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
visibility: hidden;
|
||||||
|
transition: opacity 0.12s ease, visibility 0.12s ease;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ann-picker.is-visible {
|
||||||
|
opacity: 1;
|
||||||
|
visibility: visible;
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ann-picker-swatches {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.45rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ann-picker-swatch {
|
||||||
|
width: 1.05rem;
|
||||||
|
height: 1.05rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 0;
|
||||||
|
flex-shrink: 0;
|
||||||
|
outline: none;
|
||||||
|
transition: transform 0.1s ease, border-color 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ann-picker-swatch--amber { background: rgba(245, 158, 11, 0.9); }
|
||||||
|
.ann-picker-swatch--sage { background: rgba(107, 158, 120, 0.9); }
|
||||||
|
.ann-picker-swatch--steel { background: rgba(112, 150, 184, 0.9); }
|
||||||
|
.ann-picker-swatch--rose { background: rgba(200, 116, 116, 0.9); }
|
||||||
|
|
||||||
|
.ann-picker-swatch.is-selected,
|
||||||
|
.ann-picker-swatch:focus-visible {
|
||||||
|
border-color: var(--bg);
|
||||||
|
transform: scale(1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ann-picker-note {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: inherit;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
padding: 0.28rem 0.4rem;
|
||||||
|
margin-bottom: 0.45rem;
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ann-picker-note::placeholder { opacity: 0.5; }
|
||||||
|
|
||||||
|
.ann-picker-note:focus {
|
||||||
|
border-color: rgba(255, 255, 255, 0.45);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ann-picker-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ann-picker-submit {
|
||||||
|
background: rgba(255, 255, 255, 0.15);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
border-radius: 3px;
|
||||||
|
color: inherit;
|
||||||
|
font-family: var(--font-sans);
|
||||||
|
font-size: 0.72rem;
|
||||||
|
padding: 0.22rem 0.55rem;
|
||||||
|
cursor: pointer;
|
||||||
|
line-height: 1.4;
|
||||||
|
transition: background 0.1s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.ann-picker-submit:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.28);
|
||||||
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
REDUCE MOTION
|
REDUCE MOTION
|
||||||
============================================================ */
|
============================================================ */
|
||||||
|
|
||||||
[data-reduce-motion] mark.user-annotation { transition: none; }
|
[data-reduce-motion] mark.user-annotation { transition: none; }
|
||||||
[data-reduce-motion] .ann-tooltip { transition: none; }
|
[data-reduce-motion] .ann-tooltip { transition: none; }
|
||||||
|
[data-reduce-motion] .ann-picker { transition: none; }
|
||||||
|
|
|
||||||
|
|
@ -206,6 +206,13 @@ nav.site-nav {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-btn--danger {
|
||||||
|
color: rgba(180, 80, 80, 0.85);
|
||||||
|
}
|
||||||
|
.settings-btn--danger:hover {
|
||||||
|
color: rgba(180, 80, 80, 1);
|
||||||
|
}
|
||||||
|
|
||||||
.settings-col {
|
.settings-col {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
||||||
|
|
@ -215,31 +215,42 @@ abbr {
|
||||||
sup { font-variant-position: super; line-height: 1; }
|
sup { font-variant-position: super; line-height: 1; }
|
||||||
sub { font-variant-position: sub; line-height: 1; }
|
sub { font-variant-position: sub; line-height: 1; }
|
||||||
|
|
||||||
/* Realistic Ink Highlighting */
|
/* Realistic Ink Highlighting — excludes user annotation marks */
|
||||||
#markdownBody mark {
|
#markdownBody mark:not(.user-annotation) {
|
||||||
background-color: transparent;
|
background-color: transparent;
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
104deg,
|
104deg,
|
||||||
rgba(250, 235, 120, 0) 0%,
|
rgba(250, 235, 120, 0) 0%,
|
||||||
rgba(250, 235, 120, 0.8) 2%,
|
rgba(250, 235, 120, 0.8) 2%,
|
||||||
rgba(250, 220, 100, 0.9) 98%,
|
rgba(250, 220, 100, 0.9) 98%,
|
||||||
rgba(250, 220, 100, 0) 100%
|
rgba(250, 220, 100, 0) 100%
|
||||||
);
|
);
|
||||||
background-size: 100% 0.7em;
|
background-size: 100% 0.7em;
|
||||||
background-position: 0 88%;
|
background-position: 0 88%;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
padding: 0 0.2em;
|
padding: 0 0.2em;
|
||||||
color: inherit;
|
color: inherit;
|
||||||
}
|
}
|
||||||
[data-theme="dark"] #markdownBody mark {
|
[data-theme="dark"] #markdownBody mark:not(.user-annotation) {
|
||||||
background-image: linear-gradient(
|
background-image: linear-gradient(
|
||||||
104deg,
|
104deg,
|
||||||
rgba(100, 130, 180, 0) 0%,
|
rgba(100, 130, 180, 0) 0%,
|
||||||
rgba(100, 130, 180, 0.4) 2%,
|
rgba(100, 130, 180, 0.4) 2%,
|
||||||
rgba(80, 110, 160, 0.5) 98%,
|
rgba(80, 110, 160, 0.5) 98%,
|
||||||
rgba(80, 110, 160, 0) 100%
|
rgba(80, 110, 160, 0) 100%
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root:not([data-theme="light"]) #markdownBody mark:not(.user-annotation) {
|
||||||
|
background-image: linear-gradient(
|
||||||
|
104deg,
|
||||||
|
rgba(100, 130, 180, 0) 0%,
|
||||||
|
rgba(100, 130, 180, 0.4) 2%,
|
||||||
|
rgba(80, 110, 160, 0.5) 98%,
|
||||||
|
rgba(80, 110, 160, 0) 100%
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
|
|
|
||||||
|
|
@ -234,6 +234,17 @@
|
||||||
return ann;
|
return ann;
|
||||||
},
|
},
|
||||||
remove: removeById,
|
remove: removeById,
|
||||||
|
clearAll: function () {
|
||||||
|
saveAll([]);
|
||||||
|
document.querySelectorAll('mark.user-annotation').forEach(function (mark) {
|
||||||
|
var parent = mark.parentNode;
|
||||||
|
if (!parent) return;
|
||||||
|
while (mark.firstChild) parent.insertBefore(mark.firstChild, mark);
|
||||||
|
parent.removeChild(mark);
|
||||||
|
parent.normalize();
|
||||||
|
});
|
||||||
|
hideTooltip(true);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
document.addEventListener('DOMContentLoaded', function () {
|
||||||
|
|
|
||||||
|
|
@ -5,10 +5,8 @@
|
||||||
code (known lang) → Copy · [MDN / Hoogle / Docs…]
|
code (known lang) → Copy · [MDN / Hoogle / Docs…]
|
||||||
code (unknown) → Copy
|
code (unknown) → Copy
|
||||||
math → Copy · nLab · OEIS · Wolfram
|
math → Copy · nLab · OEIS · Wolfram
|
||||||
prose (multi-word) → Annotate* · BibTeX · Copy · DuckDuckGo · Here · Wikipedia
|
prose (multi-word) → Annotate · BibTeX · Copy · DuckDuckGo · Here · Wikipedia
|
||||||
prose (one word) → Annotate* · BibTeX · Copy · Define · DuckDuckGo · Here · Wikipedia
|
prose (one word) → Annotate · BibTeX · Copy · Define · DuckDuckGo · Here · Wikipedia
|
||||||
|
|
||||||
(* = placeholder, not yet wired)
|
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
@ -17,6 +15,8 @@
|
||||||
|
|
||||||
var popup = null;
|
var popup = null;
|
||||||
var showTimer = null;
|
var showTimer = null;
|
||||||
|
var picker = null;
|
||||||
|
var pickerColor = 'amber';
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
Documentation providers keyed by Prism language identifier.
|
Documentation providers keyed by Prism language identifier.
|
||||||
|
|
@ -58,7 +58,7 @@
|
||||||
document.addEventListener('keyup', onKeyUp);
|
document.addEventListener('keyup', onKeyUp);
|
||||||
document.addEventListener('mousedown', onMouseDown);
|
document.addEventListener('mousedown', onMouseDown);
|
||||||
document.addEventListener('keydown', onKeyDown);
|
document.addEventListener('keydown', onKeyDown);
|
||||||
window.addEventListener('scroll', hide, { passive: true });
|
window.addEventListener('scroll', function () { hide(); hidePicker(); }, { passive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
|
|
@ -80,11 +80,13 @@
|
||||||
|
|
||||||
function onMouseDown(e) {
|
function onMouseDown(e) {
|
||||||
if (popup.contains(e.target)) return;
|
if (popup.contains(e.target)) return;
|
||||||
|
if (picker && picker.classList.contains('is-visible') && picker.contains(e.target)) return;
|
||||||
hide();
|
hide();
|
||||||
|
hidePicker();
|
||||||
}
|
}
|
||||||
|
|
||||||
function onKeyDown(e) {
|
function onKeyDown(e) {
|
||||||
if (e.key === 'Escape') hide();
|
if (e.key === 'Escape') { hide(); hidePicker(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ------------------------------------------------------------------
|
/* ------------------------------------------------------------------
|
||||||
|
|
@ -149,7 +151,7 @@
|
||||||
|
|
||||||
position(rect);
|
position(rect);
|
||||||
popup.style.visibility = '';
|
popup.style.visibility = '';
|
||||||
bindActions(text);
|
bindActions(text, rect);
|
||||||
}
|
}
|
||||||
|
|
||||||
function hide() {
|
function hide() {
|
||||||
|
|
@ -211,8 +213,9 @@
|
||||||
+ btn('wolfram', 'Wolfram');
|
+ btn('wolfram', 'Wolfram');
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Prose — alphabetical: BibTeX · Copy · [Define] · DuckDuckGo · Here · Wikipedia */
|
/* Prose: Annotate · BibTeX · Copy · [Define] · DuckDuckGo · Here · Wikipedia */
|
||||||
return btn('cite', 'BibTeX')
|
return btn('annotate', 'Annotate')
|
||||||
|
+ btn('cite', 'BibTeX')
|
||||||
+ btn('copy', 'Copy')
|
+ btn('copy', 'Copy')
|
||||||
+ (oneWord ? btn('define', 'Define') : '')
|
+ (oneWord ? btn('define', 'Define') : '')
|
||||||
+ btn('search', 'DuckDuckGo')
|
+ btn('search', 'DuckDuckGo')
|
||||||
|
|
@ -240,11 +243,11 @@
|
||||||
Action bindings
|
Action bindings
|
||||||
------------------------------------------------------------------ */
|
------------------------------------------------------------------ */
|
||||||
|
|
||||||
function bindActions(text) {
|
function bindActions(text, rect) {
|
||||||
popup.querySelectorAll('[data-action]').forEach(function (el) {
|
popup.querySelectorAll('[data-action]').forEach(function (el) {
|
||||||
if (el.getAttribute('aria-disabled') === 'true') return;
|
if (el.getAttribute('aria-disabled') === 'true') return;
|
||||||
el.addEventListener('click', function () {
|
el.addEventListener('click', function () {
|
||||||
dispatch(el.getAttribute('data-action'), text, el);
|
dispatch(el.getAttribute('data-action'), text, el, rect);
|
||||||
hide();
|
hide();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -325,7 +328,7 @@
|
||||||
Action dispatch
|
Action dispatch
|
||||||
------------------------------------------------------------------ */
|
------------------------------------------------------------------ */
|
||||||
|
|
||||||
function dispatch(action, text, el) {
|
function dispatch(action, text, el, rect) {
|
||||||
var q = encodeURIComponent(text);
|
var q = encodeURIComponent(text);
|
||||||
if (action === 'search') {
|
if (action === 'search') {
|
||||||
window.open('https://duckduckgo.com/?q=' + q, '_blank', 'noopener,noreferrer');
|
window.open('https://duckduckgo.com/?q=' + q, '_blank', 'noopener,noreferrer');
|
||||||
|
|
@ -362,8 +365,97 @@
|
||||||
} else if (action === 'here') {
|
} else if (action === 'here') {
|
||||||
/* Site search via Pagefind — opens search page with query pre-filled. */
|
/* Site search via Pagefind — opens search page with query pre-filled. */
|
||||||
window.open('/search.html?q=' + q, '_blank', 'noopener,noreferrer');
|
window.open('/search.html?q=' + q, '_blank', 'noopener,noreferrer');
|
||||||
|
|
||||||
|
} else if (action === 'annotate') {
|
||||||
|
showAnnotatePicker(text, rect);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ------------------------------------------------------------------
|
||||||
|
Annotate picker — color swatches + optional note input
|
||||||
|
------------------------------------------------------------------ */
|
||||||
|
|
||||||
|
function showAnnotatePicker(text, selRect) {
|
||||||
|
if (!picker) {
|
||||||
|
picker = document.createElement('div');
|
||||||
|
picker.className = 'ann-picker';
|
||||||
|
picker.setAttribute('role', 'dialog');
|
||||||
|
picker.setAttribute('aria-label', 'Annotate selection');
|
||||||
|
document.body.appendChild(picker);
|
||||||
|
}
|
||||||
|
|
||||||
|
pickerColor = 'amber';
|
||||||
|
picker.innerHTML =
|
||||||
|
'<div class="ann-picker-swatches">'
|
||||||
|
+ swatchBtn('amber') + swatchBtn('sage') + swatchBtn('steel') + swatchBtn('rose')
|
||||||
|
+ '</div>'
|
||||||
|
+ '<input class="ann-picker-note" type="text" placeholder="Note (optional)" maxlength="200">'
|
||||||
|
+ '<div class="ann-picker-actions">'
|
||||||
|
+ '<button class="ann-picker-submit">Highlight</button>'
|
||||||
|
+ '</div>';
|
||||||
|
|
||||||
|
picker.style.visibility = 'hidden';
|
||||||
|
picker.classList.add('is-visible');
|
||||||
|
positionPicker(selRect);
|
||||||
|
picker.style.visibility = '';
|
||||||
|
|
||||||
|
var note = picker.querySelector('.ann-picker-note');
|
||||||
|
var submitBtn = picker.querySelector('.ann-picker-submit');
|
||||||
|
|
||||||
|
picker.querySelector('.ann-picker-swatch[data-color="amber"]').classList.add('is-selected');
|
||||||
|
|
||||||
|
picker.querySelectorAll('.ann-picker-swatch').forEach(function (sw) {
|
||||||
|
sw.addEventListener('click', function () {
|
||||||
|
picker.querySelectorAll('.ann-picker-swatch').forEach(function (s) {
|
||||||
|
s.classList.remove('is-selected');
|
||||||
|
});
|
||||||
|
sw.classList.add('is-selected');
|
||||||
|
pickerColor = sw.getAttribute('data-color');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function commit() {
|
||||||
|
if (window.Annotations) {
|
||||||
|
window.Annotations.add(text, pickerColor, note.value.trim());
|
||||||
|
}
|
||||||
|
hidePicker();
|
||||||
|
}
|
||||||
|
|
||||||
|
submitBtn.addEventListener('click', commit);
|
||||||
|
note.addEventListener('keydown', function (e) {
|
||||||
|
if (e.key === 'Enter') { e.preventDefault(); commit(); }
|
||||||
|
if (e.key === 'Escape') { hidePicker(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
setTimeout(function () { note.focus(); }, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
function hidePicker() {
|
||||||
|
if (picker) picker.classList.remove('is-visible');
|
||||||
|
}
|
||||||
|
|
||||||
|
function positionPicker(rect) {
|
||||||
|
var pw = picker.offsetWidth;
|
||||||
|
var ph = picker.offsetHeight;
|
||||||
|
var GAP = 8;
|
||||||
|
var sy = window.scrollY;
|
||||||
|
var sx = window.scrollX;
|
||||||
|
var vw = window.innerWidth;
|
||||||
|
|
||||||
|
var left = rect.left + sx + rect.width / 2 - pw / 2;
|
||||||
|
left = Math.max(sx + GAP, Math.min(left, sx + vw - pw - GAP));
|
||||||
|
|
||||||
|
var top = rect.top + sy - ph - GAP;
|
||||||
|
if (top < sy + GAP) top = rect.bottom + sy + GAP;
|
||||||
|
|
||||||
|
picker.style.left = left + 'px';
|
||||||
|
picker.style.top = top + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
function swatchBtn(color) {
|
||||||
|
return '<button class="ann-picker-swatch ann-picker-swatch--' + color
|
||||||
|
+ '" data-color="' + color + '" aria-label="' + color + '"></button>';
|
||||||
|
}
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', init);
|
document.addEventListener('DOMContentLoaded', init);
|
||||||
}());
|
}());
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,14 @@
|
||||||
else if (action === 'text-larger') shiftSize(+1);
|
else if (action === 'text-larger') shiftSize(+1);
|
||||||
else if (action === 'focus-mode') toggleDataAttr('focus-mode', 'data-focus-mode');
|
else if (action === 'focus-mode') toggleDataAttr('focus-mode', 'data-focus-mode');
|
||||||
else if (action === 'reduce-motion') toggleDataAttr('reduce-motion', 'data-reduce-motion');
|
else if (action === 'reduce-motion') toggleDataAttr('reduce-motion', 'data-reduce-motion');
|
||||||
else if (action === 'print') { setOpen(false); window.print(); }
|
else if (action === 'print') { setOpen(false); window.print(); }
|
||||||
|
else if (action === 'clear-annotations') { clearAnnotations(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearAnnotations() {
|
||||||
|
if (!confirm('Remove all highlights and annotations across every page?')) return;
|
||||||
|
if (window.Annotations) window.Annotations.clearAll();
|
||||||
|
setOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Theme ----------------------------------------------------------- */
|
/* Theme ----------------------------------------------------------- */
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="settings-section">
|
<div class="settings-section">
|
||||||
<button class="settings-btn settings-btn--full" data-action="print">Print</button>
|
<button class="settings-btn settings-btn--full" data-action="print">Print</button>
|
||||||
|
<button class="settings-btn settings-btn--full settings-btn--danger" data-action="clear-annotations">Clear Annotations</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue