pandorga 1.4.9 → 1.5.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b29a2005613455748e1e0a7dce5e7938b00e1482b8ae003f1b836252eb869695
4
- data.tar.gz: 435e4bbb834478e0e5b7b5f337297b06c85ecec4dede4decbf35142d092e3b9d
3
+ metadata.gz: c33cb11f60569d5280aa7079b6f59ad7776f90705f54f67d958d8b149390714d
4
+ data.tar.gz: 4a52dcb27be1f4d2a809417bc9b98e89e3c8aa8d92e642fca9ebab156942a192
5
5
  SHA512:
6
- metadata.gz: 34677acf3f620cbc6084c8424516c568081013d0c97d9d0ac3376b1cf5e39b0306c41d254428c52c3c88d185a13cc4d8fb64571e6c4e0a7823e4ac112c2f4c9c
7
- data.tar.gz: ade0e7ec4632f9113ce3fde3e6f35d282f05870efda7bf9149c28708a5ac2cc8410d6de62cde76c0f4fcce80c219d02ab41f9b75042f300162006cf7c22d3181
6
+ metadata.gz: ecf8762ab684b723db57bb8d291609a3decf8ecae02f21b0158c3fd34afce67b9a34347255e790e26d480e8ef0b2eaab577dbe3d6ed571a02ce713cc5a2e09d9
7
+ data.tar.gz: 0c30da38132b4b87f2caf982a73f7c8eea1cf157600ecffb9b6f87e24fe289a47952fe3f9c8291c11719531a90cabd0149db89ba13b076c341a4eafabd6577b5
data/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.5.1] — 2026-10-10
4
+
5
+ ### Fixed
6
+
7
+ - The shell has a "Skip to content" link. When `site.author` is blank, the
8
+ home link's accessible name uses `pandorga.identity.name`, then the top-bar
9
+ wordmark, instead of a bare dash. The footer wordmark uses the same
10
+ fallback. A site that sets `site.author` is unchanged.
11
+ - `pandorga doctor` says when `pandorga.pages` is missing, and says when the
12
+ list is empty.
13
+ - `./scripts/validate.sh` fails if a listed gate file is missing.
14
+
15
+ ### Changed
16
+
17
+ - Getting started and the README name the scaffold pin `~> 1.3`.
18
+ - `pandorga new` comments the `v1.5.0` tag.
19
+ - Home CSS drops an empty `@media (min-width: 768px)` block and the unused
20
+ `.home-profile-summary` rules.
21
+ - Chord chart and transcript include comments are English. The visible
22
+ transcript labels are unchanged.
23
+
24
+ No `pandorga:` config keys were added or renamed.
25
+
26
+ ## [1.5.0] — 2026-10-10
27
+
28
+ ### Added
29
+
30
+ - Optional `pandorga.identity.first_name` and `pandorga.identity.last_name`.
31
+ The home hero uses them as the given name and the family name. The top
32
+ bar shows `first_name`. `last_name` is kept whole, spaces included.
33
+ Omit either key and that part still comes from splitting `identity.name`
34
+ on the first space, as in 1.4.9. `identity.name` stays required.
35
+
36
+ ### Changed
37
+
38
+ - The home hero subtitle uses `--type-body`, the same size as the summary
39
+ under the name.
40
+ - Below 768px the hero name is 3pt larger, and a wrap breaks only between
41
+ the given name and the family name.
42
+
3
43
  ## [1.4.9] — 2026-10-09
4
44
 
5
45
  ### Changed
data/README.md CHANGED
@@ -17,7 +17,7 @@ An aesthetic Jekyll publishing platform for creative and technical writing. The
17
17
  Scaffold a site (Ada Example defaults, `content.backend: static`):
18
18
 
