pandorga 1.14.0 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +31 -0
- data/DESIGN.md +15 -30
- data/_includes/content-runtime/43-blocks.html +0 -3
- data/_includes/content-runtime/60-fragments.html +83 -149
- data/_includes/content-runtime/90-bootstrap.html +3 -0
- data/_includes/cv/cv-box.html +10 -4
- data/_includes/home/band-index.html +22 -7
- data/_includes/home/band-listing.html +7 -3
- data/_includes/page/timeline-section.html +8 -3
- data/_includes/theme/theme-vars-block.liquid +3 -0
- data/_layouts/articles.html +0 -2
- data/_layouts/bibliography.html +0 -2
- data/_layouts/blog.html +0 -5
- data/_layouts/cv.html +27 -8
- data/_layouts/home.html +2 -13
- data/_layouts/projects.html +17 -9
- data/_layouts/resources.html +0 -9
- data/_layouts/shell.html +1 -7
- data/assets/css/article.css +10 -2
- data/assets/css/base.css +68 -44
- data/assets/css/ledger/bibliography.css +0 -31
- data/assets/css/ledger/chrome.css +51 -2
- data/assets/css/ledger/cv.css +0 -40
- data/assets/css/ledger/detail.css +20 -6
- data/assets/css/ledger/home.css +261 -402
- data/assets/css/ledger/projects.css +17 -19
- data/assets/css/ledger/rascunhos.css +0 -27
- data/assets/css/ledger/sources.css +0 -57
- data/assets/css/ledger/writing.css +0 -62
- data/lib/pandorga/commands/new.rb +2 -2
- data/lib/pandorga/commands/thumbnails.rb +3 -2
- data/lib/pandorga/registry/schema.yml +0 -3
- data/lib/pandorga/thumbnail_variants.rb +29 -3
- data/lib/pandorga/version.rb +1 -1
- data/scripts/check_home_lists.mjs +33 -5
- data/scripts/content/hero_portraits.rb +59 -11
- data/scripts/test/test-css-structure.rb +0 -1
- data/scripts/test/test-hero-16x9.rb +379 -0
- data/scripts/test/test-hero-portraits.mjs +10 -20
- data/scripts/test/test-hero-portraits.rb +31 -25
- data/scripts/test/test-highlighter-pills.rb +12 -2
- data/scripts/test/test-home-lists.rb +5 -0
- data/scripts/test/test-new.rb +6 -6
- data/scripts/test/test-shell-a11y.rb +2 -1
- data/scripts/test/test-studio-section-texts.rb +625 -0
- data/scripts/test/test-theme-contrast.rb +13 -2
- data/scripts/test/test-v3-layout.rb +559 -0
- data/scripts/validate.sh +3 -0
- data/studio/assets/{index-BJyN-kB3.js → index-B9h1WUYY.js} +1 -1
- data/studio/index.html +1 -1
- data/studio/schema.yml +126 -2
- data/studio-app/src/lib/yaml-doc.js +6 -0
- data/templates/network/studio.yml +10 -0
- metadata +5 -2
|
@@ -71,8 +71,8 @@ def mix(color_a, color_b, percentage)
|
|
|
71
71
|
end
|
|
72
72
|
|
|
73
73
|
themes = [
|
|
74
|
-
{ name: "light", theme: light, menu_strength: 18, strong_strength: 28, facet_strength: 14, quiet_strength: 12 },
|
|
75
|
-
{ name: "dark", theme: dark, menu_strength: 14, strong_strength: 22, facet_strength: 14, quiet_strength: 10 }
|
|
74
|
+
{ name: "light", theme: light, menu_strength: 18, strong_strength: 28, hover_strength: 30, facet_strength: 14, quiet_strength: 12 },
|
|
75
|
+
{ name: "dark", theme: dark, menu_strength: 14, strong_strength: 22, hover_strength: 24, facet_strength: 14, quiet_strength: 10 }
|
|
76
76
|
]
|
|
77
77
|
|
|
78
78
|
themes.each do |entry|
|
|
@@ -114,6 +114,17 @@ themes.each do |entry|
|
|
|
114
114
|
fail!("PLT-AC-46 Menu active #{mode} #{accent} on #{bg}: #{c.round(2)}:1 < 4.5:1") if c < 4.5
|
|
115
115
|
end
|
|
116
116
|
|
|
117
|
+
# F9: Text calls to action (PLT-AC-49): the emphasis ink over the strong
|
|
118
|
+
# and hover mark heads, on every step a card or the canvas can show.
|
|
119
|
+
(surfaces + [neutrals["panel_contrast"]]).compact.uniq.each do |bg|
|
|
120
|
+
# The ink is `--action-ink-mix`: 74% accent in light, 85% in dark.
|
|
121
|
+
f9_text = mix(accent, on_surface, (mode == "light") ? 74 : 85)
|
|
122
|
+
[entry[:strong_strength], entry[:hover_strength]].each do |strength|
|
|
123
|
+
c = contrast(f9_text, mix(accent, bg, strength))
|
|
124
|
+
fail!("PLT-AC-49 F9 #{mode} #{accent} at #{strength}% on #{bg}: #{c.round(2)}:1 < 4.5:1") if c < 4.5
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
117
128
|
# F6: Solid mark (inverted on-primary text over solid accent at glyph)
|
|
118
129
|
c_f6 = contrast(on_primary, accent)
|
|
119
130
|
fail!("PLT-AC-46 F6 #{mode} #{on_primary} on #{accent}: #{c_f6.round(2)}:1 < 4.5:1") if c_f6 < 4.5
|
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
# PLT-AC-49. Canvas and card layout (docs/features/v3-layout.md).
|
|
5
|
+
#
|
|
6
|
+
# Each check names the acceptance item it covers (49.1 … 49.14). Pass item
|
|
7
|
+
# numbers to run a subset: `ruby scripts/test/test-v3-layout.rb 5 7`.
|
|
8
|
+
# The parser understands nested @media blocks and splits selector lists only
|
|
9
|
+
# on top-level commas, so `:is()` / `:where()` lists stay whole.
|
|
10
|
+
|
|
11
|
+
require "pathname"
|
|
12
|
+
require "set"
|
|
13
|
+
|
|
14
|
+
ROOT = Pathname.new(__dir__).join("../..").expand_path
|
|
15
|
+
CSS = ROOT.join("assets/css")
|
|
16
|
+
require "set"
|
|
17
|
+
ONLY_ITEMS = ARGV.map { |a| a.sub(/\A(PLT-AC-)?49\./, "") }.to_set
|
|
18
|
+
|
|
19
|
+
MOBILE = "max-width:767px"
|
|
20
|
+
WIDE = "min-width:768px"
|
|
21
|
+
|
|
22
|
+
$failures = []
|
|
23
|
+
|
|
24
|
+
def item(id)
|
|
25
|
+
return if !ONLY_ITEMS.empty? && !ONLY_ITEMS.include?(id.to_s)
|
|
26
|
+
|
|
27
|
+
@item = id
|
|
28
|
+
yield
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def fail!(msg)
|
|
32
|
+
$failures << "FAIL PLT-AC-49.#{@item}: #{msg}"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def strip_comments(text)
|
|
36
|
+
text.gsub(%r{/\*.*?\*/}m, "")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def split_top(text, sep)
|
|
40
|
+
parts = []
|
|
41
|
+
depth = 0
|
|
42
|
+
quote = nil
|
|
43
|
+
buf = +""
|
|
44
|
+
text.each_char do |c|
|
|
45
|
+
if quote
|
|
46
|
+
quote = nil if c == quote
|
|
47
|
+
elsif c == '"' || c == "'"
|
|
48
|
+
quote = c
|
|
49
|
+
elsif c == "(" || c == "["
|
|
50
|
+
depth += 1
|
|
51
|
+
elsif c == ")" || c == "]"
|
|
52
|
+
depth -= 1
|
|
53
|
+
elsif c == sep && depth.zero?
|
|
54
|
+
parts << buf
|
|
55
|
+
buf = +""
|
|
56
|
+
next
|
|
57
|
+
end
|
|
58
|
+
buf << c
|
|
59
|
+
end
|
|
60
|
+
parts << buf
|
|
61
|
+
parts.map(&:strip).reject(&:empty?)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def norm_selector(sel)
|
|
65
|
+
sel.gsub(/\s+/, " ").gsub(/\s*([>+~])\s*/, '\1').strip
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def norm_media(head)
|
|
69
|
+
head.sub(/\A@media/, "").gsub(/\s+/, "")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def parse_rules(css, media = nil, out = [])
|
|
73
|
+
i = 0
|
|
74
|
+
head = +""
|
|
75
|
+
while i < css.length
|
|
76
|
+
c = css[i]
|
|
77
|
+
if c == "{"
|
|
78
|
+
depth = 1
|
|
79
|
+
j = i + 1
|
|
80
|
+
while j < css.length && depth.positive?
|
|
81
|
+
depth += 1 if css[j] == "{"
|
|
82
|
+
depth -= 1 if css[j] == "}"
|
|
83
|
+
j += 1
|
|
84
|
+
end
|
|
85
|
+
body = css[(i + 1)...(j - 1)]
|
|
86
|
+
h = head.strip
|
|
87
|
+
if h.start_with?("@media")
|
|
88
|
+
parse_rules(body, [media, norm_media(h)].compact.join("and"), out)
|
|
89
|
+
elsif h.start_with?("@supports", "@layer", "@container")
|
|
90
|
+
parse_rules(body, media, out)
|
|
91
|
+
elsif !h.start_with?("@")
|
|
92
|
+
decls = split_top(body, ";").filter_map do |d|
|
|
93
|
+
prop, val = d.split(":", 2)
|
|
94
|
+
next unless val
|
|
95
|
+
|
|
96
|
+
[prop.strip.downcase, val.gsub(/\s+/, " ").strip]
|
|
97
|
+
end
|
|
98
|
+
split_top(h, ",").each do |sel|
|
|
99
|
+
out << { sel: norm_selector(sel), decls: decls, media: media }
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
head = +""
|
|
103
|
+
i = j
|
|
104
|
+
elsif c == ";" && head.strip.start_with?("@")
|
|
105
|
+
head = +""
|
|
106
|
+
i += 1
|
|
107
|
+
elsif c == "}"
|
|
108
|
+
i += 1
|
|
109
|
+
else
|
|
110
|
+
head << c
|
|
111
|
+
i += 1
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
out
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
SHEETS = Hash.new do |h, rel|
|
|
118
|
+
text = strip_comments(File.read(CSS.join(rel)))
|
|
119
|
+
h[rel] = { text: text, rules: parse_rules(text) }
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def media_match?(rule_media, media)
|
|
123
|
+
return rule_media.nil? if media.nil?
|
|
124
|
+
|
|
125
|
+
!rule_media.nil? && rule_media.include?(media)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Ordered declarations of every rule for `selector` under `media`
|
|
129
|
+
# (nil = outside any @media), in source order.
|
|
130
|
+
def decls(rel, selector, media = nil)
|
|
131
|
+
sel = norm_selector(selector)
|
|
132
|
+
SHEETS[rel][:rules].select { |r| r[:sel] == sel && media_match?(r[:media], media) }.flat_map { |r| r[:decls] }
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def value(list, prop)
|
|
136
|
+
list.reverse.find { |p, _| p == prop }&.last
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def background(list)
|
|
140
|
+
list.reverse.find { |p, _| p == "background" || p == "background-color" }&.last
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def where(rel, selector, media)
|
|
144
|
+
"#{rel} #{selector}#{media ? " @media (#{media})" : ''}"
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def expect(rel, selector, expected, media: nil)
|
|
148
|
+
list = decls(rel, selector, media)
|
|
149
|
+
if list.empty?
|
|
150
|
+
fail!("#{where(rel, selector, media)} has no rule")
|
|
151
|
+
return
|
|
152
|
+
end
|
|
153
|
+
expected.each do |prop, want|
|
|
154
|
+
got = prop == :background ? background(list) : value(list, prop)
|
|
155
|
+
next if got == want
|
|
156
|
+
|
|
157
|
+
fail!("#{where(rel, selector, media)} #{prop}: expected `#{want}`, found `#{got || 'unset'}`")
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def expect_unset(rel, selector, props, media: nil)
|
|
162
|
+
list = decls(rel, selector, media)
|
|
163
|
+
props.each do |prop|
|
|
164
|
+
got = value(list, prop)
|
|
165
|
+
fail!("#{where(rel, selector, media)} must not set #{prop} (found `#{got}`)") if got
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
# `border` must come before `border-top` so the shorthand does not reset it.
|
|
170
|
+
def expect_top_after_border(rel, selector)
|
|
171
|
+
list = decls(rel, selector)
|
|
172
|
+
b = list.rindex { |p, _| p == "border" }
|
|
173
|
+
t = list.rindex { |p, _| p == "border-top" }
|
|
174
|
+
return if b && t && t > b
|
|
175
|
+
|
|
176
|
+
fail!("#{rel} #{selector}: `border-top` must follow the `border` shorthand")
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def expect_clamp(rel, selector, lines, media: nil, display: true)
|
|
180
|
+
want = { "-webkit-line-clamp" => lines.to_s, "line-clamp" => lines.to_s }
|
|
181
|
+
want.merge!("display" => "-webkit-box", "-webkit-box-orient" => "vertical", "overflow" => "hidden") if display
|
|
182
|
+
expect(rel, selector, want, media: media)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def no_contrast_surface(rel, pattern)
|
|
186
|
+
SHEETS[rel][:rules].each do |r|
|
|
187
|
+
next unless r[:sel].match?(pattern)
|
|
188
|
+
|
|
189
|
+
r[:decls].each do |_, v|
|
|
190
|
+
next unless v.match?(/--color-surface-(card|high)-contrast\b/)
|
|
191
|
+
|
|
192
|
+
fail!("#{rel} #{r[:sel]} still paints #{v[/--color-surface-\w+-contrast/]}")
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def read(rel)
|
|
198
|
+
File.read(ROOT.join(rel))
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
# 49.1 Canvas -----------------------------------------------------------------
|
|
202
|
+
item(1) do
|
|
203
|
+
unless read("_includes/theme/theme-vars-block.liquid").include?("--color-canvas: {{ tn.canvas | default: tn.panel_contrast")
|
|
204
|
+
fail!("theme-vars-block must emit --color-canvas from neutrals.canvas, else panel_contrast")
|
|
205
|
+
end
|
|
206
|
+
expect("base.css", "html body", { background: "var(--color-canvas)" })
|
|
207
|
+
expect("ledger/home.css", ".home-band--hero", { background: "transparent" })
|
|
208
|
+
expect("ledger/home.css", ".home-band--contrast", { background: "transparent" })
|
|
209
|
+
no_contrast_surface("base.css", /./)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
# 49.2 Cards ------------------------------------------------------------------
|
|
213
|
+
item(2) do
|
|
214
|
+
%w[.ledger-card .ledger-system-card].each do |sel|
|
|
215
|
+
expect("base.css", sel, {
|
|
216
|
+
background: "var(--color-surface-card)",
|
|
217
|
+
"border" => "1px solid var(--border-ink)",
|
|
218
|
+
"border-radius" => "var(--radius-card)"
|
|
219
|
+
})
|
|
220
|
+
end
|
|
221
|
+
SHEETS["base.css"][:rules].each do |r|
|
|
222
|
+
next unless r[:sel].match?(/\[data-theme=["']?light|:not\(\[data-theme=["']?dark/)
|
|
223
|
+
next unless r[:sel].match?(/\.ledger-(system-)?card\z/)
|
|
224
|
+
|
|
225
|
+
fail!("base.css #{r[:sel]}: remove the light-only card border override")
|
|
226
|
+
end
|
|
227
|
+
expect("ledger/home.css", ".home-featured", {
|
|
228
|
+
background: "var(--color-surface-card)",
|
|
229
|
+
"border" => "1px solid var(--border-ink)",
|
|
230
|
+
"border-top" => "2px solid var(--color-primary)"
|
|
231
|
+
})
|
|
232
|
+
expect_top_after_border("ledger/home.css", ".home-featured")
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
# 49.3 Decoration -------------------------------------------------------------
|
|
236
|
+
RETIRED_SHARDS = %w[
|
|
237
|
+
home-hero-shard home-featured-shard projects-lead-shard projects-hero-shard
|
|
238
|
+
writing-lead-shard rascunhos-shard bib-masthead-shard bib-foot-shard
|
|
239
|
+
sources-masthead-shard sources-foot-shard cv-dossier-shard
|
|
240
|
+
].freeze
|
|
241
|
+
|
|
242
|
+
item(3) do
|
|
243
|
+
home = SHEETS["ledger/home.css"][:text]
|
|
244
|
+
fail!("home.css still draws a radial-gradient pool") if home.include?("radial-gradient(")
|
|
245
|
+
fail!("home.css still blurs a pool (filter: blur)") if home.match?(/(?<![-\w])filter:\s*blur\(/)
|
|
246
|
+
keep = %w[_includes/page/site-footer.html _includes/home/band-index.html]
|
|
247
|
+
Dir[ROOT.join("{_layouts,_includes}/**/*.html").to_s].sort.each do |path|
|
|
248
|
+
rel = Pathname.new(path).relative_path_from(ROOT).to_s
|
|
249
|
+
next if keep.include?(rel)
|
|
250
|
+
|
|
251
|
+
fail!("#{rel} still renders a decorative shard") if File.read(path).include?("shard-accent")
|
|
252
|
+
end
|
|
253
|
+
Dir[CSS.join("**/*.css").to_s].sort.each do |path|
|
|
254
|
+
text = strip_comments(File.read(path))
|
|
255
|
+
RETIRED_SHARDS.each do |cls|
|
|
256
|
+
fail!("#{Pathname.new(path).relative_path_from(ROOT)} still styles .#{cls}") if text.match?(/\.#{cls}\b/)
|
|
257
|
+
end
|
|
258
|
+
end
|
|
259
|
+
if read("_includes/content-runtime/43-blocks.html").include?("ledger-frame-corners")
|
|
260
|
+
fail!("43-blocks.html still emits ledger-frame-corners on cards")
|
|
261
|
+
end
|
|
262
|
+
unless read("_includes/page/site-footer.html").include?("shard-accent")
|
|
263
|
+
fail!("the footer must keep its shards")
|
|
264
|
+
end
|
|
265
|
+
end
|
|
266
|
+
|
|
267
|
+
# 49.4 Top bar ----------------------------------------------------------------
|
|
268
|
+
item(4) do
|
|
269
|
+
shell = read("_layouts/shell.html")
|
|
270
|
+
fail!("shell.html top bar must be <header class=\"site-topbar sticky top-0 z-50\">") unless shell.include?('<header class="site-topbar sticky top-0 z-50">')
|
|
271
|
+
fail!("shell.html still draws the tonal separator under the top bar") if shell.include?("bg-surface-container-low h-[1px]")
|
|
272
|
+
expect("ledger/chrome.css", ".site-topbar", {
|
|
273
|
+
"background-color" => "color-mix(in srgb, var(--color-canvas) 92%, transparent)",
|
|
274
|
+
"backdrop-filter" => "blur(10px)",
|
|
275
|
+
"-webkit-backdrop-filter" => "blur(10px)",
|
|
276
|
+
"border-bottom" => "1px solid var(--border-gentle-divider)"
|
|
277
|
+
})
|
|
278
|
+
%w[#theme-toggle-btn #mobile-menu-btn].each do |sel|
|
|
279
|
+
expect("ledger/chrome.css", sel, {
|
|
280
|
+
"width" => "2.25rem",
|
|
281
|
+
"height" => "2.25rem",
|
|
282
|
+
"border" => "1px solid var(--color-outline-variant)",
|
|
283
|
+
"background-color" => "var(--color-surface-card)",
|
|
284
|
+
"color" => "var(--color-on-surface)"
|
|
285
|
+
})
|
|
286
|
+
end
|
|
287
|
+
end
|
|
288
|
+
|
|
289
|
+
# 49.5 Band header ------------------------------------------------------------
|
|
290
|
+
item(5) do
|
|
291
|
+
h = "ledger/home.css"
|
|
292
|
+
expect(h, ".home-band:not(.home-band--hero) .ledger-section-banner", {
|
|
293
|
+
"display" => "grid", "grid-template-columns" => "minmax(0, 1fr) auto"
|
|
294
|
+
})
|
|
295
|
+
expect(h, ".home-band .ledger-section-lead", { "display" => "contents" })
|
|
296
|
+
expect(h, ".home-band .ledger-section-banner .ledger-kicker", { "grid-column" => "1 / -1", "grid-row" => "1" })
|
|
297
|
+
expect(h, ".home-band .ledger-kicker:empty", { "display" => "none" })
|
|
298
|
+
expect(h, ".home-band .ledger-section-banner .section-title", {
|
|
299
|
+
"grid-column" => "1", "grid-row" => "2", "font-size" => "var(--type-section-sm)"
|
|
300
|
+
})
|
|
301
|
+
expect(h, ".home-band .ledger-section-banner .section-title", { "font-size" => "var(--type-section)" }, media: WIDE)
|
|
302
|
+
expect(h, ".home-band .home-section-aside", { "grid-column" => "2", "grid-row" => "2" })
|
|
303
|
+
expect(h, ".home-band .home-section-note", {
|
|
304
|
+
"grid-column" => "1 / -1", "grid-row" => "3", "font-size" => "var(--type-dek)"
|
|
305
|
+
})
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
# 49.6 Card type --------------------------------------------------------------
|
|
309
|
+
item(6) do
|
|
310
|
+
b = "base.css"
|
|
311
|
+
expect(b, ".ledger-meta", {
|
|
312
|
+
"font-family" => "var(--font-mono)", "letter-spacing" => "0", "font-size" => "var(--type-micro)"
|
|
313
|
+
})
|
|
314
|
+
%w[.card-title .ledger-card-title].each do |sel|
|
|
315
|
+
expect(b, sel, { "font-weight" => "var(--weight-emphasis)" })
|
|
316
|
+
expect(b, sel, { "font-size" => "var(--type-card-sm)", "line-height" => "1.25" }, media: MOBILE)
|
|
317
|
+
end
|
|
318
|
+
%w[.ledger-card-body .ledger-system-card].each do |sel|
|
|
319
|
+
expect(b, sel, { "padding" => "var(--space-md)" }, media: MOBILE)
|
|
320
|
+
end
|
|
321
|
+
expect_clamp(b, ".ledger-card-copy", 3)
|
|
322
|
+
expect_clamp(b, ".ledger-card-copy", 4, media: WIDE, display: false)
|
|
323
|
+
expect(b, ".ledger-card-copy", { "font-size" => "var(--type-dek)" }, media: MOBILE)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# 49.7 Featured article -------------------------------------------------------
|
|
327
|
+
item(7) do
|
|
328
|
+
h = "ledger/home.css"
|
|
329
|
+
expect(h, ".home-featured", {
|
|
330
|
+
"display" => "flex", "flex-direction" => "column", "padding" => "0"
|
|
331
|
+
}, media: MOBILE)
|
|
332
|
+
expect(h, ".home-featured > .ledger-poster", {
|
|
333
|
+
"order" => "-1", "width" => "100%", "margin" => "0", "aspect-ratio" => "16 / 9", "border" => "0"
|
|
334
|
+
}, media: MOBILE)
|
|
335
|
+
expect(h, ".home-featured-lead", { "padding" => "var(--space-md)" }, media: MOBILE)
|
|
336
|
+
expect_clamp(h, ".home-featured-copy", 3)
|
|
337
|
+
expect_clamp(h, ".home-featured-copy", 4, media: WIDE, display: false)
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# 49.8 Index panels -----------------------------------------------------------
|
|
341
|
+
item(8) do
|
|
342
|
+
band = read("_includes/home/band-index.html")
|
|
343
|
+
fail!("band-index.html still renders a shard") if band.include?("shard-accent")
|
|
344
|
+
head = band.index('class="home-index-panel-head"')
|
|
345
|
+
figure = band.index('<figure class="home-index-plate"')
|
|
346
|
+
if head.nil? || figure.nil?
|
|
347
|
+
fail!("band-index.html needs .home-index-panel-head before figure.home-index-plate")
|
|
348
|
+
else
|
|
349
|
+
block = band[head...figure]
|
|
350
|
+
unless figure > head && block.include?('class="home-index-title"') && block.include?("ledger-cta") && block.include?("</div>")
|
|
351
|
+
fail!("band-index.html: .home-index-panel-head must hold the h3 title and the Open link, before the plate")
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
h = "ledger/home.css"
|
|
355
|
+
expect(h, ".home-index-panel", {
|
|
356
|
+
"padding" => "0", "gap" => "0",
|
|
357
|
+
background: "var(--color-surface-card)", "border" => "1px solid var(--border-ink)"
|
|
358
|
+
})
|
|
359
|
+
expect(h, ".home-index-panel-head", {
|
|
360
|
+
"display" => "flex", "justify-content" => "space-between", "align-items" => "center",
|
|
361
|
+
"padding" => "var(--space-sm) var(--space-md)"
|
|
362
|
+
})
|
|
363
|
+
expect(h, ".home-index-plate", { "border-top" => "1px solid var(--border-gentle-divider)" })
|
|
364
|
+
fail!("home.css still styles .home-index-shard") if SHEETS[h][:text].include?(".home-index-shard")
|
|
365
|
+
no_contrast_surface(h, /home-index-panel/)
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# 49.9 Rascunhos --------------------------------------------------------------
|
|
369
|
+
item(9) do
|
|
370
|
+
h = "ledger/home.css"
|
|
371
|
+
rules = SHEETS[h][:rules]
|
|
372
|
+
wide = ->(r) { r[:media]&.include?(WIDE) }
|
|
373
|
+
# Selectors with `:nth-child()` are matched without whitespace, so
|
|
374
|
+
# `nth-child(4n + 1)` and `nth-child(4n+1)` count as the same rule.
|
|
375
|
+
loose = ->(sel) { sel.gsub(/\s+/, "") }
|
|
376
|
+
find = lambda do |sel, media|
|
|
377
|
+
rules.select { |r| loose.(r[:sel]) == loose.(sel) && media_match?(r[:media], media) }.flat_map { |r| r[:decls] }
|
|
378
|
+
end
|
|
379
|
+
want = lambda do |sel, media, expected|
|
|
380
|
+
list = find.(sel, media)
|
|
381
|
+
if list.empty?
|
|
382
|
+
fail!("#{where(h, sel, media)} has no rule")
|
|
383
|
+
next
|
|
384
|
+
end
|
|
385
|
+
expected.each do |prop, v|
|
|
386
|
+
got = value(list, prop)
|
|
387
|
+
fail!("#{where(h, sel, media)} #{prop}: expected `#{v}`, found `#{got || 'unset'}`") unless got == v
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
# Desktop keeps the dropped-sheet tilt; phones never rotate a card.
|
|
392
|
+
%w[4n+1 4n+2 4n+3 4n].each do |n|
|
|
393
|
+
t = value(find.(".home-draft-list>*:nth-child(#{n})", WIDE), "transform")
|
|
394
|
+
next if t&.match?(/\Arotate\(\s*-?(?!0(\.0+)?deg)[\d.]+deg\s*\)\z/)
|
|
395
|
+
|
|
396
|
+
fail!("#{where(h, ".home-draft-list>*:nth-child(#{n})", WIDE)} must keep a non-zero rotate(), found `#{t || 'unset'}`")
|
|
397
|
+
end
|
|
398
|
+
rules.each do |r|
|
|
399
|
+
next if wide.(r)
|
|
400
|
+
|
|
401
|
+
r[:decls].each do |p, v|
|
|
402
|
+
fail!("#{r[:sel]}#{r[:media] ? " @media (#{r[:media]})" : ''} sets #{p}: #{v}; rotate() only from 768px") if v.include?("rotate(")
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
# Phones: two cards side by side, the third a landscape card across both
|
|
407
|
+
# columns, the rest hidden.
|
|
408
|
+
third = ".home-draft-list>.home-draft-card:nth-child(3)"
|
|
409
|
+
want.(third, MOBILE, { "grid-column" => "1 / -1", "flex-direction" => "row" })
|
|
410
|
+
want.("#{third} .ledger-poster", MOBILE, {
|
|
411
|
+
"flex" => "0 0 38%", "aspect-ratio" => "auto", "align-self" => "stretch"
|
|
412
|
+
})
|
|
413
|
+
want.("#{third} .ledger-poster img", MOBILE, { "position" => "absolute", "inset" => "0" })
|
|
414
|
+
want.("#{third} .ledger-card-body", MOBILE, { "flex" => "1 1 auto", "min-width" => "0" })
|
|
415
|
+
want.(".home-draft-list>.home-draft-card:nth-child(n + 4)", MOBILE, { "display" => "none" })
|
|
416
|
+
rules.each do |r|
|
|
417
|
+
next unless r[:sel].include?("home-draft") && r[:sel].include?("nth-child(3)") && wide.(r)
|
|
418
|
+
next unless r[:decls].any? { |p, _| %w[grid-column flex-direction].include?(p) }
|
|
419
|
+
|
|
420
|
+
fail!("#{r[:sel]} @media (#{r[:media]}) reshapes the third card from 768px; phones only")
|
|
421
|
+
end
|
|
422
|
+
rules.each do |r|
|
|
423
|
+
next unless loose.(r[:sel]).include?("nth-child(n+4)") && r[:sel].include?("home-draft") && !r[:media]&.include?(MOBILE)
|
|
424
|
+
|
|
425
|
+
fail!("#{r[:sel]} hides drafts outside phones") if r[:decls].any? { |p, v| p == "display" && v == "none" }
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
expect(h, ".home-draft-card .ledger-poster", { "aspect-ratio" => "4 / 3" }, media: MOBILE)
|
|
429
|
+
expect(h, ".home-draft-card .ledger-meta", {
|
|
430
|
+
"flex-wrap" => "nowrap", "white-space" => "nowrap", "overflow" => "hidden"
|
|
431
|
+
}, media: MOBILE)
|
|
432
|
+
expect(h, ".home-draft-card .ledger-card-title", { "font-size" => "var(--type-dek)" }, media: MOBILE)
|
|
433
|
+
expect(h, ".home-draft-card .ledger-card-copy", { "font-size" => "var(--type-label)" }, media: MOBILE)
|
|
434
|
+
expect_clamp(h, ".home-draft-card .ledger-card-title", 3)
|
|
435
|
+
expect_clamp(h, ".home-draft-card .ledger-card-copy", 3)
|
|
436
|
+
expect_clamp(h, ".home-draft-card .ledger-card-copy", 4, media: WIDE, display: false)
|
|
437
|
+
rules.each do |r|
|
|
438
|
+
next unless r[:sel].include?("home-draft-card .ledger-card-copy")
|
|
439
|
+
|
|
440
|
+
r[:decls].each do |p, v|
|
|
441
|
+
fail!("#{r[:sel]} still clamps to #{v}") if p.end_with?("line-clamp") && %w[5 6].include?(v)
|
|
442
|
+
fail!("#{r[:sel]} keeps the 5-6 line filler (#{p}: #{v})") if (p == "min-height" && v.include?("--type-subtitle")) || (p == "height" && v == "0")
|
|
443
|
+
end
|
|
444
|
+
end
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# 49.10 Projects --------------------------------------------------------------
|
|
448
|
+
item(10) do
|
|
449
|
+
h = "ledger/home.css"
|
|
450
|
+
expect(h, ".home-project-inventory-row", {
|
|
451
|
+
background: "var(--color-surface-card)", "border" => "1px solid var(--border-ink)"
|
|
452
|
+
})
|
|
453
|
+
no_contrast_surface(h, /home-project/)
|
|
454
|
+
p = "ledger/projects.css"
|
|
455
|
+
expect(p, ".projects-page .projects-lead", {
|
|
456
|
+
"border" => "1px solid var(--border-ink)", "border-top" => "2px solid var(--color-primary)"
|
|
457
|
+
})
|
|
458
|
+
expect_top_after_border(p, ".projects-page .projects-lead")
|
|
459
|
+
expect_unset(p, ".projects-page .projects-lead", %w[border-left border-left-color])
|
|
460
|
+
expect_unset(p, ".projects-page .projects-lead:hover", %w[border-left border-left-color])
|
|
461
|
+
expect(p, ".projects-page .projects-lead-body", { "padding" => "var(--space-md)" }, media: MOBILE)
|
|
462
|
+
expect(p, ".projects-page .projects-grid", { "gap" => "var(--space-md)" }, media: MOBILE)
|
|
463
|
+
expect(p, ".projects-page .ledger-system-card > .ledger-poster", {
|
|
464
|
+
"margin" => "calc(var(--space-md) * -1) calc(var(--space-md) * -1) 0"
|
|
465
|
+
}, media: MOBILE)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# 49.11 Reading view ----------------------------------------------------------
|
|
469
|
+
item(11) do
|
|
470
|
+
d = "ledger/detail.css"
|
|
471
|
+
side = [".text-detail-sidebar", ".text-detail-sidebar .article-sidebar-heading", ".text-detail-sidebar .article-meta"]
|
|
472
|
+
side.each do |sel|
|
|
473
|
+
expect(d, sel, { background: "var(--color-surface-card)" })
|
|
474
|
+
expect(d, sel, { background: "transparent" }, media: MOBILE)
|
|
475
|
+
end
|
|
476
|
+
expect(d, ".text-detail-sidebar", { "border" => "0" }, media: MOBILE)
|
|
477
|
+
a = "article.css"
|
|
478
|
+
expect(a, ".text-page-main", {
|
|
479
|
+
background: "var(--color-surface-card)", "border" => "1px solid var(--border-ink)"
|
|
480
|
+
})
|
|
481
|
+
expect(a, ".text-page-main", {
|
|
482
|
+
background: "transparent", "border" => "0", "padding" => "0"
|
|
483
|
+
}, media: MOBILE)
|
|
484
|
+
end
|
|
485
|
+
|
|
486
|
+
# 49.12 Text calls to action --------------------------------------------------
|
|
487
|
+
PLAIN_CTA = ".ledger-cta:where(:not(.ledger-cta--emphasis):not(.ledger-cta--emphasis-alt))"
|
|
488
|
+
CTA_INK = "color-mix(in srgb, var(--color-primary) var(--action-ink-mix), var(--color-on-surface))"
|
|
489
|
+
|
|
490
|
+
item(12) do
|
|
491
|
+
b = "base.css"
|
|
492
|
+
shared = SHEETS[b][:rules].find do |r|
|
|
493
|
+
r[:sel] == PLAIN_CTA && r[:media].nil? && background(r[:decls]).to_s.include?("var(--mark-1)")
|
|
494
|
+
end
|
|
495
|
+
fail!("base.css: #{PLAIN_CTA} must join the shared highlighter mark rule") unless shared
|
|
496
|
+
expect(b, PLAIN_CTA, {
|
|
497
|
+
"--mark-1" => "var(--mark-strong-a)", "--mark-2" => "var(--mark-strong-b)", "--mark-3" => "var(--mark-strong-c)",
|
|
498
|
+
"color" => CTA_INK
|
|
499
|
+
})
|
|
500
|
+
expect(b, "#{PLAIN_CTA}:hover", {
|
|
501
|
+
"--mark-1" => "var(--mark-hover-a)", "--mark-2" => "var(--mark-hover-b)", "--mark-3" => "var(--mark-hover-c)",
|
|
502
|
+
"color" => CTA_INK
|
|
503
|
+
})
|
|
504
|
+
Dir[CSS.join("**/*.css").to_s].sort.each do |path|
|
|
505
|
+
rel = Pathname.new(path).relative_path_from(CSS).to_s
|
|
506
|
+
SHEETS[rel][:rules].each do |r|
|
|
507
|
+
next unless r[:sel].include?(".ledger-cta")
|
|
508
|
+
|
|
509
|
+
r[:decls].each do |p, v|
|
|
510
|
+
next unless p.start_with?("text-decoration") && v.include?("underline")
|
|
511
|
+
|
|
512
|
+
fail!("#{rel} #{r[:sel]} still underlines a call to action")
|
|
513
|
+
end
|
|
514
|
+
end
|
|
515
|
+
end
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
# 49.13 Guards ----------------------------------------------------------------
|
|
519
|
+
IMPORTANT_BASELINE = {
|
|
520
|
+
"article.css" => 5, "tailwind.css" => 9, "ledger/bibliography.css" => 1,
|
|
521
|
+
"ledger/detail.css" => 4, "ledger/sources.css" => 1
|
|
522
|
+
}.freeze
|
|
523
|
+
|
|
524
|
+
item(13) do
|
|
525
|
+
Dir[CSS.join("**/*.css").to_s].sort.each do |path|
|
|
526
|
+
rel = Pathname.new(path).relative_path_from(CSS).to_s
|
|
527
|
+
count = SHEETS[rel][:text].scan("!important").size
|
|
528
|
+
allowed = IMPORTANT_BASELINE.fetch(rel, 0)
|
|
529
|
+
fail!("#{rel} has #{count} !important (baseline #{allowed})") if count > allowed
|
|
530
|
+
end
|
|
531
|
+
end
|
|
532
|
+
|
|
533
|
+
# 49.14 Top bar fit -----------------------------------------------------------
|
|
534
|
+
RAIL_FROM = "min-width:1280px"
|
|
535
|
+
|
|
536
|
+
item(14) do
|
|
537
|
+
chrome = "ledger/chrome.css"
|
|
538
|
+
expect(chrome, ".site-rail", { "display" => "none" })
|
|
539
|
+
expect(chrome, ".site-rail", { "display" => "flex" }, media: RAIL_FROM)
|
|
540
|
+
expect(chrome, "#site-menu", { "display" => "none" }, media: RAIL_FROM)
|
|
541
|
+
SHEETS[chrome][:rules].each do |r|
|
|
542
|
+
next unless r[:media]
|
|
543
|
+
next unless %w[.site-rail #site-menu].include?(r[:sel])
|
|
544
|
+
next unless r[:decls].any? { |p, _| p == "display" }
|
|
545
|
+
next if r[:media] == "(#{RAIL_FROM})"
|
|
546
|
+
|
|
547
|
+
fail!("#{chrome} #{r[:sel]} sets display under @media #{r[:media]}; only (#{RAIL_FROM}) may")
|
|
548
|
+
end
|
|
549
|
+
expect(chrome, ".site-rail", { "gap" => "var(--space-sm)" }, media: "min-width:1280px)and(max-width:1439px")
|
|
550
|
+
expect(chrome, ".site-rail", { "gap" => "var(--space-lg)" })
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
if $failures.empty?
|
|
554
|
+
puts "PLT-AC-49 v3 layout: OK"
|
|
555
|
+
else
|
|
556
|
+
puts $failures
|
|
557
|
+
warn "#{$failures.size} PLT-AC-49 check(s) failed"
|
|
558
|
+
exit 1
|
|
559
|
+
end
|
data/scripts/validate.sh
CHANGED
|
@@ -71,9 +71,12 @@ TESTS=(
|
|
|
71
71
|
scripts/test/test-css-delivery.rb
|
|
72
72
|
scripts/test/test-discovery.rb
|
|
73
73
|
scripts/test/test-hero-portraits.rb
|
|
74
|
+
scripts/test/test-hero-16x9.rb
|
|
74
75
|
scripts/test/test-thumbnail-variants.rb
|
|
75
76
|
scripts/test/test-highlighter-pills.rb
|
|
76
77
|
scripts/test/test-timeline-pan.rb
|
|
78
|
+
scripts/test/test-v3-layout.rb
|
|
79
|
+
scripts/test/test-studio-section-texts.rb
|
|
77
80
|
)
|
|
78
81
|
|
|
79
82
|
failed=0
|