pandorga 1.3.6 → 1.3.7

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: c6a4f2b9d6301de61dc8a70de9ea3e47838f8896ac2b87f0cd77e07665c0030d
4
- data.tar.gz: 41c8cefdadd4e2b58d2cf424f587bcb70c98531b870f04a8bfdef7ee0420a33c
3
+ metadata.gz: ea6a5c5a2e3a25b789f30f4ade187c45f3167de07673934e29ee608923e4b1a9
4
+ data.tar.gz: 211bcfc642bd1b0f1c587fca41a225f264d2121bb914bc469d66c6e82fb58337
5
5
  SHA512:
6
- metadata.gz: 26e0bbd8c1f915e22a81e34f365e70e7302cd983701730ecdef31bb11a3892a78fad827651cd364276a7cd87b74566b5dd9f60ff208cd9ad9b90bb3df764f7c0
7
- data.tar.gz: 3a4c0e1a3641a3558c02867e832424e12f2bcbf20d8d9db0675387f314ed550d780cd870930cc03ecdc8fb5e75898de6d79698ac78fc636b4ab2e9c243e8827e
6
+ metadata.gz: 87f3c7a0881d7a68b901be98353bebc45419482c25334bd721eaaa989bc25ceeb362d36bb3dcdff6f380c3a1b7c359e3679e4b78d6b1b1b84fb1ff2c7b1a8575
7
+ data.tar.gz: 25e0640b55107ff5c5f3c948547c3d91662a70b36e8bf40d717e6a7ca6af0056ac53114de574fb0db1c577cfea41b6602ac45c397234e9a2bfbc724973cbf301
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.3.7] — 2026-10-06
4
+
5
+ ### Added
6
+
7
+ - Home projects band specimen bench: project specimen, rack card, and tape
8
+ blocks, with inventory rows and glyph/initial fallbacks when an icon is
9
+ missing
10
+
11
+ ### Fixed
12
+
13
+ - Specimen stays half-width and 16:9; title always sits below the plate with
14
+ roomier copy and links; drafting tape removed from the home layout
15
+
3
16
  ## [1.3.6] — 2026-10-05
4
17
 
5
18
  ### Fixed
data/DESIGN.md CHANGED
@@ -935,11 +935,20 @@ Letter-spacing in use, by frequency: `0.1em` (12), `0.08em` (10), `0.12em` (7),
935
935
  writing, rascunhos, systems, ideas graph. The featured band carries a 6rem ×
936
936
  3px `primary` rule at its top-left corner as a structural mark. The three
937
937
  writing cards take `primary` / `secondary` / `tertiary` 2px top rules in
938
- rotation. The systems band is a specimen + inventory bench from 900px —
939
- a 40% / 60% split: 16:9 specimen card beside a panel inventory rail whose
940
- height matches the full specimen card, with equal-flex rows (thumb 16:9
941
- left, meta right); list click updates the specimen, specimen click opens
942
- the project. Below 768px the hero portrait runs to the screen edge, pulled
938
+ rotation. The systems band is a specimen bench. The specimen is a
939
+ fixed-height system card (2px `primary` top rule; strip with folio, label,
940
+ start year and status; a 16:9 plate, the poster with an icon badge or the
941
+ icon plate, the name's initial standing in for a missing icon; the title
942
+ always below the plate; a tagline clamped to the
943
+ height the plate leaves; tags, outbound links and an emphasis "Open
944
+ project" cue). From 900px it takes 50% of the band, beside a 34%
945
+ `panelContrast` filing rack of equal-flex project cards (16:9 thumb, code
946
+ line, title, tagline; the selected card steps to `surfaceHigh` with a 3px
947
+ `primary` inset rule), the rest left blank. Rack click swaps the specimen
948
+ in place; specimen click opens the project. Below 900px the rack is an
949
+ icon strip above the specimen, one equal column per project (up to six)
950
+ so it spans the band. Contract: `PLT-AC-13`–`17` in
951
+ `templates/portfolio/README.md`. Below 768px the hero portrait runs to the screen edge, pulled
943
952
  out of the band's gutter by exactly `--gutter-mobile` on each side and losing
944
953
  its side hairlines, which at the edge would read as a frame cut off.
945
954
  * **Writing index.** Masthead over a hairline, then a `panelContrast` filter
@@ -324,9 +324,15 @@
324
324
  ].join('');
325
325
  }
326
326
 
