pandorga 1.13.0 → 1.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c03095737ae31747d865ba734912bb725e2d88586405dd847c63d340b42bbfa3
4
- data.tar.gz: 764bc60f9ef00c00e33391511a7cb5680438660dd5a622af0801add004670f31
3
+ metadata.gz: 7c0e521f70aa70fc0ce916794fa641053d95c52bc8dd26dede245c0e2c5e2c01
4
+ data.tar.gz: fec855e38c0d1e66b27d3b4d2de82908ff5a4aa728d683dfcdb40108cfee321c
5
5
  SHA512:
6
- metadata.gz: 392a4fbde9ea59d1b7dfea8206f45c81f6d104d3b70efcb9a6c67afa21fc9738eefc589b537c8e045bdf7cdb13527ade3ec8c3a1d5683ff0c4f5134a72743dbc
7
- data.tar.gz: 44b0d25b56e010e2ce1d2106c3a2c688a5b3ee61c322b55f39c89beca4d9821c31f5942de335f351289bab176a6752c809205ae7aae97f517351003eb71ce0a5
6
+ metadata.gz: af25c90b077bca4e7f2a931c3ac7fbbe8a4eefabbe4a90899556d5e1acac28a9dce8f990854f92d2e8661756de3d815b6a808ab65834bd61fd370c9abc6292f4
7
+ data.tar.gz: 79a4f364abd01627311ac54866368e3705d577bb27049ed3de5e91aa2d00f9584cb51ca83d4ce7ce5af5128fb2ef3148d7c124b79bb8a8dee749ff0a6e288552
data/CHANGELOG.md CHANGED
@@ -2,6 +2,23 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [1.14.1] — 2026-10-20
6
+
7
+ ### Fixed
8
+
9
+ - Hovered Back, Share, Cite, and reading-size buttons on article pages meet 4.5:1 in the light theme; the green section text on the gray hover mark was 4.48:1 (PLT-AC-46).
10
+
11
+ ## [1.14.0] — 2026-10-20
12
+
13
+ ### Changed
14
+
15
+ - Pills, tags, facets, filters, and buttons use a borderless felt-pen highlighter wash with deterministic nth-child geometry variants, quiet and strong mark strengths, solid fill on hover, and section-accented active rail links (PLT-AC-46).
16
+ - The CV timeline pans in time: drag the chart, use the ‹ Today › controls, or focus it and use the arrow keys, Page Up/Down, Home and End. Zoom, resize and lane toggles keep the date at the center in view. Selecting a period now takes Shift+drag (a plain drag pans) (PLT-AC-47).
17
+
18
+ ### Fixed
19
+
20
+ - The CV timeline no longer handles an input twice when the inline placeholder boots before the timeline data loads; zoom buttons and resize were affected before this change (PLT-AC-47).
21
+
5
22
  ## [1.13.0] — 2026-10-19
6
23
 
7
24
  ### Added
@@ -21,6 +21,10 @@
21
21
  </ul>
22
22
  </div>
23
23
 
24
+ <div class="cv-timeline-control-group">
25
+ <p class="cv-timeline-hint font-body text-xs text-on-surface-variant">Shift+drag to select a period</p>
26
+ </div>
27
+
24
28
  <div class="cv-timeline-control-group" id="timeline-selection-panel" hidden>
25
29
  <h3 class="cv-timeline-control-title card-meta">Selected period</h3>
26
30
  <p id="timeline-selection-range" class="cv-timeline-range code-technical"></p>
@@ -58,9 +62,12 @@
58
62
  </div>
59
63
 
60
64
  <div class="cv-timeline-canvas">
61
- <div id="timeline-chart-wrapper" class="applet-canvas relative w-full overflow-x-auto">
65
+ <div id="timeline-chart-wrapper" class="applet-canvas relative w-full overflow-x-auto" tabindex="0" role="region" aria-label="Career timeline, use arrow keys to move in time">
62
66
  <div id="timeline-chart" class="relative"></div>
63
67
  <div id="timeline-zoom-controls" class="timeline-applet-control cv-timeline-zoom sticky left-0 z-10 flex items-center gap-2 border px-2 py-1.5 w-fit ml-2 mt-4 mb-4 pointer-events-auto">
