pandorga 2.0.2 → 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 +4 -4
- data/CHANGELOG.md +15 -0
- data/DESIGN.md +1 -1
- data/_layouts/shell.html +25 -0
- data/assets/css/base.css +15 -4
- data/assets/css/ledger/bibliography.css +0 -2
- data/assets/css/ledger/chrome.css +12 -0
- data/assets/css/ledger/detail.css +28 -7
- data/assets/css/ledger/home.css +7 -1
- data/assets/css/ledger/rascunhos.css +0 -7
- data/lib/pandorga/commands/new.rb +1 -1
- data/lib/pandorga/version.rb +1 -1
- data/scripts/test/test-listing-entry-titles.rb +107 -0
- data/scripts/test/test-mobile-density-home-measure.rb +161 -0
- data/scripts/test/test-v3-layout.rb +1 -1
- data/scripts/validate.sh +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2dc703ecd304ea907703cb189abd6ef09f0747c42e36766b365b6cab275f50f5
|
|
4
|
+
data.tar.gz: 1ae4bbec818b879199980c53e8c00e46d91c611b984f56f4d31fb5046b0fb1b0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 4a87bd7621049333217620dcb59f44abda1d072edec0ea4aa06c362a60fee39d9c2b2ff4d75530c2b346caac34b715dd06bee92f0c4b880ce7308921a5cd2eed
|
|
7
|
+
data.tar.gz: f033221a651aae72e83efefb0839c2eafc4ce5ddfb2405ef13c4359c326fea85c7489224ef8e5d2e31f8a934cb7b7efd5c598481e51a6423de61d281f6f44b3d
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
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
|
+
|
|
14
|
+
## [2.0.3] — 2026-10-25
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
|
|
18
|
+
- Entry titles on the Articles, Sources and Bibliography pages use the Rascunhos size at every width. The Articles rows and Sources cards were a step smaller and shrank again below 768px, and the Bibliography cards were a step smaller (PLT-AC-55).
|
|
19
|
+
|
|
5
20
|
## [2.0.2] — 2026-10-25
|
|
6
21
|
|
|
7
22
|
### Added
|
data/DESIGN.md
CHANGED
|
@@ -762,7 +762,7 @@ those tokens — no one-off rem sizes for roles that already have a step.
|
|
|
762
762
|
| Page H1 | Cinzel Decorative | 400 | `--type-page` / `-lg` |
|
|
763
763
|
| Section | Cinzel Decorative | 400 | `--type-section` / `-lg` |
|
|
764
764
|
| Detail title | Newsreader | 600 | `--type-title` / `-lg` |
|
|
765
|
-
| Card / row title | Newsreader | 600 | `--type-card` / `-lg` |
|
|
765
|
+
| Card / row title | Newsreader | 600 | `--type-card` / `-lg`; listing entry titles always `-lg` (PLT-AC-55) |
|
|
766
766
|
| Prose | Newsreader | 300 | `--type-prose` |
|
|
767
767
|
| UI body | Newsreader | 300 | `--type-body` |
|
|
768
768
|
| Detail standfirst | Newsreader | 300 | `--type-subtitle` (= body − 1pt) |
|
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
|
|
data/assets/css/base.css
CHANGED
|
@@ -527,11 +527,11 @@ html[data-theme="dark"] {
|
|
|
527
527
|
--color-link-hover: color-mix(in srgb, var(--color-tertiary) 72%, var(--color-on-surface));
|
|
528
528
|
}
|
|
529
529
|
|
|
530
|
-
/* Card / row / collection entry titles — content-title voice (Regular 400).
|
|
530
|
+
/* Card / row / collection entry titles — content-title voice (Regular 400).
|
|
531
|
+
Listing rows and source cards also carry .card-title, so they get the voice
|
|
532
|
+
here and their size from the listing entry rule below. */
|
|
531
533
|
.card-title,
|
|
532
|
-
.ledger-card-title
|
|
533
|
-
.ledger-row-title,
|
|
534
|
-
.source-card-title {
|
|
534
|
+
.ledger-card-title {
|
|
535
535
|
font-family: var(--font-content-title);
|
|
536
536
|
font-size: var(--type-card);
|
|
537
537
|
font-weight: var(--weight-title);
|
|
@@ -554,6 +554,17 @@ html[data-theme="dark"] {
|
|
|
554
554
|
}
|
|
555
555
|
}
|
|
556
556
|
|
|
557
|
+
/* Listing entry titles (PLT-AC-55). The Articles and Rascunhos rows and the
|
|
558
|
+
Sources and Bibliography cards set their entry title at the Rascunhos step
|
|
559
|
+
at every width. This rule follows the small-screen step above on purpose,
|
|
560
|
+
so it holds below 768px too. */
|
|
561
|
+
.ledger-row-title,
|
|
562
|
+
.source-card-title,
|
|
563
|
+
.bib-card-title {
|
|
564
|
+
font-size: var(--type-card-lg);
|
|
565
|
+
line-height: 1.25;
|
|
566
|
+
}
|
|
567
|
+
|
|
557
568
|
/* Label-face meta on CV identity lines, etc. */
|
|
558
569
|
.card-meta {
|
|
559
570
|
font-family: var(--font-label);
|
|
@@ -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-
|
|
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-
|
|
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
|
-
/*
|
|
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
|
-
|
|
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,
|
data/assets/css/ledger/home.css
CHANGED
|
@@ -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:
|
|
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;
|
|
@@ -204,13 +204,6 @@
|
|
|
204
204
|
text-transform: uppercase;
|
|
205
205
|
}
|
|
206
206
|
|
|
207
|
-
/* Entry titles — a step above the shared ledger-row scale so the notebook
|
|
208
|
-
line reads as a title, not a caption. */
|
|
209
|
-
.rascunhos-row .ledger-row-title {
|
|
210
|
-
font-size: var(--type-card-lg);
|
|
211
|
-
line-height: 1.25;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
207
|
/* ══ Ledger entry ══
|
|
215
208
|
Ruled paper, not a stack of cards: the shared row's card fill and padding
|
|
216
209
|
come off, and what separates two entries is the hairline under each one.
|
|
@@ -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.
|
|
27
|
+
# GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v2.0.4"
|
|
28
28
|
gem "csv"
|
|
29
29
|
gem "base64"
|
|
30
30
|
RUBY
|
data/lib/pandorga/version.rb
CHANGED
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# PLT-AC-55: listing entry titles on the Articles, Rascunhos, Sources and
|
|
5
|
+
# Bibliography pages share one size, the Rascunhos step, at every width.
|
|
6
|
+
|
|
7
|
+
require "pathname"
|
|
8
|
+
|
|
9
|
+
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
10
|
+
CSS_ROOT = ROOT.join("assets/css")
|
|
11
|
+
TITLE_CLASSES = %w[.ledger-row-title .source-card-title .bib-card-title].freeze
|
|
12
|
+
|
|
13
|
+
def fail!(message)
|
|
14
|
+
warn "FAIL: #{message}"
|
|
15
|
+
exit 1
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def strip_comments(source)
|
|
19
|
+
source.gsub(%r{/\*.*?\*/}m, "")
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Yields [selectors, declarations, offset] for every rule, descending into
|
|
23
|
+
# at-rule blocks such as media queries.
|
|
24
|
+
def each_rule(source, &block)
|
|
25
|
+
depth_start = 0
|
|
26
|
+
i = 0
|
|
27
|
+
while (open = source.index("{", i))
|
|
28
|
+
prelude = source[i...open].strip
|
|
29
|
+
depth = 1
|
|
30
|
+
j = open + 1
|
|
31
|
+
while depth.positive? && j < source.length
|
|
32
|
+
depth += 1 if source[j] == "{"
|
|
33
|
+
depth -= 1 if source[j] == "}"
|
|
34
|
+
j += 1
|
|
35
|
+
end
|
|
36
|
+
body = source[(open + 1)...(j - 1)]
|
|
37
|
+
if prelude.start_with?("@")
|
|
38
|
+
each_rule(body, &block) if body.include?("{")
|
|
39
|
+
else
|
|
40
|
+
selectors = prelude.split(",").map { |s| s.strip.gsub(/\s+/, " ") }
|
|
41
|
+
yield selectors, body, open + depth_start
|
|
42
|
+
end
|
|
43
|
+
i = j
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def last_compound(selector)
|
|
48
|
+
selector.split(/\s*[ >+~]\s*/).last.to_s
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def declares?(body, property)
|
|
52
|
+
body.match?(/(?:^|;|\s)#{Regexp.escape(property)}\s*:/)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
puts "Testing listing entry titles (PLT-AC-55)..."
|
|
56
|
+
|
|
57
|
+
base = strip_comments(CSS_ROOT.join("base.css").read)
|
|
58
|
+
shared = []
|
|
59
|
+
each_rule(base) do |selectors, body, offset|
|
|
60
|
+
shared << [body, offset] if selectors.sort == TITLE_CLASSES.sort
|
|
61
|
+
end
|
|
62
|
+
fail!("base.css needs exactly one rule for #{TITLE_CLASSES.join(', ')}") unless shared.size == 1
|
|
63
|
+
body, shared_offset = shared.first
|
|
64
|
+
fail!("shared rule must set font-size: var(--type-card-lg)") unless body.match?(/font-size:\s*var\(--type-card-lg\)\s*;/)
|
|
65
|
+
fail!("shared rule must set line-height: 1.25") unless body.match?(/line-height:\s*1\.25\s*;/)
|
|
66
|
+
|
|
67
|
+
small_step = base.index("var(--type-card-sm)", base.index("--type-card-sm:") + 1)
|
|
68
|
+
fail!("small-screen .card-title step not found in base.css") unless small_step
|
|
69
|
+
fail!("shared rule must follow the small-screen .card-title step") unless shared_offset > small_step
|
|
70
|
+
puts " shared rule: OK"
|
|
71
|
+
|
|
72
|
+
offenders = []
|
|
73
|
+
Dir.glob(CSS_ROOT.join("**", "*.css")).sort.each do |path|
|
|
74
|
+
source = strip_comments(File.read(path))
|
|
75
|
+
each_rule(source) do |selectors, rule_body, _offset|
|
|
76
|
+
next if selectors.sort == TITLE_CLASSES.sort && path == CSS_ROOT.join("base.css").to_s
|
|
77
|
+
|
|
78
|
+
targeted = selectors.select do |sel|
|
|
79
|
+
compound = last_compound(sel)
|
|
80
|
+
TITLE_CLASSES.any? { |cls| compound.include?(cls) && !compound.match?(/#{Regexp.escape(cls)}[-\w]/) }
|
|
81
|
+
end
|
|
82
|
+
next if targeted.empty?
|
|
83
|
+
|
|
84
|
+
%w[font-size line-height].each do |prop|
|
|
85
|
+
next unless declares?(rule_body, prop)
|
|
86
|
+
|
|
87
|
+
offenders << "#{Pathname.new(path).relative_path_from(ROOT)}: #{targeted.join(', ')} sets #{prop}"
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
fail!("entry title overrides:\n #{offenders.join("\n ")}") unless offenders.empty?
|
|
92
|
+
puts " no overrides: OK"
|
|
93
|
+
|
|
94
|
+
markup = {
|
|
95
|
+
"_includes/content-runtime/43-blocks.html" => "ledger-row-title",
|
|
96
|
+
"_layouts/resources.html" => "source-card-title",
|
|
97
|
+
"_layouts/bibliography.html" => "bib-card-title"
|
|
98
|
+
}
|
|
99
|
+
markup.each do |file, cls|
|
|
100
|
+
fail!("#{file} no longer emits #{cls}") unless ROOT.join(file).read.include?(cls)
|
|
101
|
+
end
|
|
102
|
+
%w[_layouts/articles.html _layouts/blog.html].each do |file|
|
|
103
|
+
fail!("#{file} no longer renders rows with renderLedgerRow") unless ROOT.join(file).read.include?("renderLedgerRow(")
|
|
104
|
+
end
|
|
105
|
+
puts " markup: OK"
|
|
106
|
+
|
|
107
|
+
puts "All listing entry title checks passed."
|
|
@@ -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" => "
|
|
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
|
@@ -62,6 +62,7 @@ TESTS=(
|
|
|
62
62
|
scripts/test/test-theme-contrast.rb
|
|
63
63
|
scripts/test/test-hydrate-reserve.rb
|
|
64
64
|
scripts/test/test-hero-summary.rb
|
|
65
|
+
scripts/test/test-listing-entry-titles.rb
|
|
65
66
|
scripts/test/test-lighthouse-policy.rb
|
|
66
67
|
scripts/test/test-home-lists.rb
|
|
67
68
|
scripts/test/test-home-articles.rb
|
|
@@ -79,6 +80,7 @@ TESTS=(
|
|
|
79
80
|
scripts/test/test-studio-section-texts.rb
|
|
80
81
|
scripts/test/test-home-article-cards.rb
|
|
81
82
|
scripts/test/test-parallel-content-publish.rb
|
|
83
|
+
scripts/test/test-mobile-density-home-measure.rb
|
|
82
84
|
)
|
|
83
85
|
|
|
84
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.
|
|
4
|
+
version: 2.0.4
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- The Pandorga contributors
|
|
@@ -449,9 +449,11 @@ files:
|
|
|
449
449
|
- scripts/test/test-language-export.rb
|
|
450
450
|
- scripts/test/test-ledger-blocks.rb
|
|
451
451
|
- scripts/test/test-lighthouse-policy.rb
|
|
452
|
+
- scripts/test/test-listing-entry-titles.rb
|
|
452
453
|
- scripts/test/test-listing-filter-contract.rb
|
|
453
454
|
- scripts/test/test-math-row-breaks.mjs
|
|
454
455
|
- scripts/test/test-math-row-breaks.rb
|
|
456
|
+
- scripts/test/test-mobile-density-home-measure.rb
|
|
455
457
|
- scripts/test/test-new.rb
|
|
456
458
|
- scripts/test/test-no-personal-data.rb
|
|
457
459
|
- scripts/test/test-parallel-content-publish.rb
|