19
19
  ```bash
20
- gem install pandorga # once 1.1 is on RubyGems; or use a local checkout
20
+ gem install pandorga # or use a local checkout
21
21
  pandorga new my-site
22
22
  cd my-site
23
23
  bundle install
@@ -1,14 +1,14 @@
1
1
  {% comment %}
2
2
  chord-tab.html
3
- Exibe uma cifra estruturada em N colunas monoespaçadas, usando toda a largura disponível.
4
- Cada coluna respeita o limite de 79 caracteres por linha.
5
- Uso: {% include chord-tab.html tab=tab_data %}
6
- Onde tab_data tem:
7
- intro: "linha de acordes do intro (opcional)"
3
+ Renders a chord chart in N monospaced columns, using the full available width.
4
+ Each column stays within 79 characters per line.
5
+ Use: {% include chord-tab.html tab=tab_data %}
6
+ tab_data has:
7
+ intro: "intro chord line (optional)"
8
8
  sections:
9
- - label: "rótulo da seção (opcional)"
9
+ - label: "section label (optional)"
10
10
  content: |
11
- linhas de cifra/letra
11
+ chord and lyric lines
12
12
  {% endcomment %}
13
13
 
14
14
  <div class="chord-tab-container mt-16 mb-8"
@@ -29,6 +29,7 @@
29
29
  {%- endcomment -%}
30
30
 
31
31
  {%- assign footer_active = include.nav_active -%}
32
+ {%- assign footer_name = site.author | default: site.pandorga.identity.name -%}
32
33
  {%- assign footer_license = site.content_license -%}
33
34
 
34
35
  <footer class="site-footer">
@@ -39,9 +40,9 @@
39
40
  <div class="site-footer-grid">
40
41
 
41
42
  <div class="site-footer-block site-footer-masthead">
42
- <a href="/" class="site-footer-wordmark-link" aria-label="{{ site.author }} – Home">
43
+ <a href="/" class="site-footer-wordmark-link" aria-label="{% if footer_name != blank %}{{ footer_name }} – Home{% else %}Home{% endif %}">
43
44
  {% include theme/site-logo.html %}
44
- <span class="site-footer-wordmark">{{ site.author }}</span>
45
+ <span class="site-footer-wordmark">{{ footer_name }}</span>
45
46
  </a>
46
47
  {%- comment -%}
47
48
  The same fragment the home hero mounts, so the line the site opens
@@ -1,11 +1,11 @@
1
1
  {% comment %}
2
2
  transcription.html
3
- Exibe uma transcrição estruturada, com até 4 personagens (cores definidas por ordem de aparição).
4
- Uso: {% include transcription.html transcription=page.transcription youtube_id='fpmZiY24U0k' %}
5
- Ou para usar um arquivo em _data:
3
+ Renders a structured transcript, with up to 4 speakers (colors follow order of appearance).
4
+ Use: {% include transcription.html transcription=page.transcription youtube_id='fpmZiY24U0k' %}
5
+ Or to load a file from _data:
6
6
  {% assign transcription_data = site.data.transcriptions[page.transcription].transcription %}
7
7
  {% include transcription.html transcription=transcription_data youtube_id='fpmZiY24U0k' %}
8
- Também suporta url e image para thumbnail:
8
+ Also accepts url and image for a thumbnail:
9
9
  {% include transcription.html transcription=transcription_data url='https://...' image='https://...' %}
10
10
 
11
11
  Speaker labels are title-cased at render time (Patrícia, not PATRÍCIA) so
@@ -26,7 +26,7 @@
26
26
  <hr class="border-t border-gentle-divider mb-6">
27
27
  {% if include.youtube_id %}
28
28
  <div class="mb-2">
29
- {% include youtube.html id=include.youtube_id %} <!-- Exibe o vídeo do YouTube -->
29
+ {% include youtube.html id=include.youtube_id %} <!-- YouTube video -->
30
30
  <span class="font-label text-xs text-on-surface-variant">Transcrição do vídeo</span>
31
31
  </div>
32
32
  {% elsif include.url and include.image %}
data/_layouts/home.html CHANGED
@@ -5,7 +5,8 @@ layout: shell
5
5
  {%- comment -%}
6
6
  Home — composition from the page registry (PLT §5.4).
7
7
 
8
- The hero reads identity plus content fragments. Decorative art is a slot
8
+ The hero reads identity (first_name and last_name, or a split of name)
9
+ plus content fragments. Decorative art is a slot
9
10
  (`pandorga.home.hero_art`, default page/hero-default.html). Listing and
10
11
  index bands come from site.data.pandorga_home_bands in registry order.
11
12
  Band copy (kicker, title, note) arrives from content/pages/headers.yml
@@ -13,10 +14,7 @@ layout: shell
13
14
  in this layout.
14
15
  {%- endcomment -%}
15
16
 
16
- {%- assign _identity_name = site.pandorga.identity.name | default: site.author -%}
17
- {%- assign _name_parts = _identity_name | split: " " -%}
18
- {%- assign _name_given = _name_parts | first -%}
19
- {%- assign _name_family = _name_parts | slice: 1, 8 | join: " " -%}
17
+ {%- assign _hero_name = site.pandorga.identity | pandorga_hero_name: site.author, site.title -%}
20
18
  {%- assign _cv_page = site.pages | where: "key", "cv" | first -%}
21
19
  {%- assign _hero_art = site.pandorga.home.hero_art | default: "page/hero-default.html" -%}
22
20
  {%- assign _home_bands = site.data.pandorga_home_bands -%}
@@ -33,7 +31,7 @@ layout: shell
33
31
  <div id="home-subtitle-content" class="home-hero-tagline font-label"
34
32
  data-content-fragment="home/subtitle"></div>
35
33
  <h1 class="home-hero-title">
36
- <span class="home-hero-given">{{ _name_given }}</span>{%- if _name_family != "" %} <span class="home-hero-family">{{ _name_family }}</span>{%- endif %}
34
+ <span class="home-hero-given">{{ _hero_name.given }}</span>{%- if _hero_name.family != "" %} <span class="home-hero-family">{{ _hero_name.family }}</span>{%- endif %}
37
35
  </h1>
38
36
  <div id="home-cv-summary" class="home-hero-summary font-body"
39
37
  data-content-fragment="cv/summary"></div>
data/_layouts/shell.html CHANGED
@@ -83,7 +83,10 @@
83
83
  </head>
84
84
 
85
85
  <body class="bg-background text-on-surface">
86
+ <a class="skip-link" href="#page-content">Skip to content</a>
86
87
 
88
+ {% assign _nav_name = site.pandorga.identity | pandorga_hero_name: site.author, site.title %}
89
+ {% assign _home_label = site.author | default: site.pandorga.identity.name | default: _nav_name.wordmark %}
87
90
  {% assign nav_active = nil %}
88
91
  {% if page.collection == "articles" %}{% assign nav_active = "articles" %}{% endif %}
89
92
  {% if page.collection == "posts" %}{% assign nav_active = "blog" %}{% endif %}
@@ -97,9 +100,9 @@
97
100
  <nav class="sticky top-0 z-50 bg-surface">
98
101
  <div class="flex justify-between items-center w-full px-6 sm:px-8 md:px-10 pt-5 pb-3.5 leading-none max-w-7xl mx-auto">
99
102
  <div class="flex items-center gap-3 sm:gap-4 min-w-0">
100
- <a href="/" aria-label="{{ site.author }} – Home" class="shrink-0 inline-flex items-center gap-3 site-wordmark-link">
103
+ <a href="/" aria-label="{% if _home_label != blank %}{{ _home_label }} – Home{% else %}Home{% endif %}" class="shrink-0 inline-flex items-center gap-3 site-wordmark-link">
101
104
  {% include theme/site-logo.html %}
102
- <span class="site-wordmark">{{ site.author | default: site.title | split: " " | first }}</span>
105
+ <span class="site-wordmark">{{ _nav_name.wordmark }}</span>
103
106
  </a>
104
107
  </div>
105
108
 
@@ -189,7 +192,9 @@
189
192
 
190
193
  {% include page/content-runtime.html %}
191
194
 
195
+ <div id="page-content" tabindex="-1">
192
196
  {{ content }}
197
+ </div>
193
198
 
194
199
  {% include page/figure-lightbox.html %}
195
200
 
@@ -6,6 +6,29 @@
6
6
  one source of navigation; only the presentation changes. The Explore menu
7
7
  stays for narrow viewports, where six links do not fit. */
8
8
 
9
+ .skip-link {
10
+ position: absolute;
11
+ left: 0.75rem;
12
+ top: 0.75rem;
13
+ z-index: 80;
14
+ padding: 0.45rem 0.75rem;
15
+ background: var(--color-surface);
16
+ color: var(--color-on-surface);
17
+ border: 1px solid var(--color-outline);
18
+ text-decoration: none;
19
+ font-family: var(--font-label);
20
+ font-size: var(--type-label);
21
+ transform: translateY(-150%);
22
+ }
23
+
24
+ .skip-link:focus {
25
+ transform: none;
26
+ }
27
+
28
+ #page-content:focus {
29
+ outline: none;
30
+ }
31
+
9
32
  .site-wordmark-link {
10
33
  display: inline-flex;
11
34
  align-items: center;
@@ -219,19 +219,14 @@
219
219
  margin: 0 0 var(--space-sm);
220
220
  color: var(--color-on-surface);
221
221
  font-family: var(--font-label);
222
- font-size: var(--type-label-lg);
222
+ /* Same size as the summary under the name, which inherits --type-body. */
223
+ font-size: var(--type-body);
223
224
  font-weight: var(--weight-label);
224
225
  letter-spacing: 0.03em;
225
226
  line-height: 1.35;
226
227
  max-width: 100%;
227
228
  }
228
229
 
229
- @media (min-width: 1024px) {
230
- .home-hero-tagline {
231
- font-size: calc(var(--type-label-lg) + 2pt);
232
- }
233
- }
234
-
235
230
  .home-hero-tagline::after {
236
231
  content: "";
237
232
  flex: 1 1 auto;
@@ -262,10 +257,13 @@
262
257
  max-width: 100%;
263
258
  }
264
259
 
265
- @media (max-width: 767px) {
266
- .home-hero-title {
267
- white-space: normal;
268
- }
260
+ .home-hero-given,
261
+ .home-hero-family {
262
+ display: inline;
263
+ }
264
+
265
+ .home-hero-family {
266
+ color: var(--color-primary);
269
267
  }
270
268
 
271
269
  @media (min-width: 640px) {
@@ -274,19 +272,34 @@
274
272
  }
275
273
  }
276
274
 
277
- @media (min-width: 768px) {
275
+ /* Below 768px the name is 3pt larger than the clamp above, and the only
276
+ wrap point is the gap between the given name and the family name. */
277
+ @media (max-width: 639px) {
278
278
  .home-hero-title {
279
- white-space: nowrap;
279
+ white-space: normal;
280
+ font-size: calc(clamp(var(--type-section), calc(100cqi * 0.92 / 14.5), var(--type-page-lg)) + 3pt);
280
281
  }
281
282
  }
282
283
 
283
- .home-hero-given,
284
- .home-hero-family {
285
- display: inline;
284
+ @media (min-width: 640px) and (max-width: 767px) {
285
+ .home-hero-title {
286
+ white-space: normal;
287
+ font-size: calc(clamp(var(--type-section-lg), calc(100cqi * 0.92 / 14.5), var(--type-page-lg)) + 3pt);
288
+ }
286
289
  }
287
290
 
288
- .home-hero-family {
289
- color: var(--color-primary);
291
+ @media (max-width: 767px) {
292
+ .home-hero-given,
293
+ .home-hero-family {
294
+ display: inline-block;
295
+ white-space: nowrap;
296
+ }
297
+ }
298
+
299
+ @media (min-width: 768px) {
300
+ .home-hero-title {
301
+ white-space: nowrap;
302
+ }
290
303
  }
291
304
 
292
305
  .home-hero-summary {
@@ -1485,26 +1498,6 @@ html[data-theme="light"] .home-hero-plate {
1485
1498
  }
1486
1499
  }
1487
1500
 
1488
- @media (min-width: 768px) {}
1489
-
1490
- /* ── Profile band, hero plate scale, section action order ──
1491
- Appended after the parallel page work; see the redesign PR. */
1492
-
1493
- /* The portrait sits at the end of its grid column so its right edge meets
1494
- the edge every band below aligns to. Cap lives on the earlier
1495
- `.home-hero-portrait` rule (Borders Edition). */
1496
- .home-profile-summary {
1497
- max-width: var(--reading-measure);
1498
- }
1499
-
1500
- .home-profile-summary p {
1501
- margin-bottom: var(--space-md);
1502
- }
1503
-
1504
- .home-profile-summary p:last-child {
1505
- margin-bottom: 0;
1506
- }
1507
-
1508
1501
  /* ── Reference band: Sources + Network of Ideas ──────────────────────────
1509
1502
  Two destinations in one band, each a card whose poster is a plate drawn
1510
1503
  from that section's own content by the export and fetched at runtime.
@@ -8,6 +8,8 @@ plugins:
8
8
  pandorga:
9
9
  identity:
10
10
  name: "Ada Example"
11
+ first_name: "Ada"
12
+ last_name: "Example"
11
13
  short_name: "A. Example"
12
14
  url: "https://example.org"
13
15
  locale: en
@@ -30,7 +30,9 @@ module Pandorga
30
30
  end
31
31
  pages = pandorga["pages"]
32
32
  if pages.nil?
33
- warnings << "pandorga.pages is empty — no pages will be generated"
33
+ warnings << "pandorga.pages is missing; no pages will be generated"
34
+ elsif pages.is_a?(Array) && pages.empty?
35
+ warnings << "pandorga.pages is empty; no pages will be generated"
34
36
  elsif !pages.is_a?(Array)
35
37
  errors << "pandorga.pages must be an array"
36
38
  else
@@ -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.11"
27
+ # GitHub tag: gem "pandorga", github: "alexandrelheinen/pandorga", tag: "v1.5.0"
28
28
  gem "csv"
29
29
  gem "base64"
30
30
  RUBY
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Spec: docs/features/hero-name.md
4
+ # Refs: PLT-AC-14
5
+ module Pandorga
6
+ # Given name, family name, and top-bar wordmark for the home hero.
7
+ module IdentityName
8
+ module_function
9
+
10
+ # identity may use string or symbol keys. author and title are the
11
+ # Jekyll site.author and site.title fallbacks.
12
+ def resolve(identity, author: nil, title: nil)
13
+ fields = normalize(identity)
14
+ given_field = explicit(fields, "first_name")
15
+ family_field = explicit(fields, "last_name")
16
+ legacy = tokens(text(fields["name"]).empty? ? author : fields["name"])
17
+
18
+ given = present(given_field) ? given_field : legacy.first.to_s
19
+ family = present(family_field) ? family_field : legacy.drop(1).take(8).join(" ")
20
+ wordmark = if present(given_field)
21
+ given_field
22
+ else
23
+ source = [fields["name"], author, title].map { |value| text(value) }.find { |value| !value.empty? }
24
+ tokens(source).first.to_s
25
+ end
26
+
27
+ { "given" => given, "family" => family, "wordmark" => wordmark }
28
+ end
29
+
30
+ def normalize(identity)
31
+ raw = case identity
32
+ when Hash then identity
33
+ else identity.respond_to?(:to_hash) ? identity.to_hash : {}
34
+ end
35
+ raw.each_with_object({}) { |(key, value), out| out[key.to_s] = value }
36
+ end
37
+
38
+ # :missing when the key is absent or null. Blank is handled by present.
39
+ def explicit(fields, key)
40
+ return :missing unless fields.key?(key)
41
+ return :missing if fields[key].nil?
42
+
43
+ fields[key].to_s.strip
44
+ end
45
+
46
+ def present(value)
47
+ value.is_a?(String) && !value.empty?
48
+ end
49
+
50
+ def text(value)
51
+ value.to_s.strip
52
+ end
53
+
54
+ def tokens(value)
55
+ text(value).split(" ")
56
+ end
57
+ end
58
+ end
59
+
60
+ # Liquid filter used by the home hero and the top bar.
61
+ module PandorgaHeroNameFilter
62
+ def pandorga_hero_name(identity, author = nil, title = nil)
63
+ Pandorga::IdentityName.resolve(identity, author: author, title: title)
64
+ end
65
+ end
66
+
67
+ Liquid::Template.register_filter(PandorgaHeroNameFilter) if defined?(Liquid::Template)
@@ -8,7 +8,15 @@ identity:
8
8
  name:
9
9
  type: string
10
10
  required: true
11
- notes: Title suffix, default author, BibTeX
11
+ notes: Title suffix, default author, BibTeX. Also the legacy hero name when `first_name` or `last_name` is omitted
12
+ first_name:
13
+ type: string
14
+ required: false
15
+ notes: Hero given name and top-bar wordmark. Omitted or blank, the first word of `name` is used
16
+ last_name:
17
+ type: string
18
+ required: false
19
+ notes: Hero family name, kept whole (spaces allowed). Omitted or blank, the remainder of `name` is used
12
20
  short_name:
13
21
  type: string
14
22
  required: false
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pandorga
4
- VERSION = "1.4.9"
4
+ VERSION = "1.5.1"
5
5
  end
@@ -64,6 +64,14 @@ sheets.each do |path|
64
64
  errors << "#{relative}: #{depth} unclosed brace(s) — every rule after the " \
65
65
  "offending block is swallowed and silently stops applying"
66
66
  end
67
+
68
+ if cleaned.match?(/\{[ \t\r\n]*\}/)
69
+ errors << "#{relative}: empty ruleset (a block that matches nothing)"
70
+ end
71
+
72
+ if source.include?("home-profile-summary")
73
+ errors << "#{relative}: .home-profile-summary is unused"
74
+ end
67
75
  end
68
76
 
69
77
  fail!("CSS structure problems:\n - #{errors.join("\n - ")}") unless errors.empty?
@@ -3,6 +3,7 @@
3
3
  # Refs: PLT-0.3, PLT-AC-5
4
4
  require "pathname"
5
5
  require "open3"
6
+ require "fileutils"
6
7
 
7
8
  ROOT = Pathname.new(__dir__).join("../..").expand_path
8
9
  EXAMPLE = ROOT.join("examples/minimal")
@@ -36,4 +37,41 @@ unless err.include?("pandorga:")
36
37
  exit 1
37
38
  end
38
39
 
40
+ missing = ROOT.join("tmp/doctor-missing-pages")
41
+ FileUtils.rm_rf(missing)
42
+ missing.mkpath
43
+ missing.join("_config.yml").write(<<~YAML)
44
+ pandorga:
45
+ identity:
46
+ name: Ada Example
47
+ YAML
48
+ _out, err, st = run_doctor(missing)
49
+ unless st.success?
50
+ warn "FAIL test-doctor: missing pages should warn, not fail\n#{err}"
51
+ exit 1
52
+ end
53
+ unless err.include?("pandorga.pages is missing")
54
+ warn "FAIL test-doctor: expected a missing-pages warning\n#{err}"
55
+ exit 1
56
+ end
57
+
58
+ empty = ROOT.join("tmp/doctor-empty-pages")
59
+ FileUtils.rm_rf(empty)
60
+ empty.mkpath
61
+ empty.join("_config.yml").write(<<~YAML)
62
+ pandorga:
63
+ identity:
64
+ name: Ada Example
65
+ pages: []
66
+ YAML
67
+ _out, err, st = run_doctor(empty)
68
+ unless st.success?
69
+ warn "FAIL test-doctor: empty pages should warn, not fail\n#{err}"
70
+ exit 1
71
+ end
72
+ unless err.include?("pandorga.pages is empty")
73
+ warn "FAIL test-doctor: expected an empty-pages warning\n#{err}"
74
+ exit 1
75
+ end
76
+
39
77
  puts "PASS test-doctor"
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Hero name fields and headline size (docs/features/hero-name.md).
5
+ #
6
+ # PLT-AC-13 subtitle uses the body size
7
+ # PLT-AC-14 first_name / last_name, with the 1.4.9 split as fallback
8
+ # PLT-AC-15 phone name is +3pt and wraps only between the two names
9
+
10
+ require "pathname"
11
+
12
+ ROOT = Pathname.new(__dir__).join("..", "..").expand_path
13
+ HOME = ROOT.join("_layouts/home.html")
14
+ SHELL = ROOT.join("_layouts/shell.html")
15
+ CSS = ROOT.join("assets/css/ledger/home.css")
16
+ SCHEMA = ROOT.join("lib/pandorga/registry/schema.yml")
17
+
18
+ def check!(condition, message)
19
+ raise "Assertion failed: #{message}" unless condition
20
+ end
21
+
22
+ puts "Testing hero name..."
23
+
24
+ # ── PLT-AC-13 ───────────────────────────────────────────────────────────────
25
+
26
+ css = CSS.read
27
+ tagline = css[/\.home-hero-tagline \{.*?\n\}/m]
28
+ check!(tagline, "missing .home-hero-tagline rule")
29
+ check!(tagline.include?("font-size: var(--type-body);"),
30
+ "subtitle must use the body size, got:\n#{tagline}")
31
+ check!(!tagline.include?("type-label-lg"),
32
+ "subtitle must not keep the label size")
33
+ check!(!css.include?("calc(var(--type-label-lg) + 2pt)"),
34
+ "desktop subtitle must not add 2pt")
35
+
36
+ summary = css[/\.home-hero-summary \{.*?\n\}/m]
37
+ check!(summary, "missing .home-hero-summary rule")
38
+ check!(!summary.include?("font-size:"),
39
+ "summary inherits the page body size and must not set its own")
40
+
41
+ # ── PLT-AC-14 ───────────────────────────────────────────────────────────────
42
+
43
+ schema = SCHEMA.read
44
+ check!(schema.include?("first_name:"), "schema missing first_name")
45
+ check!(schema.include?("last_name:"), "schema missing last_name")
46
+ check!(schema.include?("name:") && schema.include?("required: true"),
47
+ "identity.name stays required")
48
+
49
+ home = HOME.read
50
+ shell = SHELL.read
51
+ check!(home.include?("pandorga_hero_name"),
52
+ "home hero must resolve the name through pandorga_hero_name")
53
+ check!(home.include?("home-hero-given") && home.include?("home-hero-family"),
54
+ "hero keeps a given span and a family span")
55
+ check!(shell.include?("pandorga_hero_name"),
56
+ "top bar must resolve the wordmark through pandorga_hero_name")
57
+ check!(shell.include?(".wordmark"),
58
+ "top bar prints the wordmark from the resolver")
59
+ check!(!shell.include?('site.author | default: site.title | split: " " | first'),
60
+ "top bar must not split site.author on its own")
61
+
62
+ require "liquid"
63
+ require_relative "../../lib/pandorga/jekyll/plugins/identity_name"
64
+
65
+ template = Liquid::Template.parse(<<~LIQUID)
66
+ {% assign hero = identity | pandorga_hero_name: author, title %}
67
+ {{ hero.given }}|{{ hero.family }}|{{ hero.wordmark }}
68
+ LIQUID
69
+ rendered = template.render(
70
+ "identity" => { "first_name" => "Ada", "last_name" => "Rivera Santos" },
71
+ "author" => "Other Person",
72
+ "title" => "Site Title"
73
+ )
74
+ check!(rendered.strip == "Ada|Rivera Santos|Ada",
75
+ "Liquid filter must keep a multi-word last name, got #{rendered.inspect}")
76
+
77
+ both = Pandorga::IdentityName.resolve(
78
+ { "name" => "Ada Rivera Santos", "first_name" => "Ada", "last_name" => "Rivera Santos" },
79
+ author: "Other Person",
80
+ title: "Site Title"
81
+ )
82
+ check!(both["given"] == "Ada", "given name, got #{both.inspect}")
83
+ check!(both["family"] == "Rivera Santos", "multi-word family name must stay whole, got #{both.inspect}")
84
+ check!(both["wordmark"] == "Ada", "top bar shows the first name, got #{both.inspect}")
85
+
86
+ spaced = Pandorga::IdentityName.resolve(
87
+ { "first_name" => " Mary Ann ", "last_name" => " Rivera Santos " }
88
+ )
89
+ check!(spaced["given"] == "Mary Ann" && spaced["wordmark"] == "Mary Ann",
90
+ "first_name is kept whole, got #{spaced.inspect}")
91
+ check!(spaced["family"] == "Rivera Santos",
92
+ "last_name is trimmed but not split, got #{spaced.inspect}")
93
+
94
+ legacy = Pandorga::IdentityName.resolve(
95
+ { "name" => "Ada Rivera Santos" },
96
+ author: "Other Person",
97
+ title: "Site Title"
98
+ )
99
+ check!(legacy["given"] == "Ada", "legacy given name, got #{legacy.inspect}")
100
+ check!(legacy["family"] == "Rivera Santos", "legacy family name, got #{legacy.inspect}")
101
+ check!(legacy["wordmark"] == "Ada", "legacy wordmark prefers identity.name, got #{legacy.inspect}")
102
+
103
+ partial_family = Pandorga::IdentityName.resolve(
104
+ { "name" => "Someone Else", "last_name" => "Rivera Santos" }
105
+ )
106
+ check!(partial_family["given"] == "Someone", "missing first_name uses the first word of name")
107
+ check!(partial_family["family"] == "Rivera Santos", "explicit last_name replaces the remainder")
108
+ check!(partial_family["wordmark"] == "Someone", "missing first_name wordmark is the first word of name")
109
+
110
+ partial_given = Pandorga::IdentityName.resolve({ "name" => "Ada Example", "first_name" => "Ada" })
111
+ check!(partial_given["family"] == "Example", "missing last_name keeps the remainder of name")
112
+ check!(partial_given["wordmark"] == "Ada", "wordmark uses first_name")
113
+
114
+ blank_last = Pandorga::IdentityName.resolve({ "name" => "Ada Example", "last_name" => "" })
115
+ check!(blank_last["family"] == "Example", "blank last_name keeps the 1.4.9 remainder")
116
+
117
+ author_only = Pandorga::IdentityName.resolve({}, author: "Other Person", title: "Site Title")
118
+ check!(author_only["given"] == "Other" && author_only["family"] == "Person",
119
+ "name falls back to site.author, got #{author_only.inspect}")
120
+ check!(author_only["wordmark"] == "Other", "wordmark falls back to site.author")
121
+
122
+ title_only = Pandorga::IdentityName.resolve({}, title: "Site Title")
123
+ check!(title_only["given"] == "" && title_only["family"] == "",
124
+ "hero does not take site.title, got #{title_only.inspect}")
125
+ check!(title_only["wordmark"] == "Site", "wordmark falls back to site.title")
126
+
127
+ long_name = (["Ada"] + (1..9).map { |index| "Tok#{index}" }).join(" ")
128
+ long = Pandorga::IdentityName.resolve({ "name" => long_name })
129
+ check!(long["family"] == (1..8).map { |index| "Tok#{index}" }.join(" "),
130
+ "legacy remainder stays eight words, got #{long["family"].inspect}")
131
+
132
+ symbols = Pandorga::IdentityName.resolve({ first_name: "Ada", last_name: "Rivera Santos" })
133
+ check!(symbols["family"] == "Rivera Santos", "symbol keys work, got #{symbols.inspect}")
134
+
135
+ # ── PLT-AC-15 ───────────────────────────────────────────────────────────────
136
+
137
+ hero = css[/\.home-hero-title \{.*?(?=\.home-hero-summary)/m]
138
+ check!(hero, "missing hero title block")
139
+ check!(hero.include?("font-size: clamp(var(--type-section), calc(100cqi * 0.92 / 14.5), var(--type-page-lg));"),
140
+ "base name size stays the 1.4.9 clamp")
141
+ check!(hero.include?("@media (max-width: 639px)"), "missing phone size query")
142
+ check!(hero.include?("calc(clamp(var(--type-section), calc(100cqi * 0.92 / 14.5), var(--type-page-lg)) + 3pt)"),
143
+ "below 640px the name must be the current clamp plus 3pt")
144
+ check!(hero.include?("@media (min-width: 640px) and (max-width: 767px)"),
145
+ "missing 640-767 size query")
146
+ check!(hero.include?("calc(clamp(var(--type-section-lg), calc(100cqi * 0.92 / 14.5), var(--type-page-lg)) + 3pt)"),
147
+ "from 640px to 767px the name must be the section-lg clamp plus 3pt")
148
+
149
+ plain = hero.index("@media (min-width: 640px) {")
150
+ banded = hero.index("@media (min-width: 640px) and (max-width: 767px)")
151
+ check!(plain && banded && banded > plain,
152
+ "the +3pt band must follow the plain 640px clamp so it wins")
153
+
154
+ phone = hero[/@media \(max-width: 767px\) \{.*?\n\}/m]
155
+ check!(phone && phone.include?("inline-block") && phone.include?("white-space: nowrap"),
156
+ "on a phone each name is an unbreakable unit, got:\n#{phone}")
157
+ check!(phone.include?(".home-hero-given") && phone.include?(".home-hero-family"),
158
+ "both name parts are unbreakable")
159
+
160
+ desktop = hero[/@media \(min-width: 768px\) \{.*?\n\}/m]
161
+ check!(desktop && desktop.include?("white-space: nowrap"),
162
+ "from 768px the name stays on one line")
163
+ check!(!desktop.include?("3pt"), "the +3pt bump is phone-only")
164
+
165
+ puts "PASS test-hero-name"
@@ -21,6 +21,17 @@ def run_doctor(dir)
21
21
  Open3.capture3(env, "ruby", EXE.to_s, "doctor", dir.to_s)
22
22
  end
23
23
 
24
+ guide = ROOT.join("docs/getting-started.md").read
25
+ readme = ROOT.join("README.md").read
26
+ unless guide.include?('gem at `~> 1.3`')
27
+ warn "FAIL test-new: getting-started must document the scaffold pin ~> 1.3"
28
+ exit 1
29
+ end
30
+ if readme.include?("once 1.1 is on RubyGems")
31
+ warn "FAIL test-new: README still says the gem is unpublished at 1.1"
32
+ exit 1
33
+ end
34
+
24
35
  FileUtils.rm_rf(DEST)
25
36
 
26
37
  stdout, stderr, status = run_new(DEST.to_s)
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # Shell accessibility (skip link and home-link name).
5
+ # The top bar and footer used site.author alone, so a site with no author
6
+ # exposed the accessible name " – Home" and hid the visible wordmark.
7
+
8
+ require "pathname"
9
+
10
+ ROOT = Pathname.new(__dir__).join("..", "..").expand_path
11
+ SHELL = ROOT.join("_layouts/shell.html").read
12
+ FOOTER = ROOT.join("_includes/page/site-footer.html").read
13
+ CSS = ROOT.join("assets/css/ledger/chrome.css").read
14
+
15
+ def fail!(message)
16
+ warn "FAIL test-shell-a11y: #{message}"
17
+ exit 1
18
+ end
19
+
20
+ fail!("missing skip link") unless SHELL.include?('class="skip-link"') && SHELL.include?('href="#page-content"')
21
+ fail!("missing skip target") unless SHELL.include?('id="page-content"')
22
+ fail!("skip link is not shown on focus") unless CSS.include?(".skip-link:focus")
23
+
24
+ bare = 'aria-label="{{ site.author }} – Home"'
25
+ fail!("shell home link still uses site.author alone") if SHELL.include?(bare)
26
+ fail!("footer home link still uses site.author alone") if FOOTER.include?(bare)
27
+ fail!("shell home link needs an identity.name fallback") unless SHELL.include?("site.pandorga.identity.name")
28
+ fail!("footer home link needs an identity.name fallback") unless FOOTER.include?("site.pandorga.identity.name")
29
+
30
+ puts "PASS test-shell-a11y"
@@ -46,6 +46,14 @@ TEMPLATES.each do |name|
46
46
  end
47
47
  end
48
48
 
49
+ %w[_includes/chord-tab.html _includes/transcription.html].each do |rel|
50
+ text = ROOT.join(rel).read
51
+ if text.include?("Exibe")
52
+ warn "FAIL test-template-packages: #{rel} still has a Portuguese comment"
53
+ failed = true
54
+ end
55
+ end
56
+
49
57
  exit 1 if failed
50
58
 
51
59
  puts "PASS test-template-packages"
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ # A listed gate that is not on disk must fail the run. Skipping it lets a
5
+ # renamed test disappear from CI while validate.sh still exits 0.
6
+
7
+ require "pathname"
8
+
9
+ ROOT = Pathname.new(__dir__).join("..", "..").expand_path
10
+ SCRIPT = ROOT.join("scripts/validate.sh")
11
+
12
+ def fail!(message)
13
+ warn "FAIL test-validate-allowlist: #{message}"
14
+ exit 1
15
+ end
16
+
17
+ source = SCRIPT.read
18
+ block = source[/if \[\[ ! -f "\$test" \]\]; then.*?fi/m]
19
+ fail!("missing-file branch not found") unless block
20
+ fail!("a missing listed test must set failed=1") unless block.include?("failed=1")
21
+ fail!("a missing listed test must not be skipped") if block.include?("skip")
22
+
23
+ listed = source.scan(%r{scripts/test/test-[A-Za-z0-9_.-]+})
24
+ fail!("allowlist is empty") if listed.empty?
25
+
26
+ missing = listed.uniq.reject { |rel| ROOT.join(rel).file? }
27
+ fail!("listed tests are not on disk: #{missing.join(', ')}") unless missing.empty?
28
+
29
+ puts "PASS test-validate-allowlist"
data/scripts/validate.sh CHANGED
@@ -51,15 +51,19 @@ TESTS=(
51
51
  scripts/test/test-absent-field-absent-element.rb
52
52
  scripts/test/test-ledger-blocks.rb
53
53
  scripts/test/test-home-band-art.rb
54
+ scripts/test/test-hero-name.rb
54
55
  scripts/test/test-r2-json-sync.rb
55
56
  scripts/test/test-writing-slug-limits.rb
56
57
  scripts/test/test-examples-build.rb
58
+ scripts/test/test-validate-allowlist.rb
59
+ scripts/test/test-shell-a11y.rb
57
60
  )
58
61
 
59
62
  failed=0
60
63
  for test in "${TESTS[@]}"; do
61
64
  if [[ ! -f "$test" ]]; then
62
- echo "-- ${test} (missing, skip)"
65
+ echo "-- ${test} (missing)"
66
+ failed=1
63
67
  continue
64
68
  fi
65
69
  echo "-- ${test}"
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.4.9
4
+ version: 1.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - The Pandorga contributors
@@ -285,6 +285,7 @@ files:
285
285
  - lib/pandorga/jekyll/plugins/force_utf8_encoding.rb
286
286
  - lib/pandorga/jekyll/plugins/git_chronology.rb
287
287
  - lib/pandorga/jekyll/plugins/git_metadata.rb
288
+ - lib/pandorga/jekyll/plugins/identity_name.rb
288
289
  - lib/pandorga/jekyll/plugins/job_sections.rb
289
290
  - lib/pandorga/jekyll/plugins/lib/content_validators.rb
290
291
  - lib/pandorga/jekyll/plugins/lib/resource_validator.rb
@@ -391,6 +392,7 @@ files:
391
392
  - scripts/test/test-exported-fields-reach-the-view.rb
392
393
  - scripts/test/test-form-control-theming.rb
393
394
  - scripts/test/test-git-chronology.rb
395
+ - scripts/test/test-hero-name.rb
394
396
  - scripts/test/test-hero-portraits.mjs
395
397
  - scripts/test/test-hero-portraits.rb
396
398
  - scripts/test/test-home-band-art.rb
@@ -408,6 +410,7 @@ files:
408
410
  - scripts/test/test-r2-json-sync.rb
409
411
  - scripts/test/test-registry-home.rb
410
412
  - scripts/test/test-runtime-js-syntax.rb
413
+ - scripts/test/test-shell-a11y.rb
411
414
  - scripts/test/test-slim-writing-index.rb
412
415
  - scripts/test/test-studio-api-contract.rb
413
416
  - scripts/test/test-studio-content-files.rb
@@ -423,6 +426,7 @@ files:
423
426
  - scripts/test/test-text-excerpt.rb
424
427
  - scripts/test/test-unwrap-media-src.mjs
425
428
  - scripts/test/test-unwrap-media-src.rb
429
+ - scripts/test/test-validate-allowlist.rb
426
430
  - scripts/test/test-withdrawn-content-export.rb
427
431
  - scripts/test/test-writing-index-export-slugs.rb
428
432
  - scripts/test/test-writing-slug-limits.rb