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 +4 -4
- data/CHANGELOG.md +17 -0
- data/_includes/page/timeline-section.html +228 -29
- data/_layouts/shell.html +1 -1
- data/assets/css/base.css +177 -116
- data/assets/css/ledger/bibliography.css +15 -10
- data/assets/css/ledger/chrome.css +3 -12
- data/assets/css/ledger/cv.css +21 -0
- data/assets/css/ledger/detail.css +29 -15
- data/assets/css/ledger/home.css +6 -2
- data/assets/css/ledger/sources.css +27 -12
- data/assets/css/ledger/writing.css +10 -15
- data/lib/pandorga/commands/new.rb +1 -1
- data/lib/pandorga/version.rb +1 -1
- data/scripts/test/test-highlighter-pills.rb +316 -0
- data/scripts/test/test-shell-a11y.rb +38 -0
- data/scripts/test/test-theme-contrast.rb +91 -0
- data/scripts/test/test-timeline-pan.mjs +147 -0
- data/scripts/test/test-timeline-pan.rb +116 -0
- data/scripts/validate.sh +7 -0
- metadata +4 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// PLT-AC-47. Pure helper unit tests sliced from _includes/page/timeline-section.html
|
|
3
|
+
// Executes panTarget and centerPreservingScroll from source rather than a copy.
|
|
4
|
+
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import path from 'path';
|
|
7
|
+
import { fileURLToPath } from 'url';
|
|
8
|
+
|
|
9
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..');
|
|
10
|
+
const source = fs.readFileSync(
|
|
11
|
+
path.join(root, '_includes/page/timeline-section.html'),
|
|
12
|
+
'utf8'
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
const start = source.indexOf('function panTarget');
|
|
16
|
+
const end = source.indexOf('function parseDate');
|
|
17
|
+
if (start < 0 || end < start) {
|
|
18
|
+
console.error('FAIL: panTarget / centerPreservingScroll missing from timeline-section.html');
|
|
19
|
+
process.exit(1);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const slice = source.slice(start, end);
|
|
23
|
+
if (!slice.includes('function centerPreservingScroll')) {
|
|
24
|
+
console.error('FAIL: centerPreservingScroll missing from slice');
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const api = new Function(
|
|
29
|
+
`${slice}\nreturn { panTarget, centerPreservingScroll };`
|
|
30
|
+
)();
|
|
31
|
+
|
|
32
|
+
const { panTarget, centerPreservingScroll } = api;
|
|
33
|
+
|
|
34
|
+
let failed = 0;
|
|
35
|
+
|
|
36
|
+
function check(condition, message) {
|
|
37
|
+
if (!condition) {
|
|
38
|
+
console.error(`FAIL: ${message}`);
|
|
39
|
+
failed += 1;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ── panTarget tests (PLT-AC-47) ─────────────────────────────────────────────
|
|
44
|
+
// Check step sizes 10% (0.1) and 90% (0.9), and clamping at both bounds (0 and maxScroll).
|
|
45
|
+
|
|
46
|
+
const visibleW = 1000;
|
|
47
|
+
const maxScroll = 4000;
|
|
48
|
+
|
|
49
|
+
// Step 10% right (forward in time)
|
|
50
|
+
check(panTarget(500, visibleW, maxScroll, 0.1) === 600, 'panTarget 10% right from 500 should be 600');
|
|
51
|
+
// Step 10% left (backward in time)
|
|
52
|
+
check(panTarget(500, visibleW, maxScroll, -0.1) === 400, 'panTarget 10% left from 500 should be 400');
|
|
53
|
+
|
|
54
|
+
// Step 90% right
|
|
55
|
+
check(panTarget(500, visibleW, maxScroll, 0.9) === 1400, 'panTarget 90% right from 500 should be 1400');
|
|
56
|
+
// Step 90% left
|
|
57
|
+
check(panTarget(1500, visibleW, maxScroll, -0.9) === 600, 'panTarget 90% left from 1500 should be 600');
|
|
58
|
+
|
|
59
|
+
// Lower bound clamping (scrollLeft <= 0)
|
|
60
|
+
check(panTarget(50, visibleW, maxScroll, -0.1) === 0, 'panTarget clamps at lower bound (10% left)');
|
|
61
|
+
check(panTarget(500, visibleW, maxScroll, -0.9) === 0, 'panTarget clamps at lower bound (90% left)');
|
|
62
|
+
check(panTarget(0, visibleW, maxScroll, -0.1) === 0, 'panTarget at 0 stays at 0');
|
|
63
|
+
|
|
64
|
+
// Upper bound clamping (scrollLeft >= maxScroll)
|
|
65
|
+
check(panTarget(3950, visibleW, maxScroll, 0.1) === 4000, 'panTarget clamps at upper bound (10% right)');
|
|
66
|
+
check(panTarget(3500, visibleW, maxScroll, 0.9) === 4000, 'panTarget clamps at upper bound (90% right)');
|
|
67
|
+
check(panTarget(4000, visibleW, maxScroll, 0.1) === 4000, 'panTarget at maxScroll stays at maxScroll');
|
|
68
|
+
|
|
69
|
+
// ── centerPreservingScroll tests (PLT-AC-47) ────────────────────────────────
|
|
70
|
+
// Center time preserved (within 1 day) across zoom factors 0.75 and 1.33.
|
|
71
|
+
|
|
72
|
+
const MS_DAY = 86400000;
|
|
73
|
+
const MS_YEAR = 365.25 * MS_DAY;
|
|
74
|
+
const LABEL_W = 96;
|
|
75
|
+
|
|
76
|
+
// Simulated timeline: 10 years of range
|
|
77
|
+
const minT = new Date(2015, 0, 1).getTime();
|
|
78
|
+
const maxT = new Date(2025, 0, 1).getTime();
|
|
79
|
+
const rangeMs = maxT - minT;
|
|
80
|
+
|
|
81
|
+
const initialZoomYears = 2;
|
|
82
|
+
const visibleChartW = visibleW - LABEL_W;
|
|
83
|
+
const initialChartW = visibleChartW * (rangeMs / (initialZoomYears * MS_YEAR));
|
|
84
|
+
|
|
85
|
+
function makeXPosMs(chartW) {
|
|
86
|
+
return function (t) {
|
|
87
|
+
return LABEL_W + ((t - minT) / rangeMs) * chartW;
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function timeFromX(x, chartW) {
|
|
92
|
+
const chartX = x - LABEL_W;
|
|
93
|
+
return minT + (chartX / chartW) * rangeMs;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Suppose wrapper was scrolled to 1200px at 2 yr zoom
|
|
97
|
+
const initialScrollLeft = 1200;
|
|
98
|
+
const initialCenterPx = initialScrollLeft + visibleW / 2;
|
|
99
|
+
const centerMs = timeFromX(initialCenterPx, initialChartW);
|
|
100
|
+
|
|
101
|
+
// Zoom in: factor 0.75
|
|
102
|
+
const zoomInYears = initialZoomYears * 0.75;
|
|
103
|
+
const chartWZoomIn = visibleChartW * (rangeMs / (zoomInYears * MS_YEAR));
|
|
104
|
+
const maxScrollZoomIn = Math.max(0, LABEL_W + chartWZoomIn + 32 - visibleW);
|
|
105
|
+
const xPosMsZoomIn = makeXPosMs(chartWZoomIn);
|
|
106
|
+
|
|
107
|
+
const newScrollLeftZoomIn = centerPreservingScroll(centerMs, xPosMsZoomIn, visibleW, maxScrollZoomIn);
|
|
108
|
+
const newCenterPxZoomIn = newScrollLeftZoomIn + visibleW / 2;
|
|
109
|
+
const centerMsAfterZoomIn = timeFromX(newCenterPxZoomIn, chartWZoomIn);
|
|
110
|
+
const diffDaysZoomIn = Math.abs(centerMsAfterZoomIn - centerMs) / MS_DAY;
|
|
111
|
+
|
|
112
|
+
check(
|
|
113
|
+
diffDaysZoomIn <= 1.0,
|
|
114
|
+
`center time preserved within 1 day on zoom factor 0.75 (drift: ${diffDaysZoomIn.toFixed(3)} days)`
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// Zoom out: factor 1.33
|
|
118
|
+
const zoomOutYears = initialZoomYears * 1.33;
|
|
119
|
+
const chartWZoomOut = visibleChartW * (rangeMs / (zoomOutYears * MS_YEAR));
|
|
120
|
+
const maxScrollZoomOut = Math.max(0, LABEL_W + chartWZoomOut + 32 - visibleW);
|
|
121
|
+
const xPosMsZoomOut = makeXPosMs(chartWZoomOut);
|
|
122
|
+
|
|
123
|
+
const newScrollLeftZoomOut = centerPreservingScroll(centerMs, xPosMsZoomOut, visibleW, maxScrollZoomOut);
|
|
124
|
+
const newCenterPxZoomOut = newScrollLeftZoomOut + visibleW / 2;
|
|
125
|
+
const centerMsAfterZoomOut = timeFromX(newCenterPxZoomOut, chartWZoomOut);
|
|
126
|
+
const diffDaysZoomOut = Math.abs(centerMsAfterZoomOut - centerMs) / MS_DAY;
|
|
127
|
+
|
|
128
|
+
check(
|
|
129
|
+
diffDaysZoomOut <= 1.0,
|
|
130
|
+
`center time preserved within 1 day on zoom factor 1.33 (drift: ${diffDaysZoomOut.toFixed(3)} days)`
|
|
131
|
+
);
|
|
132
|
+
|
|
133
|
+
// Clamping at lower bound
|
|
134
|
+
const startCenterMs = minT; // Start of timeline
|
|
135
|
+
const clampedStartScroll = centerPreservingScroll(startCenterMs, xPosMsZoomIn, visibleW, maxScrollZoomIn);
|
|
136
|
+
check(clampedStartScroll === 0, 'centerPreservingScroll clamps to 0 at timeline start');
|
|
137
|
+
|
|
138
|
+
// Clamping at upper bound
|
|
139
|
+
const endCenterMs = maxT; // End of timeline
|
|
140
|
+
const clampedEndScroll = centerPreservingScroll(endCenterMs, xPosMsZoomIn, visibleW, maxScrollZoomIn);
|
|
141
|
+
check(clampedEndScroll === Math.round(maxScrollZoomIn), 'centerPreservingScroll clamps to maxScroll at timeline end');
|
|
142
|
+
|
|
143
|
+
if (failed) {
|
|
144
|
+
console.error(`timeline pan: ${failed} failure(s)`);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
console.log('PASS test-timeline-pan.mjs (PLT-AC-47)');
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# CV timeline temporal pan static gate (PLT-AC-47, docs/features/timeline-pan.md).
|
|
5
|
+
#
|
|
6
|
+
# Asserts:
|
|
7
|
+
# - wrapper has tabindex="0", role="region", and aria-label
|
|
8
|
+
# - pan prev/next and Today buttons exist with aria-labels
|
|
9
|
+
# - script registers pointerdown, pointermove, pointerup, and keydown handlers
|
|
10
|
+
# - shiftKey is checked on the selection path
|
|
11
|
+
# - changeZoom no longer calls scrollToToday()
|
|
12
|
+
# - prefers-reduced-motion check is present
|
|
13
|
+
# - hint text "Shift+drag to select a period" is present
|
|
14
|
+
|
|
15
|
+
require "pathname"
|
|
16
|
+
|
|
17
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
18
|
+
TIMELINE_HTML = ROOT.join("_includes/page/timeline-section.html")
|
|
19
|
+
|
|
20
|
+
def fail!(msg)
|
|
21
|
+
warn "FAIL test-timeline-pan: #{msg}"
|
|
22
|
+
exit 1
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
source = TIMELINE_HTML.read
|
|
26
|
+
|
|
27
|
+
# 1. Wrapper accessibility attributes
|
|
28
|
+
wrapper_match = source[/<div[^>]*id=["']timeline-chart-wrapper["'][^>]*>/]
|
|
29
|
+
fail!("missing #timeline-chart-wrapper") unless wrapper_match
|
|
30
|
+
fail!("wrapper missing tabindex=\"0\"") unless wrapper_match.include?('tabindex="0"')
|
|
31
|
+
fail!("wrapper missing role=\"region\"") unless wrapper_match.include?('role="region"')
|
|
32
|
+
fail!("wrapper missing aria-label") unless wrapper_match.include?('aria-label=')
|
|
33
|
+
|
|
34
|
+
# 2. Pan and Today buttons with aria-labels inside #timeline-zoom-controls
|
|
35
|
+
zoom_controls = source[/<div[^>]*id=["']timeline-zoom-controls["'][^>]*>.*?<\/div>/m]
|
|
36
|
+
fail!("missing #timeline-zoom-controls") unless zoom_controls
|
|
37
|
+
|
|
38
|
+
fail!("missing pan prev button (‹)") unless zoom_controls.include?("‹")
|
|
39
|
+
fail!("missing pan next button (›)") unless zoom_controls.include?("›")
|
|
40
|
+
fail!("missing Today button") unless zoom_controls.include?("Today")
|
|
41
|
+
|
|
42
|
+
prev_btn = zoom_controls[/<button[^>]*>[^<]*‹[^<]*<\/button>/m]
|
|
43
|
+
fail!("pan prev button missing aria-label") unless prev_btn && prev_btn.include?("aria-label=")
|
|
44
|
+
|
|
45
|
+
next_btn = zoom_controls[/<button[^>]*>[^<]*›[^<]*<\/button>/m]
|
|
46
|
+
fail!("pan next button missing aria-label") unless next_btn && next_btn.include?("aria-label=")
|
|
47
|
+
|
|
48
|
+
today_btn = zoom_controls[/<button[^>]*>[^<]*Today[^<]*<\/button>/m]
|
|
49
|
+
fail!("Today button missing aria-label") unless today_btn && today_btn.include?("aria-label=")
|
|
50
|
+
|
|
51
|
+
# 3. Pointer and keyboard event handlers registered in the script
|
|
52
|
+
script_match = source[/<script>(.*?)<\/script>/m, 1]
|
|
53
|
+
fail!("inline script missing") unless script_match
|
|
54
|
+
|
|
55
|
+
fail!("script missing pointerdown handler") unless script_match.include?("pointerdown")
|
|
56
|
+
fail!("script missing pointermove handler") unless script_match.include?("pointermove")
|
|
57
|
+
fail!("script missing pointerup handler") unless script_match.include?("pointerup")
|
|
58
|
+
fail!("script missing keydown handler") unless script_match.include?("keydown")
|
|
59
|
+
|
|
60
|
+
# 4. shiftKey checked on the selection path
|
|
61
|
+
fail!("script does not check shiftKey for selection") unless script_match.include?("shiftKey")
|
|
62
|
+
|
|
63
|
+
# 5. changeZoom no longer calls scrollToToday()
|
|
64
|
+
# Brace-matched body, so a nested block cannot hide a later call.
|
|
65
|
+
def function_body(script, name)
|
|
66
|
+
head = script.index(/function\s+#{name}\s*\([^)]*\)\s*\{/)
|
|
67
|
+
return nil unless head
|
|
68
|
+
|
|
69
|
+
open_at = script.index("{", head)
|
|
70
|
+
depth = 0
|
|
71
|
+
script[open_at..].each_char.with_index do |char, offset|
|
|
72
|
+
depth += 1 if char == "{"
|
|
73
|
+
depth -= 1 if char == "}"
|
|
74
|
+
return script[open_at + 1, offset - 1] if depth.zero?
|
|
75
|
+
end
|
|
76
|
+
nil
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
change_zoom_body = function_body(script_match, "changeZoom")
|
|
80
|
+
fail!("missing changeZoom function") unless change_zoom_body
|
|
81
|
+
if change_zoom_body.include?("scrollToToday")
|
|
82
|
+
fail!("changeZoom must not call scrollToToday(); zoom must preserve center date")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# 6. prefers-reduced-motion check present
|
|
86
|
+
fail!("missing prefers-reduced-motion check") unless script_match.include?("prefers-reduced-motion")
|
|
87
|
+
|
|
88
|
+
# 7. Selection hint text present
|
|
89
|
+
fail!("missing hint text 'Shift+drag to select a period'") unless source.include?("Shift+drag to select a period")
|
|
90
|
+
|
|
91
|
+
# 8. Pan buttons expose their bound state to assistive tech
|
|
92
|
+
fail!("pan buttons must set aria-disabled at bounds") unless script_match.include?("aria-disabled")
|
|
93
|
+
|
|
94
|
+
# 9. A second boot (inline stub, then runtime JSON) must not stack listeners
|
|
95
|
+
boot_body = function_body(script_match, "bootTimeline")
|
|
96
|
+
fail!("missing bootTimeline function") unless boot_body
|
|
97
|
+
fail!("bootTimeline must abort the previous boot's listeners") unless boot_body.include?("AbortController") && boot_body.include?(".abort()")
|
|
98
|
+
%w[pointerdown pointermove keydown resize].each do |type|
|
|
99
|
+
head = boot_body.index(/addEventListener\('#{type}',\s*function\s*\([^)]*\)\s*\{/)
|
|
100
|
+
fail!("missing #{type} listener in bootTimeline") unless head
|
|
101
|
+
open_at = boot_body.index("{", head)
|
|
102
|
+
depth = 0
|
|
103
|
+
close_at = nil
|
|
104
|
+
boot_body[open_at..].each_char.with_index do |char, offset|
|
|
105
|
+
depth += 1 if char == "{"
|
|
106
|
+
depth -= 1 if char == "}"
|
|
107
|
+
if depth.zero?
|
|
108
|
+
close_at = open_at + offset
|
|
109
|
+
break
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
tail = close_at && boot_body[(close_at + 1)..]
|
|
113
|
+
fail!("#{type} listener must take the boot's abort signal") unless tail&.match?(/\A,\s*bootScope\s*\)/)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
puts "PASS test-timeline-pan (PLT-AC-47)"
|
data/scripts/validate.sh
CHANGED
|
@@ -72,6 +72,8 @@ TESTS=(
|
|
|
72
72
|
scripts/test/test-discovery.rb
|
|
73
73
|
scripts/test/test-hero-portraits.rb
|
|
74
74
|
scripts/test/test-thumbnail-variants.rb
|
|
75
|
+
scripts/test/test-highlighter-pills.rb
|
|
76
|
+
scripts/test/test-timeline-pan.rb
|
|
75
77
|
)
|
|
76
78
|
|
|
77
79
|
failed=0
|
|
@@ -107,6 +109,11 @@ if ! node scripts/test/test-studio-pipeline-focus.mjs; then
|
|
|
107
109
|
failed=1
|
|
108
110
|
fi
|
|
109
111
|
|
|
112
|
+
echo "-- scripts/test/test-timeline-pan.mjs"
|
|
113
|
+
if ! node scripts/test/test-timeline-pan.mjs; then
|
|
114
|
+
failed=1
|
|
115
|
+
fi
|
|
116
|
+
|
|
110
117
|
if [[ "$failed" -ne 0 ]]; then
|
|
111
118
|
echo "VALIDATE FAILED"
|
|
112
119
|
exit 1
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pandorga
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.14.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- The Pandorga contributors
|
|
@@ -437,6 +437,7 @@ files:
|
|
|
437
437
|
- scripts/test/test-hero-portraits.mjs
|
|
438
438
|
- scripts/test/test-hero-portraits.rb
|
|
439
439
|
- scripts/test/test-hero-summary.rb
|
|
440
|
+
- scripts/test/test-highlighter-pills.rb
|
|
440
441
|
- scripts/test/test-home-articles.rb
|
|
441
442
|
- scripts/test/test-home-band-art.rb
|
|
442
443
|
- scripts/test/test-home-band-intro.rb
|
|
@@ -477,6 +478,8 @@ files:
|
|
|
477
478
|
- scripts/test/test-text-excerpt.rb
|
|
478
479
|
- scripts/test/test-theme-contrast.rb
|
|
479
480
|
- scripts/test/test-thumbnail-variants.rb
|
|
481
|
+
- scripts/test/test-timeline-pan.mjs
|
|
482
|
+
- scripts/test/test-timeline-pan.rb
|
|
480
483
|
- scripts/test/test-unwrap-media-src.mjs
|
|
481
484
|
- scripts/test/test-unwrap-media-src.rb
|
|
482
485
|
- scripts/test/test-validate-allowlist.rb
|