feat(voyage): implement hidden-by-default sidebar-rail with ordered list + filter + jumplist count
Step 19 of v4.3 playground plan. Sidebar now default aria-hidden=true
(translateX collapses panel, leaves 40px FAB rail). FAB toggle has
data-action=toggle-sidebar for keyboard binding (] in Step 20).
New annotation-list section in sidebar panel:
- filter radiogroup: Alle (default), Åpne (unresolved), Resolved
- voyage-jumplist ordered list with numbered badges matching the
gutter-badge ordering (sorted by line ASC)
- aria-live jumplist count: "X av N" (filtered/total)
- click list-item -> setActiveAnchor + scrollIntoView + data-active
renderAnnotationList wires into mountRender so list refreshes on every
render. Filter state (voyageFilterState) persists across renders within
the session.
Trace: SC6, research/04 Dim 1 (hidden-by-default) + Insight 1 +
Recommendation sidebar/navigation.
This commit is contained in:
parent
84f41014f9
commit
6db7c72511
2 changed files with 201 additions and 9 deletions
|
|
@ -555,6 +555,78 @@
|
|||
}
|
||||
.voyage-fab__badge[hidden] { display: none; }
|
||||
|
||||
/* v4.3 Step 19 — annotation-list (sidebar): filter buttons + ordered list + jumplist count */
|
||||
.voyage-annotation-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--space-3);
|
||||
border-bottom: 1px solid var(--color-border-subtle);
|
||||
}
|
||||
.voyage-annotation-list__header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-2);
|
||||
margin-bottom: var(--space-2);
|
||||
}
|
||||
.voyage-annotation-list__filter {
|
||||
display: inline-flex;
|
||||
gap: var(--space-1);
|
||||
}
|
||||
.voyage-filter-btn {
|
||||
padding: 0.15rem 0.5rem;
|
||||
border: 1px solid var(--color-border-subtle);
|
||||
background: var(--color-surface);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
.voyage-filter-btn[aria-pressed="true"] {
|
||||
background: var(--color-scope-voyage);
|
||||
color: #fff;
|
||||
border-color: var(--color-scope-voyage);
|
||||
}
|
||||
.voyage-annotation-list__count {
|
||||
font-size: var(--font-size-xs);
|
||||
color: var(--color-text-tertiary, var(--color-text-secondary));
|
||||
white-space: nowrap;
|
||||
}
|
||||
.voyage-annotation-list__items {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
max-height: 30vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.voyage-annotation-list__items li {
|
||||
padding: var(--space-2);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-sm);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-2);
|
||||
}
|
||||
.voyage-annotation-list__items li:hover {
|
||||
background: var(--color-bg-soft);
|
||||
}
|
||||
.voyage-annotation-list__items li[data-active="true"] {
|
||||
background: rgba(255, 235, 59, 0.18);
|
||||
}
|
||||
.voyage-annotation-list__items .voyage-jumplist-num {
|
||||
flex: 0 0 auto;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
border-radius: 50%;
|
||||
background: var(--color-scope-voyage);
|
||||
color: #fff;
|
||||
font-size: 0.7rem;
|
||||
font-weight: 600;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
/* Tabs */
|
||||
[role="tablist"].voyage-tabs {
|
||||
display: flex;
|
||||
|
|
@ -816,20 +888,23 @@
|
|||
</section>
|
||||
</main>
|
||||
|
||||
<!-- Step 10 — sidebar with tabs + critique-card-list -->
|
||||
<!-- v4.3 Step 19 — sidebar-rail (hidden-by-default) with ordered annotation-list,
|
||||
filter (Alle/Åpne/Resolved), and jumplist count "X av N". Toggle via
|
||||
FAB data-action="toggle-sidebar" or ] keyboard shortcut (Step 20). -->
|
||||
<aside
|
||||
id="voyage-sidebar"
|
||||
class="voyage-sidebar"
|
||||
aria-label="Annotation drafts sidebar"
|
||||
aria-hidden="false"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<div class="voyage-sidebar__rail">
|
||||
<button
|
||||
type="button"
|
||||
id="voyage-sidebar-toggle"
|
||||
class="voyage-fab"
|
||||
data-action="toggle-sidebar"
|
||||
aria-controls="voyage-sidebar"
|
||||
aria-expanded="true"
|
||||
aria-expanded="false"
|
||||
aria-label="Skjul/vis annotation-panel"
|
||||
>
|
||||
<span
|
||||
|
|
@ -842,6 +917,26 @@
|
|||
</button>
|
||||
</div>
|
||||
<div class="voyage-sidebar__panel">
|
||||
<!-- v4.3 Step 19 — ordered annotation list with filter + jumplist count -->
|
||||
<div class="voyage-annotation-list" aria-label="Ordered annotation list">
|
||||
<div class="voyage-annotation-list__header">
|
||||
<div
|
||||
role="radiogroup"
|
||||
class="voyage-annotation-list__filter"
|
||||
aria-label="Filtrer annotations"
|
||||
>
|
||||
<button type="button" class="voyage-filter-btn" data-filter="all" aria-pressed="true">Alle</button>
|
||||
<button type="button" class="voyage-filter-btn" data-filter="open" aria-pressed="false">Åpne</button>
|
||||
<button type="button" class="voyage-filter-btn" data-filter="resolved" aria-pressed="false">Resolved</button>
|
||||
</div>
|
||||
<div
|
||||
id="voyage-jumplist-count"
|
||||
class="voyage-annotation-list__count"
|
||||
aria-live="polite"
|
||||
>0 av 0</div>
|
||||
</div>
|
||||
<ol id="voyage-jumplist" class="voyage-annotation-list__items"></ol>
|
||||
</div>
|
||||
<div role="tablist" class="voyage-tabs" aria-label="Draft og revisjons-faner">
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -2049,12 +2144,13 @@ playground first-run shows a complete round-trip-able artifact.
|
|||
return btn;
|
||||
}
|
||||
|
||||
// Re-run anchor-badge (Step 18) + gesture-3 wiring after each render.
|
||||
// Re-run anchor-badge (Step 18) + gesture-3 + sidebar list (Step 19) wiring after each render.
|
||||
var originalMountRender = mountRender;
|
||||
mountRender = function (text) {
|
||||
originalMountRender(text);
|
||||
injectAnchorBadges();
|
||||
ensurePageNoteButton();
|
||||
renderAnnotationList();
|
||||
};
|
||||
|
||||
// ---- Modal event wiring -------------------------------------------
|
||||
|
|
@ -2216,16 +2312,83 @@ playground first-run shows a complete round-trip-able artifact.
|
|||
}
|
||||
}
|
||||
|
||||
// v4.3 Step 19 — current sidebar filter state ('all' | 'open' | 'resolved')
|
||||
var voyageFilterState = 'all';
|
||||
|
||||
function toggleSidebar() {
|
||||
var toggle = $('voyage-sidebar-toggle');
|
||||
var sidebar = $('voyage-sidebar');
|
||||
if (!toggle || !sidebar) return;
|
||||
var hidden = sidebar.getAttribute('aria-hidden') === 'true';
|
||||
sidebar.setAttribute('aria-hidden', hidden ? 'false' : 'true');
|
||||
toggle.setAttribute('aria-expanded', hidden ? 'true' : 'false');
|
||||
}
|
||||
|
||||
// v4.3 Step 19 — render ordered annotation-list inside sidebar.
|
||||
// Item ordering matches the gutter-badge numbering 1..N (sorted by line ASC).
|
||||
// Filter state ('all' | 'open' | 'resolved') controls which items render.
|
||||
// Click a list-item -> scrollIntoView + setActiveAnchor on the matching badge.
|
||||
function renderAnnotationList() {
|
||||
var list = $('voyage-jumplist');
|
||||
var countEl = $('voyage-jumplist-count');
|
||||
if (!list || !countEl) return;
|
||||
var ta = $('voyage-paste-input');
|
||||
var fm = quickParseFrontmatter(ta ? ta.value : '');
|
||||
var key = deriveStorageKey(fm);
|
||||
var drafts = loadDrafts(key);
|
||||
var sorted = (Array.isArray(drafts) ? drafts.slice() : []).sort(function (a, b) {
|
||||
return (Number(a.line) || 0) - (Number(b.line) || 0);
|
||||
});
|
||||
var filtered = sorted.filter(function (d) {
|
||||
if (voyageFilterState === 'all') return true;
|
||||
if (voyageFilterState === 'resolved') return Boolean(d.resolved);
|
||||
return !d.resolved; // 'open'
|
||||
});
|
||||
list.innerHTML = '';
|
||||
for (var i = 0; i < filtered.length; i++) {
|
||||
var d = filtered[i];
|
||||
var origIdx = sorted.indexOf(d);
|
||||
var li = document.createElement('li');
|
||||
li.setAttribute('data-anchor-id', d.id);
|
||||
var num = document.createElement('span');
|
||||
num.className = 'voyage-jumplist-num';
|
||||
num.textContent = String(origIdx + 1);
|
||||
var label = document.createElement('span');
|
||||
label.className = 'voyage-jumplist-label';
|
||||
label.textContent = (d.target_anchor || 'page') + (d.line ? ' · linje ' + d.line : '');
|
||||
li.appendChild(num);
|
||||
li.appendChild(label);
|
||||
(function (anchorId) {
|
||||
li.addEventListener('click', function () {
|
||||
setActiveAnchor(anchorId);
|
||||
var items = list.querySelectorAll('li');
|
||||
for (var k = 0; k < items.length; k++) items[k].removeAttribute('data-active');
|
||||
li.setAttribute('data-active', 'true');
|
||||
});
|
||||
})(d.id);
|
||||
list.appendChild(li);
|
||||
}
|
||||
countEl.textContent = filtered.length + ' av ' + sorted.length;
|
||||
}
|
||||
|
||||
function wireSidebar() {
|
||||
var toggle = $('voyage-sidebar-toggle');
|
||||
var sidebar = $('voyage-sidebar');
|
||||
if (toggle && sidebar) {
|
||||
toggle.addEventListener('click', function () {
|
||||
var hidden = sidebar.getAttribute('aria-hidden') === 'true';
|
||||
sidebar.setAttribute('aria-hidden', hidden ? 'false' : 'true');
|
||||
toggle.setAttribute('aria-expanded', hidden ? 'true' : 'false');
|
||||
});
|
||||
toggle.addEventListener('click', toggleSidebar);
|
||||
}
|
||||
// Filter buttons (Alle / Åpne / Resolved)
|
||||
document.querySelectorAll('.voyage-filter-btn').forEach(function (btn) {
|
||||
btn.addEventListener('click', function () {
|
||||
var f = btn.getAttribute('data-filter');
|
||||
if (!f) return;
|
||||
voyageFilterState = f;
|
||||
document.querySelectorAll('.voyage-filter-btn').forEach(function (b) {
|
||||
b.setAttribute('aria-pressed', b === btn ? 'true' : 'false');
|
||||
});
|
||||
renderAnnotationList();
|
||||
});
|
||||
});
|
||||
var draftsTab = $('voyage-tab-drafts');
|
||||
var historyTab = $('voyage-tab-history');
|
||||
if (draftsTab) draftsTab.addEventListener('click', function () { selectTab('voyage-tab-drafts'); });
|
||||
|
|
|
|||
|
|
@ -250,3 +250,32 @@ test('voyage-playground.html declares injectAnchorBadges JS function (v4.3 Step
|
|||
const text = readFileSync(HTML, 'utf-8');
|
||||
assert.match(text, /function\s+injectAnchorBadges\s*\(\s*\)/, 'injectAnchorBadges() function required');
|
||||
});
|
||||
|
||||
test('voyage-playground.html declares voyage-sidebar hidden-by-default (v4.3 Step 19)', () => {
|
||||
const text = readFileSync(HTML, 'utf-8');
|
||||
assert.match(text, /id="voyage-sidebar"[\s\S]{0,200}aria-hidden="true"/, 'voyage-sidebar must default aria-hidden="true"');
|
||||
});
|
||||
|
||||
test('voyage-playground.html declares data-action="toggle-sidebar" on FAB (v4.3 Step 19)', () => {
|
||||
const text = readFileSync(HTML, 'utf-8');
|
||||
assert.match(text, /data-action="toggle-sidebar"/, 'data-action="toggle-sidebar" required on FAB toggle button');
|
||||
});
|
||||
|
||||
test('voyage-playground.html declares voyage-jumplist + count "X av N" (v4.3 Step 19)', () => {
|
||||
const text = readFileSync(HTML, 'utf-8');
|
||||
assert.match(text, /id="voyage-jumplist"/, 'voyage-jumplist ordered list required');
|
||||
assert.match(text, /id="voyage-jumplist-count"/, 'voyage-jumplist-count container required');
|
||||
assert.match(text, /' av '/, '"X av N" jumplist count format string required in JS');
|
||||
});
|
||||
|
||||
test('voyage-playground.html declares filter-buttons Alle/Åpne/Resolved (v4.3 Step 19)', () => {
|
||||
const text = readFileSync(HTML, 'utf-8');
|
||||
assert.match(text, /data-filter="all"/, 'filter button data-filter="all" required');
|
||||
assert.match(text, /data-filter="open"/, 'filter button data-filter="open" required');
|
||||
assert.match(text, /data-filter="resolved"/, 'filter button data-filter="resolved" required');
|
||||
});
|
||||
|
||||
test('voyage-playground.html declares renderAnnotationList JS function (v4.3 Step 19)', () => {
|
||||
const text = readFileSync(HTML, 'utf-8');
|
||||
assert.match(text, /function\s+renderAnnotationList\s*\(\s*\)/, 'renderAnnotationList() function required');
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue