pandorga 2.0.3 → 2.0.4

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: 221dacb578ada5bb29237157b9e2fcffc4789053fe0566eae9f626967334b291
4
- data.tar.gz: eedd7e320f83cf654525e469eddf66f013f7229324304d13ed983ad38a0b58eb
3
+ metadata.gz: 2dc703ecd304ea907703cb189abd6ef09f0747c42e36766b365b6cab275f50f5
4
+ data.tar.gz: 1ae4bbec818b879199980c53e8c00e46d91c611b984f56f4d31fb5046b0fb1b0
5
5
  SHA512:
6
- metadata.gz: 41b852a904d013dbc020d1d14fe33b56d9355a5cf201255fc14df14ae095c2457da292ba2f357cce007b45a886fd047b2c9312a4bc1d3eb9f2f69b995270845c
7
- data.tar.gz: d48976bdd3f548a0480766c9e6449566ce3527c5e78bc603711681d32c0ab144d6a8c5c7c60c02fd65553a1160c02eb3f10a8791e284ec0e3c98cdd4019f7f19
6
+ metadata.gz: 4a87bd7621049333217620dcb59f44abda1d072edec0ea4aa06c362a60fee39d9c2b2ff4d75530c2b346caac34b715dd06bee92f0c4b880ce7308921a5cd2eed
7
+ data.tar.gz: f033221a651aae72e83efefb0839c2eafc4ce5ddfb2405ef13c4359c326fea85c7489224ef8e5d2e31f8a934cb7b7efd5c598481e51a6423de61d281f6f44b3d
data/CHANGELOG.md CHANGED
@@ -2,6 +2,15 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## [2.0.4] — 2026-10-28
6
+
7
+ ### Fixed
8
+
9
+ - Mobile article top compresses dates and action buttons into one band and closes the gap before the reading column.
10
+ - Mobile top bar is shorter and hides on scroll-down (shows on scroll-up, focus, or Explore open).
11
+ - Home hero summary uses a ~65ch measure on desktop.
12
+ - Home section "View all" sits beside the section title instead of at the far right of the band.
13
+
5
14
  ## [2.0.3] — 2026-10-25
6
15
 
7
16
  ### Fixed
data/_layouts/shell.html CHANGED
@@ -287,11 +287,13 @@
287
287
  var wrap = document.getElementById('site-menu');
288
288
  var btn = document.getElementById('mobile-menu-btn');
289
289
  var menu = document.getElementById('mobile-menu');
290
+ var topbar = document.querySelector('.site-topbar');
290
291
  if (!wrap || !btn || !menu) return;
291
292
 
292
293
  function setOpen(open) {
293
294
  menu.classList.toggle('hidden', !open);
294
295
  btn.setAttribute('aria-expanded', String(open));
296
+ if (open && topbar) topbar.classList.remove('topbar--hidden');
295
297
  }
296
298
 
297
299
  btn.addEventListener('click', function (event) {
@@ -304,6 +306,29 @@
304
306
  document.addEventListener('keydown', function (event) {
305
307
  if (event.key === 'Escape') setOpen(false);
306
308
  });
309
+
310
+ if (!topbar) return;
311
+ var lastY = window.scrollY || 0;
312
+ var threshold = 8;
313
+
314
+ function onScroll() {
315
+ var y = window.scrollY || 0;
316
+ var diff = y - lastY;
317
+
318
+ if (y <= 32 || !menu.classList.contains('hidden')) {
319
+ topbar.classList.remove('topbar--hidden');
320
+ } else if (diff > threshold) {
321
+ topbar.classList.add('topbar--hidden');
322
+ } else if (diff < -threshold) {
323
+ topbar.classList.remove('topbar--hidden');
324
+ }
325
+ lastY = y;
326
+ }
327
+
328
+ window.addEventListener('scroll', onScroll, { passive: true });
329
+ topbar.addEventListener('focusin', function () {
330
+ topbar.classList.remove('topbar--hidden');
331
+ });
307
332
  })();
308
333
  </script>
309
334
 
@@ -35,6 +35,18 @@
35
35
  -webkit-backdrop-filter: blur(10px);
36
36
  backdrop-filter: blur(10px);
37
37
  border-bottom: 1px solid var(--border-gentle-divider);
38
+ transition: transform var(--motion-duration-fast) var(--motion-ease-default);
39
+ }
40
+
41
+ @media (max-width: 767px) {
42
+ .site-topbar > div {
43
+ padding-top: 0.625rem;
44
+ padding-bottom: 0.625rem;
45
+ }
46
+
47
+ .site-topbar.topbar--hidden {
48
+ transform: translateY(-100%);
49
+ }
38
50
  }
