@piercebarney/whs-eleventy 2026.10.5 → 2026.10.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/compliance.js +51 -1
- package/package.json +1 -1
- package/standard/CHANGELOG.md +65 -0
- package/standard/core.md +9 -6
package/lib/compliance.js
CHANGED
|
@@ -51,6 +51,37 @@ function walk(dir) {
|
|
|
51
51
|
return out;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
// Mask out every @media (prefers-reduced-motion: no-preference) { ... }
|
|
55
|
+
// block's entire contents (string search + brace-counting to find the
|
|
56
|
+
// matching close, so nested rulesets inside the media block are correctly
|
|
57
|
+
// included in what gets masked) — whatever remains is, by construction,
|
|
58
|
+
// outside the required gate.
|
|
59
|
+
function maskGatedMotionBlocks(css) {
|
|
60
|
+
const marker = /@media[^{]*prefers-reduced-motion\s*:\s*no-preference[^{]*\{/;
|
|
61
|
+
let out = "";
|
|
62
|
+
let rest = css;
|
|
63
|
+
let m;
|
|
64
|
+
while ((m = rest.match(marker))) {
|
|
65
|
+
let depth = 1;
|
|
66
|
+
let i = m.index + m[0].length;
|
|
67
|
+
while (i < rest.length && depth > 0) {
|
|
68
|
+
if (rest[i] === "{") depth++;
|
|
69
|
+
else if (rest[i] === "}") depth--;
|
|
70
|
+
i++;
|
|
71
|
+
}
|
|
72
|
+
out += rest.slice(0, m.index);
|
|
73
|
+
rest = rest.slice(i);
|
|
74
|
+
}
|
|
75
|
+
return out + rest;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// (?<![\w-]) rather than a bare \b: a custom property like --pico-transition:
|
|
79
|
+
// is not a transition *declaration* — \b alone still matches right after the
|
|
80
|
+
// hyphen in "-transition", which false-positives on exactly that shape (found
|
|
81
|
+
// in the reference template's own vendored pico.min.css).
|
|
82
|
+
const MOTION_PROP = /(?<![\w-])(transition|animation|scroll-behavior)\s*:/;
|
|
83
|
+
const hasUngatedMotion = (css) => MOTION_PROP.test(maskGatedMotionBlocks(css));
|
|
84
|
+
|
|
54
85
|
// grep the tracked source tree (not _site, not node_modules).
|
|
55
86
|
function grepSrc(re, exts = [".js", ".njk", ".css", ".json"]) {
|
|
56
87
|
const roots = ["src", "static", "scripts"].map((d) => path.join(ROOT, d));
|
|
@@ -594,9 +625,28 @@ const CHECKS = {
|
|
|
594
625
|
a11y: () => {
|
|
595
626
|
if (!/\ba11y\b/.test(JSON.parse(read("package.json") || "{}").scripts?.check || ""))
|
|
596
627
|
return { status: FAIL, note: "no a11y stage in the gate" };
|
|
628
|
+
// Scan the authored source tree (src/static/scripts), not built _site —
|
|
629
|
+
// a passthrough-copied third-party stylesheet (e.g. a CSS framework
|
|
630
|
+
// vendored via addPassthroughCopy) lands in _site but was never authored
|
|
631
|
+
// by, or gate-able by, this project; scanning it produced a false FAIL
|
|
632
|
+
// against the reference template's own vendored pico.min.css.
|
|
633
|
+
const cssFiles = ["src", "static", "scripts"]
|
|
634
|
+
.map((d) => path.join(ROOT, d))
|
|
635
|
+
.flatMap((r) => walk(r))
|
|
636
|
+
.filter((f) => f.endsWith(".css"));
|
|
637
|
+
const ungated = cssFiles.filter((f) => hasUngatedMotion(fs.readFileSync(f, "utf8")));
|
|
638
|
+
if (ungated.length)
|
|
639
|
+
return {
|
|
640
|
+
status: FAIL,
|
|
641
|
+
note: `transition/animation not gated behind prefers-reduced-motion in ${ungated
|
|
642
|
+
.map((f) => path.relative(ROOT, f))
|
|
643
|
+
.join(", ")}`,
|
|
644
|
+
};
|
|
597
645
|
return {
|
|
598
646
|
status: MANUAL,
|
|
599
|
-
note:
|
|
647
|
+
note:
|
|
648
|
+
"axe scan is in the gate; prefers-reduced-motion gating confirmed; " +
|
|
649
|
+
"confirm remaining checklist items beyond the scan (focus order, live regions)",
|
|
600
650
|
};
|
|
601
651
|
},
|
|
602
652
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@piercebarney/whs-eleventy",
|
|
3
|
-
"version": "2026.10.
|
|
3
|
+
"version": "2026.10.7",
|
|
4
4
|
"description": "The web house style's Eleventy + Netlify tooling — the compliance sweep, the infra doctor, the link/CSP integrity check, and the a11y scan, shared by every project on the stack.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"whs": "cli.js"
|
package/standard/CHANGELOG.md
CHANGED
|
@@ -21,6 +21,71 @@ enforced, new *opt-in* chapters/capabilities, tooling-only changes — is
|
|
|
21
21
|
|
|
22
22
|
---
|
|
23
23
|
|
|
24
|
+
## 2026-10-07 [breaking]
|
|
25
|
+
|
|
26
|
+
### core: a11y
|
|
27
|
+
|
|
28
|
+
`core.md#a11y`'s Contract never named a WCAG version/level as its
|
|
29
|
+
conformance target — only "WCAG AA" for contrast. It now declares **WCAG 2.1
|
|
30
|
+
Level AA** explicitly as the target, anchored to US law (the ADA Title II
|
|
31
|
+
rule and US web-accessibility litigation/settlements reference WCAG 2.1 AA;
|
|
32
|
+
Section 508's WCAG 2.0 AA is already covered by it) rather than WCAG 2.2 AA
|
|
33
|
+
— the upgrade trigger to 2.2 AA is recorded, not scheduled, for once US rules
|
|
34
|
+
reference it. **Breaking:** a project that was previously compliant by
|
|
35
|
+
meeting the checklist plus passing the automated scan must now conform to
|
|
36
|
+
all of WCAG 2.1 AA, including criteria the checklist and scan don't
|
|
37
|
+
individually name (e.g. captions, reflow, keyboard operability everywhere)
|
|
38
|
+
— a real added requirement, not just a clarification of an already-met bar.
|
|
39
|
+
No tooling change: Eleventy's `whs a11y` scan already runs `wcag2a`/
|
|
40
|
+
`wcag2aa`/`wcag21a`/`wcag21aa` — it already matched this target. SvelteKit's
|
|
41
|
+
and Phoenix's scan gaps (no automated a11y tooling yet) are pre-existing and
|
|
42
|
+
unrelated to this change. Decided by the owner in
|
|
43
|
+
`feedback/2026-09-26-external-standards-survey.md` ("Decisions (2026-09-26)"
|
|
44
|
+
item 1); see that file for the full WCAG-version rationale, including why
|
|
45
|
+
2.1 and not 2.2.
|
|
46
|
+
|
|
47
|
+
### core: styling
|
|
48
|
+
|
|
49
|
+
The contrast line in `#styling` now also says "WCAG 2.1 AA" (was "WCAG AA"),
|
|
50
|
+
matching the version now named in `#a11y`.
|
|
51
|
+
|
|
52
|
+
### stacks/eleventy-netlify.md, stacks/phoenix.md, stacks/sveltekit-netlify.md
|
|
53
|
+
|
|
54
|
+
Each of `stacks/eleventy-netlify.md`, `stacks/phoenix.md`, and
|
|
55
|
+
`stacks/sveltekit-netlify.md`'s `#a11y` Core-contract pointer line now
|
|
56
|
+
mentions WCAG 2.1 AA alongside the existing checklist/scan pointer — no
|
|
57
|
+
change to any stack's own mechanism.
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## 2026-10-06 [breaking]
|
|
62
|
+
|
|
63
|
+
### packages/whs-eleventy
|
|
64
|
+
|
|
65
|
+
`a11y`'s chapter in `lib/compliance.js` was 100% unconditional `MANUAL` — it
|
|
66
|
+
never actually checked any of the Contract specs it listed. This adds real
|
|
67
|
+
mechanization for one of them: `core.md#a11y` requires every transition/
|
|
68
|
+
animation to be gated behind `@media (prefers-reduced-motion: no-preference)`
|
|
69
|
+
(also called out in `core.md`'s own anti-patterns list). Found via a real
|
|
70
|
+
project's manual anti-patterns review catching a violation the compliance
|
|
71
|
+
sweep itself missed. The check scans authored source (`src/static/scripts`),
|
|
72
|
+
not built output, so vendored/passthrough-copied third-party CSS a project
|
|
73
|
+
doesn't author is correctly excluded. Unlike this week's other silent-pass
|
|
74
|
+
fixes, this doesn't just correct a check that had a redundant real backstop
|
|
75
|
+
elsewhere (`client-logic`, 2026-10-03) or a pure-doc mismatch with no
|
|
76
|
+
enforcement (`forms`, 2026-10-04) — this is genuinely new detection where
|
|
77
|
+
none existed before. That means it can flip an already-`MANUAL`-passing real
|
|
78
|
+
project's `a11y` status to `FAIL` on its very next compliance run, if that
|
|
79
|
+
project has real, ungated transition/animation CSS in its own authored
|
|
80
|
+
source — not a bug in this fix, but the intended, working check finally
|
|
81
|
+
running. Known, narrow limitation: vendor-prefixed declarations
|
|
82
|
+
(`-webkit-transition:` etc.) aren't detected — an acceptable, low-probability
|
|
83
|
+
gap, consistent with other crude-but-honest heuristics already in this file.
|
|
84
|
+
No `core.md` chapter changed — `packages/whs-eleventy`-only. No `feedback/`
|
|
85
|
+
file. Fixed in `6576a04`.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
24
89
|
## 2026-10-05 [non-breaking]
|
|
25
90
|
|
|
26
91
|
### templates/commands
|
package/standard/core.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Web project house style — CORE (stack-agnostic)
|
|
2
2
|
|
|
3
|
-
**Version:** 2026-10-
|
|
3
|
+
**Version:** 2026-10-07 · **Status:** active
|
|
4
4
|
|
|
5
5
|
This is the stack-agnostic contract every web project follows, regardless of
|
|
6
6
|
framework, host, or CSS system. It says **what** must be true, with concrete
|
|
@@ -244,7 +244,7 @@ contrast-correct at the token level.
|
|
|
244
244
|
- **Strict-CSP-safe:** no `style-src 'unsafe-inline'`, no inline `style=`
|
|
245
245
|
attributes in templates, no per-page `<style>` blocks. Utility-class systems
|
|
246
246
|
(Tailwind) are fine; inline style strings are not.
|
|
247
|
-
- Contrast meets WCAG AA. When a pair fails, fix the **token**, not the usage.
|
|
247
|
+
- Contrast meets WCAG 2.1 AA. When a pair fails, fix the **token**, not the usage.
|
|
248
248
|
- Theme is applied via a data attribute set before first paint (`#theme`).
|
|
249
249
|
- A second stylesheet only for a genuinely separable widget, justified in a
|
|
250
250
|
comment.
|
|
@@ -834,8 +834,11 @@ states.
|
|
|
834
834
|
|
|
835
835
|
## a11y — Accessibility checklist
|
|
836
836
|
|
|
837
|
-
**Contract.** Every page
|
|
838
|
-
|
|
837
|
+
**Contract.** Every page conforms to **WCAG 2.1 Level AA** — the level US
|
|
838
|
+
accessibility law references (the ADA Title II rule; Section 508's WCAG 2.0 AA
|
|
839
|
+
is covered by it). The checklist below is the house's concrete mechanism for
|
|
840
|
+
the parts of AA most often missed, not the whole of it. An automated a11y scan
|
|
841
|
+
over every page runs in the gate — and is understood to catch regressions, not
|
|
839
842
|
everything.
|
|
840
843
|
|
|
841
844
|
**Specs.**
|
|
@@ -858,7 +861,7 @@ everything.
|
|
|
858
861
|
keyboard button — don't re-bind Enter/Space or add `role`/`tabindex`.
|
|
859
862
|
- **Icons:** decorative → `aria-hidden="true"`; meaningful → `<title>` in the
|
|
860
863
|
SVG or `aria-label` on the control.
|
|
861
|
-
- **Contrast** meets WCAG AA; fix failures at the token (`#brand-source`), not
|
|
864
|
+
- **Contrast** meets WCAG 2.1 AA; fix failures at the token (`#brand-source`), not
|
|
862
865
|
with a one-off override.
|
|
863
866
|
- **Focus visibility:** a visible focus ring on every interactive element; if
|
|
864
867
|
you override the default, keep one.
|
|
@@ -1467,7 +1470,7 @@ governs it and follow that chapter — the slugs are the index (a `<script>` →
|
|
|
1467
1470
|
- production-url: https://example.com
|
|
1468
1471
|
- content-type: tool # tool | article (article => feeds)
|
|
1469
1472
|
- publishing-rate: ~5 pages/week
|
|
1470
|
-
- standard-version: 2026-10-
|
|
1473
|
+
- standard-version: 2026-10-07 # recommended — enables standard-version drift tracking (#compliance)
|
|
1471
1474
|
```
|
|
1472
1475
|
|
|
1473
1476
|
If the section is absent, the assistant's first action is to run the flow above
|