pandorga 1.5.1 → 1.6.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 +39 -0
- data/_includes/content-runtime/41-listing-helpers.html +7 -0
- data/_includes/content-runtime/43-blocks.html +4 -3
- data/_includes/content-runtime/60-fragments.html +77 -11
- data/_includes/theme/critical-preload.html +35 -0
- data/_includes/theme/json-ld.html +35 -0
- data/_layouts/articles.html +27 -5
- data/_layouts/cv.html +20 -14
- data/_layouts/home.html +23 -3
- data/_layouts/shell.html +48 -11
- data/assets/css/ledger/cv.css +13 -0
- data/assets/css/ledger/home.css +27 -0
- data/assets/css/ledger/writing.css +30 -3
- data/assets/css/tailwind.css +3 -0
- data/assets/tailwind-play.js +2 -1
- data/lib/pandorga/discovery.rb +78 -0
- data/lib/pandorga/jekyll/plugins/discovery.rb +32 -0
- data/lib/pandorga/version.rb +1 -1
- data/scripts/cdn-allowlist.json +2 -5
- data/scripts/tailwind/build.sh +19 -0
- data/scripts/tailwind/input.css +12 -0
- data/scripts/tailwind/tailwind.config.js +102 -0
- data/scripts/test/render-parity/blocks/project-inventory-row.json +3 -2
- data/scripts/test/test-css-delivery.rb +32 -0
- data/scripts/test/test-discovery.rb +46 -0
- data/scripts/test/test-examples-build.rb +15 -0
- data/scripts/test/test-hero-portraits.mjs +1 -1
- data/scripts/test/test-hydrate-reserve.rb +68 -0
- data/scripts/validate.sh +3 -0
- metadata +12 -2
- data/assets/css/cv.css +0 -13
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5686edc300ca78ccb310e5f59909212720c54656998508c697f56e2689e4c21b
|
|
4
|
+
data.tar.gz: 8b6a24524c80d7e9917c784d6230016406fc5e2791f5349ca09aa509ef650fd3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c649b33b2fa0afdc8f93c3558a9e07ba8d56f18e4dff63064d9eecdca0701587c7d64fcb030ddcfc51f71f6169ba3ef8745e1c555ad8cbcc10215baf7a82d81a
|
|
7
|
+
data.tar.gz: 79ad62fc9e7cfca6a70416fed7865ce1a4c70be646c05262ad2fe37579f4281b0ad4eaf1a5732b8fb70879c2269759b566750a6b49ee7e4f6dda19f36e282195
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,44 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.1] — 2026-10-10
|
|
4
|
+
|
|
5
|
+
### Fixed
|
|
6
|
+
|
|
7
|
+
- The articles featured slot reserved 16rem whenever it was empty, so page 2
|
|
8
|
+
and later kept a blank band between the header rule and the filter. The
|
|
9
|
+
reserve now lasts only until the listing decides. A page with no specimen
|
|
10
|
+
collapses that slot.
|
|
11
|
+
|
|
12
|
+
No `pandorga:` config keys were added or renamed.
|
|
13
|
+
|
|
14
|
+
## [1.6.0] — 2026-10-10
|
|
15
|
+
|
|
16
|
+
### Added
|
|
17
|
+
|
|
18
|
+
- `/sitemap.xml` lists the generated pages. `/robots.txt` allows the public
|
|
19
|
+
site, skips `/studio/`, and names the sitemap. Both use
|
|
20
|
+
`pandorga.identity.url` when it is set, otherwise `site.url`.
|
|
21
|
+
- JSON-LD `WebSite` and `Person` in the shell, from `identity.name` and the
|
|
22
|
+
optional `identity.url`. `site.description` is included when it is set.
|
|
23
|
+
- Hero portrait entries may include optional `srcset` and `sizes`. A portrait
|
|
24
|
+
that only has `src` is unchanged.
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
|
|
28
|
+
- The home subtitle and summary are requested before the stylesheets and
|
|
29
|
+
painted without a "Loading..." swap. Article rows, CV sections, and empty
|
|
30
|
+
home bands keep a reserved box until the real content replaces it.
|
|
31
|
+
- The hero subtitle stays at `--type-body`, the same size as the summary.
|
|
32
|
+
- `.writing-revised` uses the solid variant ink. The transparent mix was
|
|
33
|
+
3.69 and 3.90 on the light paper.
|
|
34
|
+
- A selectable project inventory row is a `button`.
|
|
35
|
+
- Utilities ship as `assets/css/tailwind.css`. The shell no longer loads the
|
|
36
|
+
Tailwind Play CDN. Each layout links the shared chrome and its own ledger
|
|
37
|
+
sheet, not every sheet.
|
|
38
|
+
|
|
39
|
+
No `pandorga:` config keys were added or renamed. `srcset` and `sizes` are
|
|
40
|
+
optional fields on objects in `pages/home/portraits.json`.
|
|
41
|
+
|
|
3
42
|
## [1.5.1] — 2026-10-10
|
|
4
43
|
|
|
5
44
|
### Fixed
|
|
@@ -60,6 +60,13 @@
|
|
|
60
60
|
},
|
|
61
61
|
showLoadingState: function (element, message) {
|
|
62
62
|
if (!element) return;
|
|
63
|
+
// Reserved skeletons already occupy the box. Replacing them with a
|
|
64
|
+
// one-line message is the shift the listing then repeats when the
|
|
65
|
+
// cards arrive.
|
|
66
|
+
if (element.hasAttribute('data-content-reserve')) {
|
|
67
|
+
element.setAttribute('aria-busy', 'true');
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
63
70
|
element.innerHTML = '<p class="font-body text-on-surface-variant italic col-span-full">' + escapeHtml(message) + '</p>';
|
|
64
71
|
},
|
|
65
72
|
showErrorState: function (element, message) {
|
|
@@ -344,14 +344,15 @@
|
|
|
344
344
|
var slug = item.slug || f.slug || '';
|
|
345
345
|
var index = options.index;
|
|
346
346
|
|
|
347
|
-
|
|
347
|
+
// A selectable row is a real button. role=button on an article is
|
|
348
|
+
// what the audit flags, and the button already handles Enter and Space.
|
|
349
|
+
var row = document.createElement(selectable ? 'button' : 'article');
|
|
348
350
|
row.className = 'home-project-inventory-row group' +
|
|
349
351
|
(selected ? ' is-selected' : '');
|
|
352
|
+
if (selectable) row.type = 'button';
|
|
350
353
|
if (slug) row.setAttribute('data-slug', slug);
|
|
351
354
|
|
|
352
355
|
if (selectable) {
|
|
353
|
-
row.setAttribute('role', 'button');
|
|
354
|
-
row.setAttribute('tabindex', '0');
|
|
355
356
|
row.setAttribute('aria-pressed', selected ? 'true' : 'false');
|
|
356
357
|
row.setAttribute('aria-label', name);
|
|
357
358
|
}
|
|
@@ -13,6 +13,19 @@
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
async function listPaths(collection, manifestKey) {
|
|
16
|
+
var pre = window.__pandorgaPreload && window.__pandorgaPreload['manifest.json'];
|
|
17
|
+
if (pre && typeof pre.then === 'function') {
|
|
18
|
+
try {
|
|
19
|
+
var preloaded = await pre;
|
|
20
|
+
assertManifestSchema(preloaded);
|
|
21
|
+
var preloadedFiles = preloaded.files && preloaded.files[manifestKey];
|
|
22
|
+
if (Array.isArray(preloadedFiles)) return preloadedFiles;
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (error && String(error.message || '').indexOf('Unsupported content schema') === 0) throw error;
|
|
25
|
+
delete window.__pandorgaPreload['manifest.json'];
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
var manifestResponse = await fetch(buildUrl('manifest.json'));
|
|
17
30
|
if (manifestResponse.ok) {
|
|
18
31
|
var manifest = await manifestResponse.json();
|
|
@@ -63,16 +76,31 @@
|
|
|
63
76
|
return collectionCache[cacheKey];
|
|
64
77
|
}
|
|
65
78
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
if (jsonCache[key]) return jsonCache[key];
|
|
69
|
-
jsonCache[key] = fetch(buildUrl(path)).then(function (response) {
|
|
79
|
+
function fetchJson(path) {
|
|
80
|
+
return fetch(buildUrl(path)).then(function (response) {
|
|
70
81
|
if (!response.ok) throw new Error('Content JSON fetch failed: ' + path);
|
|
71
82
|
return response.json();
|
|
72
|
-
}).catch(function (error) {
|
|
73
|
-
delete jsonCache[key];
|
|
74
|
-
throw error;
|
|
75
83
|
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function loadJson(path) {
|
|
87
|
+
var key = String(path || '');
|
|
88
|
+
if (jsonCache[key]) return jsonCache[key];
|
|
89
|
+
var pre = window.__pandorgaPreload && window.__pandorgaPreload[key];
|
|
90
|
+
jsonCache[key] = Promise.resolve()
|
|
91
|
+
.then(function () {
|
|
92
|
+
if (pre && typeof pre.then === 'function') return pre;
|
|
93
|
+
return fetchJson(path);
|
|
94
|
+
})
|
|
95
|
+
.catch(function () {
|
|
96
|
+
if (!(pre && typeof pre.then === 'function')) throw new Error('Content JSON fetch failed: ' + path);
|
|
97
|
+
delete window.__pandorgaPreload[key];
|
|
98
|
+
return fetchJson(path);
|
|
99
|
+
})
|
|
100
|
+
.catch(function (error) {
|
|
101
|
+
delete jsonCache[key];
|
|
102
|
+
throw error;
|
|
103
|
+
});
|
|
76
104
|
return jsonCache[key];
|
|
77
105
|
}
|
|
78
106
|
|
|
@@ -192,10 +220,21 @@
|
|
|
192
220
|
options = options || {};
|
|
193
221
|
var el = typeof elementOrId === 'string' ? document.getElementById(elementOrId) : elementOrId;
|
|
194
222
|
if (!el) return Promise.resolve();
|
|
195
|
-
|
|
196
|
-
|
|
223
|
+
// A quiet slot keeps its reserved box. Writing "Loading..." into the
|
|
224
|
+
// hero summary makes that placeholder the LCP, then swaps it away.
|
|
225
|
+
var quiet = el.hasAttribute('data-content-quiet');
|
|
226
|
+
var loadingText = quiet ? '' : (options.loadingText || 'Loading...');
|
|
227
|
+
if (loadingText) {
|
|
228
|
+
el.innerHTML = '<p class="font-body text-on-surface-variant italic">' + escapeHtml(loadingText) + '</p>';
|
|
229
|
+
}
|
|
197
230
|
return loadJson(pageFragmentPath(relativePath)).then(function (fragment) {
|
|
198
|
-
|
|
231
|
+
var html = renderMarkdown(fragment.body_markdown || '');
|
|
232
|
+
if (el.getAttribute('data-content-painted') === '1' && el.innerHTML === html) {
|
|
233
|
+
el.removeAttribute('data-content-painted');
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
el.innerHTML = html;
|
|
237
|
+
el.removeAttribute('data-content-painted');
|
|
199
238
|
}).catch(function (error) {
|
|
200
239
|
console.error(error);
|
|
201
240
|
el.innerHTML = '<p class="font-body text-on-surface-variant italic">' + escapeHtml(options.errorText || 'Content could not be loaded right now.') + '</p>';
|
|
@@ -473,11 +512,36 @@
|
|
|
473
512
|
var src = typeof entry.src === 'string' ? entry.src.trim() : '';
|
|
474
513
|
if (!src) continue;
|
|
475
514
|
var alt = typeof entry.alt === 'string' ? entry.alt.trim() : '';
|
|
476
|
-
|
|
515
|
+
var record = { src: src, alt: alt };
|
|
516
|
+
// Optional responsive fields. Absent keys stay off the record so a
|
|
517
|
+
// pool that only has src keeps the previous shape.
|
|
518
|
+
var srcset = typeof entry.srcset === 'string' ? entry.srcset.trim() : '';
|
|
519
|
+
var sizes = typeof entry.sizes === 'string' ? entry.sizes.trim() : '';
|
|
520
|
+
if (srcset) record.srcset = srcset;
|
|
521
|
+
if (sizes) record.sizes = sizes;
|
|
522
|
+
entries.push(record);
|
|
477
523
|
}
|
|
478
524
|
return entries;
|
|
479
525
|
}
|
|
480
526
|
|
|
527
|
+
function resolveSrcset(srcset) {
|
|
528
|
+
return String(srcset || '').split(',').map(function (part) {
|
|
529
|
+
var bits = part.trim().split(/\s+/);
|
|
530
|
+
if (!bits[0]) return '';
|
|
531
|
+
bits[0] = resolveAssetUrl(bits[0]);
|
|
532
|
+
return bits.filter(Boolean).join(' ');
|
|
533
|
+
}).filter(Boolean).join(', ');
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function applyHeroPortraitSources(img, chosen) {
|
|
537
|
+
var srcset = resolveSrcset(chosen && chosen.srcset);
|
|
538
|
+
var sizes = chosen && typeof chosen.sizes === 'string' ? chosen.sizes.trim() : '';
|
|
539
|
+
if (srcset) img.setAttribute('srcset', srcset);
|
|
540
|
+
else img.removeAttribute('srcset');
|
|
541
|
+
if (sizes) img.setAttribute('sizes', sizes);
|
|
542
|
+
else img.removeAttribute('sizes');
|
|
543
|
+
}
|
|
544
|
+
|
|
481
545
|
function pickHeroPortrait(entries, random) {
|
|
482
546
|
if (!entries || !entries.length) return null;
|
|
483
547
|
var roll = typeof random === 'function' ? random() : Math.random();
|
|
@@ -491,6 +555,7 @@
|
|
|
491
555
|
if (!nextSrc) return Promise.resolve(false);
|
|
492
556
|
if (img.getAttribute('src') === nextSrc) {
|
|
493
557
|
img.alt = nextAlt;
|
|
558
|
+
applyHeroPortraitSources(img, chosen);
|
|
494
559
|
img.classList.add('is-shown');
|
|
495
560
|
return Promise.resolve(true);
|
|
496
561
|
}
|
|
@@ -499,6 +564,7 @@
|
|
|
499
564
|
probe.onload = function () {
|
|
500
565
|
img.src = nextSrc;
|
|
501
566
|
img.alt = nextAlt;
|
|
567
|
+
applyHeroPortraitSources(img, chosen);
|
|
502
568
|
img.classList.add('is-shown');
|
|
503
569
|
resolve(true);
|
|
504
570
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
Start the JSON fetches the first paint depends on, before the stylesheets
|
|
3
|
+
and the content runtime. loadJson and listPaths reuse window.__pandorgaPreload.
|
|
4
|
+
{%- endcomment -%}
|
|
5
|
+
{%- case page.layout -%}
|
|
6
|
+
{%- when "home" -%}
|
|
7
|
+
{%- assign _preload_paths = "pages/cv/summary.json|pages/home/subtitle.json|pages/home/portraits.json|pages/headers.json|data/writing_index.json" -%}
|
|
8
|
+
{%- when "articles" -%}
|
|
9
|
+
{%- assign _preload_paths = "data/writing_index.json|pages/headers.json|manifest.json" -%}
|
|
10
|
+
{%- when "blog" -%}
|
|
11
|
+
{%- assign _preload_paths = "data/writing_index.json|pages/headers.json|manifest.json" -%}
|
|
12
|
+
{%- when "cv" -%}
|
|
13
|
+
{%- assign _preload_paths = "pages/cv/summary.json|pages/cv/disclaimer.json|data/education.json|data/languages.json|pages/headers.json|manifest.json" -%}
|
|
14
|
+
{%- when "projects" -%}
|
|
15
|
+
{%- assign _preload_paths = "data/writing_index.json|pages/headers.json|manifest.json" -%}
|
|
16
|
+
{%- else -%}
|
|
17
|
+
{%- assign _preload_paths = "" -%}
|
|
18
|
+
{%- endcase -%}
|
|
19
|
+
{%- if _preload_paths != blank -%}
|
|
20
|
+
<script>
|
|
21
|
+
(function () {
|
|
22
|
+
var base = {{ site.content_api_base_url | default: "" | jsonify }};
|
|
23
|
+
base = String(base || "").replace(/\/$/, "");
|
|
24
|
+
var paths = {{ _preload_paths | split: "|" | jsonify }};
|
|
25
|
+
window.__pandorgaPreload = window.__pandorgaPreload || {};
|
|
26
|
+
paths.forEach(function (path) {
|
|
27
|
+
var url = base ? base + "/" + path : "/" + path;
|
|
28
|
+
window.__pandorgaPreload[path] = fetch(url, { credentials: "same-origin" }).then(function (response) {
|
|
29
|
+
if (!response.ok) throw new Error(path);
|
|
30
|
+
return response.json();
|
|
31
|
+
});
|
|
32
|
+
});
|
|
33
|
+
})();
|
|
34
|
+
</script>
|
|
35
|
+
{%- endif -%}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{%- comment -%}
|
|
2
|
+
WebSite and Person from identity. No new required keys: name is already
|
|
3
|
+
required, and url is the existing optional canonical origin.
|
|
4
|
+
{%- endcomment -%}
|
|
5
|
+
{%- assign ld_name = site.pandorga.identity.name | default: site.author | default: site.title -%}
|
|
6
|
+
{%- assign ld_url = site.pandorga.identity.url | default: site.url -%}
|
|
7
|
+
{%- if ld_url != blank -%}
|
|
8
|
+
{%- assign ld_website = ld_url -%}
|
|
9
|
+
{%- else -%}
|
|
10
|
+
{%- assign ld_website = "/" | absolute_url -%}
|
|
11
|
+
{%- endif -%}
|
|
12
|
+
{%- if ld_name != blank -%}
|
|
13
|
+
<script type="application/ld+json">
|
|
14
|
+
{
|
|
15
|
+
"@context": "https://schema.org",
|
|
16
|
+
"@graph": [
|
|
17
|
+
{
|
|
18
|
+
"@type": "WebSite",
|
|
19
|
+
"name": {{ ld_name | jsonify }},
|
|
20
|
+
"url": {{ ld_website | jsonify }}
|
|
21
|
+
{%- if site.description != blank -%},
|
|
22
|
+
"description": {{ site.description | jsonify }}
|
|
23
|
+
{%- endif -%}
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"@type": "Person",
|
|
27
|
+
"name": {{ ld_name | jsonify }}
|
|
28
|
+
{%- if ld_url != blank -%},
|
|
29
|
+
"url": {{ ld_url | jsonify }}
|
|
30
|
+
{%- endif -%}
|
|
31
|
+
}
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
</script>
|
|
35
|
+
{%- endif -%}
|
data/_layouts/articles.html
CHANGED
|
@@ -30,7 +30,16 @@ layout: shell
|
|
|
30
30
|
image column, and one that reserves space for a picture that does not
|
|
31
31
|
exist is worse than no panel at all.
|
|
32
32
|
{%- endcomment -%}
|
|
33
|
-
<div id="articles-lead"></div>
|
|
33
|
+
<div id="articles-lead" class="is-pending"></div>
|
|
34
|
+
<script>
|
|
35
|
+
(function () {
|
|
36
|
+
var page = parseInt(new URLSearchParams(window.location.search).get('page') || '1', 10);
|
|
37
|
+
var lead = document.getElementById('articles-lead');
|
|
38
|
+
if (!lead || !(page > 1)) return;
|
|
39
|
+
lead.classList.remove('is-pending');
|
|
40
|
+
lead.hidden = true;
|
|
41
|
+
})();
|
|
42
|
+
</script>
|
|
34
43
|
|
|
35
44
|
<div class="writing-body">
|
|
36
45
|
|
|
@@ -90,8 +99,14 @@ layout: shell
|
|
|
90
99
|
</div>
|
|
91
100
|
|
|
92
101
|
<div class="writing-archive" id="articles-list">
|
|
93
|
-
<section id="empty-state" class="writing-archive-
|
|
94
|
-
<p class="
|
|
102
|
+
<section id="empty-state" class="writing-archive-reserve" aria-busy="true">
|
|
103
|
+
<p class="sr-only">Loading articles...</p>
|
|
104
|
+
<span class="writing-reserve-row" aria-hidden="true"></span>
|
|
105
|
+
<span class="writing-reserve-row" aria-hidden="true"></span>
|
|
106
|
+
<span class="writing-reserve-row" aria-hidden="true"></span>
|
|
107
|
+
<span class="writing-reserve-row" aria-hidden="true"></span>
|
|
108
|
+
<span class="writing-reserve-row" aria-hidden="true"></span>
|
|
109
|
+
<span class="writing-reserve-row" aria-hidden="true"></span>
|
|
95
110
|
</section>
|
|
96
111
|
<section id="no-results" class="writing-archive-state hidden">
|
|
97
112
|
<p class="font-body text-on-surface-variant italic">Nothing matches these filters yet.</p>
|
|
@@ -253,7 +268,11 @@ layout: shell
|
|
|
253
268
|
function renderListing(state) {
|
|
254
269
|
Array.from(grid.querySelectorAll('[data-dynamic-article]')).forEach(function (el) { el.remove(); });
|
|
255
270
|
if (emptyState) emptyState.remove();
|
|
256
|
-
if (leadRoot)
|
|
271
|
+
if (leadRoot) {
|
|
272
|
+
leadRoot.classList.remove('is-pending');
|
|
273
|
+
leadRoot.innerHTML = '';
|
|
274
|
+
leadRoot.hidden = true;
|
|
275
|
+
}
|
|
257
276
|
|
|
258
277
|
var visible = visibleArticles(state);
|
|
259
278
|
if (orderNote) orderNote.textContent = SORT_LABELS[state.sort] || SORT_LABELS.updated;
|
|
@@ -281,7 +300,10 @@ layout: shell
|
|
|
281
300
|
|
|
282
301
|
var totalPages = Math.max(1, Math.ceil(rest.length / perPage));
|
|
283
302
|
if (currentPage > totalPages) currentPage = totalPages;
|
|
284
|
-
if (currentPage === 1)
|
|
303
|
+
if (currentPage === 1) {
|
|
304
|
+
renderLead(featured);
|
|
305
|
+
if (leadRoot && leadRoot.childElementCount) leadRoot.hidden = false;
|
|
306
|
+
}
|
|
285
307
|
|
|
286
308
|
rest.slice((currentPage - 1) * perPage, currentPage * perPage).forEach(renderRow);
|
|
287
309
|
listing.renderPagination(controls, currentPage, totalPages);
|
data/_layouts/cv.html
CHANGED
|
@@ -25,43 +25,45 @@ layout: shell
|
|
|
25
25
|
|
|
26
26
|
<div id="cv-disclaimer" class="cv-disclaimer-box">
|
|
27
27
|
<span class="material-symbols-outlined cv-dossier-note-icon" aria-hidden="true">info</span>
|
|
28
|
-
<div class="cv-dossier-note font-body" data-content-fragment="cv/disclaimer">
|
|
29
|
-
<p class="font-body text-on-surface-variant italic">Loading...</p>
|
|
30
|
-
</div>
|
|
28
|
+
<div class="cv-dossier-note font-body" data-content-fragment="cv/disclaimer" data-content-quiet></div>
|
|
31
29
|
</div>
|
|
32
30
|
|
|
33
31
|
<div id="cv-listing">
|
|
34
32
|
{% capture cv_summary %}
|
|
35
|
-
<div class="cv-summary font-body" data-content-fragment="cv/summary">
|
|
36
|
-
<p class="font-body text-on-surface-variant italic">Loading...</p>
|
|
37
|
-
</div>
|
|
33
|
+
<div class="cv-summary font-body" data-content-fragment="cv/summary" data-content-quiet></div>
|
|
38
34
|
{% endcapture %}
|
|
39
35
|
{% include cv/cv-box.html id="summary" title="Summary" content=cv_summary %}
|
|
40
36
|
|
|
41
37
|
{% capture cv_experience %}
|
|
42
|
-
<div id="cv-experience-list" class="cv-entry-stack">
|
|
43
|
-
<
|
|
38
|
+
<div id="cv-experience-list" class="cv-entry-stack" data-content-reserve aria-busy="true">
|
|
39
|
+
<span class="cv-reserve-card" aria-hidden="true"></span>
|
|
40
|
+
<span class="cv-reserve-card" aria-hidden="true"></span>
|
|
41
|
+
<span class="cv-reserve-card" aria-hidden="true"></span>
|
|
44
42
|
</div>
|
|
45
43
|
{% endcapture %}
|
|
46
44
|
{% include cv/cv-box.html id="experience" title="Experience" content=cv_experience %}
|
|
47
45
|
|
|
48
46
|
{% capture cv_products %}
|
|
49
|
-
<div id="cv-products-grid" class="cv-product-grid">
|
|
50
|
-
<
|
|
47
|
+
<div id="cv-products-grid" class="cv-product-grid" data-content-reserve aria-busy="true">
|
|
48
|
+
<span class="cv-reserve-card" aria-hidden="true"></span>
|
|
49
|
+
<span class="cv-reserve-card" aria-hidden="true"></span>
|
|
50
|
+
<span class="cv-reserve-card" aria-hidden="true"></span>
|
|
51
51
|
</div>
|
|
52
52
|
{% endcapture %}
|
|
53
53
|
{% include cv/cv-box.html id="products" title="Products" content=cv_products %}
|
|
54
54
|
|
|
55
55
|
{% capture cv_education %}
|
|
56
|
-
<div id="cv-education-list" class="cv-entry-stack">
|
|
57
|
-
<
|
|
56
|
+
<div id="cv-education-list" class="cv-entry-stack" data-content-reserve aria-busy="true">
|
|
57
|
+
<span class="cv-reserve-card" aria-hidden="true"></span>
|
|
58
|
+
<span class="cv-reserve-card" aria-hidden="true"></span>
|
|
58
59
|
</div>
|
|
59
60
|
{% endcapture %}
|
|
60
61
|
{% include cv/cv-box.html id="education" title="Education" content=cv_education %}
|
|
61
62
|
|
|
62
63
|
{% capture cv_languages %}
|
|
63
|
-
<div id="cv-languages-list" class="cv-language-list">
|
|
64
|
-
<
|
|
64
|
+
<div id="cv-languages-list" class="cv-language-list" data-content-reserve aria-busy="true">
|
|
65
|
+
<span class="cv-reserve-card" aria-hidden="true"></span>
|
|
66
|
+
<span class="cv-reserve-card" aria-hidden="true"></span>
|
|
65
67
|
</div>
|
|
66
68
|
{% endcapture %}
|
|
67
69
|
{% include cv/cv-box.html id="languages" title="Languages" content=cv_languages %}
|
|
@@ -327,6 +329,7 @@ layout: shell
|
|
|
327
329
|
productMap[product.slug] = product;
|
|
328
330
|
});
|
|
329
331
|
experienceList.innerHTML = '';
|
|
332
|
+
experienceList.removeAttribute('aria-busy');
|
|
330
333
|
jobs.sort(function (a, b) {
|
|
331
334
|
var as = (a.front_matter && a.front_matter.start) || '';
|
|
332
335
|
var bs = (b.front_matter && b.front_matter.start) || '';
|
|
@@ -344,6 +347,7 @@ layout: shell
|
|
|
344
347
|
runtime.Listing.showLoadingState(educationList, 'Loading education...');
|
|
345
348
|
runtime.loadJson('data/education.json').then(function (entries) {
|
|
346
349
|
educationList.innerHTML = '';
|
|
350
|
+
educationList.removeAttribute('aria-busy');
|
|
347
351
|
entries.forEach(function (entry) { educationList.appendChild(renderEducation(entry)); });
|
|
348
352
|
}).catch(function (error) {
|
|
349
353
|
console.error(error);
|
|
@@ -355,6 +359,7 @@ layout: shell
|
|
|
355
359
|
runtime.Listing.showLoadingState(languagesList, 'Loading languages...');
|
|
356
360
|
runtime.loadJson('data/languages.json').then(function (entries) {
|
|
357
361
|
languagesList.innerHTML = '';
|
|
362
|
+
languagesList.removeAttribute('aria-busy');
|
|
358
363
|
entries.forEach(function (entry) { languagesList.appendChild(renderLanguage(entry)); });
|
|
359
364
|
}).catch(function (error) {
|
|
360
365
|
console.error(error);
|
|
@@ -366,6 +371,7 @@ layout: shell
|
|
|
366
371
|
runtime.Listing.showLoadingState(productsGrid, 'Loading products...');
|
|
367
372
|
runtime.loadWritingIndex('products').then(function (products) {
|
|
368
373
|
productsGrid.innerHTML = '';
|
|
374
|
+
productsGrid.removeAttribute('aria-busy');
|
|
369
375
|
var ordered = products.slice().sort(compareProductsByEndDate);
|
|
370
376
|
ordered.forEach(function (product) {
|
|
371
377
|
productsGrid.appendChild(renderProduct(product));
|
data/_layouts/home.html
CHANGED
|
@@ -29,12 +29,12 @@ layout: shell
|
|
|
29
29
|
<div class="home-hero">
|
|
30
30
|
<div class="home-hero-copy">
|
|
31
31
|
<div id="home-subtitle-content" class="home-hero-tagline font-label"
|
|
32
|
-
data-content-fragment="home/subtitle"></div>
|
|
32
|
+
data-content-fragment="home/subtitle" data-content-quiet></div>
|
|
33
33
|
<h1 class="home-hero-title">
|
|
34
34
|
<span class="home-hero-given">{{ _hero_name.given }}</span>{%- if _hero_name.family != "" %} <span class="home-hero-family">{{ _hero_name.family }}</span>{%- endif %}
|
|
35
35
|
</h1>
|
|
36
36
|
<div id="home-cv-summary" class="home-hero-summary font-body"
|
|
37
|
-
data-content-fragment="cv/summary"></div>
|
|
37
|
+
data-content-fragment="cv/summary" data-content-quiet></div>
|
|
38
38
|
<div class="home-hero-actions">
|
|
39
39
|
{%- if _cv_page -%}
|
|
40
40
|
<a class="ledger-cta ledger-cta--emphasis home-hero-cta" href="{{ _cv_page.url }}">
|
|
@@ -50,7 +50,7 @@ layout: shell
|
|
|
50
50
|
<div class="home-hero-plate-wrap">
|
|
51
51
|
<span class="ledger-frame-crosshairs" aria-hidden="true"></span>
|
|
52
52
|
<figure class="home-hero-plate">
|
|
53
|
-
<img alt="" width="1200" height="800" data-content-hero-portrait />
|
|
53
|
+
<img alt="" width="1200" height="800" fetchpriority="high" data-content-hero-portrait />
|
|
54
54
|
<span class="ledger-frame-corners" aria-hidden="true"></span>
|
|
55
55
|
</figure>
|
|
56
56
|
</div>
|
|
@@ -58,6 +58,26 @@ layout: shell
|
|
|
58
58
|
</div>
|
|
59
59
|
</div>
|
|
60
60
|
</section>
|
|
61
|
+
<script>
|
|
62
|
+
(function () {
|
|
63
|
+
var runtime = window.ContentRuntime;
|
|
64
|
+
var pre = window.__pandorgaPreload || {};
|
|
65
|
+
function paint(id, path) {
|
|
66
|
+
var el = document.getElementById(id);
|
|
67
|
+
var pending = pre[path];
|
|
68
|
+
if (!el || !pending || !runtime || !runtime.renderMarkdown) return;
|
|
69
|
+
pending.then(function (fragment) {
|
|
70
|
+
if (!fragment || el.getAttribute('data-content-painted') === '1') return;
|
|
71
|
+
var html = runtime.renderMarkdown(fragment.body_markdown || '');
|
|
72
|
+
if (!html) return;
|
|
73
|
+
el.innerHTML = html;
|
|
74
|
+
el.setAttribute('data-content-painted', '1');
|
|
75
|
+
}).catch(function () {});
|
|
76
|
+
}
|
|
77
|
+
paint('home-subtitle-content', 'pages/home/subtitle.json');
|
|
78
|
+
paint('home-cv-summary', 'pages/cv/summary.json');
|
|
79
|
+
})();
|
|
80
|
+
</script>
|
|
61
81
|
|
|
62
82
|
{%- for band in _home_bands -%}
|
|
63
83
|
{%- if band.kind == "index" -%}
|
data/_layouts/shell.html
CHANGED
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
{% assign canonical_path = page.canonical_url | default: page.url %}
|
|
14
14
|
<title>{{ meta_title | strip | escape_once }}</title>
|
|
15
15
|
<meta name="description" content="{{ meta_description | escape_once }}" />
|
|
16
|
+
{% include theme/json-ld.html %}
|
|
16
17
|
<!-- Open Graph / Social Sharing -->
|
|
17
18
|
{% assign og_type = page.og_type | default: "website" %}
|
|
18
19
|
<meta property="og:type" content="{{ og_type }}" />
|
|
@@ -57,17 +58,54 @@
|
|
|
57
58
|
<meta name="twitter:image" content="{{ og_image_url }}" />
|
|
58
59
|
<!-- Canonical URL -->
|
|
59
60
|
<link rel="canonical" href="{{ canonical_path | absolute_url }}" />
|
|
61
|
+
{% include theme/critical-preload.html %}
|
|
60
62
|
<link rel="stylesheet" href="/assets/css/base.css" />
|
|
61
|
-
<link rel="stylesheet" href="/assets/css/article.css" />
|
|
62
|
-
{% include theme/theme-vars.html %}
|
|
63
|
-
<link rel="stylesheet" href="/assets/css/cv.css" />
|
|
64
63
|
<link rel="stylesheet" href="/assets/css/global-link.css" />
|
|
65
|
-
{
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
64
|
+
{% include theme/theme-vars.html %}
|
|
65
|
+
<link rel="stylesheet" href="/assets/css/ledger/chrome.css" />
|
|
66
|
+
<link rel="stylesheet" href="/assets/css/ledger/footer.css" />
|
|
67
|
+
{%- comment -%}
|
|
68
|
+
Ledger sheets stay with the layout that uses them. article.css is the
|
|
69
|
+
reading column, so only detail-capable layouts load it. An unknown
|
|
70
|
+
layout still receives every sheet.
|
|
71
|
+
{%- endcomment -%}
|
|
72
|
+
{%- case page.layout -%}
|
|
73
|
+
{%- when "home" -%}
|
|
74
|
+
{%- assign _ledger = "home" | split: "|" -%}
|
|
75
|
+
{%- assign _article_css = false -%}
|
|
76
|
+
{%- when "articles" -%}
|
|
77
|
+
{%- assign _ledger = "writing|detail" | split: "|" -%}
|
|
78
|
+
{%- assign _article_css = true -%}
|
|
79
|
+
{%- when "blog" -%}
|
|
80
|
+
{%- assign _ledger = "rascunhos|detail" | split: "|" -%}
|
|
81
|
+
{%- assign _article_css = true -%}
|
|
82
|
+
{%- when "cv" -%}
|
|
83
|
+
{%- assign _ledger = "cv|detail" | split: "|" -%}
|
|
84
|
+
{%- assign _article_css = true -%}
|
|
85
|
+
{%- when "projects" -%}
|
|
86
|
+
{%- assign _ledger = "projects|detail" | split: "|" -%}
|
|
87
|
+
{%- assign _article_css = true -%}
|
|
88
|
+
{%- when "resources" -%}
|
|
89
|
+
{%- assign _ledger = "sources" | split: "|" -%}
|
|
90
|
+
{%- assign _article_css = false -%}
|
|
91
|
+
{%- when "network" -%}
|
|
92
|
+
{%- assign _ledger = "network" | split: "|" -%}
|
|
93
|
+
{%- assign _article_css = false -%}
|
|
94
|
+
{%- when "bibliography" -%}
|
|
95
|
+
{%- assign _ledger = "bibliography" | split: "|" -%}
|
|
96
|
+
{%- assign _article_css = false -%}
|
|
97
|
+
{%- when "text" -%}
|
|
98
|
+
{%- assign _ledger = "detail" | split: "|" -%}
|
|
99
|
+
{%- assign _article_css = true -%}
|
|
100
|
+
{%- else -%}
|
|
101
|
+
{%- assign _ledger = "home|writing|detail|projects|rascunhos|bibliography|network|sources|cv" | split: "|" -%}
|
|
102
|
+
{%- assign _article_css = true -%}
|
|
103
|
+
{%- endcase -%}
|
|
104
|
+
{%- if _article_css -%}
|
|
105
|
+
<link rel="stylesheet" href="/assets/css/article.css" />
|
|
106
|
+
{%- endif -%}
|
|
107
|
+
{%- for _sheet in _ledger -%}
|
|
108
|
+
<link rel="stylesheet" href="/assets/css/ledger/{{ _sheet }}.css" />
|
|
71
109
|
{%- endfor -%}
|
|
72
110
|
<link href="https://fonts.googleapis.com" rel="preconnect" />
|
|
73
111
|
<link crossorigin="" href="https://fonts.gstatic.com" rel="preconnect" />
|
|
@@ -78,8 +116,7 @@
|
|
|
78
116
|
<style>
|
|
79
117
|
:root { --drop-cap-lines: {{ site.drop_cap_lines | default: 2 }}; }
|
|
80
118
|
</style>
|
|
81
|
-
<
|
|
82
|
-
<script src="/assets/tailwind-play.js"></script>
|
|
119
|
+
<link rel="stylesheet" href="/assets/css/tailwind.css" />
|
|
83
120
|
</head>
|
|
84
121
|
|
|
85
122
|
<body class="bg-background text-on-surface">
|
data/assets/css/ledger/cv.css
CHANGED
|
@@ -419,6 +419,19 @@ html[data-theme="light"] .cv-product {
|
|
|
419
419
|
padding: var(--space-xl);
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
+
.cv-summary:empty,
|
|
423
|
+
.cv-dossier-note:empty {
|
|
424
|
+
min-height: calc(var(--type-body) * var(--leading-body) * 4);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
.cv-reserve-card {
|
|
428
|
+
display: block;
|
|
429
|
+
min-height: 6.5rem;
|
|
430
|
+
border: 1.5px solid var(--border-ink);
|
|
431
|
+
border-radius: var(--radius-card);
|
|
432
|
+
background: var(--color-surface-card);
|
|
433
|
+
}
|
|
434
|
+
|
|
422
435
|
.cv-summary p {
|
|
423
436
|
margin: 0 0 1em;
|
|
424
437
|
}
|