39
51
 
40
52
  /* Boxed controls for theme and mobile menu toggles. */
@@ -694,7 +694,8 @@ aside:has(> .text-detail-sidebar) {
694
694
 
695
695
  .text-detail-sidebar .article-toc--disclosure,
696
696
  .projects-dossier .article-toc--disclosure {
697
- margin-top: var(--space-sm);
697
+ margin-top: var(--space-xs);
698
+ margin-bottom: var(--space-xs);
698
699
  }
699
700
  }
700
701
 
@@ -1184,7 +1185,7 @@ nav.text-page-nav:not(:has(a)) {
1184
1185
  }
1185
1186
 
1186
1187
  .grid.text-detail-grid {
1187
- gap: var(--space-md);
1188
+ gap: var(--space-xs);
1188
1189
  }
1189
1190
 
1190
1191
  /* The reading column used to add its own top margin on top of this gap,
@@ -1232,12 +1233,32 @@ nav.text-page-nav:not(:has(a)) {
1232
1233
  padding-bottom: var(--space-md);
1233
1234
  }
1234
1235
 
1235
- /* The date row's own top rule leaves --space-sm above the text. The
1236
- flex gap was stacking another --space-md on the action rule, so the
1237
- date sat high in that band. Drop the gap and the action margin is the
1238
- matching --space-sm under the text. */
1236
+ /* On mobile, date row and action buttons share one compact horizontal band. */
1239
1237
  .text-detail-sidebar .article-meta {
1240
- gap: 0;
1238
+ display: flex;
1239
+ flex-direction: row;
1240
+ flex-wrap: wrap;
1241
+ align-items: center;
1242
+ justify-content: space-between;
1243
+ gap: var(--space-xs) var(--space-sm);
1244
+ margin-bottom: 0;
1245
+ padding-bottom: 0;
1246
+ }
1247
+
1248
+ .text-detail-sidebar .article-meta > div {
1249
+ margin: 0;
1250
+ padding: 0;
1251
+ }
1252
+
1253
+ .text-detail-sidebar .article-meta .date-evolution > span {
1254
+ border-top: 0;
1255
+ padding-top: 0;
1256
+ }
1257
+
1258
+ .text-detail-sidebar .article-meta .detail-actions {
1259
+ margin-top: 0;
1260
+ padding-top: 0;
1261
+ border-top: 0;
1241
1262
  }
1242
1263
 
1243
1264
  .text-detail-sidebar .article-sidebar-description,
@@ -220,6 +220,12 @@
220
220
  line-height: var(--leading-body);
221
221
  }
222
222
 
223
+ @media (min-width: 768px) {
224
+ .home-hero-summary {
225
+ max-width: 65ch;
226
+ }
227
+ }
228
+
223
229
  /* No empty min-height. A guessed line count shifts the call to action
224
230
  when the summary is shorter or taller than the reserve. The paragraph
225
231
  is in the HTML when content/pages/home/summary.md or
@@ -415,7 +421,7 @@ html[data-theme="light"] .home-hero-plate {
415
421
  The banner is a 2-column grid so the lead's children join the template. */