68
+ <button type="button" id="timeline-pan-prev" class="timeline-zoom-btn timeline-applet-control font-label text-sm leading-none w-8 h-8 border text-on-surface-variant hover:border-primary hover:text-primary transition-colors" aria-label="Pan left">‹</button>
69
+ <button type="button" id="timeline-pan-today" class="timeline-zoom-btn timeline-applet-control font-label text-sm leading-none h-8 px-2 border text-on-surface-variant hover:border-primary hover:text-primary transition-colors" aria-label="Scroll to today">Today</button>
70
+ <button type="button" id="timeline-pan-next" class="timeline-zoom-btn timeline-applet-control font-label text-sm leading-none w-8 h-8 border text-on-surface-variant hover:border-primary hover:text-primary transition-colors" aria-label="Pan right">›</button>
64
71
  <button type="button" id="timeline-zoom-out" class="timeline-zoom-btn timeline-applet-control font-label text-sm leading-none w-8 h-8 border text-on-surface-variant hover:border-primary hover:text-primary transition-colors" aria-label="Zoom out">−</button>
65
72
  <span id="timeline-zoom-label" class="cv-timeline-zoom-label code-technical min-w-[3.5rem] text-center">2 yr</span>
66
73
  <button type="button" id="timeline-zoom-in" class="timeline-zoom-btn timeline-applet-control font-label text-sm leading-none w-8 h-8 border text-on-surface-variant hover:border-primary hover:text-primary transition-colors" aria-label="Zoom in">+</button>
@@ -182,6 +189,13 @@
182
189
  var el = document.getElementById('timeline-data');
183
190
 
