pandorga 1.6.9 → 1.7.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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +37 -0
  3. data/DESIGN.md +9 -9
  4. data/_includes/content-runtime/30-includes.html +1 -1
  5. data/_includes/content-runtime/43-blocks.html +1 -1
  6. data/_includes/content-runtime/50-detail.html +5 -5
  7. data/_includes/content-runtime/60-fragments.html +1 -1
  8. data/_includes/page/network-graph.html +2 -2
  9. data/_includes/page/site-footer.html +2 -2
  10. data/_includes/theme/font-loader.html +2 -54
  11. data/_includes/theme/theme-vars-block.liquid +10 -18
  12. data/_layouts/articles.html +1 -1
  13. data/_layouts/projects.html +2 -2
  14. data/_layouts/resources.html +1 -1
  15. data/_layouts/shell.html +1 -1
  16. data/_layouts/text.html +4 -4
  17. data/_plugins/lib/writing_entry.rb +1 -1
  18. data/assets/css/article.css +3 -3
  19. data/assets/css/base.css +0 -79
  20. data/assets/css/global-link.css +0 -1
  21. data/assets/css/ledger/chrome.css +1 -1
  22. data/assets/css/ledger/cv.css +1 -1
  23. data/assets/css/ledger/detail.css +1 -1
  24. data/assets/css/ledger/footer.css +1 -1
  25. data/assets/css/ledger/home.css +13 -42
  26. data/assets/css/ledger/projects.css +2 -2
  27. data/assets/css/ledger/sources.css +3 -3
  28. data/assets/css/ledger/writing.css +1 -1
  29. data/examples/full/_config.yml +9 -0
  30. data/lib/pandorga/fonts.rb +235 -0
  31. data/lib/pandorga/jekyll/plugins/fonts.rb +17 -0
  32. data/lib/pandorga/jekyll/plugins/lib/writing_entry.rb +1 -1
  33. data/lib/pandorga/registry/schema.yml +21 -0
  34. data/lib/pandorga/version.rb +1 -1
  35. data/scripts/content/export-content-json.rb +1 -1
  36. data/scripts/generate-configuration-doc.rb +1 -1
  37. data/scripts/test/render-parity/load-runtime.mjs +1 -1
  38. data/scripts/test/test-css-structure.rb +20 -0
  39. data/scripts/test/test-discovery.rb +5 -0
  40. data/scripts/test/test-examples-build.rb +9 -0
  41. data/scripts/test/test-fonts.rb +91 -0
  42. data/scripts/test/test-listing-filter-contract.rb +1 -1
  43. data/scripts/test/test-shell-a11y.rb +21 -0
  44. data/scripts/validate.sh +1 -0
  45. metadata +4 -1
@@ -49,18 +49,6 @@
49
49
  }
50
50
  }
51
51
 
52
- /* Structural edge: the short rule the mockup runs along the top-left of the
53
- featured band, marking it as the page's first statement after the hero. */
54
- .home-band--ruled::before {
55
- content: "";
56
- position: absolute;
57
- top: 0;
58
- left: 0;
59
- width: 6rem;
60
- height: 3px;
61
- background: var(--color-primary);
62
- }
63
-
64
52
  /* ── Hero ── */
65
53
  /* The band was 3rem above the name and 4.5rem below, which read as the block
66
54
  sitting low in its own space. Equal above and below, at 60% of the larger
@@ -117,7 +105,7 @@
117
105
  }
118
106
  }
119
107
 
120
- /* Ambient pool beNewsreader the hero.
108
+ /* Ambient pool behind the hero.
121
109
  *
122
110
  * Same weight as the earlier primary-container bloom, but keyed to neutral
123
111
  * ink/outline so it stays grey paper depth instead of a blue spotlight. */
@@ -418,7 +406,7 @@ html[data-theme="light"] .home-hero-plate {
418
406
  box-shadow: none;
419
407
  }
420
408
 