416
422
  .home-band:not(.home-band--hero) .ledger-section-banner {
417
423
  display: grid;
418
- grid-template-columns: minmax(0, 1fr) auto;
424
+ grid-template-columns: auto auto 1fr;
419
425
  column-gap: var(--space-md);
420
426
  row-gap: var(--space-xs);
421
427
  align-items: baseline;
@@ -24,7 +24,7 @@ module Pandorga
24
24
 
25
25
  gem "pandorga", "~> 2.0"
26
26
  # Local checkout: gem "pandorga", path: "../pandorga"
27
- # GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v2.0.3"
27
+ # GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v2.0.4"
28
28
  gem "csv"
29
29
  gem "base64"
30
30
  RUBY
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pandorga
4
- VERSION = "2.0.3"
4
+ VERSION = "2.0.4"
5
5
  end
@@ -0,0 +1,161 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Acceptance tests for Mobile density + home measure (pandorga theme UI).
5
+ #
6
+ # 1. Article page top on mobile: meta date row and action buttons share one band,
7
+ # and TOC disclosure / reading column gaps are bounded.
8
+ # 2. Fixed top bar on mobile: shorter vertical padding (≤56-60px height) and
9
+ # hide-on-scroll functionality.
10
+ # 3. Home hero summary measure on desktop: constrained to ~60-70ch (e.g. 65ch).
11
+ # 4. Home section "View all" next to title: grid-template-columns places CTA
12
+ # beside section title, not at far right of wide band.
13
+
14
+ require "pathname"
15
+
16
+ ROOT = Pathname.new(__dir__).join("../..").expand_path
17
+ CSS = ROOT.join("assets/css")
18
+
19
+ $failures = []
20
+
21
+ def fail!(msg)
22
+ $failures << "FAIL test-mobile-density-home-measure: #{msg}"
23
+ end
24
+
25
+ def strip_comments(text)
26
+ text.gsub(%r{/\*.*?\*/}m, "")
27
+ end
28
+
29
+ def parse_css_rules(css_text)
30
+ rules = []
31
+ i = 0
32
+ head = +""
33
+ while i < css_text.length
34
+ c = css_text[i]
35
+ if c == "{"
36
+ depth = 1
37
+ j = i + 1
38
+ while j < css_text.length && depth.positive?
39
+ depth += 1 if css_text[j] == "{"
40
+ depth -= 1 if css_text[j] == "}"
41
+ j += 1
42
+ end
43
+ body = css_text[(i + 1)...(j - 1)]
44
+ h = head.strip
45
+ if h.start_with?("@media")
46
+ media_cond = h.sub(/\A@media\s*/, "").gsub(/\s+/, "")
47
+ sub_rules = parse_css_rules(body)
48
+ sub_rules.each do |r|
49
+ m = r[:media] ? "#{media_cond} and #{r[:media]}" : media_cond
50
+ rules << r.merge(media: m)
51
+ end
52
+ elsif h.start_with?("@supports", "@layer", "@container")
53
+ rules.concat(parse_css_rules(body))
54
+ elsif !h.start_with?("@")
55
+ decls = body.split(";").filter_map do |d|
56
+ prop, val = d.split(":", 2)
57
+ next unless val
58
+
59
+ [prop.strip.downcase, val.gsub(/\s+/, " ").strip]
60
+ end
61
+ h.split(",").each do |sel|
62
+ s = sel.gsub(/\s+/, " ").strip
63
+ rules << { sel: s, decls: decls, media: nil }
64
+ end
65
+ end
66
+ head = +""
67
+ i = j
68
+ elsif c == "}"
69
+ i += 1
70
+ else
71
+ head << c
72
+ i += 1
73
+ end
74
+ end
75
+ rules
76
+ end
77
+
78
+ # 1. Article page top on mobile
79
+ detail_css = strip_comments(File.read(CSS.join("ledger/detail.css")))
80
+ detail_rules = parse_css_rules(detail_css)
81
+
82
+ meta_mobile_rules = detail_rules.select do |r|
83
+ r[:media] && r[:media].include?("max-width:767px") && r[:sel] == ".text-detail-sidebar .article-meta"
84
+ end
85
+
86
+ has_row_layout = meta_mobile_rules.any? do |r|
87
+ r[:decls].any? { |p, v| p == "flex-direction" && v == "row" }
88
+ end
89
+
90
+ unless has_row_layout
91
+ fail!("detail.css must set flex-direction: row on .text-detail-sidebar .article-meta under @media (max-width: 767px)")
92
+ end
93
+
94
+ toc_disclosure_mobile = detail_rules.select do |r|
95
+ r[:media] && r[:media].include?("max-width:767px") && r[:sel].include?("article-toc--disclosure")
96
+ end
97
+
98
+ has_bounded_toc_margin = toc_disclosure_mobile.any? do |r|
99
+ r[:decls].any? { |p, v| (p == "margin-top" || p == "margin-bottom" || p == "margin") && (v.include?("var(--space-xs)") || v.include?("var(--space-sm)") || v.include?("0")) }
100
+ end
101
+
102
+ unless has_bounded_toc_margin
103
+ fail!("detail.css must bound margins on .article-toc--disclosure under @media (max-width: 767px)")
104
+ end
105
+
106
+ # 2. Fixed top bar on mobile
107
+ chrome_css = strip_comments(File.read(CSS.join("ledger/chrome.css")))
108
+ chrome_rules = parse_css_rules(chrome_css)
109
+
110
+ topbar_mobile_padding = chrome_rules.any? do |r|
111
+ r[:media] && r[:media].include?("max-width:767px") && (r[:sel] == ".site-topbar > div" || r[:sel] == ".site-topbar") &&
112
+ r[:decls].any? { |p, v| (p == "padding-top" || p == "padding-bottom" || p == "padding" || p == "padding-block") && (v.include?("0.625rem") || v.include?("0.5rem") || v.include?("0.75rem") || v.include?("10px") || v.include?("12px")) }
113
+ end
114
+
115
+ unless topbar_mobile_padding
116
+ fail!("chrome.css must reduce vertical padding on .site-topbar under @media (max-width: 767px)")
117
+ end
118
+
119
+ topbar_hide_rule = chrome_rules.any? do |r|
120
+ (r[:sel].include?(".site-topbar.topbar--hidden") || r[:sel].include?(".site-topbar[data-topbar-hidden]")) &&
121
+ r[:decls].any? { |p, v| p == "transform" && v.include?("translateY(-100%)") }
122
+ end
123
+
124
+ unless topbar_hide_rule
125
+ fail!("chrome.css must define transform: translateY(-100%) on .site-topbar.topbar--hidden")
126
+ end
127
+
128
+ shell_html = File.read(ROOT.join("_layouts/shell.html"))
129
+ unless shell_html.include?("topbar--hidden") || shell_html.include?("data-topbar-hidden")
130
+ fail!("_layouts/shell.html must contain hide-on-scroll logic for .site-topbar")
131
+ end
132
+
133
+ # 3. Home hero summary measure on desktop
134
+ home_css = strip_comments(File.read(CSS.join("ledger/home.css")))
135
+ home_rules = parse_css_rules(home_css)
136
+
137
+ hero_summary_desktop = home_rules.any? do |r|
138
+ r[:media] && r[:media].include?("min-width:768px") && r[:sel] == ".home-hero-summary" &&
139
+ r[:decls].any? { |p, v| p == "max-width" && (v == "65ch" || v == "60ch" || v == "70ch") }
140
+ end
141
+
142
+ unless hero_summary_desktop
143
+ fail!("home.css must constrain .home-hero-summary max-width to ~65ch under @media (min-width: 768px)")
144
+ end
145
+
146
+ # 4. Home section "View all" next to title
147
+ banner_grid_col = home_rules.any? do |r|
148
+ r[:sel] == ".home-band:not(.home-band--hero) .ledger-section-banner" &&
149
+ r[:decls].any? { |p, v| p == "grid-template-columns" && (v.start_with?("auto auto") || v.start_with?("minmax(0, auto) auto") || v.start_with?("max-content max-content")) }
150
+ end
151
+
152
+ unless banner_grid_col
153
+ fail!("home.css must set grid-template-columns on .ledger-section-banner to place CTA beside title (e.g. auto auto 1fr)")
154
+ end
155
+
156
+ if $failures.empty?
157
+ puts "PASS test-mobile-density-home-measure"
158
+ else
159
+ $failures.each { |f| puts f }
160
+ exit 1
161
+ end
@@ -290,7 +290,7 @@ end
290
290
  item(5) do
291
291
  h = "ledger/home.css"
292
292
  expect(h, ".home-band:not(.home-band--hero) .ledger-section-banner", {
293
- "display" => "grid", "grid-template-columns" => "minmax(0, 1fr) auto"
293
+ "display" => "grid", "grid-template-columns" => "auto auto 1fr"
294
294
  })
295
295
  expect(h, ".home-band .ledger-section-lead", { "display" => "contents" })
296
296
  expect(h, ".home-band .ledger-section-banner .ledger-kicker", { "grid-column" => "1 / -1", "grid-row" => "1" })
data/scripts/validate.sh CHANGED
@@ -80,6 +80,7 @@ TESTS=(
80
80
  scripts/test/test-studio-section-texts.rb
81
81
  scripts/test/test-home-article-cards.rb
82
82
  scripts/test/test-parallel-content-publish.rb
83
+ scripts/test/test-mobile-density-home-measure.rb
83
84
  )
84
85
 
85
86
  failed=0
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: 2.0.3
4
+ version: 2.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Pandorga contributors
@@ -453,6 +453,7 @@ files:
453
453
  - scripts/test/test-listing-filter-contract.rb
454
454
  - scripts/test/test-math-row-breaks.mjs
455
455
  - scripts/test/test-math-row-breaks.rb
456
+ - scripts/test/test-mobile-density-home-measure.rb
456
457
  - scripts/test/test-new.rb
457
458
  - scripts/test/test-no-personal-data.rb
458
459
  - scripts/test/test-parallel-content-publish.rb