pandorga 1.8.4 → 1.9.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 +41 -0
- data/_includes/content-runtime/60-fragments.html +40 -2
- data/_layouts/home.html +10 -2
- data/_layouts/shell.html +9 -1
- data/assets/css/article.css +1 -1
- data/assets/css/base.css +10 -0
- data/assets/css/ledger/detail.css +2 -3
- data/assets/css/ledger/home.css +29 -10
- data/lib/pandorga/commands/new.rb +1 -1
- data/lib/pandorga/registry/schema.yml +3 -0
- data/lib/pandorga/version.rb +1 -1
- data/scripts/content/hero_portraits.rb +8 -1
- data/scripts/test/test-hero-portraits.rb +45 -3
- data/scripts/validate.sh +1 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a5c4864b0ecb92d3a9cceac946312495c0371449bbb2bc42077606bff30f704f
|
|
4
|
+
data.tar.gz: cb0dbfac5fb52318dafb2d07f9f8621d480220cd38bcef92d1d15eceac6b5784
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ccf07024fda631b0a2ee24a9a51f6dbf865cd93e8f9b141e6593d3512b478f119e8b2e43e7ed2a20730247c5752f2a4ffcdd4a4de58248b4bdb5c79cc8b5bc74
|
|
7
|
+
data.tar.gz: 9106591a4826bd84bbaa006dae1a6bec544313956e9775830363a1e356ec5a986ef9e173a93495dc9704c55f7ba862ef45dad2f81a1cd399a227979aab05c5a1
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,46 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.9.1] — 2026-10-14
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- The article description under the detail title uses `--type-prose`, the
|
|
8
|
+
same size as the body text. Family, weight, color, and spacing stay as
|
|
9
|
+
they were
|
|
10
|
+
- Bordered and boxed read-more pills do not underline the label.
|
|
11
|
+
Plain text read-more links stay underlined. Focus still shows the
|
|
12
|
+
outline
|
|
13
|
+
|
|
14
|
+
## [1.9.0] — 2026-10-13
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- Optional `pandorga.home.hero_landscape` (home front matter
|
|
19
|
+
`hero_landscape` overrides it): a 16:9 image for viewports at or below
|
|
20
|
+
767px, via `<picture>`, shown whole in a 16:9 plate
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- Without that key the phone hero plate stays 3:4. It no longer cover-crops
|
|
25
|
+
the portrait into a 4:3 band
|
|
26
|
+
- The hero image keeps `fetchpriority="high"`. A configured landscape file
|
|
27
|
+
is preloaded on narrow viewports. The portrait probe sets `fetchPriority`
|
|
28
|
+
to `high`, applies `srcset` before `src`, and the plate draw starts with
|
|
29
|
+
the other home fetches
|
|
30
|
+
- Home links `tailwind.css` before the icon scripts so that sheet is not
|
|
31
|
+
held behind parser-blocking script
|
|
32
|
+
|
|
33
|
+
Text faces still load after the first paint with `display=swap` and the
|
|
34
|
+
metric-matched fallbacks from 1.8.4. One listing shell still serves every
|
|
35
|
+
detail URL.
|
|
36
|
+
|
|
37
|
+
## [1.8.5] — 2026-10-13
|
|
38
|
+
|
|
39
|
+
### Fixed
|
|
40
|
+
|
|
41
|
+
- Home hero `sizes` nudged above the plate width so 1.75–2× DPR picks a
|
|
42
|
+
sharp 3:4 variant; document that hero variants must be portrait fills
|
|
43
|
+
|
|
3
44
|
## [1.8.4] — 2026-10-12
|
|
4
45
|
|
|
5
46
|
### Fixed
|
|
@@ -578,10 +578,13 @@
|
|
|
578
578
|
}
|
|
579
579
|
return new Promise(function (resolve) {
|
|
580
580
|
var probe = new Image();
|
|
581
|
+
probe.fetchPriority = 'high';
|
|
582
|
+
applyHeroPortraitSources(probe, chosen);
|
|
581
583
|
probe.onload = function () {
|
|
582
|
-
img.src = nextSrc;
|
|
583
584
|
img.alt = nextAlt;
|
|
584
585
|
applyHeroPortraitSources(img, chosen);
|
|
586
|
+
img.fetchPriority = 'high';
|
|
587
|
+
img.src = nextSrc;
|
|
585
588
|
img.classList.add('is-shown');
|
|
586
589
|
resolve(true);
|
|
587
590
|
};
|
|
@@ -610,9 +613,44 @@
|
|
|
610
613
|
return next();
|
|
611
614
|
}
|
|
612
615
|
|
|
616
|
+
function heroLandscapeActive(img) {
|
|
617
|
+
var url = img && img.getAttribute('data-hero-landscape');
|
|
618
|
+
if (!url || !String(url).trim() || !window.matchMedia) return false;
|
|
619
|
+
return window.matchMedia('(max-width: 767px)').matches;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
function clearHeroSources(img) {
|
|
623
|
+
img.removeAttribute('src');
|
|
624
|
+
img.removeAttribute('srcset');
|
|
625
|
+
img.removeAttribute('sizes');
|
|
626
|
+
img.alt = '';
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
function watchHeroFrame(img) {
|
|
630
|
+
var url = img.getAttribute('data-hero-landscape');
|
|
631
|
+
if (!url || !String(url).trim() || !window.matchMedia) return;
|
|
632
|
+
var mq = window.matchMedia('(max-width: 767px)');
|
|
633
|
+
if (!mq.addEventListener) return;
|
|
634
|
+
mq.addEventListener('change', function () {
|
|
635
|
+
if (mq.matches) {
|
|
636
|
+
clearHeroSources(img);
|
|
637
|
+
img.classList.add('is-shown');
|
|
638
|
+
return;
|
|
639
|
+
}
|
|
640
|
+
img.classList.remove('is-shown');
|
|
641
|
+
loadJson('pages/home/portraits.json').then(function (data) {
|
|
642
|
+
return showHeroPortrait(img, heroPortraitEntries(data));
|
|
643
|
+
}).catch(function (error) {
|
|
644
|
+
console.error(error);
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
|
|
613
649
|
function hydrateHeroPortrait() {
|
|
614
650
|
var img = document.querySelector('[data-content-hero-portrait]');
|
|
615
651
|
if (!img) return Promise.resolve();
|
|
652
|
+
watchHeroFrame(img);
|
|
653
|
+
if (heroLandscapeActive(img)) return Promise.resolve();
|
|
616
654
|
return loadJson('pages/home/portraits.json').then(function (data) {
|
|
617
655
|
return showHeroPortrait(img, heroPortraitEntries(data));
|
|
618
656
|
}).catch(function (error) {
|
|
@@ -621,6 +659,7 @@
|
|
|
621
659
|
}
|
|
622
660
|
|
|
623
661
|
function hydratePageFragments() {
|
|
662
|
+
hydrateHeroPortrait();
|
|
624
663
|
var headers = Array.prototype.slice.call(document.querySelectorAll('[data-content-page-header]'));
|
|
625
664
|
var chain = Promise.resolve();
|
|
626
665
|
headers.forEach(function (el) {
|
|
@@ -644,7 +683,6 @@
|
|
|
644
683
|
hydrateHomeSections();
|
|
645
684
|
hydrateBandArt();
|
|
646
685
|
hydrateContacts();
|
|
647
|
-
hydrateHeroPortrait();
|
|
648
686
|
});
|
|
649
687
|
}
|
|
650
688
|
|
data/_layouts/home.html
CHANGED
|
@@ -17,6 +17,7 @@ layout: shell
|
|
|
17
17
|
{%- assign _hero_name = site.pandorga.identity | pandorga_hero_name: site.author, site.title -%}
|
|
18
18
|
{%- assign _cv_page = site.pages | where: "key", "cv" | first -%}
|
|
19
19
|
{%- assign _hero_art = site.pandorga.home.hero_art | default: "page/hero-default.html" -%}
|
|
20
|
+
{%- assign _hero_landscape = page.hero_landscape | default: site.pandorga.home.hero_landscape | default: "" | strip -%}
|
|
20
21
|
{%- assign _home_bands = site.data.pandorga_home_bands -%}
|
|
21
22
|
|
|
22
23
|
<main class="home-ledger">
|
|
@@ -53,8 +54,15 @@ layout: shell
|
|
|
53
54
|
<span class="shard-accent home-hero-shard" aria-hidden="true"></span>
|
|
54
55
|
<div class="home-hero-plate-wrap">
|
|
55
56
|
<span class="ledger-frame-crosshairs" aria-hidden="true"></span>
|
|
56
|
-
<figure class="home-hero-plate">
|
|
57
|
-
|
|
57
|
+
<figure class="home-hero-plate{% if _hero_landscape != blank %} home-hero-plate--landscape{% endif %}">
|
|
58
|
+
{%- if _hero_landscape != blank -%}
|
|
59
|
+
<picture>
|
|
60
|
+
<source media="(max-width: 767px)" srcset="{{ _hero_landscape | escape }}" />
|
|
61
|
+
<img alt="" width="1600" height="900" fetchpriority="high" decoding="sync" data-content-hero-portrait data-hero-landscape="{{ _hero_landscape | escape }}" />
|
|
62
|
+
</picture>
|
|
63
|
+
{%- else -%}
|
|
64
|
+
<img alt="" width="960" height="1280" fetchpriority="high" decoding="sync" data-content-hero-portrait />
|
|
65
|
+
{%- endif -%}
|
|
58
66
|
<span class="ledger-frame-corners" aria-hidden="true"></span>
|
|
59
67
|
</figure>
|
|
60
68
|
</div>
|
data/_layouts/shell.html
CHANGED
|
@@ -66,6 +66,10 @@
|
|
|
66
66
|
{%- endcomment -%}
|
|
67
67
|
<link href="https://fonts.googleapis.com" rel="preconnect" />
|
|
68
68
|
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect" />
|
|
69
|
+
{%- assign _hero_landscape = page.hero_landscape | default: site.pandorga.home.hero_landscape | default: "" | strip -%}
|
|
70
|
+
{%- if page.layout == "home" and _hero_landscape != blank -%}
|
|
71
|
+
<link rel="preload" as="image" href="{{ _hero_landscape | escape }}" fetchpriority="high" media="(max-width: 767px)" />
|
|
72
|
+
{%- endif -%}
|
|
69
73
|
{% include theme/critical-preload.html %}
|
|
70
74
|
<link rel="stylesheet" href="/assets/css/base.css" />
|
|
71
75
|
<link rel="stylesheet" href="/assets/css/global-link.css" />
|
|
@@ -120,6 +124,11 @@
|
|
|
120
124
|
{%- for _sheet in _ledger -%}
|
|
121
125
|
<link rel="stylesheet" href="/assets/css/ledger/{{ _sheet }}.css" />
|
|
122
126
|
{%- endfor -%}
|
|
127
|
+
{%- comment -%}
|
|
128
|
+
Utilities before the icon scripts. Those scripts are parser-blocking,
|
|
129
|
+
so a sheet after them waits until they finish before the scanner sees it.
|
|
130
|
+
{%- endcomment -%}
|
|
131
|
+
<link rel="stylesheet" href="/assets/css/tailwind.css" />
|
|
123
132
|
{% include theme/font-loader.html %}
|
|
124
133
|
{%- comment -%}
|
|
125
134
|
Named icons only. The full variable face is about a megabyte, and a
|
|
@@ -194,7 +203,6 @@
|
|
|
194
203
|
<style>
|
|
195
204
|
:root { --drop-cap-lines: {{ site.drop_cap_lines | default: 2 }}; }
|
|
196
205
|
</style>
|
|
197
|
-
<link rel="stylesheet" href="/assets/css/tailwind.css" />
|
|
198
206
|
</head>
|
|
199
207
|
|
|
200
208
|
<body class="bg-background text-on-surface">
|
data/assets/css/article.css
CHANGED
|
@@ -745,7 +745,7 @@ body.figure-lightbox-open {
|
|
|
745
745
|
|
|
746
746
|
.article-sidebar-description {
|
|
747
747
|
font-family: var(--font-body);
|
|
748
|
-
font-size: var(--type-
|
|
748
|
+
font-size: var(--type-prose);
|
|
749
749
|
font-weight: var(--weight-body);
|
|
750
750
|
line-height: var(--leading-body);
|
|
751
751
|
letter-spacing: normal;
|
data/assets/css/base.css
CHANGED
|
@@ -1026,6 +1026,16 @@ a.ledger-chip:hover {
|
|
|
1026
1026
|
text-decoration-color: color-mix(in srgb, var(--color-link-hover) 75%, transparent);
|
|
1027
1027
|
}
|
|
1028
1028
|
|
|
1029
|
+
/* Pill read-more only. The box is the affordance, so the label line would
|
|
1030
|
+
cross the border. Plain text read-more keeps the underline above.
|
|
1031
|
+
Focus keeps the outline. */
|
|
1032
|
+
:is(.ledger-cta--emphasis, .ledger-cta--emphasis-alt):is(:link, :visited, :hover, :focus, :focus-visible, :active),
|
|
1033
|
+
:is(.home-featured-foot, .writing-lead-foot) :is(.home-featured-cta, .writing-lead-cta):is(:link, :visited, :hover, :focus, :focus-visible, :active),
|
|
1034
|
+
:is(.ledger-cta--emphasis, .ledger-cta--emphasis-alt):is(:link, :visited, :hover, :focus, :focus-visible, :active)>span:not(.ledger-cta-arrow):not(.ledger-cta-icon),
|
|
1035
|
+
:is(.home-featured-foot, .writing-lead-foot) :is(.home-featured-cta, .writing-lead-cta).ledger-cta:is(:link, :visited, :hover, :focus, :focus-visible, :active)>span:not(.ledger-cta-arrow):not(.ledger-cta-icon) {
|
|
1036
|
+
text-decoration: none;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1029
1039
|
.ledger-cta-arrow {
|
|
1030
1040
|
transition: transform var(--motion-duration-fast) var(--motion-ease-default);
|
|
1031
1041
|
}
|
|
@@ -464,12 +464,11 @@ aside:has(> .text-detail-sidebar) {
|
|
|
464
464
|
}
|
|
465
465
|
|
|
466
466
|
/* Lead thesis. `description` is the article's own one-sentence claim — body
|
|
467
|
-
face
|
|
468
|
-
the reading column. */
|
|
467
|
+
face at the prose size, in legend ink so it sits under the H1. */
|
|
469
468
|
.text-detail-sidebar .article-sidebar-description {
|
|
470
469
|
font-family: var(--font-body);
|
|
471
470
|
font-style: normal;
|
|
472
|
-
font-size: var(--type-
|
|
471
|
+
font-size: var(--type-prose);
|
|
473
472
|
font-weight: var(--weight-body);
|
|
474
473
|
line-height: var(--leading-body);
|
|
475
474
|
color: var(--color-legend);
|
data/assets/css/ledger/home.css
CHANGED
|
@@ -370,8 +370,10 @@
|
|
|
370
370
|
|
|
371
371
|
/* ── Portrait plate ──
|
|
372
372
|
Beside the copy on a wide page the plate is portrait (3:4): its height
|
|
373
|
-
matches the left column and the width is W = 3/4 × H.
|
|
374
|
-
|
|
373
|
+
matches the left column and the width is W = 3/4 × H. On a phone it stays
|
|
374
|
+
3:4. A wider box would cover-crop that portrait a second time. When
|
|
375
|
+
hero_landscape is set, the phone plate is 16:9 and the picture source is
|
|
376
|
+
that file. */
|
|
375
377
|
.home-hero-plate {
|
|
376
378
|
position: relative;
|
|
377
379
|
z-index: 1;
|
|
@@ -389,12 +391,6 @@
|
|
|
389
391
|
box-shadow: inset 0 0 0 1px color-mix(in srgb, var(--color-on-surface) 10%, transparent);
|
|
390
392
|
}
|
|
391
393
|
|
|
392
|
-
@media (max-width: 767px) {
|
|
393
|
-
.home-hero-plate {
|
|
394
|
-
aspect-ratio: 4 / 3;
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
394
|
:root:not([data-theme="dark"]) .home-hero-plate,
|
|
399
395
|
html[data-theme="light"] .home-hero-plate {
|
|
400
396
|
border-width: 1px;
|
|
@@ -451,6 +447,13 @@ html[data-theme="light"] .home-hero-plate {
|
|
|
451
447
|
}
|
|
452
448
|
}
|
|
453
449
|
|
|
450
|
+
.home-hero-plate picture {
|
|
451
|
+
display: block;
|
|
452
|
+
width: 100%;
|
|
453
|
+
height: 100%;
|
|
454
|
+
margin: 0;
|
|
455
|
+
}
|
|
456
|
+
|
|
454
457
|
.home-hero-plate img {
|
|
455
458
|
width: 100%;
|
|
456
459
|
height: 100%;
|
|
@@ -469,6 +472,21 @@ html[data-theme="light"] .home-hero-plate {
|
|
|
469
472
|
opacity: 1;
|
|
470
473
|
}
|
|
471
474
|
|
|
475
|
+
/* 16:9 file in a 16:9 plate. contain shows the whole frame. The image is
|
|
476
|
+
visible without waiting for the portrait runtime, so it can be the LCP. */
|
|
477
|
+
@media (max-width: 767px) {
|
|
478
|
+
.home-hero-plate--landscape {
|
|
479
|
+
aspect-ratio: 16 / 9;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
.home-hero-plate--landscape img {
|
|
483
|
+
object-fit: contain;
|
|
484
|
+
object-position: center center;
|
|
485
|
+
opacity: 1;
|
|
486
|
+
transition: none;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
472
490
|
.home-hero-plate:hover img {
|
|
473
491
|
transform: scale(1.02);
|
|
474
492
|
}
|
|
@@ -1933,8 +1951,9 @@ html[data-theme="light"] .home-featured .ledger-poster {
|
|
|
1933
1951
|
ends flush with the viewport and adds no scrollable width; this must stay
|
|
1934
1952
|
below 768px, which is where .home-band-inner swaps that token for --gutter.
|
|
1935
1953
|
The side hairlines go with it: a border pinned to the screen edge reads as a
|
|
1936
|
-
frame cut off rather than as the plate's edge.
|
|
1937
|
-
|
|
1954
|
+
frame cut off rather than as the plate's edge. The plate stays 3:4, the
|
|
1955
|
+
same frame as the portrait file, so cover does not crop it again. A
|
|
1956
|
+
configured hero_landscape switches this query to 16:9 and contain. */
|
|
1938
1957
|
@media (max-width: 767px) {
|
|
1939
1958
|
.home-hero-portrait {
|
|
1940
1959
|
max-width: none;
|
|
@@ -24,7 +24,7 @@ module Pandorga
|
|
|
24
24
|
|
|
25
25
|
gem "pandorga", "~> 1.3"
|
|
26
26
|
# Local checkout: gem "pandorga", path: "../pandorga"
|
|
27
|
-
# GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.
|
|
27
|
+
# GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.9.1"
|
|
28
28
|
gem "csv"
|
|
29
29
|
gem "base64"
|
|
30
30
|
RUBY
|
|
@@ -49,6 +49,9 @@ home:
|
|
|
49
49
|
hero_art:
|
|
50
50
|
type: string
|
|
51
51
|
notes: Include path for hero decoration (default `page/hero-default.html`)
|
|
52
|
+
hero_landscape:
|
|
53
|
+
type: string
|
|
54
|
+
notes: Optional URL of a 16:9 image used at or below 767px. Home front matter `hero_landscape` overrides this key. Omitted, the phone plate stays 3:4
|
|
52
55
|
|
|
53
56
|
fonts:
|
|
54
57
|
title: "`pandorga.fonts`"
|
data/lib/pandorga/version.rb
CHANGED
|
@@ -4,13 +4,20 @@
|
|
|
4
4
|
# paths to pages/home/portraits.json. The author uploads a photograph under
|
|
5
5
|
# content/media/images/hero/ and does not keep a second list.
|
|
6
6
|
# Optional width variants live under variants/ so they are not extra draws.
|
|
7
|
+
# Variants should be 3:4 portrait fills (not landscape width-only resizes):
|
|
8
|
+
# the plate is object-fit cover at 3:4, including on a phone, so a 480×270
|
|
9
|
+
# landscape candidate is too short and looks soft. A separate 16:9 file for
|
|
10
|
+
# narrow viewports is pandorga.home.hero_landscape, not a pool variant.
|
|
7
11
|
|
|
8
12
|
require "pathname"
|
|
9
13
|
|
|
10
14
|
module HeroPortraits
|
|
11
15
|
IMAGE_EXTENSIONS = %w[.jpg .jpeg .png .webp .gif].freeze
|
|
12
16
|
WIDTHS = [480, 960, 1440].freeze
|
|
13
|
-
|
|
17
|
+
# Slightly above the plate CSS width (14/16rem at html 110%) so 1.75–2×
|
|
18
|
+
# DPR picks the next 3:4 variant instead of a soft 480w. Variants must be
|
|
19
|
+
# portrait fills; landscape width-only resizes are too short under cover.
|
|
20
|
+
SIZES = "(max-width: 767px) 100vw, (max-width: 1023px) 18rem, 20rem"
|
|
14
21
|
|
|
15
22
|
module_function
|
|
16
23
|
|
|
@@ -23,6 +23,7 @@ require_relative "../content/hero_portraits"
|
|
|
23
23
|
ROOT = Pathname.new(__dir__).join("..", "..").expand_path
|
|
24
24
|
EXPORT = ROOT.join("scripts/content/export-content-json.rb")
|
|
25
25
|
LAYOUT = ROOT.join("_layouts/home.html")
|
|
26
|
+
SHELL = ROOT.join("_layouts/shell.html")
|
|
26
27
|
HOME_CSS = ROOT.join("assets/css/ledger/home.css")
|
|
27
28
|
RUNTIME = ROOT.join("_includes/content-runtime/60-fragments.html")
|
|
28
29
|
SCHEMA = ROOT.join("studio/schema.yml")
|
|
@@ -96,9 +97,28 @@ check!(!layout.include?("profile-monologue"),
|
|
|
96
97
|
"the deleted shell photograph must not be referenced")
|
|
97
98
|
check!(!ROOT.join("assets/images/profile-monologue.JPG").exist?,
|
|
98
99
|
"profile-monologue.JPG must not exist")
|
|
99
|
-
|
|
100
|
-
check!(
|
|
101
|
-
|
|
100
|
+
hero_imgs = layout.scan(/<img[^>]*data-content-hero-portrait[^>]*>/m)
|
|
101
|
+
check!(hero_imgs.any?, "the plate image is missing")
|
|
102
|
+
hero_imgs.each do |hero_img|
|
|
103
|
+
check!(!hero_img.include?("src=") && hero_img.include?('alt=""'),
|
|
104
|
+
"the plate starts without a photographic src and keeps an empty alt, got #{hero_img.inspect}")
|
|
105
|
+
check!(hero_img.include?('fetchpriority="high"'),
|
|
106
|
+
"the hero image must request fetchpriority high, got #{hero_img.inspect}")
|
|
107
|
+
end
|
|
108
|
+
check!(layout.include?("page.hero_landscape") && layout.include?("site.pandorga.home.hero_landscape"),
|
|
109
|
+
"narrow hero reads front matter and pandorga.home.hero_landscape")
|
|
110
|
+
check!(layout.include?('media="(max-width: 767px)"') && layout.include?("<picture>"),
|
|
111
|
+
"a configured landscape file is a picture source for narrow viewports")
|
|
112
|
+
check!(layout.include?("data-hero-landscape"),
|
|
113
|
+
"the image carries the landscape URL for the runtime")
|
|
114
|
+
shell = SHELL.read
|
|
115
|
+
check!(shell.include?('rel="preload" as="image"') && shell.include?('fetchpriority="high"') &&
|
|
116
|
+
shell.include?('media="(max-width: 767px)"') && shell.include?("hero_landscape"),
|
|
117
|
+
"the shell preloads a configured landscape image on narrow viewports")
|
|
118
|
+
tailwind_at = shell.index('href="/assets/css/tailwind.css"')
|
|
119
|
+
icons_at = shell.index("window.__pandorgaIconsHref")
|
|
120
|
+
check!(tailwind_at && icons_at && tailwind_at < icons_at,
|
|
121
|
+
"tailwind must be linked before the icon scripts")
|
|
102
122
|
|
|
103
123
|
home_css = HOME_CSS.read
|
|
104
124
|
check!(home_css.include?("background: black"),
|
|
@@ -109,6 +129,14 @@ check!(plate_img&.match?(/opacity:\s*0/) && plate_img.match?(/transition:[^;]*op
|
|
|
109
129
|
shown = home_css[/^\.home-hero-plate img\.is-shown \{.*?\n\}/m]
|
|
110
130
|
check!(shown&.match?(/opacity:\s*1/),
|
|
111
131
|
"a ready photograph fades to full opacity, got #{shown.inspect}")
|
|
132
|
+
check!(!home_css.include?("aspect-ratio: 4 / 3"),
|
|
133
|
+
"the phone plate must not cover-crop the portrait into 4:3")
|
|
134
|
+
landscape_plate = home_css[/@media \(max-width: 767px\) \{\s*\.home-hero-plate--landscape \{[^}]*\}/m]
|
|
135
|
+
check!(landscape_plate&.include?("aspect-ratio: 16 / 9"),
|
|
136
|
+
"a landscape plate is 16:9 on a phone, got #{landscape_plate.inspect}")
|
|
137
|
+
landscape_img = home_css[/\.home-hero-plate--landscape img \{[^}]*\}/m]
|
|
138
|
+
check!(landscape_img&.include?("object-fit: contain") && landscape_img.include?("opacity: 1"),
|
|
139
|
+
"the 16:9 file is shown whole and can paint without the portrait fade, got #{landscape_img.inspect}")
|
|
112
140
|
|
|
113
141
|
runtime = RUNTIME.read
|
|
114
142
|
start = runtime.index("function heroPortraitEntries")
|
|
@@ -125,6 +153,20 @@ onload = body[/probe\.onload = function \(\) \{.*?\n \};/m]
|
|
|
125
153
|
check!(onload&.include?("img.src = nextSrc") && onload.include?("img.alt = nextAlt") &&
|
|
126
154
|
onload.include?("img.classList.add('is-shown')"),
|
|
127
155
|
"src, alt, and the fade-in must be applied together after load, got #{onload.inspect}")
|
|
156
|
+
check!(onload && onload.index("applyHeroPortraitSources") && onload.index("img.src = nextSrc") &&
|
|
157
|
+
onload.index("applyHeroPortraitSources") < onload.index("img.src = nextSrc"),
|
|
158
|
+
"srcset must be applied before src so the full file is not the first request")
|
|
159
|
+
check!(body.include?("probe.fetchPriority = 'high'"),
|
|
160
|
+
"the portrait probe must use fetchPriority high")
|
|
161
|
+
check!(body.include?("heroLandscapeActive") && body.include?("data-hero-landscape"),
|
|
162
|
+
"a narrow viewport with hero_landscape must not swap in a portrait")
|
|
163
|
+
check!(runtime.include?("hydrateHeroPortrait();"),
|
|
164
|
+
"hydratePageFragments must draw the plate before the later fragment chain")
|
|
165
|
+
plate_call = runtime.index("function hydratePageFragments")
|
|
166
|
+
later = runtime.index("hydrateHomeSections();", plate_call || 0)
|
|
167
|
+
check!(plate_call && runtime.index("hydrateHeroPortrait();", plate_call) &&
|
|
168
|
+
runtime.index("hydrateHeroPortrait();", plate_call) < later,
|
|
169
|
+
"the plate draw must start at the beginning of hydratePageFragments")
|
|
128
170
|
onerror = body[/probe\.onerror = function \(\) \{.*?\n \};/m]
|
|
129
171
|
check!(onerror && !onerror.include?("img.src") && !onerror.include?("is-shown"),
|
|
130
172
|
"a failed image load must leave the plate black, got #{onerror.inspect}")
|
data/scripts/validate.sh
CHANGED