421
- /* Silk facet beNewsreader the portrait — restored; the Borders pass had hidden it. */
409
+ /* Silk facet behind the portrait — restored; the Borders pass had hidden it. */
422
410
  .home-hero-shard {
423
411
  top: -2.25rem;
424
412
  right: -2rem;
@@ -576,8 +564,7 @@ html[data-theme="light"] .home-hero-plate {
576
564
  was the piece the band leads with, and a reader scanning down met four
577
565
  equal entries. It now takes the card rung of the surface ladder, is fenced
578
566
  by the same hairline every other panel on the site carries, and opens on a
579
- rule in the primary — the structural edge this page already uses on
580
- `.home-band--ruled` — so the eye stops at the top of it. The image inside is
567
+ rule in the primary, so the eye stops at the top of it. The image inside is
581
568
  inset by the panel's own padding rather than running to the band edge, which
582
569
  is the other half of the promotion: the lead contains its picture, the cards
583
570
  below are contained by theirs.
@@ -646,7 +633,7 @@ html[data-theme="light"] .home-hero-plate {
646
633
  cubist half of the identity.
647
634
 
648
635
  The pair is placed around the poster rather than across the card, because
649
- the poster is opaque and a shard beNewsreader it is a shard nobody sees. The
636
+ the poster is opaque and a shard behind it is a shard nobody sees. The
650
637
  alpha takes the top-left, over the index column, where there is nothing but
651
638
  chrome and air. The beta takes the bottom-right and comes out from *under*
652
639
  the poster into the strip below it, which is the same move the index band
@@ -1050,8 +1037,8 @@ html[data-theme="light"] .home-hero-plate {
1050
1037
  box-sizing: border-box;
1051
1038
  }
1052
1039
 
1053
- /* The triad top rule (`.ledger-card--edge-primary`), restated at a weight
1054
- that beats the light-paper hairline reset on `.ledger-system-card`. */
1040
+ /* A primary top rule, at a weight that beats the light-paper hairline
1041
+ reset on `.ledger-system-card`. */
1055
1042
  :root .home-projects-bench .home-project-specimen,
1056
1043
  :root .home-projects-bench .home-project-specimen:hover {
1057
1044
  border-top: 2px solid var(--color-primary);
@@ -1568,11 +1555,11 @@ button.home-project-inventory-row {
1568
1555
  The figure is hidden until the markup lands, so a plate that never arrives
1569
1556
  leaves the panel with its title and its link rather than an empty frame. */
1570
1557
 
1571
- /* Ambient pool beNewsreader the pair. Dark palette only, for the reason set out
1558
+ /* Ambient pool behind the pair. Dark palette only, for the reason set out
1572
1559
  above the hero's.
1573
1560
  *
1574
- * The two panels sat side by side with a visible seam and nothing beNewsreader
1575
- * them. One pool centred on that seam, beNewsreader both, lights the pair as a
1561
+ * The two panels sat side by side with a visible seam and nothing behind
1562
+ * them. One pool centred on that seam, behind both, lights the pair as a
1576
1563
  * single object rather than giving each panel an opening of its own. Held at
1577
1564
  * 5% against the hero's 7%: at equal strength the page reads as having two
1578
1565
  * openings, and only one of them is where it starts.
@@ -1580,7 +1567,7 @@ button.home-project-inventory-row {
1580
1567
  * Centred is right here and nowhere else — the seam is the thing being lit,
1581
1568
  * so the offset is vertical instead, which is what keeps the pool partly
1582
1569
  * outside the band it backs. `.home-band` already carries `position:
1583
- * relative`; `isolation: isolate` is what stops `z-index: -1` escaping beNewsreader
1570
+ * relative`; `isolation: isolate` is what stops `z-index: -1` escaping behind
1584
1571
  * the page background. */
1585
1572
  .home-band--index {
1586
1573
  isolation: isolate;
@@ -1642,7 +1629,7 @@ button.home-project-inventory-row {
1642
1629
  the set, rotating hue the way every other shard on the site does.
1643
1630
  *
1644
1631
  * They sit in the foot of the panel, under the title and the link: the plate
1645
- * above carries an opaque background, so a shard beNewsreader it would be a shard
1632
+ * above carries an opaque background, so a shard behind it would be a shard
1646
1633
  * nobody sees. `overflow: hidden` on the panel above keeps the negative
1647
1634
  * offsets out of the document scroll width, and the shared utility already
1648
1635
  * unmounts both below 640px. */
@@ -1663,21 +1650,12 @@ button.home-project-inventory-row {
1663
1650
  background: var(--color-primary);
1664
1651
  }
1665
1652
 
1666
- .home-index-shard--gamma {
1667
- right: -3rem;
1668
- bottom: -3.5rem;
1669
- width: 11rem;
1670
- height: 11rem;
1671
- background: var(--color-tertiary);
1672
- }
1673
-
1674
1653
  /* Between the shard cut-off and the layout each panel is the full
1675
1654
  band wide on small screens, so a shard sized for half of it stops being a corner accent. */
1676
1655
  @media (max-width: 899px) {
1677
1656
 
1678
1657
  .home-index-shard,
1679
- .home-index-shard--beta,
1680
- .home-index-shard--gamma {
1658
+ .home-index-shard--beta {
1681
1659
  width: 8rem;
1682
1660
  height: 8rem;
1683
1661
  bottom: -2.5rem;
@@ -1688,7 +1666,7 @@ button.home-project-inventory-row {
1688
1666
  padding-top: var(--space-xl);
1689
1667
  }
1690
1668
 
1691
- /* The title and the link have to sit clear of the shard beNewsreader them. The
1669
+ /* The title and the link have to sit clear of the shard behind them. The
1692
1670
  plate is excluded because it is the one child that must stay full-bleed,
1693
1671
  and the shard because it is the thing being sat clear of. */
1694
1672
  .home-index-panel> :not(.home-index-plate):not(.shard-accent) {
@@ -1697,13 +1675,6 @@ button.home-project-inventory-row {
1697
1675
  margin-inline: var(--space-lg);
1698
1676
  }
1699
1677
 
1700
- .home-index-note {
1701
- margin: 0;
1702
- font-size: var(--type-body);
1703
- line-height: 1.5;
1704
- flex: 1 1 auto;
1705
- }
1706
-
1707
1678
  .home-index-title {
1708
1679
  margin: 0;
1709
1680
  font-size: var(--type-card);
@@ -183,7 +183,7 @@
183
183
  position: relative;
184
184
  display: block;
185
185
  text-decoration: none;
186
- /* Empty blueprint as texture beNewsreader the plate — not a second view mode. */
186
+ /* Empty blueprint as texture behind the plate — not a second view mode. */
187
187
  background-color: var(--color-panel-contrast);
188
188
  background-image:
189
189
  linear-gradient(color-mix(in srgb, var(--color-outline) 10%, transparent) 1px, transparent 1px),
@@ -350,7 +350,7 @@
350
350
 
351
351
  /* ══ Repositories ══
352
352
  The mockup closed on a source-code ledger. Its licence stamp, spec
353
- download and commit hashes have nothing beNewsreader them; the repository rows
353
+ download and commit hashes have nothing behind them; the repository rows
354
354
  do — `github` is set on five of the seven projects. */
355
355
  .projects-page .projects-repos {
356
356
  margin-top: var(--space-3xl);
@@ -9,7 +9,7 @@
9
9
 
10
10
  /* ── Shards ──
11
11
  The page had no depth treatment of any kind, and it is the wrong shape for
12
- the ambient pool the home bands carry: a pool beNewsreader a grid of many small
12
+ the ambient pool the home bands carry: a pool behind a grid of many small
13
13
  cards only darkens one corner of it. Shards instead, the two canonical
14
14
  polygons from base.css, one at each end of the run — top right above the
15
15
  grid, bottom left below it, the diagonal pair the CV dossier uses inside
@@ -56,7 +56,7 @@
56
56
  }
57
57
 
58
58
  /* A card is not positioned, so without this the shard — which is — would be
59
- painted over the bottom row rather than beNewsreader it. The rule the whole
59
+ painted over the bottom row rather than behind it. The rule the whole
60
60
  family answers to is that a backdrop never touches the object in front of
61
61
  it. */
62
62
  .sources-body>.listing-card-grid,
@@ -65,7 +65,7 @@
65
65
  z-index: 1;
66
66
  }
67
67
 
68
- /* The foot of the page, beside the pagination. It reaches up beNewsreader the last
68
+ /* The foot of the page, beside the pagination. It reaches up behind the last
69
69
  row of cards. In dark those cards are opaque again, so what shows is the
70
70
  wedge in the gutter and the band across the pagination strip. */
71
71
  .sources-foot-shard {
@@ -70,7 +70,7 @@
70
70
  The alpha sits bottom left, under the foot of the copy, the one corner the
71
71
  plate never reaches at any width. The beta is a true corner accent at the
72
72
  top right: it is offset far enough up that its visible part is the triangle
73
- above the plate's top edge, and it tucks beNewsreader the plate as it descends
73
+ above the plate's top edge, and it tucks behind the plate as it descends
74
74
  rather than fighting it. Neither corner repeats the home panel's pair, and
75
75
  the hues are the only two this page has — `.page-accent--green` rebinds
76
76
  primary onto secondary, so green and ochre is the whole palette here. */
@@ -18,6 +18,15 @@ pandorga:
18
18
  base_url: ""
19
19
  home:
20
20
  hero_art: page/hero-default.html
21
+ fonts:
22
+ title: "Cinzel Decorative"
23
+ body:
24
+ family: "Newsreader"
25
+ weights: [300, 400, 500, 600, 700]
26
+ mono:
27
+ family: "Courier Prime"
28
+ weights: [400, 700]
29
+ italic: true
21
30
  taxonomies:
22
31
  articles:
23
32
  tags: [Writing]
@@ -0,0 +1,235 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Spec: docs/features/fonts.md
4
+ # Refs: PLT-AC-30
5
+ module Pandorga
6
+ # Google Fonts families for one site. An omitted role keeps the
7
+ # Architectural Ledger face. A set role replaces that face only.
8
+ module Fonts
9
+ module_function
10
+
11
+ ROLES = %i[title label body mono code subtitle cv_body].freeze
12
+ # These follow another role when the theme used one family for both
13
+ # and the site did not name them. Changing the body then changes the
14
+ # tagline, instead of leaving Newsreader behind.
15
+ FOLLOW = { subtitle: :body, code: :mono, cv_body: :body }.freeze
16
+ MONO_FALLBACK = "ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, Liberation Mono, monospace"
17
+ FAMILY_NAME = /\A[\p{L}\p{N}][\p{L}\p{N} .+\-]*\z/
18
+
19
+ DEFAULTS = {
20
+ title: { family: "Cinzel Decorative", spec: "wght@400;700;900", fallback: "serif" },
21
+ subtitle: {
22
+ family: "Newsreader",
23
+ spec: "ital,opsz,wght@0,6..72,300;0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,300;1,6..72,400;1,6..72,500",
24
+ fallback: "serif"
25
+ },
26
+ label: { family: "Marcellus SC", spec: "wght@400;700", fallback: "serif" },
27
+ body: { family: "Newsreader", spec: "wght@300;400;500;600;700", fallback: "Georgia, serif" },
28
+ mono: { family: "Courier Prime", spec: "ital,wght@0,400;0,700;1,400;1,700", fallback: MONO_FALLBACK },
29
+ code: { family: "Courier Prime", spec: "ital,wght@0,300;0,400;0,700;1,400;1,700", fallback: MONO_FALLBACK },
30
+ cv_body: { family: "Newsreader", spec: "wght@300;400;500;600;700", fallback: "Georgia, serif" }
31
+ }.freeze
32
+
33
+ def resolve(site_fonts, theme_fonts = nil)
34
+ site = string_keys(site_fonts)
35
+ theme = string_keys(theme_fonts)
36
+ resolved = {}
37
+ ROLES.each do |role|
38
+ resolved[role] = resolve_role(role, site[role.to_s], theme)
39
+ end
40
+ FOLLOW.each do |role, source|
41
+ next if named?(site[role.to_s])
42
+ next unless same_family?(theme, role, source)
43
+
44
+ resolved[role] = resolved[role].merge(
45
+ family: resolved[source][:family],
46
+ spec: resolved[source][:spec]
47
+ )
48
+ end
49
+ resolved
50
+ end
51
+
52
+ def from_site(site)
53
+ pandorga = lookup(site, "pandorga")
54
+ fonts = lookup(pandorga, "fonts")
55
+ data = lookup(site, "data")
56
+ themes = lookup(data, "themes")
57
+ shared = lookup(themes, "shared")
58
+ resolve(fonts, lookup(shared, "fonts"))
59
+ end
60
+
61
+ def stylesheet_href(resolved)
62
+ seen = []
63
+ parts = []
64
+ ROLES.each do |role|
65
+ face = resolved[role]
66
+ family = face[:family].to_s
67
+ next if family.empty? || seen.include?(family)
68
+
69
+ seen << family
70
+ parts << "#{family.tr(' ', '+')}:#{face[:spec]}"
71
+ end
72
+ return "" if parts.empty?
73
+
74
+ "https://fonts.googleapis.com/css2?family=#{parts.join('&family=')}&display=swap"
75
+ end
76
+
77
+ def css_stack(resolved, role)
78
+ face = resolved[role.to_sym]
79
+ return "" unless face
80
+
81
+ quoted = face[:family].to_s.empty? ? nil : "'#{escape_css(face[:family])}'"
82
+ [quoted, face[:fallback].to_s.strip].compact.reject(&:empty?).join(", ")
83
+ end
84
+
85
+ def resolve_role(role, override, theme)
86
+ base = DEFAULTS.fetch(role)
87
+ family = text(theme[role.to_s])
88
+ family = base[:family] if family.empty?
89
+ spec = text(theme["google_#{role}_spec"])
90
+ spec = base[:spec] if spec.empty?
91
+ fallback = fallback_for(role, theme, base)
92
+
93
+ parsed = parse_override(override)
94
+ return { family: family, spec: spec, fallback: fallback } unless parsed
95
+
96
+ {
97
+ family: parsed[:family],
98
+ spec: spec_for(parsed, family, spec, base[:family]),
99
+ fallback: fallback
100
+ }
101
+ end
102
+
103
+ def fallback_for(role, theme, base)
104
+ if role == :cv_body
105
+ body = text(theme["body_fallback"])
106
+ return body unless body.empty?
107
+ end
108
+ own = text(theme["#{role}_fallback"])
109
+ own.empty? ? base[:fallback] : own
110
+ end
111
+
112
+ def spec_for(parsed, theme_family, theme_spec, default_family)
113
+ if parsed[:weights] || !parsed[:italic].nil?
114
+ weights = parsed[:weights] || [400, 700]
115
+ if parsed[:italic]
116
+ # Roman tuples first, then italic, so the axis list stays sorted.
117
+ tuples = weights.map { |weight| "0,#{weight}" } + weights.map { |weight| "1,#{weight}" }
118
+ "ital,wght@#{tuples.join(';')}"
119
+ else
120
+ "wght@#{weights.join(';')}"
121
+ end
122
+ elsif parsed[:family] == theme_family || parsed[:family] == default_family
123
+ theme_spec
124
+ else
125
+ "wght@400;700"
126
+ end
127
+ end
128
+
129
+ def parse_override(value)
130
+ case value
131
+ when String
132
+ family = clean_family(value)
133
+ family && { family: family, weights: nil, italic: nil }
134
+ when Hash
135
+ fields = string_keys(value)
136
+ family = clean_family(fields["family"] || fields["name"])
137
+ return nil unless family
138
+
139
+ { family: family, weights: parse_weights(fields["weights"]), italic: italic_flag(fields) }
140
+ end
141
+ end
142
+
143
+ def italic_flag(fields)
144
+ return nil unless fields.key?("italic")
145
+
146
+ value = fields["italic"]
147
+ case value
148
+ when true, "true", "yes", 1, "1" then true
149
+ else false
150
+ end
151
+ end
152
+
153
+ def parse_weights(value)
154
+ return nil if value.nil?
155
+
156
+ list = case value
157
+ when Array then value
158
+ when Integer then [value]
159
+ when String then value.split(",")
160
+ else return nil
161
+ end
162
+ numbers = []
163
+ list.each do |item|
164
+ text = item.to_s.strip
165
+ next if text.empty?
166
+ return nil unless text.match?(/\A\d+\z/)
167
+
168
+ number = text.to_i
169
+ return nil unless number.between?(1, 1000)
170
+
171
+ numbers << number
172
+ end
173
+ return nil if numbers.empty?
174
+
175
+ numbers.uniq.sort
176
+ end
177
+
178
+ def clean_family(value)
179
+ family = value.to_s.strip
180
+ return nil if family.empty? || !FAMILY_NAME.match?(family)
181
+
182
+ family
183
+ end
184
+
185
+ def named?(value)
186
+ !parse_override(value).nil?
187
+ end
188
+
189
+ def same_family?(theme, role, source)
190
+ role_family = text(theme[role.to_s])
191
+ source_family = text(theme[source.to_s])
192
+ role_family = DEFAULTS.fetch(role)[:family] if role_family.empty?
193
+ source_family = DEFAULTS.fetch(source)[:family] if source_family.empty?
194
+ role_family == source_family
195
+ end
196
+
197
+ def text(value)
198
+ value.to_s.strip
199
+ end
200
+
201
+ def string_keys(value)
202
+ return {} unless value.is_a?(Hash)
203
+
204
+ value.each_with_object({}) { |(key, item), out| out[key.to_s] = item }
205
+ end
206
+
207
+ def lookup(object, key)
208
+ return nil if object.nil?
209
+
210
+ string_key = key.to_s
211
+ if object.is_a?(Hash)
212
+ return object[string_key] if object.key?(string_key)
213
+ return object[string_key.to_sym] if object.key?(string_key.to_sym)
214
+
215
+ return nil
216
+ end
217
+
218
+ if object.respond_to?(:[])
219
+ begin
220
+ found = object[string_key]
221
+ return found unless found.nil?
222
+ rescue StandardError
223
+ nil
224
+ end
225
+ end
226
+ return object.public_send(string_key) if object.respond_to?(string_key)
227
+
228
+ nil
229
+ end
230
+
231
+ def escape_css(family)
232
+ family.to_s.gsub("\\", "\\\\\\\\").gsub("'", "\\\\'").gsub(/[\r\n]/, "")
233
+ end
234
+ end
235
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../fonts"
4
+
5
+ # Liquid filters for pandorga.fonts. The stylesheet URL and the CSS stacks
6
+ # come from one resolver so the head and the tokens cannot drift.
7
+ module PandorgaFontFilter
8
+ def pandorga_font_stylesheet(site)
9
+ Pandorga::Fonts.stylesheet_href(Pandorga::Fonts.from_site(site))
10
+ end
11
+
12
+ def pandorga_font_stack(site, role)
13
+ Pandorga::Fonts.css_stack(Pandorga::Fonts.from_site(site), role)
14
+ end
15
+ end
16
+
17
+ Liquid::Template.register_filter(PandorgaFontFilter) if defined?(Liquid::Template)
@@ -15,7 +15,7 @@ require_relative "text_excerpt"
15
15
  # (standalone, for scripts/content/export-content-json.rb). They differ only in how
16
16
  # they derive the repo-relative path used for Git chronology, so each passes
17
17
  # its own. Everything downstream lives here, because a field added or a bug
18
- # fixed in one copy used to leave the other silently beNewsreader.
18
+ # fixed in one copy used to leave the other silently behind.
19
19
  module WritingEntry
20
20
  XCITE_RE = /\{%-?\s*include\s+xcite\.html\s+key=["']([^"']+)["'][^%]*-?%\}/
21
21
 
@@ -50,6 +50,27 @@ home:
50
50
  type: string
51
51
  notes: Include path for hero decoration (default `page/hero-default.html`)
52
52
 
53
+ fonts:
54
+ title: "`pandorga.fonts`"
55
+ intro: |
56
+ Each value is a Google Fonts family name, or a map with `family` plus optional `weights` (integers) and `italic` (boolean). An omitted role keeps the Architectural Ledger face. `code` follows `mono`, and the tagline and CV body follow `body`, when the theme used one family for both. Text faces still load after the first paint, with `display=swap`. A `_data/themes/shared.yml` file remains the fallback under any role this map does not set.
57
+ keys:
58
+ title:
59
+ type: string or map
60
+ notes: Display face. Default family `Cinzel Decorative`, weights 400, 700, and 900
61
+ body:
62
+ type: string or map
63
+ notes: Reading face. Default family `Newsreader`, weights 300, 400, 500, 600, and 700
64
+ mono:
65
+ type: string or map
66
+ notes: Monospace face. Default family `Courier Prime`, roman and italic at 400 and 700
67
+ label:
68
+ type: string or map
69
+ notes: Optional chrome face. Default family `Marcellus SC`, weights 400 and 700
70
+ subtitle:
71
+ type: string or map
72
+ notes: Optional tagline face. Omitted, it follows `body` when the theme used the same family
73
+
53
74
  taxonomies:
54
75
  title: "`pandorga.taxonomies`"
55
76
  prose: |
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Pandorga
4
- VERSION = "1.6.9"
4
+ VERSION = "1.7.1"
5
5
  end
@@ -269,7 +269,7 @@ def prune_stale_output_files!
269
269
  end
270
270
 
271
271
  stale.each(&:delete)
272
- # Remove empty directories left beNewsreader by unpublish/rename.
272
+ # Remove empty directories left behind by unpublish/rename.
273
273
  OUTPUT_ROOT.find.sort.reverse_each do |path|
274
274
  next unless path.directory?
275
275
  next if path == OUTPUT_ROOT
@@ -66,7 +66,7 @@ def generate(schema)
66
66
  out << "via `scripts/generate-configuration-doc.rb` (`PLT-AC-11`)."
67
67
  out << ""
68
68
 
69
- %w[identity content home].each do |section|
69
+ %w[identity content home fonts].each do |section|
70
70
  data = schema.fetch(section)
71
71
  out << "## #{data.fetch('title')}"
72
72
  out << ""
@@ -5,7 +5,7 @@
5
5
  // and a Jekyll build never executes them, which is how a block that renders
6
6
  // an element with no backing field ships green. This loader concatenates the
7
7
  // partials in the exact order _includes/page/content-runtime.html includes
8
- // them, evaluates the result beNewsreader a deliberately thin DOM shim, and hands
8
+ // them, evaluates the result behind a deliberately thin DOM shim, and hands
9
9
  // back the same object 90-bootstrap.html returns to the browser.
10
10
  //
11
11
  // The shim is small on purpose: createElement, className, setAttribute,
@@ -72,6 +72,26 @@ sheets.each do |path|
72
72
  if source.include?("home-profile-summary")
73
73
  errors << "#{relative}: .home-profile-summary is unused"
74
74
  end
75
+
76
+ # Selectors that matched no template, runtime string, or current content.
77
+ # A comment may still name one; a ruleset may not.
78
+ retired = %w[
79
+ page-title
80
+ ledger-marker--primary
81
+ ledger-card--edge-primary
82
+ ledger-card--edge-secondary
83
+ ledger-card--edge-tertiary
84
+ ledger-section-rule
85
+ home-band--ruled
86
+ home-index-shard--gamma
87
+ home-index-note
88
+ post-content
89
+ ]
90
+ retired.each do |name|
91
+ next unless cleaned.match?(/[.#]#{Regexp.escape(name)}\b/)
92
+
93
+ errors << "#{relative}: .#{name} is unused"
94
+ end
75
95
  end
76
96
 
77
97
  fail!("CSS structure problems:\n - #{errors.join("\n - ")}") unless errors.empty?
@@ -41,6 +41,11 @@ fail!("portrait srcset is not read") unless runtime.include?("record.srcset = sr
41
41
  fail!("portrait sources are not applied") unless runtime.include?("applyHeroPortraitSources")
42
42
  shell = ROOT.join("_layouts/shell.html").read
43
43
  fail!("shell missing JSON-LD") unless shell.include?("theme/json-ld.html")
44
+ og_images = shell.scan(/property="og:image"/)
45
+ fail!("shell emits #{og_images.size} og:image properties; one page, one image") unless og_images.size == 1
46
+ image_meta = shell[/<meta name="image"[^>]*>/]
47
+ fail!("shell missing name=image") unless image_meta
48
+ fail!("name=image also sets property=og:image") if image_meta.include?("property=")
44
49
  fail!("discovery generator is not loaded from the plugin dir") unless ROOT.join("lib/pandorga/jekyll/plugins/discovery.rb").file?
45
50
 
46
51
  puts "PASS test-discovery"
@@ -90,6 +90,15 @@ end
90
90
  warn "FAIL test-examples-build: #{name} home is missing JSON-LD"
91
91
  exit 1
92
92
  end
93
+ ledger_faces = "family=Cinzel+Decorative:wght@400;700;900"
94
+ unless html.include?(ledger_faces) && html.include?("&display=swap") && html.include?("__pandorgaFontsHref")
95
+ warn "FAIL test-examples-build: #{name} home dropped the ledger font request"
96
+ exit 1
97
+ end
98
+ unless html.include?("--font-title: 'Cinzel Decorative', serif")
99
+ warn "FAIL test-examples-build: #{name} home dropped the title face"
100
+ exit 1
101
+ end
93
102
  end
94
103
 
95
104
  puts "PASS test-examples-build"