184
191
  function bootTimeline(payload) {
192
+ /* The inline stub can boot before the runtime JSON arrives, and the JSON
193
+ then boots again. Abort the earlier boot's listeners on persistent
194
+ elements so each input is handled once, by the current data. */
195
+ if (bootTimeline.listeners) bootTimeline.listeners.abort();
196
+ var listeners = new AbortController();
197
+ bootTimeline.listeners = listeners;
198
+ var bootScope = { signal: listeners.signal };
185
199
  var meta = payload.meta || {};
186
200
  var events = payload.events || [];
187
201
  var buckets = Array.isArray(payload.writing_buckets) ? payload.writing_buckets : [];
@@ -255,7 +269,25 @@
255
269
  var zoomLabel = document.getElementById('timeline-zoom-label');
256
270
  var selectionLayer = null;
257
271
  var selectionBand = null;
258
- var dragState = { active: false, anchorMs: 0 };
272
+ var registeredTracks = new Set();
273
+ var isSelecting = false;
274
+ var isPanCandidate = false;
275
+ var isPanning = false;
276
+ var pointerId = null;
277
+ var anchorMs = 0;
278
+ var startX = 0;
279
+ var startScroll = 0;
280
+
281
+ function panTarget(scrollLeft, visibleW, maxScroll, step) {
282
+ // step is a signed fraction of the visible width (e.g. 0.1, -0.9)
283
+ return Math.round(Math.min(maxScroll, Math.max(0, scrollLeft + step * visibleW)));
284
+ }
285
+
286
+ function centerPreservingScroll(centerMs, xPosMs, visibleW, maxScroll) {
287
+ // xPosMs maps a timestamp to an x pixel within the scroll content
288
+ var target = xPosMs(centerMs) - visibleW / 2;
289
+ return Math.round(Math.min(maxScroll, Math.max(0, target)));
290
+ }
259
291
 
260
292
  function parseDate(s) {
261
293
  var p = s.split('-');
@@ -287,12 +319,14 @@
287
319
  var maxYear = new Date(maxT).getFullYear();
288
320
  var rangeMs = maxT - minT || MS_DAY;
289
321
  var chartW = 720;
322
+ var renderedClientW = 0;
290
323
 
291
324
  function visibleChartWidth() {
292
325
  return Math.max(480, (wrapper.clientWidth || 720) - LABEL_W);
293
326
  }
294
327
 
295
328
  function updateChartWidth() {
329
+ renderedClientW = wrapper.clientWidth;
296
330
  var visibleW = visibleChartWidth();
297
331
  var maxZoomYears = Math.max(MIN_ZOOM_YEARS, rangeMs / MS_YEAR);
298
332
  if (zoomYears > maxZoomYears) zoomYears = maxZoomYears;
@@ -311,13 +345,26 @@
311
345
  if (zoomLabel) zoomLabel.textContent = formatZoomLabel();
312
346
  }
313
347
 
348
+ function centerTimeMs() {
349
+ /* Use the width the chart was last rendered at: on resize the wrapper
350
+ is already at its new width while chartW still maps the old one. */
351
+ var x = wrapper.scrollLeft + (renderedClientW || wrapper.clientWidth) / 2;
352
+ return minT + ((x - LABEL_W) / chartW) * rangeMs;
353
+ }
354
+
355
+ function renderKeepingCenter() {
356
+ var c = centerTimeMs();
357
+ render();
358
+ wrapper.scrollLeft = centerPreservingScroll(c, xPosMs, wrapper.clientWidth, Math.max(0, container.offsetWidth - wrapper.clientWidth));
359
+ if (typeof updatePanButtons === 'function') updatePanButtons();
360
+ }
361
+
314
362
  function changeZoom(direction) {
315
363
  var maxZoomYears = Math.max(MIN_ZOOM_YEARS, rangeMs / MS_YEAR);
316
364
  var factor = direction > 0 ? 0.75 : 1.33;
317
365
  zoomYears = Math.max(MIN_ZOOM_YEARS, Math.min(maxZoomYears, zoomYears * factor));
318
366
  updateZoomLabel();
319
- render();
320
- scrollToToday();
367
+ renderKeepingCenter();
321
368
  }
322
369
 
323
370
  function scrollToToday() {
@@ -606,23 +653,82 @@
606
653
  }
607
654
 
608
655
  function setupSelectionDrag(track) {
609
- track.addEventListener('mousedown', function (e) {
610
- if (e.button !== 0 || e.target !== track) return;
611
- dragState.active = true;
612
- dragState.anchorMs = timeFromClientX(e.clientX);
613
- setSelection(dragState.anchorMs, dragState.anchorMs);
614
- e.preventDefault();
615
- });
656
+ registeredTracks.add(track);
616
657
  }
617
658
 
618
- window.addEventListener('mousemove', function (e) {
619
- if (!dragState.active) return;
620
- setSelection(dragState.anchorMs, timeFromClientX(e.clientX));
621
- });
659
+ if (wrapper) {
660
+ wrapper.addEventListener('pointerdown', function (e) {
661
+ if (e.button !== 0) return;
662
+ if (e.pointerType === 'touch') return;
663
+
664
+ if (e.shiftKey && registeredTracks.has(e.target)) {
665
+ isSelecting = true;
666
+ pointerId = e.pointerId;
667
+ anchorMs = timeFromClientX(e.clientX);
668
+ setSelection(anchorMs, anchorMs);
669
+ if (wrapper.setPointerCapture) {
670
+ try { wrapper.setPointerCapture(e.pointerId); } catch (err) {}
671
+ }
672
+ e.preventDefault();
673
+ } else {
674
+ isPanCandidate = true;
675
+ isPanning = false;
676
+ pointerId = e.pointerId;
677
+ startX = e.clientX;
678
+ startScroll = wrapper.scrollLeft;
679
+ }
680
+ }, bootScope);
681
+
682
+ wrapper.addEventListener('pointermove', function (e) {
683
+ if (isSelecting) {
684
+ setSelection(anchorMs, timeFromClientX(e.clientX));
685
+ } else if (isPanCandidate) {
686
+ var dx = e.clientX - startX;
687
+ if (!isPanning && Math.abs(dx) >= 4) {
688
+ isPanning = true;
689
+ if (wrapper.setPointerCapture) {
690
+ try { wrapper.setPointerCapture(e.pointerId); } catch (err) {}
691
+ }
692
+ wrapper.classList.add('is-dragging');
693
+ hideTip();
694
+ }
695
+ if (isPanning) {
696
+ wrapper.scrollLeft = startScroll - dx;
697
+ }
698
+ }
699
+ }, bootScope);
622
700
 
623
- window.addEventListener('mouseup', function () {
624
- dragState.active = false;
625
- });
701
+ var endPointerDrag = function () {
702
+ if (!isSelecting && !isPanCandidate) return;
703
+
704
+ if (pointerId !== null && wrapper.hasPointerCapture && wrapper.hasPointerCapture(pointerId)) {
705
+ try { wrapper.releasePointerCapture(pointerId); } catch (err) {}
706
+ }
707
+ wrapper.classList.remove('is-dragging');
708
+
709
+ var panned = isPanning;
710
+
711
+ isSelecting = false;
712
+ isPanCandidate = false;
713
+ isPanning = false;
714
+ pointerId = null;
715
+
716
+ if (panned) {
717
+ var suppressClick = function (clickEvent) {
718
+ clickEvent.stopPropagation();
719
+ clickEvent.preventDefault();
720
+ window.removeEventListener('click', suppressClick, true);
721
+ };
722
+ window.addEventListener('click', suppressClick, true);
723
+ setTimeout(function () {
724
+ window.removeEventListener('click', suppressClick, true);
725
+ }, 100);
726
+ }
727
+ };
728
+
729
+ wrapper.addEventListener('pointerup', endPointerDrag, bootScope);
730
+ wrapper.addEventListener('pointercancel', endPointerDrag, bootScope);
731
+ }
626
732
 
627
733
  function renderBars(track, items, color, barHeight, ongoingAllowed, laneH, gap) {
628
734
  items.forEach(function (ev) {
@@ -639,6 +745,7 @@
639
745
  }
640
746
 
641
747
  function render() {
748
+ registeredTracks.clear();
642
749
  updateChartWidth();
643
750
  container.innerHTML = '';
644
751
  barRegistry = [];
@@ -798,16 +905,16 @@
798
905
  container.style.height = y + 'px';
799
906
  container.style.minHeight = '';
800
907
  applySelectionHighlight();
908
+ updatePanButtons();
801
909
  }
802
910
 
803
911
  var resizeTimer;
804
912
  window.addEventListener('resize', function () {
805
913
  clearTimeout(resizeTimer);
806
914
  resizeTimer = setTimeout(function () {
807
- render();
808
- scrollToToday();
915
+ renderKeepingCenter();
809
916
  }, 150);
810
- });
917
+ }, bootScope);
811
918
 
812
919
  /* The bars carry their colour as an inline style, so a palette swap has to
813
920
  redraw them — the CSS variables alone cannot reach an already-painted
@@ -815,37 +922,129 @@
815
922
  document.addEventListener('shell-theme-change', function () {
816
923
  refreshColors();
817
924
  render();
818
- });
925
+ }, bootScope);
819
926
 
820
927
  document.querySelectorAll('.timeline-lane-toggle').forEach(function (cb) {
821
928
  cb.addEventListener('change', function () {
822
929
  laneVisible[cb.dataset.lane] = cb.checked;
823
- render();
824
- });
930
+ renderKeepingCenter();
931
+ }, bootScope);
825
932
  });
826
933
 
827
934
  var expandCb = document.getElementById('timeline-writing-expanded');
828
935
  if (expandCb) {
829
936
  expandCb.addEventListener('change', function () {
830
937
  writingExpanded = expandCb.checked;
831
- render();
832
- });
938
+ renderKeepingCenter();
939
+ }, bootScope);
833
940
  }
834
941
 
835
942
  if (selectionClear) {
836
- selectionClear.addEventListener('click', clearSelection);
943
+ selectionClear.addEventListener('click', clearSelection, bootScope);
944
+ }
945
+
946
+ function maxScrollLeft() {
947
+ return Math.max(0, container.offsetWidth - wrapper.clientWidth);
948
+ }
949
+
950
+ var reduceMotion = window.matchMedia && window.matchMedia('(prefers-reduced-motion: reduce)');
951
+
952
+ function scrollBehavior() {
953
+ return reduceMotion && reduceMotion.matches ? 'auto' : 'smooth';
954
+ }
955
+
956
+ function panBy(step) {
957
+ wrapper.scrollTo({
958
+ left: panTarget(wrapper.scrollLeft, wrapper.clientWidth, maxScrollLeft(), step),
959
+ behavior: scrollBehavior()
960
+ });
961
+ }
962
+
963
+ var panPrevBtn = document.getElementById('timeline-pan-prev');
964
+ if (panPrevBtn) {
965
+ panPrevBtn.addEventListener('click', function () { panBy(-0.9); }, bootScope);
966
+ }
967
+
968
+ var panNextBtn = document.getElementById('timeline-pan-next');
969
+ if (panNextBtn) {
970
+ panNextBtn.addEventListener('click', function () { panBy(0.9); }, bootScope);
971
+ }
972
+
973
+ var panTodayBtn = document.getElementById('timeline-pan-today');
974
+ if (panTodayBtn) {
975
+ panTodayBtn.addEventListener('click', function () {
976
+ var todayX = xPosMs(todayT);
977
+ var target = Math.max(0, Math.min(maxScrollLeft(), todayX - wrapper.clientWidth + 24));
978
+ wrapper.scrollTo({ left: target, behavior: scrollBehavior() });
979
+ }, bootScope);
980
+ }
981
+
982
+ function setPanButtonDisabled(btn, disabled) {
983
+ if (!btn) return;
984
+ btn.disabled = disabled;
985
+ btn.setAttribute('aria-disabled', disabled ? 'true' : 'false');
986
+ }
987
+
988
+ function updatePanButtons() {
989
+ setPanButtonDisabled(panPrevBtn, wrapper.scrollLeft <= 0);
990
+ setPanButtonDisabled(panNextBtn, wrapper.scrollLeft >= maxScrollLeft() - 1);
991
+ }
992
+
993
+ if (wrapper) {
994
+ wrapper.addEventListener('scroll', updatePanButtons, { passive: true, signal: listeners.signal });
995
+ wrapper.addEventListener('keydown', function (e) {
996
+ if (e.target !== wrapper || e.ctrlKey || e.metaKey || e.altKey) return;
997
+ var handled = true;
998
+ switch (e.key) {
999
+ case 'ArrowLeft':
1000
+ panBy(e.shiftKey ? -0.9 : -0.1);
1001
+ break;
1002
+ case 'ArrowRight':
1003
+ panBy(e.shiftKey ? 0.9 : 0.1);
1004
+ break;
1005
+ case 'PageUp':
1006
+ panBy(-0.9);
1007
+ break;
1008
+ case 'PageDown':
1009
+ panBy(0.9);
1010
+ break;
1011
+ case 'Home':
1012
+ wrapper.scrollTo({ left: 0, behavior: scrollBehavior() });
1013
+ break;
1014
+ case 'End':
1015
+ var todayX = xPosMs(todayT);
1016
+ var target = Math.max(0, Math.min(maxScrollLeft(), todayX - wrapper.clientWidth + 24));
1017
+ wrapper.scrollTo({ left: target, behavior: scrollBehavior() });
1018
+ break;
1019
+ case '+':
1020
+ case '=':
1021
+ changeZoom(1);
1022
+ break;
1023
+ case '-':
1024
+ case '−':
1025
+ changeZoom(-1);
1026
+ break;
1027
+ default:
1028
+ handled = false;
1029
+ break;
1030
+ }
1031
+ if (handled) {
1032
+ e.preventDefault();
1033
+ }
1034
+ }, bootScope);
837
1035
  }
838
1036
 
839
1037
  if (zoomInBtn) {
840
- zoomInBtn.addEventListener('click', function () { changeZoom(1); });
1038
+ zoomInBtn.addEventListener('click', function () { changeZoom(1); }, bootScope);
841
1039
  }
842
1040
  if (zoomOutBtn) {
843
- zoomOutBtn.addEventListener('click', function () { changeZoom(-1); });
1041
+ zoomOutBtn.addEventListener('click', function () { changeZoom(-1); }, bootScope);
844
1042
  }
845
1043
 
846
1044
  updateZoomLabel();
847
1045
  render();
848
1046
  scrollToToday();
1047
+ updatePanButtons();
849
1048
  }
850
1049
 
851
1050
  function loadTimeline() {
data/_layouts/shell.html CHANGED
@@ -240,7 +240,7 @@
240
240
  {%- for _item in _group.pages -%}
241
241
  {%- assign _key = _item.key -%}
242
242
  {%- assign _p = _all_pages | where: "key", _key | first -%}
243
- <a href="{{ _p.url }}" class="site-rail-link{% if nav_active == _key %} is-active{% endif %}"
243
+ <a href="{{ _p.url }}" class="site-rail-link{% if nav_active == _key %} is-active{% if _p.accent and _p.accent != 'blue' %} page-accent--{{ _p.accent }}{% endif %}{% endif %}"
244
244
  {% if nav_active == _key %}aria-current="page"{% endif %}>{{ _p.title }}</a>
245
245
  {%- endfor -%}
246
246
  {%- endfor -%}