327
- /* Compact inventory row for the home portfolio bench.
328
- * Desktop: index + thumb + title + project icon. Mobile strip uses the
329
- * thumb/initial only. Selectable rows update the specimen. */
327
+ function twoDigits(value) {
328
+ var label = String(Number(value));
329
+ return label.length < 2 ? '0' + label : label;
330
+ }
331
+
332
+ /* Rack card for the home portfolio bench.
333
+ * Desktop: thumb (or icon, or initial) beside a code line, title and
334
+ * tagline. The mobile strip shows the project glyph only. Selectable rows
335
+ * update the specimen. */
330
336
  function renderProjectInventoryRow(item, options) {
331
337
  options = options || {};
332
338
  var f = blockFields(item);
@@ -352,22 +358,28 @@
352
358
 
353
359
  var media = renderPosterMedia(fm.thumbnail, name);
354
360
  var initial = String(name || '?').charAt(0).toUpperCase();
355
- if (!media) {
361
+ if (!media && fm.icon) {
362
+ media = '<div class="home-project-inventory-mark" aria-hidden="true">' +
363
+ '<span class="material-symbols-outlined">' + escapeHtml(fm.icon) + '</span></div>';
364
+ } else if (!media) {
356
365
  media = '<div class="home-project-inventory-mark home-project-inventory-mark--initial" aria-hidden="true">' +
357
366
  escapeHtml(initial) + '</div>';
358
367
  }
359
368
 
360
- var html = '';
361
- if (index != null && index !== '') {
362
- var label = String(Number(index));
363
- if (label.length < 2) label = '0' + label;
364
- html += '<span class="home-project-inventory-index" aria-hidden="true">' +
365
- escapeHtml(label) + '</span>';
366
- }
367
- if (media) {
368
- html += '<div class="home-project-inventory-media">' + media + '</div>';
369
- }
369
+ var html = '<div class="home-project-inventory-media">' + media + '</div>';
370
370
  html += '<div class="home-project-inventory-body">';
371
+
372
+ var code = renderMetaStrip([
373
+ index != null && index !== ''
374
+ ? '<span class="code-technical home-project-inventory-index">' + escapeHtml(twoDigits(index)) + '</span>'
375
+ : '',
376
+ fm.label ? escapeHtml(fm.label) : ''
377
+ ]);
378
+ var status = fm.status ? '<span class="ledger-status">' + escapeHtml(fm.status) + '</span>' : '';
379
+ if (code || status) {
380
+ html += '<div class="home-project-inventory-code">' + code + status + '</div>';
381
+ }
382
+
371
383
  if (selectable) {
372
384
  html += '<h3 class="home-project-inventory-title card-title">' +
373
385
  escapeHtml(name) + '</h3>';
@@ -376,6 +388,9 @@
376
388
  '<a class="ledger-card-stretch" href="' + escapeAttr(href) + '">' +
377
389
  escapeHtml(name) + '</a></h3>';
378
390
  }
391
+ if (fm.tagline) {
392
+ html += '<p class="home-project-inventory-copy font-body">' + escapeHtml(fm.tagline) + '</p>';
393
+ }
379
394
  html += '</div>';
380
395
  if (fm.icon) {
381
396
  html += '<span class="home-project-inventory-glyph material-symbols-outlined" aria-hidden="true">' +
@@ -441,12 +456,85 @@
441
456
  // Outbound links only. `.ledger-card-foot` draws a rule above itself, so
442
457
  // an empty one rules off nothing; renderLedgerRow and renderArchiveCard
443
458
  // guard it the same way.
459
+ var links = renderProjectLinks(fm, options);
460
+ if (links) {
461
+ html += '<div class="ledger-card-foot ledger-card-links">' + links + '</div>';
462
+ }
463
+
464
+ card.innerHTML = html;
465
+ return card;
466
+ }
467
+
468
+ function renderProjectLinks(fm, options) {
444
469
  var links = [];
445
470
  if (fm.github) links.push(renderCtaLink(fm.github, options.repoLabel || 'Repository', { icon: 'code' }));
446
471
  if (fm.article_url) links.push(renderCtaLink(fm.article_url, options.articleLabel || 'Article', { icon: 'article' }));
447
472
  if (fm.external_url) links.push(renderCtaLink(fm.external_url, options.externalLabel || 'Visit', { icon: 'public' }));
448
- if (links.length) {
449
- html += '<div class="ledger-card-foot ledger-card-links">' + links.join('') + '</div>';
473
+ return links.join('');
474
+ }
475
+
476
+ /* Home specimen — the featured project on the portfolio bench. A system
477
+ * card whose strip carries folio, label, start year and status, with the
478
+ * title below a 16:9 plate: the thumbnail (icon badge on it) or else the
479
+ * icon plate. `openLabel` adds a visual "open" cue; it is not a second
480
+ * link, the stretched title is. */
481
+ function renderProjectSpecimen(item, options) {
482
+ options = options || {};
483
+ var f = blockFields(item);
484
+ var fm = f.fm;
485
+ var href = options.href || '#';
486
+ var name = fm.project || f.title;
487
+
488
+ var card = document.createElement('article');
489
+ card.className = 'ledger-system-card home-project-specimen group';
490
+
491
+ var year = String(fm.start_date || '').match(/^\d{4}/);
492
+ var meta = renderMetaStrip([
493
+ options.folio ? '<span class="code-technical home-specimen-folio">#' + escapeHtml(twoDigits(options.folio)) + '</span>' : '',
494
+ fm.label ? escapeHtml(fm.label) : '',
495
+ year ? '<span>' + escapeHtml(year[0]) + '</span>' : ''
496
+ ]);
497
+ var status = fm.status ? '<span class="ledger-status">' + escapeHtml(fm.status) + '</span>' : '';
498
+
499
+ var html = '';
500
+ if (meta || status) {
501
+ html += '<div class="home-specimen-strip">' + meta + status + '</div>';
502
+ }
503
+
504
+ // The name's initial stands in for a missing icon, so every project
505
+ // carries the same badge and plate elements.
506
+ var mark = fm.icon
507
+ ? { cls: 'material-symbols-outlined', text: fm.icon }
508
+ : { cls: 'is-initial', text: String(name || '').charAt(0).toUpperCase() };
509
+ var poster = renderPosterMedia(fm.thumbnail, name);
510
+ html += '<div class="home-specimen-figure">';
511
+ if (poster) {
512
+ html += poster + '<span class="home-specimen-badge ' + mark.cls + '" aria-hidden="true">' +
513
+ escapeHtml(mark.text) + '</span>';
514
+ } else {
515
+ html += '<div class="projects-plate projects-plate--mark" aria-hidden="true">' +
516
+ '<span class="' + mark.cls + ' projects-plate-glyph">' + escapeHtml(mark.text) + '</span></div>';
517
+ }
518
+ html += '<h3 class="card-title ledger-card-title home-specimen-title"><a class="ledger-card-stretch" href="' +
519
+ escapeAttr(href) + '">' + escapeHtml(name) + '</a></h3>';
520
+ html += '</div>';
521
+
522
+ var summary = fm.tagline || f.preview;
523
+ if (summary) {
524
+ html += '<p class="ledger-card-copy font-body">' + escapeHtml(summary) + '</p>';
525
+ }
526
+
527
+ html += renderTagChips(f.tags, { hrefFor: options.tagHref });
528
+
529
+ var links = renderProjectLinks(fm, options);
530
+ var open = options.openLabel
531
+ ? '<span class="ledger-cta ledger-cta--emphasis home-specimen-open" aria-hidden="true"><span>' +
532
+ escapeHtml(options.openLabel) +
533
+ '</span><span class="material-symbols-outlined ledger-cta-arrow">chevron_right</span></span>'
534
+ : '';
535
+ if (links || open) {
536
+ html += '<div class="ledger-card-foot home-specimen-foot">' +
537
+ (links ? '<div class="ledger-card-links">' + links + '</div>' : '') + open + '</div>';
450
538
  }
451
539
 
452
540
  card.innerHTML = html;
@@ -64,6 +64,7 @@
64
64
  renderArchiveCard: renderArchiveCard,
65
65
  renderSystemCard: renderSystemCard,
66
66
  renderProjectInventoryRow: renderProjectInventoryRow,
67
+ renderProjectSpecimen: renderProjectSpecimen,
67
68
  renderPosterMedia: renderPosterMedia,
68
69
  ensureBibliographyIndex: ensureBibliographyIndex,
69
70
  bibField: bibField,
@@ -46,7 +46,7 @@
46
46
  data-home-list
47
47
  data-tag-base="{{ _path }}"
48
48
  >
49
- <div data-home-specimen></div>
49
+ <div class="home-project-specimen-slot" data-home-specimen></div>
50
50
  <div class="home-project-inventory" data-home-inventory aria-label="Project inventory"></div>
51
51
  </div>
52
52
  {%- else -%}
data/_layouts/home.html CHANGED
@@ -234,32 +234,47 @@ layout: shell
234
234
  eligible: function () { return true; }
235
235
  });
236
236
  var selectedSlug = (featuredPick && featuredPick.slug) || portfolio[0].slug;
237
- var iconSlots = 6;
237
+ var iconSlots = Math.min(portfolio.length, 6);
238
238
 
239
239
  function entryBySlug(slug) {
240
240
  return portfolio.filter(function (entry) { return entry.slug === slug; })[0] || portfolio[0];
241
241
  }
242
242
 
243
+ function positionOf(slug) {
244
+ for (var i = 0; i < portfolio.length; i += 1) {
245
+ if (portfolio[i].slug === slug) return i;
246
+ }
247
+ return 0;
248
+ }
249
+
250
+ /* Specimen and inventory sizes are CSS-fixed (no ResizeObserver), so
251
+ swapping projects never moves the band. */
243
252
  function paintSpecimen(entry) {
244
253
  specimenSlot.innerHTML = '';
245
- /* No card-level drafting corners / edge tick — markers live on the
246
- poster plate only, so they do not overlap the image. Meta strip
247
- is suppressed; tags take that slot below the plate. Specimen and
248
- inventory sizes are CSS-fixed (no ResizeObserver). */
249
- specimenSlot.appendChild(runtime.renderSystemCard(asItem(entry), {
254
+ specimenSlot.appendChild(runtime.renderProjectSpecimen(asItem(entry), {
250
255
  href: entry.url || runtime.projectUrl(entry.slug),
251
256
  tagHref: systemTagHref,
252
- className: 'home-project-specimen',
253
- glyphFallback: true,
254
- metaParts: []
257
+ folio: positionOf(entry.slug) + 1,
258
+ openLabel: 'Open project'
255
259
  }));
256
260
  }
257
261
 
262
+ function select(slug) {
263
+ if (selectedSlug === slug) return;
264
+ selectedSlug = slug;
265
+ paintSpecimen(entryBySlug(slug));
266
+ Array.prototype.forEach.call(inventorySlot.children, function (node) {
267
+ var isSelected = node.getAttribute('data-slug') === selectedSlug;
268
+ node.classList.toggle('is-selected', isSelected);
269
+ node.setAttribute('aria-pressed', isSelected ? 'true' : 'false');
270
+ });
271
+ }
272
+
258
273
  function paintInventory() {
259
274
  inventorySlot.hidden = false;
260
275
  inventorySlot.innerHTML = '';
261
- /* Always reserve six mobile columns so one project does not stretch
262
- a single giant button across the band. */
276
+ /* One mobile column per project, up to six a row, so the strip
277
+ always spans the band. */
263
278
  inventorySlot.style.setProperty('--home-project-icon-count', String(iconSlots));
264
279
  portfolio.forEach(function (entry, i) {
265
280
  var row = runtime.renderProjectInventoryRow(asItem(entry), {
@@ -267,21 +282,11 @@ layout: shell
267
282
  selected: entry.slug === selectedSlug,
268
283
  index: i + 1
269
284
  });
270
- function select() {
271
- if (selectedSlug === entry.slug) return;
272
- selectedSlug = entry.slug;
273
- paintSpecimen(entry);
274
- Array.prototype.forEach.call(inventorySlot.children, function (node) {
275
- var isSelected = node.getAttribute('data-slug') === selectedSlug;
276
- node.classList.toggle('is-selected', isSelected);
277
- node.setAttribute('aria-pressed', isSelected ? 'true' : 'false');
278
- });
279
- }
280
- row.addEventListener('click', select);
285
+ row.addEventListener('click', function () { select(entry.slug); });
281
286
  row.addEventListener('keydown', function (event) {
282
287
  if (event.key === 'Enter' || event.key === ' ') {
283
288
  event.preventDefault();
284
- select();
289
+ select(entry.slug);
285
290
  }
286
291
  });
287
292
  inventorySlot.appendChild(row);
@@ -943,10 +943,13 @@ html[data-theme="light"] .home-hero-plate {
943
943
  }
944
944
  }
945
945
 
946
- /* ── Engineering artifacts: specimen + index folio ──
947
- Fixed specimen chrome (16:9 plate + reserved body). Mobile icon strip
948
- always reserves six equal slots at a fixed button height. Desktop inventory
949
- sits in a panel container matched to the specimen height via CSS, not JS. */
946
+ /* ── Engineering artifacts: specimen bench ──
947
+ A specimen card beside a filing rack of project cards. The specimen has a
948
+ fixed height, so swapping projects never resizes the band. Every plate is
949
+ 16:9, the ratio the thumbnails are drawn at, with the title below it; the
950
+ tagline takes the height left. From 900px the specimen is half the band and the rack a
951
+ narrower column beside it, the rest left blank. Below 900px the rack is
952
+ an icon strip above the specimen, one column per project. */
950
953
  .home-projects-bench {
951
954
  --home-specimen-height: 32rem;
952
955
  --home-icon-btn-size: 2.75rem;
@@ -957,55 +960,115 @@ html[data-theme="light"] .home-hero-plate {
957
960
  gap: var(--space-md);
958
961
  }
959
962
 
963
+ .home-project-inventory {
964
+ order: -1;
965
+ }
966
+
960
967
  .home-project-specimen {
961
- position: relative;
962
- display: flex;
963
- flex-direction: column;
964
- gap: var(--space-md);
965
- padding: var(--space-md);
966
- padding-bottom: var(--space-lg);
967
- max-width: none;
968
- width: 100%;
969
968
  height: var(--home-specimen-height);
970
969
  overflow: hidden;
971
- border: 0;
972
- background: var(--color-panel-contrast);
970
+ gap: var(--space-sm);
971
+ padding: var(--space-md);
973
972
  box-sizing: border-box;
974
973
  }
975
974
 
976
- :root:not([data-theme="dark"]) .home-project-specimen,
977
- html[data-theme="light"] .home-project-specimen,
978
- [data-theme="dark"] .home-project-specimen {
979
- border: 0;
975
+ /* The triad top rule (`.ledger-card--edge-primary`), restated at a weight
976
+ that beats the light-paper hairline reset on `.ledger-system-card`. */
977
+ :root .home-projects-bench .home-project-specimen,
978
+ :root .home-projects-bench .home-project-specimen:hover {
979
+ border-top: 2px solid var(--color-primary);
980
+ }
981
+
982
+ .home-band--contrast .home-project-specimen {
983
+ background: var(--color-surface-card-contrast);
984
+ }
985
+
986
+ .home-band--contrast .home-project-specimen:hover {
987
+ background: var(--color-surface-high-contrast);
980
988
  }
981
989
 
982
- .home-project-specimen>.ledger-poster,
983
- .home-project-specimen>.projects-plate {
990
+ .home-specimen-strip,
991
+ .home-project-inventory-code {
992
+ display: flex;
984
993
  flex: 0 0 auto;
985
- width: auto;
986
- margin: calc(var(--space-md) * -1) calc(var(--space-md) * -1) 0;
987
- aspect-ratio: 16 / 9;
988
- max-height: calc(var(--home-specimen-height) * 0.5);
989
- position: relative;
994
+ align-items: center;
995
+ justify-content: space-between;
996
+ gap: var(--space-sm);
997
+ min-width: 0;
998
+ }
999
+
1000
+ .home-specimen-strip .ledger-meta,
1001
+ .home-project-inventory-code .ledger-meta {
1002
+ min-width: 0;
1003
+ flex-wrap: nowrap;
990
1004
  overflow: hidden;
1005
+ white-space: nowrap;
991
1006
  }
992
1007
 
993
- .home-project-specimen>.ledger-poster img,
994
- .home-project-specimen>.ledger-poster video {
1008
+ .home-specimen-folio,
1009
+ .home-project-inventory-index {
1010
+ color: var(--color-primary);
1011
+ }
1012
+
1013
+ .home-specimen-strip>.ledger-status,
1014
+ .home-project-inventory-code>.ledger-status {
1015
+ flex: 0 0 auto;
1016
+ font-family: var(--font-label);
1017
+ font-size: var(--type-micro);
1018
+ letter-spacing: 0.06em;
1019
+ text-transform: uppercase;
1020
+ line-height: 1.4;
1021
+ padding: 0.1rem 0.45rem;
1022
+ background: color-mix(in srgb, var(--color-secondary) 14%, transparent);
1023
+ }
1024
+
1025
+ /* The figure holds the plate and, always below it, the title. The badge
1026
+ shares the poster's grid cell; when the image fails, renderPosterMedia
1027
+ removes it and the badge hides with it. */
1028
+ .home-specimen-figure {
1029
+ flex: 0 0 auto;
1030
+ display: grid;
1031
+ gap: var(--space-md);
1032
+ }
1033
+
1034
+ .home-specimen-figure>.ledger-poster,
1035
+ .home-specimen-figure>.projects-plate {
1036
+ grid-area: 1 / 1;
995
1037
  width: 100%;
996
- height: 100%;
997
- object-fit: cover;
1038
+ aspect-ratio: 16 / 9;
1039
+ }
1040
+
1041
+ .home-specimen-figure>.projects-plate {
1042
+ position: relative;
1043
+ display: flex;
1044
+ align-items: center;
1045
+ justify-content: center;
1046
+ background: var(--color-surface-high);
1047
+ color: var(--color-primary);
1048
+ }
1049
+
1050
+ .home-specimen-figure .projects-plate-glyph {
1051
+ font-size: 3rem;
1052
+ }
1053
+
1054
+ .home-specimen-figure .is-initial {
1055
+ font-family: var(--font-content-title);
1056
+ font-weight: var(--weight-title);
1057
+ line-height: 1;
1058
+ }
1059
+
1060
+ .home-specimen-figure:has(> .ledger-poster)>.home-specimen-badge.is-initial {
1061
+ font-size: var(--type-subtitle);
998
1062
  }
999
1063
 
1000
1064
  /* Glyph plates have no .ledger-poster::after — mirror the image corners. */
1001
- .home-project-specimen>.projects-plate::after {
1065
+ .home-specimen-figure>.projects-plate::after {
1002
1066
  content: "";
1003
1067
  --frame-corner-size: 0.7rem;
1004
1068
  --frame-corner-thickness: 1px;
1005
1069
  position: absolute;
1006
1070
  inset: 0.3rem;
1007
1071
  pointer-events: none;
1008
- z-index: 2;
1009
1072
  background:
1010
1073
  linear-gradient(var(--color-primary), var(--color-primary)) top left / var(--frame-corner-size) var(--frame-corner-thickness) no-repeat,
1011
1074
  linear-gradient(var(--color-primary), var(--color-primary)) top left / var(--frame-corner-thickness) var(--frame-corner-size) no-repeat,
@@ -1017,82 +1080,111 @@ html[data-theme="light"] .home-project-specimen,
1017
1080
  linear-gradient(var(--color-primary), var(--color-primary)) bottom right / var(--frame-corner-thickness) var(--frame-corner-size) no-repeat;
1018
1081
  }
1019
1082
 
1020
- .home-project-specimen .projects-plate--mark {
1021
- display: flex;
1083
+ .home-specimen-figure>.home-specimen-badge {
1084
+ display: none;
1085
+ }
1086
+
1087
+ .home-specimen-figure:has(> .ledger-poster)>.home-specimen-badge {
1088
+ grid-area: 1 / 1;
1089
+ justify-self: end;
1090
+ align-self: start;
1091
+ z-index: 3;
1092
+ display: inline-flex;
1022
1093
  align-items: center;
1023
1094
  justify-content: center;
1024
- background: var(--color-surface-high);
1095
+ width: 2.25rem;
1096
+ height: 2.25rem;
1097
+ margin: var(--space-sm);
1098
+ font-size: 1.25rem;
1099
+ background: var(--color-surface-card);
1025
1100
  color: var(--color-primary);
1026
1101
  }
1027
1102
 
1028
- .home-project-specimen .projects-plate-glyph {
1029
- font-size: 3rem;
1030
- }
1031
-
1032
- .home-project-specimen .ledger-meta {
1033
- display: none;
1034
- }
1035
-
1036
- .home-project-specimen .ledger-card-title,
1037
- .home-project-specimen .ledger-card-title a {
1038
- flex: 0 0 auto;
1039
- font-family: var(--font-content-title);
1103
+ .home-project-specimen .home-specimen-title {
1104
+ margin: 0;
1040
1105
  font-size: var(--type-card-lg);
1041
- font-weight: var(--weight-title);
1042
- font-synthesis: none;
1043
1106
  line-height: 1.2;
1044
- color: var(--color-on-surface);
1045
- min-height: calc(1.2em * 1);
1046
- margin: var(--space-sm) 0 0;
1047
1107
  overflow: hidden;
1048
1108
  text-overflow: ellipsis;
1049
1109
  white-space: nowrap;
1050
1110
  }
1051
1111
 
1052
- .home-project-specimen:hover .ledger-card-title a,
1053
- .home-project-specimen:focus-within .ledger-card-title a {
1054
- color: var(--color-primary);
1055
- }
1056
-
1112
+ /* The tagline takes whatever height the plate leaves, clipped at a whole
1113
+ line by the clamp; the chips and the foot keep to the bottom edge. */
1057
1114
  .home-project-specimen .ledger-card-copy {
1058
- flex: 0 0 auto;
1059
- font-family: var(--font-body);
1115
+ flex: 0 1 auto;
1116
+ min-height: 0;
1060
1117
  font-size: var(--type-body);
1061
- font-weight: var(--weight-body);
1062
1118
  line-height: var(--leading-body);
1063
1119
  color: var(--color-on-surface-variant);
1064
1120
  display: -webkit-box;
1065
1121
  -webkit-box-orient: vertical;
1066
- -webkit-line-clamp: 3;
1067
- line-clamp: 3;
1122
+ -webkit-line-clamp: var(--home-specimen-copy-lines, 4);
1123
+ line-clamp: var(--home-specimen-copy-lines, 4);
1068
1124
  overflow: hidden;
1069
- min-height: calc(var(--type-body) * var(--leading-body) * 3);
1070
- margin: 0;
1071
1125
  }
1072
1126
 
1073
1127
  .home-project-specimen .ledger-chip-row {
1074
- display: flex;
1075
- flex-wrap: wrap;
1128
+ flex: 0 0 auto;
1129
+ flex-wrap: nowrap;
1130
+ overflow: hidden;
1076
1131
  gap: var(--space-xs);
1077
- margin: 0;
1132
+ margin: auto 0 0;
1133
+ }
1134
+
1135
+ .home-specimen-foot {
1136
+ flex: 0 0 auto;
1137
+ flex-wrap: nowrap;
1138
+ gap: var(--space-md);
1078
1139
  margin-top: auto;
1140
+ padding-top: var(--space-md);
1141
+ }
1142
+
1143
+ .home-project-specimen .ledger-chip-row~.home-specimen-foot {
1144
+ margin-top: var(--space-xs);
1145
+ }
1146
+
1147
+ .home-specimen-foot .ledger-card-links {
1148
+ position: relative;
1149
+ z-index: 1;
1150
+ min-width: 0;
1151
+ display: flex;
1152
+ align-items: center;
1153
+ flex-wrap: wrap;
1154
+ gap: var(--space-sm) var(--space-lg);
1155
+ padding-top: 0;
1156
+ border-top: 0;
1157
+ }
1158
+
1159
+ .home-specimen-open {
1160
+ margin-left: auto;
1079
1161
  flex: 0 0 auto;
1080
- max-height: none;
1081
- overflow: visible;
1082
1162
  }
1083
1163
 
1084
- .home-project-specimen .ledger-card-links {
1085
- display: none;
1164
+ .home-project-specimen:hover .home-specimen-open {
1165
+ color: var(--color-on-primary, var(--color-surface));
1166
+ border-color: var(--color-link-hover);
1167
+ background: var(--color-primary);
1086
1168
  }
1087
1169
 
1088
- /* Mobile: six equal icon slots, fixed button height (empty slots stay empty). */
1170
+ .home-project-specimen:hover .home-specimen-open .ledger-cta-arrow {
1171
+ transform: translateX(4px);
1172
+ }
1173
+
1174
+ @media (prefers-reduced-motion: reduce) {
1175
+ .home-project-specimen:hover .home-specimen-open .ledger-cta-arrow {
1176
+ transform: none;
1177
+ }
1178
+ }
1179
+
1180
+ /* Mobile: one equal column per project (the script sets the count, up to
1181
+ six), so the strip spans the band; fixed button height. */
1089
1182
  .home-project-inventory {
1090
1183
  display: grid;
1091
1184
  grid-template-columns: repeat(var(--home-project-icon-count, 6), minmax(0, 1fr));
1092
1185
  gap: var(--space-xs);
1093
1186
  min-width: 0;
1094
1187
  min-height: 0;
1095
- height: auto !important;
1096
1188
  overflow: visible;
1097
1189
  background: transparent;
1098
1190
  border: 0;
@@ -1104,22 +1196,16 @@ html[data-theme="light"] .home-project-specimen,
1104
1196
  display: flex;
1105
1197
  align-items: center;
1106
1198
  justify-content: center;
1107
- flex: none;
1108
- width: auto;
1109
1199
  min-width: 0;
1110
1200
  height: var(--home-icon-btn-size);
1111
- aspect-ratio: auto;
1112
- gap: 0;
1113
1201
  padding: 0;
1114
1202
  border: 1px solid var(--border-gentle-divider);
1115
1203
  background: var(--color-panel-contrast);
1116
1204
  cursor: pointer;
1117
- box-shadow: none;
1118
1205
  transition: background-color var(--motion-duration-fast) var(--motion-ease-default),
1119
1206
  border-color var(--motion-duration-fast) var(--motion-ease-default);
1120
1207
  }
1121
1208
 
1122
- .home-project-inventory-index,
1123
1209
  .home-project-inventory-media,
1124
1210
  .home-project-inventory-body {
1125
1211
  display: none;
@@ -1129,9 +1215,6 @@ html[data-theme="light"] .home-project-specimen,
1129
1215
  display: inline-flex;
1130
1216
  align-items: center;
1131
1217
  justify-content: center;
1132
- width: auto;
1133
- height: auto;
1134
- margin: 0;
1135
1218
  font-size: 1.25rem;
1136
1219
  line-height: 1;
1137
1220
  color: var(--color-on-surface-variant);
@@ -1154,7 +1237,6 @@ html[data-theme="light"] .home-project-specimen,
1154
1237
  outline: 1px solid var(--color-primary);
1155
1238
  outline-offset: -1px;
1156
1239
  background: var(--color-surface-high);
1157
- box-shadow: none;
1158
1240
  }
1159
1241
 
1160
1242
  .home-project-inventory-row.is-selected .home-project-inventory-glyph,
@@ -1169,93 +1251,68 @@ html[data-theme="light"] .home-project-specimen,
1169
1251
  background: var(--color-surface-high-contrast);
1170
1252
  }
1171
1253
 
1172
- .home-project-inventory-row .ledger-meta {
1173
- display: none;
1174
- }
1175
-
1176
1254
  @media (min-width: 900px) {
1255
+
1256
+ /* The rack panel stretches to the specimen's fixed height. */
1177
1257
  .home-projects-bench {
1178
1258
  --home-specimen-height: 36rem;
1179
1259
  display: grid;
1180
- grid-template-columns: minmax(0, 2fr) minmax(0, 3fr);
1181
- gap: var(--space-xl);
1260
+ grid-template-columns: minmax(0, 50%) minmax(18rem, 34%);
1261
+ column-gap: var(--space-xl);
1182
1262
  align-items: stretch;
1183
1263
  }
1184
1264
 
1185
- .home-project-specimen {
1186
- height: var(--home-specimen-height);
1187
- }
1188
-
1189
- /* Panel container over the tiled band — not bare rows on the grid. */
1190
1265
  .home-project-inventory {
1266
+ order: 0;
1191
1267
  display: flex;
1192
1268
  flex-direction: column;
1193
- height: var(--home-specimen-height);
1269
+ gap: var(--space-sm);
1270
+ min-height: 0;
1194
1271
  overflow: hidden;
1195
- gap: 0;
1196
- padding: 0;
1272
+ padding: var(--space-md);
1197
1273
  background: var(--color-panel-contrast);
1198
- border: 1px solid var(--border-gentle-divider);
1274
+ }
1275
+
1276
+ .home-project-specimen .home-specimen-title {
1277
+ font-size: var(--type-title);
1199
1278
  }
1200
1279
 
1201
1280
  .home-project-inventory-row {
1202
- display: flex;
1203
- flex-direction: row;
1204
- align-items: center;
1205
- justify-content: flex-start;
1206
1281
  flex: 1 1 0;
1207
- width: auto;
1282
+ justify-content: flex-start;
1283
+ gap: var(--space-md);
1208
1284
  height: auto;
1209
1285
  min-height: 0;
1210
- aspect-ratio: auto;
1211
- gap: var(--space-md);
1212
1286
  padding: var(--space-sm) var(--space-md);
1213
- border: 0;
1214
- border-bottom: 1px solid var(--border-gentle-divider);
1215
- background: transparent;
1287
+ border: 1px solid var(--border-gentle-divider);
1288
+ background: var(--color-surface-card);
1216
1289
  outline: none;
1217
1290
  }
1218
1291
 
1219
- .home-project-inventory-row:last-child {
1220
- border-bottom: 0;
1292
+ .home-band--contrast .home-project-inventory-row {
1293
+ background: var(--color-surface-card-contrast);
1221
1294
  }
1222
1295
 
1223
- .home-project-inventory-row.is-selected {
1296
+ .home-project-inventory-row.is-selected,
1297
+ .home-band--contrast .home-project-inventory-row.is-selected {
1224
1298
  background: var(--color-surface-high);
1225
- border-color: transparent;
1299
+ border-color: var(--border-gentle-divider);
1226
1300
  outline: none;
1227
1301
  box-shadow: inset 3px 0 0 var(--color-primary);
1228
1302
  }
1229
1303
 
1230
- .home-project-inventory-index {
1231
- display: block;
1232
- flex: 0 0 1.75rem;
1233
- font-family: var(--font-mono);
1234
- font-size: var(--type-caption);
1235
- letter-spacing: 0.04em;
1236
- color: var(--color-on-surface-variant);
1237
- line-height: 1;
1238
- text-align: right;
1239
- }
1240
-
1241
- .home-project-inventory-row.is-selected .home-project-inventory-index,
1242
- .home-project-inventory-row:hover .home-project-inventory-index,
1243
- .home-project-inventory-row:focus-visible .home-project-inventory-index {
1244
- color: var(--color-primary);
1245
- }
1246
-
1247
1304
  .home-project-inventory-media {
1248
1305
  display: block;
1249
1306
  flex: 0 0 auto;
1250
1307
  width: 5.5rem;
1251
- align-self: center;
1252
- height: auto;
1253
- min-width: 0;
1254
- min-height: 0;
1255
1308
  aspect-ratio: 16 / 9;
1256
1309
  overflow: hidden;
1257
1310
  background: var(--color-surface-high);
1258
- border: 1px solid var(--border-gentle-subtle);
1311
+ }
1312
+
1313
+ .home-project-inventory-row.is-selected .home-project-inventory-media {
1314
+ outline: 1px solid var(--color-primary);
1315
+ outline-offset: 2px;
1259
1316
  }
1260
1317
 
1261
1318
  .home-project-inventory-media .ledger-poster,
@@ -1263,7 +1320,6 @@ html[data-theme="light"] .home-project-specimen,
1263
1320
  width: 100%;
1264
1321
  height: 100%;
1265
1322
  aspect-ratio: auto;
1266
- border: 0;
1267
1323
  overflow: hidden;
1268
1324
  background: var(--color-surface-high);
1269
1325
  }
@@ -1272,13 +1328,6 @@ html[data-theme="light"] .home-project-specimen,
1272
1328
  display: none;
1273
1329
  }
1274
1330
 
1275
- .home-project-inventory-media .ledger-poster img {
1276
- width: 100%;
1277
- height: 100%;
1278
- object-fit: cover;
1279
- display: block;
1280
- }
1281
-
1282
1331
  .home-project-inventory-mark {
1283
1332
  display: flex;
1284
1333
  align-items: center;
@@ -1286,6 +1335,10 @@ html[data-theme="light"] .home-project-specimen,
1286
1335
  color: var(--color-primary);
1287
1336
  }
1288
1337
 
1338
+ .home-project-inventory-mark .material-symbols-outlined {
1339
+ font-size: 1.5rem;
1340
+ }
1341
+
1289
1342
  .home-project-inventory-mark--initial {
1290
1343
  font-family: var(--font-content-title);
1291
1344
  font-size: var(--type-card);
@@ -1294,33 +1347,23 @@ html[data-theme="light"] .home-project-specimen,
1294
1347
  color: var(--color-on-surface);
1295
1348
  }
1296
1349
 
1297
- .home-project-inventory-mark .material-symbols-outlined {
1298
- font-size: 1.25rem;
1299
- }
1300
-
1301
1350
  .home-project-inventory-body {
1302
1351
  display: flex;
1303
1352
  flex-direction: column;
1304
1353
  justify-content: center;
1305
- gap: 0;
1354
+ gap: 0.1rem;
1306
1355
  flex: 1 1 0;
1307
1356
  min-width: 0;
1308
- padding: 0;
1309
1357
  }
1310
1358
 
1311
1359
  .home-project-inventory-title {
1312
1360
  margin: 0;
1313
- font-family: var(--font-content-title);
1314
- font-size: var(--type-card);
1315
- font-weight: var(--weight-title);
1316
- font-synthesis: none;
1361
+ font-size: var(--type-label-lg);
1317
1362
  line-height: 1.25;
1318
1363
  color: var(--color-on-surface);
1319
- }
1320
-
1321
- .home-project-inventory-title a {
1322
- color: inherit;
1323
- text-decoration: none;
1364
+ overflow: hidden;
1365
+ text-overflow: ellipsis;
1366
+ white-space: nowrap;
1324
1367
  }
1325
1368
 
1326
1369
  .home-project-inventory-row:hover .home-project-inventory-title,
@@ -1329,15 +1372,23 @@ html[data-theme="light"] .home-project-specimen,
1329
1372
  color: var(--color-primary);
1330
1373
  }
1331
1374
 
1332
- .home-project-inventory-glyph {
1333
- flex: 0 0 auto;
1334
- width: 1.75rem;
1335
- height: 1.75rem;
1336
- margin-left: auto;
1337
- font-size: 1.25rem;
1375
+ .home-project-inventory-copy {
1376
+ margin: 0;
1377
+ font-size: var(--type-meta);
1378
+ font-weight: var(--weight-body);
1379
+ line-height: 1.4;
1380
+ color: var(--color-on-surface-variant);
1381
+ overflow: hidden;
1382
+ text-overflow: ellipsis;
1383
+ white-space: nowrap;
1338
1384
  }
1339
- }
1340
1385
 
1386
+ /* Two classes: the glyph also carries .material-symbols-outlined, whose
1387
+ font stylesheet loads after this one and sets display. */
1388
+ .home-project-inventory-row .home-project-inventory-glyph {
1389
+ display: none;
1390
+ }
1391
+ }
1341
1392
 
1342
1393
  @media (min-width: 768px) {}
1343
1394
 
@@ -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.3.6"
27
+ # GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.3.7"
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 = "1.3.6"
4
+ VERSION = "1.3.7"
5
5
  end
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "project-inventory-row/icon mark without a thumbnail (PLT-AC-15)",
3
+ "block": "renderProjectInventoryRow",
4
+ "args": [
5
+ {
6
+ "slug": "bossa",
7
+ "front_matter": {
8
+ "project": "Bossa",
9
+ "icon": "music_note"
10
+ }
11
+ },
12
+ {
13
+ "selectable": true,
14
+ "selected": true
15
+ }
16
+ ],
17
+ "selectors": [
18
+ "div.home-project-inventory-mark",
19
+ "h3.home-project-inventory-title"
20
+ ],
21
+ "forbidden": [
22
+ "ledger-poster",
23
+ "home-project-inventory-mark--initial"
24
+ ],
25
+ "contains": [
26
+ "aria-pressed=\"true\"",
27
+ "is-selected"
28
+ ]
29
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "project-inventory-row/rack card (PLT-AC-15)",
3
+ "block": "renderProjectInventoryRow",
4
+ "args": [
5
+ {
6
+ "slug": "arco",
7
+ "front_matter": {
8
+ "project": "ARCO",
9
+ "thumbnail": "/media/projects/arco.png",
10
+ "icon": "route",
11
+ "label": "Simulation",
12
+ "status": "Ongoing",
13
+ "tagline": "A city-scale race replayed from telemetry."
14
+ }
15
+ },
16
+ {
17
+ "selectable": true,
18
+ "selected": false,
19
+ "index": 2
20
+ }
21
+ ],
22
+ "selectors": [
23
+ "article.home-project-inventory-row",
24
+ "div.home-project-inventory-media",
25
+ "div.ledger-poster",
26
+ "div.home-project-inventory-code",
27
+ "span.home-project-inventory-index",
28
+ "span.ledger-status",
29
+ "h3.home-project-inventory-title",
30
+ "p.home-project-inventory-copy",
31
+ "span.home-project-inventory-glyph"
32
+ ],
33
+ "forbidden": [
34
+ "ledger-card-stretch"
35
+ ],
36
+ "contains": [
37
+ ">02<",
38
+ ">Simulation<",
39
+ ">Ongoing<",
40
+ ">ARCO<",
41
+ ">A city-scale race replayed from telemetry.<",
42
+ ">route<",
43
+ "aria-pressed=\"false\"",
44
+ "data-slug=\"arco\""
45
+ ]
46
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "project-specimen/icon plate without a thumbnail (PLT-AC-13)",
3
+ "block": "renderProjectSpecimen",
4
+ "args": [
5
+ {
6
+ "slug": "bossa",
7
+ "front_matter": {
8
+ "project": "Bossa",
9
+ "icon": "music_note",
10
+ "tagline": "Edge nodes for street ensembles."
11
+ }
12
+ },
13
+ {
14
+ "href": "/projects/bossa/"
15
+ }
16
+ ],
17
+ "selectors": [
18
+ "div.home-specimen-figure",
19
+ "projects-plate--mark",
20
+ "h3.ledger-card-title"
21
+ ],
22
+ "forbidden": [
23
+ "ledger-poster",
24
+ "home-specimen-badge",
25
+ "home-specimen-strip",
26
+ "home-specimen-open"
27
+ ],
28
+ "contains": [
29
+ ">music_note<",
30
+ ">Bossa<"
31
+ ]
32
+ }
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "project-specimen/initial badge on a thumbnail without an icon (PLT-AC-18)",
3
+ "block": "renderProjectSpecimen",
4
+ "args": [
5
+ {
6
+ "slug": "arco",
7
+ "front_matter": {
8
+ "project": "ARCO",
9
+ "thumbnail": "/media/projects/arco.png"
10
+ }
11
+ },
12
+ {
13
+ "href": "/projects/arco/"
14
+ }
15
+ ],
16
+ "selectors": [
17
+ "ledger-poster",
18
+ "span.home-specimen-badge",
19
+ "is-initial"
20
+ ],
21
+ "forbidden": [
22
+ "projects-plate",
23
+ "material-symbols-outlined"
24
+ ],
25
+ "contains": [
26
+ "aria-hidden=\"true\">A</span>"
27
+ ],
28
+ "counts": {
29
+ "home-specimen-badge": 1
30
+ }
31
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "project-specimen/initial plate without a thumbnail or icon (PLT-AC-18)",
3
+ "block": "renderProjectSpecimen",
4
+ "args": [
5
+ {
6
+ "slug": "bossa",
7
+ "front_matter": {
8
+ "project": "bossa",
9
+ "tagline": "Edge nodes for street ensembles."
10
+ }
11
+ },
12
+ {
13
+ "href": "/projects/bossa/"
14
+ }
15
+ ],
16
+ "selectors": [
17
+ "div.home-specimen-figure",
18
+ "projects-plate--mark",
19
+ "span.is-initial",
20
+ "projects-plate-glyph",
21
+ "h3.ledger-card-title"
22
+ ],
23
+ "forbidden": [
24
+ "ledger-poster",
25
+ "material-symbols-outlined",
26
+ "home-specimen-badge"
27
+ ],
28
+ "contains": [
29
+ ">B</span></div>",
30
+ ">bossa<"
31
+ ]
32
+ }
@@ -0,0 +1,69 @@
1
+ {
2
+ "name": "project-specimen/full (PLT-AC-13)",
3
+ "block": "renderProjectSpecimen",
4
+ "args": [
5
+ {
6
+ "slug": "arco",
7
+ "front_matter": {
8
+ "project": "ARCO",
9
+ "thumbnail": "/media/projects/arco.png",
10
+ "icon": "route",
11
+ "label": "Simulation",
12
+ "status": "Ongoing",
13
+ "start_date": "2024-03-01",
14
+ "tagline": "A city-scale race replayed from telemetry.",
15
+ "tags": [
16
+ "Systems",
17
+ "Rust"
18
+ ],
19
+ "github": "https://github.com/example/arco",
20
+ "article_url": "/articles/arco/",
21
+ "external_url": "https://arco.example"
22
+ }
23
+ },
24
+ {
25
+ "href": "/projects/arco/",
26
+ "folio": 1,
27
+ "openLabel": "Open project"
28
+ }
29
+ ],
30
+ "selectors": [
31
+ "article.ledger-system-card",
32
+ "article.home-project-specimen",
33
+ "div.home-specimen-strip",
34
+ "div.ledger-meta",
35
+ "span.ledger-status",
36
+ "div.home-specimen-figure",
37
+ "div.ledger-poster",
38
+ "img",
39
+ "span.home-specimen-badge",
40
+ "h3.ledger-card-title",
41
+ "a.ledger-card-stretch",
42
+ "p.ledger-card-copy",
43
+ "div.ledger-chip-row",
44
+ "div.ledger-card-links",
45
+ "span.home-specimen-open"
46
+ ],
47
+ "forbidden": [
48
+ "projects-plate"
49
+ ],
50
+ "contains": [
51
+ ">#01<",
52
+ ">Simulation<",
53
+ ">2024<",
54
+ ">Ongoing<",
55
+ ">ARCO<",
56
+ ">Repository<",
57
+ ">Article<",
58
+ ">Visit<",
59
+ ">Open project<",
60
+ "href=\"/projects/arco/\""
61
+ ],
62
+ "counts": {
63
+ "ledger-card-stretch": 1,
64
+ "ledger-cta": 4,
65
+ "ledger-cta-icon": 3,
66
+ "ledger-chip": 2,
67
+ "ledger-meta-sep": 2
68
+ }
69
+ }
@@ -214,6 +214,46 @@ omits(
214
214
 
215
215
  separatorCount('renderSystemCard / label without status drops the separator', ['Simulation', ''], 0);
216
216
 
217
+ // PLT-AC-14: the home specimen and its rack card carry only what the project
218
+ // declares. A project that is only a name is its initial plate (PLT-AC-18)
219
+ // and a title, nothing else.
220
+ omits(
221
+ 'renderProjectSpecimen / project with no media, meta, tags, or links',
222
+ 'renderProjectSpecimen',
223
+ [{ front_matter: { project: 'Bossa' } }, { href: '/projects/bossa/' }],
224
+ [
225
+ 'ledger-poster',
226
+ 'material-symbols-outlined',
227
+ 'home-specimen-badge',
228
+ 'home-specimen-strip',
229
+ 'ledger-meta',
230
+ 'ledger-status',
231
+ 'ledger-card-copy',
232
+ 'ledger-chip-row',
233
+ 'ledger-card-foot'
234
+ ]
235
+ );
236
+
237
+ omits(
238
+ 'renderProjectSpecimen / a start_date with no year renders no year',
239
+ 'renderProjectSpecimen',
240
+ [{ front_matter: { project: 'Bossa', start_date: 'someday' } }, { href: '/projects/bossa/' }],
241
+ ['home-specimen-strip', 'ledger-meta']
242
+ );
243
+
244
+ omits(
245
+ 'renderProjectInventoryRow / project with no index, label, status, or tagline',
246
+ 'renderProjectInventoryRow',
247
+ [{ slug: 'bossa', front_matter: { project: 'Bossa' } }, { selectable: true }],
248
+ [
249
+ 'home-project-inventory-code',
250
+ 'home-project-inventory-index',
251
+ 'ledger-status',
252
+ 'home-project-inventory-copy',
253
+ 'ledger-poster'
254
+ ]
255
+ );
256
+
217
257
  // The listing preview is derived from description, then excerpt, then body.
218
258
  // With all three absent there is nothing to say, so there is no paragraph.
219
259
  omits(
@@ -0,0 +1,32 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require "open3"
5
+ require "pathname"
6
+
7
+ ROOT = Pathname.new(__dir__).join("..", "..").expand_path
8
+ FIXTURE_DIR = Pathname.new(__dir__).join("render-parity", "blocks")
9
+ RUNNER = Pathname.new(__dir__).join("render-parity", "run-block-fixture.mjs")
10
+
11
+ # The block half of test-content-render-parity, without its Markdown half.
12
+ # That gate needs `marked` from scripts/node_modules, which CI never installs,
13
+ # so it stays off validate.sh; the ledger blocks fall back cleanly without it.
14
+
15
+ def check!(condition, message = "condition was false")
16
+ raise "Assertion failed: #{message}" unless condition
17
+ end
18
+
19
+ puts "Testing ledger block fixtures..."
20
+
21
+ paths = Dir.glob(FIXTURE_DIR.join("*.json")).sort
22
+ check! paths.any?, "no ledger block fixtures found"
23
+
24
+ paths.each do |fixture_path|
25
+ stdout, stderr, status = Open3.capture3(
26
+ "node", RUNNER.to_s, fixture_path, chdir: ROOT.join("scripts").to_s
27
+ )
28
+ check! status.success?, "#{File.basename(fixture_path)} failed:\n#{stderr}\n#{stdout}"
29
+ puts " #{stdout.strip}"
30
+ end
31
+
32
+ puts "All ledger block fixtures passed."
data/scripts/validate.sh CHANGED
@@ -49,6 +49,7 @@ TESTS=(
49
49
  scripts/test/test-math-row-breaks.rb
50
50
  scripts/test/test-detail-render-parity.rb
51
51
  scripts/test/test-absent-field-absent-element.rb
52
+ scripts/test/test-ledger-blocks.rb
52
53
  scripts/test/test-home-band-art.rb
53
54
  scripts/test/test-r2-json-sync.rb
54
55
  scripts/test/test-writing-slug-limits.rb
@@ -32,6 +32,42 @@ Optional: `title`, `nav`, `home` (default limit 4).
32
32
  home: { limit: 4 }
33
33
  ```
34
34
 
35
+ ## Home band
36
+
37
+ The home band is a specimen bench: one featured project card beside a
38
+ filing rack of selectable project cards. Every element is backed by a
39
+ collection field; a missing field means a missing element.
40
+
41
+ - `PLT-AC-13` When the band renders, the specimen card shall carry a strip
42
+ with the project's folio, `label`, `start_date` year and `status`; a 16:9
43
+ plate, the `thumbnail` with the `icon` as a badge (or the `icon` plate),
44
+ with the title always below it; the `tagline`; the `tags`; and the
45
+ outbound `github`, `article_url` and `external_url` links. The title is
46
+ its only link to the project page.
47
+ - `PLT-AC-14` When a project lacks `thumbnail`, `label`, `status`,
48
+ `start_date`, `tagline`, `tags` or outbound links, the specimen card and its
49
+ rack card shall render no element for that field. A missing `icon` is
50
+ covered by `PLT-AC-18`.
51
+ - `PLT-AC-15` When the viewport is at least 900px wide, the specimen shall
52
+ take half the band and the rack a narrower column beside it, leaving the
53
+ rest blank. Each project shall appear in the rack as a card with its folio,
54
+ `label`, `status`, title, `tagline` and a 16:9 `thumbnail` (or `icon`, or
55
+ initial) mark. Selecting a card shall swap the specimen in place and mark
56
+ that card `aria-pressed="true"`.
57
+ - `PLT-AC-16` Withdrawn: the tape of previous and next arrows was removed;
58
+ the rack is the only way to change the specimen.
59
+ - `PLT-AC-17` When the viewport is narrower than 900px, the rack shall be a
60
+ strip of icon buttons above the specimen, one per project (up to six), that
61
+ together span the band's width.
62
+ - `PLT-AC-18` When a project lacks `icon`, the first letter of its name shall
63
+ stand in for it wherever the icon would appear: the specimen badge, the
64
+ specimen plate, the rack mark and the strip button. The elements and the
65
+ behavior shall be those of a project with an icon.
66
+
67
+ `PLT-AC-13`, `-14` and `-18` are covered by the block fixtures under
68
+ `scripts/test/render-parity/blocks/` and by the absent-field gate; `-15` and
69
+ `-17` are layout behavior verified in the browser.
70
+
35
71
  ## Package layout
36
72
 
37
73
  - `template.yml` — slots, detail route, export fields
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pandorga
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.6
4
+ version: 1.3.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Pandorga contributors
@@ -339,6 +339,12 @@ files:
339
339
  - scripts/test/render-parity/blocks/meta-strip.json
340
340
  - scripts/test/render-parity/blocks/poster-media-image.json
341
341
  - scripts/test/render-parity/blocks/poster-media-video.json
342
+ - scripts/test/render-parity/blocks/project-inventory-row-icon.json
343
+ - scripts/test/render-parity/blocks/project-inventory-row.json
344
+ - scripts/test/render-parity/blocks/project-specimen-glyph.json
345
+ - scripts/test/render-parity/blocks/project-specimen-initial-badge.json
346
+ - scripts/test/render-parity/blocks/project-specimen-initial.json
347
+ - scripts/test/render-parity/blocks/project-specimen.json
342
348
  - scripts/test/render-parity/blocks/section-banner.json
343
349
  - scripts/test/render-parity/blocks/system-card-custom-meta.json
344
350
  - scripts/test/render-parity/blocks/system-card.json
@@ -389,6 +395,7 @@ files:
389
395
  - scripts/test/test-home-band-art.rb
390
396
  - scripts/test/test-install-functions.rb
391
397
  - scripts/test/test-language-export.rb
398
+ - scripts/test/test-ledger-blocks.rb
392
399
  - scripts/test/test-listing-filter-contract.rb
393
400
  - scripts/test/test-math-row-breaks.mjs
394
401
  - scripts/test/test-math-row-breaks.rb