@bompus/house-rules 0.0.0-stage → 0.13.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.
Files changed (236) hide show
  1. package/CODE_OF_CONDUCT.md +83 -0
  2. package/CONTRIBUTING.md +54 -0
  3. package/LICENSE +21 -0
  4. package/README.md +378 -2
  5. package/THIRD_PARTY_NOTICES.md +188 -0
  6. package/compose.mjs +125 -0
  7. package/composition.mjs +361 -0
  8. package/config-view.mjs +209 -0
  9. package/config.mjs +381 -0
  10. package/docs/assets/banner-dark.png +0 -0
  11. package/docs/assets/banner-light.png +0 -0
  12. package/docs/assets/overview.png +0 -0
  13. package/docs/configuration.md +269 -0
  14. package/docs/skill-names.md +50 -0
  15. package/examples/person/house-rules.json +15 -0
  16. package/examples/person/rules/10-tooling.md +8 -0
  17. package/examples/person/rules/20-reporting.md +9 -0
  18. package/examples/project/AGENTS.md +18 -0
  19. package/package.json +42 -4
  20. package/rule-references.mjs +91 -0
  21. package/rules/core.md +340 -0
  22. package/rules/modifiers/backlog-outline.md +25 -0
  23. package/rules/modifiers/backlog-visual.md +16 -0
  24. package/rules/modifiers/coded-offers.md +70 -0
  25. package/rules/modifiers/cross-session-relay.md +23 -0
  26. package/rules/modifiers/effort-estimates.md +41 -0
  27. package/rules/modifiers/land-when-done.md +12 -0
  28. package/rules/modifiers/low-quota-handoff.md +20 -0
  29. package/rules/modifiers/model-default-questions.md +33 -0
  30. package/rules/modifiers/multi-agent.md +38 -0
  31. package/rules/modifiers/no-attribution.md +16 -0
  32. package/rules/modifiers/periodic-updates.md +25 -0
  33. package/rules/modifiers/plan-files.md +82 -0
  34. package/rules/modifiers/question-cards.md +48 -0
  35. package/rules/modifiers/release-batching.md +15 -0
  36. package/rules/modifiers/scratch-on-disk.md +15 -0
  37. package/rules/modifiers/shared-host-load.md +19 -0
  38. package/rules/modifiers/solo-operator.md +13 -0
  39. package/rules/modifiers/squash-landing.md +22 -0
  40. package/rules/modifiers/upstream-contributions.md +45 -0
  41. package/rules/modifiers/web-image-formats.md +35 -0
  42. package/rules/references/backlog-view.md +154 -0
  43. package/rules/references/github-actions.md +45 -0
  44. package/rules/references/landing.md +39 -0
  45. package/rules/references/multi-agent.md +51 -0
  46. package/rules/references/release-batching.md +66 -0
  47. package/rules/references/squash-landing.md +50 -0
  48. package/rules/references/thread-visuals.md +45 -0
  49. package/setup.mjs +445 -0
  50. package/skills/hr-agent-guidance-audit/LICENSE +21 -0
  51. package/skills/hr-agent-guidance-audit/SKILL.md +91 -0
  52. package/skills/hr-agent-guidance-audit/agents/openai.yaml +2 -0
  53. package/skills/hr-agent-guidance-audit/references/prune-workflow.md +72 -0
  54. package/skills/hr-agent-guidance-audit/references/records-and-lessons.md +60 -0
  55. package/skills/hr-agent-guidance-audit/references/retirement-verdicts.md +49 -0
  56. package/skills/hr-agent-guidance-refresh/LICENSE +21 -0
  57. package/skills/hr-agent-guidance-refresh/SKILL.md +36 -0
  58. package/skills/hr-agent-guidance-refresh/agents/openai.yaml +2 -0
  59. package/skills/hr-agent-guidance-refresh/references/verification.md +47 -0
  60. package/skills/hr-api-exposure-check/SKILL.md +159 -0
  61. package/skills/hr-api-exposure-check/agents/openai.yaml +3 -0
  62. package/skills/hr-audit-choices/SKILL.md +67 -0
  63. package/skills/hr-audit-choices/agents/openai.yaml +2 -0
  64. package/skills/hr-benchmarking/NOTICE.md +36 -0
  65. package/skills/hr-benchmarking/SKILL.md +279 -0
  66. package/skills/hr-benchmarking/agents/openai.yaml +2 -0
  67. package/skills/hr-benchmarking/references/javascript.md +130 -0
  68. package/skills/hr-benchmarking/references/tools.md +165 -0
  69. package/skills/hr-benchmarking/scripts/process-attribution.ps1 +20 -0
  70. package/skills/hr-benchmarking/scripts/process-attribution.py +107 -0
  71. package/skills/hr-better-accessibility/LICENSE +22 -0
  72. package/skills/hr-better-accessibility/NOTICE.md +46 -0
  73. package/skills/hr-better-accessibility/SKILL.md +119 -0
  74. package/skills/hr-better-accessibility/agents/openai.yaml +3 -0
  75. package/skills/hr-better-accessibility/focus-and-keyboard.md +130 -0
  76. package/skills/hr-better-accessibility/forms.md +82 -0
  77. package/skills/hr-better-accessibility/hit-areas.md +98 -0
  78. package/skills/hr-better-accessibility/motion-and-zoom.md +81 -0
  79. package/skills/hr-better-accessibility/screen-readers.md +102 -0
  80. package/skills/hr-better-accessibility/semantics-and-aria.md +84 -0
  81. package/skills/hr-better-colors/LICENSE +21 -0
  82. package/skills/hr-better-colors/NOTICE.md +37 -0
  83. package/skills/hr-better-colors/SKILL.md +114 -0
  84. package/skills/hr-better-colors/agents/openai.yaml +3 -0
  85. package/skills/hr-better-colors/color-formats.md +90 -0
  86. package/skills/hr-better-colors/color-usage.md +140 -0
  87. package/skills/hr-better-colors/contrast.md +80 -0
  88. package/skills/hr-better-colors/palette-generation.md +104 -0
  89. package/skills/hr-better-colors/palette-structure.md +76 -0
  90. package/skills/hr-better-colors/token-naming.md +97 -0
  91. package/skills/hr-better-layout/LICENSE +22 -0
  92. package/skills/hr-better-layout/NOTICE.md +48 -0
  93. package/skills/hr-better-layout/SKILL.md +92 -0
  94. package/skills/hr-better-layout/agents/openai.yaml +3 -0
  95. package/skills/hr-better-layout/grouping-and-alignment.md +144 -0
  96. package/skills/hr-better-layout/spacing-and-adaptivity.md +215 -0
  97. package/skills/hr-better-typography/LICENSE +22 -0
  98. package/skills/hr-better-typography/NOTICE.md +46 -0
  99. package/skills/hr-better-typography/SKILL.md +172 -0
  100. package/skills/hr-better-typography/agents/openai.yaml +3 -0
  101. package/skills/hr-better-typography/choosing-fonts.md +69 -0
  102. package/skills/hr-better-typography/css-cheat-sheet.md +65 -0
  103. package/skills/hr-better-typography/details-and-accessibility.md +119 -0
  104. package/skills/hr-better-typography/spacing-and-sizing.md +131 -0
  105. package/skills/hr-better-typography/variable-fonts-and-opentype.md +105 -0
  106. package/skills/hr-better-typography/wrapping-and-punctuation.md +57 -0
  107. package/skills/hr-better-writing/LICENSE +22 -0
  108. package/skills/hr-better-writing/NOTICE.md +46 -0
  109. package/skills/hr-better-writing/SKILL.md +122 -0
  110. package/skills/hr-better-writing/agents/openai.yaml +3 -0
  111. package/skills/hr-code-review/LICENSE +21 -0
  112. package/skills/hr-code-review/NOTICE.md +26 -0
  113. package/skills/hr-code-review/SKILL.md +163 -0
  114. package/skills/hr-code-review/agents/openai.yaml +3 -0
  115. package/skills/hr-code-review/references/acp-client.md +43 -0
  116. package/skills/hr-code-review/references/evidence.md +57 -0
  117. package/skills/hr-code-review/references/impact.md +103 -0
  118. package/skills/hr-code-review/references/panel-adjudication.md +58 -0
  119. package/skills/hr-code-review/references/panel-plan.md +61 -0
  120. package/skills/hr-code-review/references/panel-recommendations.json +105 -0
  121. package/skills/hr-code-review/references/panel-recommendations.md +46 -0
  122. package/skills/hr-code-review/references/panel-setup.md +54 -0
  123. package/skills/hr-code-review/references/reviewer-role.md +31 -0
  124. package/skills/hr-code-review/scripts/acp-panel-client.mjs +299 -0
  125. package/skills/hr-code-review/scripts/check-receipts.mjs +263 -0
  126. package/skills/hr-code-review/scripts/panel-plan.mjs +69 -0
  127. package/skills/hr-code-review/scripts/panel-preferences.mjs +220 -0
  128. package/skills/hr-design-exploration/SKILL.md +105 -0
  129. package/skills/hr-design-exploration/agents/openai.yaml +4 -0
  130. package/skills/hr-diagnosing-bugs/NOTICE.md +45 -0
  131. package/skills/hr-diagnosing-bugs/SKILL.md +108 -0
  132. package/skills/hr-diagnosing-bugs/agents/openai.yaml +3 -0
  133. package/skills/hr-diagnosing-bugs/references/reporting.md +97 -0
  134. package/skills/hr-diagnosing-bugs/scripts/check-pr-offer.mjs +47 -0
  135. package/skills/hr-explain-code/NOTICE.md +37 -0
  136. package/skills/hr-explain-code/SKILL.md +110 -0
  137. package/skills/hr-explain-code/agents/openai.yaml +5 -0
  138. package/skills/hr-explain-code/references/helper-brief.md +39 -0
  139. package/skills/hr-extract-shared-steps/SKILL.md +134 -0
  140. package/skills/hr-extract-shared-steps/agents/openai.yaml +3 -0
  141. package/skills/hr-forgotten-work-audit/SKILL.md +130 -0
  142. package/skills/hr-forgotten-work-audit/references/brief-template.md +56 -0
  143. package/skills/hr-forgotten-work-audit/references/result-format.md +46 -0
  144. package/skills/hr-handoff/LICENSE +21 -0
  145. package/skills/hr-handoff/NOTICE.md +29 -0
  146. package/skills/hr-handoff/SKILL.md +114 -0
  147. package/skills/hr-handoff/agents/openai.yaml +5 -0
  148. package/skills/hr-handoff/assets/ledger.example.json +10 -0
  149. package/skills/hr-handoff/assets/receipt.example.json +76 -0
  150. package/skills/hr-handoff/assets/source.example.json +20 -0
  151. package/skills/hr-handoff/references/reconciliation.md +124 -0
  152. package/skills/hr-handoff/references/successor-thread.md +76 -0
  153. package/skills/hr-handoff/scripts/check-reconciliation.mjs +324 -0
  154. package/skills/hr-house-rules-setup/SKILL.md +114 -0
  155. package/skills/hr-house-rules-setup/agents/openai.yaml +5 -0
  156. package/skills/hr-issue-tracker-setup/SKILL.md +49 -0
  157. package/skills/hr-issue-tracker-setup/agents/openai.yaml +5 -0
  158. package/skills/hr-lean-plan/SKILL.md +146 -0
  159. package/skills/hr-lean-plan/agents/openai.yaml +5 -0
  160. package/skills/hr-maintainability-review/SKILL.md +186 -0
  161. package/skills/hr-maintainability-review/agents/openai.yaml +5 -0
  162. package/skills/hr-manual-qa/NOTICE.md +36 -0
  163. package/skills/hr-manual-qa/SKILL.md +37 -0
  164. package/skills/hr-manual-qa/agents/openai.yaml +3 -0
  165. package/skills/hr-navigation-retro/LICENSE +21 -0
  166. package/skills/hr-navigation-retro/NOTICE.md +5 -0
  167. package/skills/hr-navigation-retro/SKILL.md +86 -0
  168. package/skills/hr-navigation-retro/agents/openai.yaml +4 -0
  169. package/skills/hr-navigation-retro/references/inputs.md +67 -0
  170. package/skills/hr-navigation-retro/scripts/retro.py +552 -0
  171. package/skills/hr-navigation-retro/scripts/transcripts.py +205 -0
  172. package/skills/hr-ordering-tests/SKILL.md +51 -0
  173. package/skills/hr-ordering-tests/agents/openai.yaml +2 -0
  174. package/skills/hr-plain-prose/SKILL.md +264 -0
  175. package/skills/hr-plain-prose/agents/openai.yaml +3 -0
  176. package/skills/hr-plain-prose/scripts/check.mjs +331 -0
  177. package/skills/hr-pr-followup/LICENSE +21 -0
  178. package/skills/hr-pr-followup/NOTICE.md +9 -0
  179. package/skills/hr-pr-followup/SKILL.md +74 -0
  180. package/skills/hr-pr-followup/agents/openai.yaml +2 -0
  181. package/skills/hr-pr-followup/references/review-helpers.md +111 -0
  182. package/skills/hr-pr-followup/scripts/check-landing.py +204 -0
  183. package/skills/hr-pr-followup/scripts/check-pr-review.ts +510 -0
  184. package/skills/hr-pr-followup/scripts/merge-pr.sh +100 -0
  185. package/skills/hr-pr-followup/scripts/resolve-pr-thread.ts +219 -0
  186. package/skills/hr-pr-followup/scripts/verify-landed.sh +184 -0
  187. package/skills/hr-progress-report/SKILL.md +49 -0
  188. package/skills/hr-progress-report/agents/openai.yaml +5 -0
  189. package/skills/hr-read-reddit/SKILL.md +44 -0
  190. package/skills/hr-read-reddit/agents/openai.yaml +2 -0
  191. package/skills/hr-read-reddit/scripts/reddit.ts +252 -0
  192. package/skills/hr-read-x-links/SKILL.md +42 -0
  193. package/skills/hr-read-x-links/agents/openai.yaml +2 -0
  194. package/skills/hr-read-x-links/scripts/x-post.ts +264 -0
  195. package/skills/hr-research-shared-links/SKILL.md +103 -0
  196. package/skills/hr-split-to-prs/LICENSE +21 -0
  197. package/skills/hr-split-to-prs/NOTICE.md +9 -0
  198. package/skills/hr-split-to-prs/SKILL.md +61 -0
  199. package/skills/hr-split-to-prs/agents/openai.yaml +2 -0
  200. package/skills/hr-sqlite/SKILL.md +174 -0
  201. package/skills/hr-sqlite/agents/openai.yaml +2 -0
  202. package/skills/hr-sqlite/references/evidence.md +94 -0
  203. package/skills/hr-sqlite/references/tool-decisions.md +39 -0
  204. package/skills/hr-sqlite/scripts/inspect-sqlite.py +245 -0
  205. package/skills/hr-stock-ui-audit/SKILL.md +132 -0
  206. package/skills/hr-stock-ui-audit/agents/openai.yaml +3 -0
  207. package/skills/hr-stock-ui-audit/scripts/scan.mjs +619 -0
  208. package/skills/hr-tdd/LICENSE +21 -0
  209. package/skills/hr-tdd/NOTICE.md +10 -0
  210. package/skills/hr-tdd/SKILL.md +40 -0
  211. package/skills/hr-tdd/agents/openai.yaml +3 -0
  212. package/skills/hr-tdd/mocking.md +39 -0
  213. package/skills/hr-tdd/tests.md +77 -0
  214. package/skills/hr-test-audit/LICENSE +21 -0
  215. package/skills/hr-test-audit/NOTICE.md +67 -0
  216. package/skills/hr-test-audit/SKILL.md +146 -0
  217. package/skills/hr-test-audit/agents/openai.yaml +3 -0
  218. package/skills/hr-test-audit/scripts/floor-guard.ts +137 -0
  219. package/skills/hr-usage-report/SKILL.md +90 -0
  220. package/skills/hr-usage-report/agents/openai.yaml +2 -0
  221. package/skills/hr-usage-report/assets/example-report.json +125 -0
  222. package/skills/hr-usage-report/assets/report-template.html +377 -0
  223. package/skills/hr-usage-report/scripts/render-report.ts +86 -0
  224. package/skills/hr-usage-report/scripts/usage-by-model.ts +499 -0
  225. package/skills/hr-what-next/SKILL.md +151 -0
  226. package/skills/hr-what-next/agents/openai.yaml +4 -0
  227. package/skills/hr-what-next/references/reporting.md +59 -0
  228. package/skills/hr-what-next/scripts/check-open-work.mjs +711 -0
  229. package/skills/hr-writing-for-agents/NOTICE.md +46 -0
  230. package/skills/hr-writing-for-agents/SKILL-MECHANICS.md +83 -0
  231. package/skills/hr-writing-for-agents/SKILL.md +108 -0
  232. package/skills/hr-writing-for-agents/agents/openai.yaml +3 -0
  233. package/skills/hr-writing-pr/NOTICE.md +48 -0
  234. package/skills/hr-writing-pr/SKILL.md +119 -0
  235. package/skills/hr-writing-pr/agents/openai.yaml +2 -0
  236. package/skills/hr-writing-pr/scripts/benefit-chart.py +200 -0
@@ -0,0 +1,20 @@
1
+ param([Parameter(Mandatory=$true)][string]$OutputPath,[Parameter(Mandatory=$true)][string]$StopPath)
2
+ $ErrorActionPreference='Stop'
3
+ $cpuCounter=New-Object System.Diagnostics.PerformanceCounter('Processor','% Processor Time','_Total')
4
+ $memoryCounter=New-Object System.Diagnostics.PerformanceCounter('Memory','Available MBytes')
5
+ $pageCounter=New-Object System.Diagnostics.PerformanceCounter('Memory','Pages Input/sec')
6
+ $records=@()
7
+ try {
8
+ [void]$cpuCounter.NextValue()
9
+ for($i=0;$i -lt 120;$i++) {
10
+ $items=@(); $missing=0
11
+ foreach($p in Get-Process) {
12
+ try { if($null -ne $p.CPU) { $items+=@{pid=$p.Id;name=$p.ProcessName;cpuSeconds=$p.CPU;started=$p.StartTime.ToUniversalTime().ToString('o')} } } catch { $missing++ }
13
+ }
14
+ $self=Get-Process -Id $PID
15
+ $records+=@{utc=[DateTime]::UtcNow.ToString('o');totalCpuPercent=$cpuCounter.NextValue();availableBytes=$memoryCounter.NextValue()*1MB;pagesInputPerSec=$pageCounter.NextValue();processes=$items;missingProcesses=$missing;observerCpuSeconds=$self.CPU;observerWorkingSet=$self.WorkingSet64}
16
+ if(Test-Path $StopPath) { break }
17
+ [Threading.Thread]::Sleep(1000)
18
+ }
19
+ ConvertTo-Json -InputObject $records -Depth 5 -Compress | Set-Content -LiteralPath $OutputPath -Encoding utf8
20
+ } finally { $cpuCounter.Dispose();$memoryCounter.Dispose();$pageCounter.Dispose() }
@@ -0,0 +1,107 @@
1
+ """Finite Linux CPU attribution; does not acquire admission, stop processes or print argv."""
2
+ import argparse
3
+ import json
4
+ import os
5
+ from pathlib import Path
6
+ import resource
7
+ import time
8
+ from datetime import datetime, timezone
9
+
10
+
11
+ def read_process(stat):
12
+ end = stat.rfind(")")
13
+ fields = stat[end + 2:].split()
14
+ return {"name": stat[stat.find("(") + 1:end], "parentPid": int(fields[1]),
15
+ "cpuTicks": int(fields[11]) + int(fields[12]),
16
+ "reapedChildCpuTicks": int(fields[13]) + int(fields[14]), "birthTicks": fields[19],
17
+ "rssPages": int(fields[21])}
18
+
19
+
20
+ def snapshot(proc=Path("/proc")):
21
+ processes = {}
22
+ missing = 0
23
+ started = time.monotonic()
24
+ for path in proc.iterdir():
25
+ if not path.name.isdigit():
26
+ continue
27
+ try:
28
+ processes[int(path.name)] = read_process((path / "stat").read_text(encoding="utf-8", errors="replace"))
29
+ except (OSError, ValueError, IndexError):
30
+ missing += 1
31
+ cpu_lines = [line.split() for line in (proc / "stat").read_text().splitlines() if line.startswith("cpu")]
32
+ ticks = list(map(int, cpu_lines[0][1:9]))
33
+ per_cpu = {line[0][3:]: list(map(int, line[1:9])) for line in cpu_lines[1:]}
34
+ peak = int(next(line for line in (proc / "self/status").read_text().splitlines()
35
+ if line.startswith("VmHWM:")).split()[1]) * 1024
36
+ usage = resource.getrusage(resource.RUSAGE_SELF)
37
+ return {"monotonic": time.monotonic(), "utc": datetime.now(timezone.utc).isoformat(),
38
+ # Busy excludes idle, iowait and steal: steal is time the hypervisor ran
39
+ # something else, not work by any guest thread.
40
+ "hostBusyTicks": sum(ticks) - ticks[3] - ticks[4] - ticks[7],
41
+ "cpuBusyTicks": {cpu: sum(t) - t[3] - t[4] - t[7] for cpu, t in per_cpu.items()},
42
+ "processes": processes,
43
+ "missingProcesses": missing, "scanSeconds": time.monotonic() - started,
44
+ "observerCpuSeconds": usage.ru_utime + usage.ru_stime,
45
+ "observerPeakRssBytes": peak}
46
+
47
+
48
+ def cpu_busy_cores(before, after, scale):
49
+ # A CPU brought online or offline between snapshots has no interval value;
50
+ # report it as None so a gate cannot read it as idle.
51
+ return {cpu: (after[cpu] - before[cpu]) / scale if cpu in before and cpu in after else None
52
+ for cpu in sorted(before.keys() | after.keys(), key=int)}
53
+
54
+
55
+ def compare(before, after, hz):
56
+ seconds = after["monotonic"] - before["monotonic"]
57
+ rows = []
58
+ prior = before["processes"]
59
+ current = after["processes"]
60
+ matched = set()
61
+ for pid, process in current.items():
62
+ old = prior.get(pid)
63
+ if old is None or old["birthTicks"] != process["birthTicks"]:
64
+ continue
65
+ matched.add(pid)
66
+ cpu = process["cpuTicks"] - old["cpuTicks"]
67
+ # When the parent reaps children, their total CPU is added to its
68
+ # cutime/cstime. The delta attributes that CPU to the parent; it can
69
+ # include CPU the children used before the interval started.
70
+ reaped = process.get("reapedChildCpuTicks", 0) - old.get("reapedChildCpuTicks", 0)
71
+ if cpu > 0 or reaped > 0:
72
+ rows.append({"pid": pid, **process, "busyCores": cpu / hz / seconds,
73
+ "reapedChildCores": reaped / hz / seconds})
74
+ return {"startUtc": before["utc"], "endUtc": after["utc"], "seconds": seconds,
75
+ "hostBusyCores": (after["hostBusyTicks"] - before["hostBusyTicks"]) / hz / seconds,
76
+ "cpuBusyCores": cpu_busy_cores(before.get("cpuBusyTicks", {}), after.get("cpuBusyTicks", {}),
77
+ hz * seconds),
78
+ "unmatchedBefore": len(prior) - len(matched),
79
+ "unmatchedAfter": len(current) - len(matched),
80
+ "processCpu": sorted(rows, key=lambda row: -row["busyCores"])}
81
+
82
+
83
+ def main():
84
+ parser = argparse.ArgumentParser(description=__doc__)
85
+ parser.add_argument("--seconds", type=int, default=20)
86
+ parser.add_argument("--output", type=Path, required=True)
87
+ args = parser.parse_args()
88
+ if not 1 <= args.seconds <= 300:
89
+ parser.error("seconds must be between 1 and 300")
90
+ hz = os.sysconf("SC_CLK_TCK")
91
+ samples = [snapshot()]
92
+ for _ in range(args.seconds):
93
+ time.sleep(1)
94
+ samples.append(snapshot())
95
+ intervals = [compare(a, b, hz) for a, b in zip(samples, samples[1:])]
96
+ result = {"version": 2, "observerPid": os.getpid(), "clockTicksPerSecond": hz,
97
+ "bootId": Path("/proc/sys/kernel/random/boot_id").read_text().strip(),
98
+ "samples": samples, "intervals": intervals,
99
+ "limits": "Snapshots miss processes that start and exit between samples. reapedChildCores attributes children's CPU to the parent in the interval it reaps them, including CPU they used before the interval. Busy CPU excludes idle, iowait and steal. CPU attribution does not prove interference or grant admission. Observer overhead is included; no negligible-impact claim."}
100
+ args.output.write_text(json.dumps(result, indent=2))
101
+ print(json.dumps({"output": str(args.output), "intervals": len(intervals),
102
+ "peakHostBusyCores": max(row["hostBusyCores"] for row in intervals),
103
+ "observerPeakRssBytes": samples[-1]["observerPeakRssBytes"]}))
104
+
105
+
106
+ if __name__ == "__main__":
107
+ main()
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Jakub Krehel
4
+ Copyright (c) 2026 Emil Kowalski
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
@@ -0,0 +1,46 @@
1
+ # Source notices
2
+
3
+ This notice accompanies the hr-better-accessibility bundle when copied on its own.
4
+
5
+ ## jakubkrehel/skills
6
+
7
+ Source: https://github.com/jakubkrehel/skills (compared at revision `267330e1adfc66a718fb65fa6918c1f06d0a689e`)
8
+
9
+ - `skills/hr-better-accessibility/`, `skills/hr-better-colors/`, `skills/hr-better-layout/`,
10
+ `skills/hr-better-typography/` and `skills/hr-better-writing/`: adapted skill bundles,
11
+ including their supporting references and discovery metadata.
12
+
13
+ Copyright (c) 2026 Jakub Krehel
14
+
15
+ ## emilkowalski/skills
16
+
17
+ Source: https://github.com/emilkowalski/skills (compared at revision `e8a175de22ae1e49370fc144c1f3bb9aeedf988d`)
18
+
19
+ - `skills/hr-better-accessibility/` and `skills/hr-better-layout/`: mobile behavior and
20
+ viewport guidance adapted from `skills/mobile-native/SKILL.md`.
21
+
22
+ Copyright (c) 2026 Emil Kowalski
23
+
24
+ ## house-rules and MIT permission notice
25
+
26
+ MIT License
27
+
28
+ Copyright (c) 2026 Aaron Queen
29
+
30
+ Permission is hereby granted, free of charge, to any person obtaining a copy
31
+ of this software and associated documentation files (the "Software"), to deal
32
+ in the Software without restriction, including without limitation the rights
33
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
34
+ copies of the Software, and to permit persons to whom the Software is
35
+ furnished to do so, subject to the following conditions:
36
+
37
+ The above copyright notice and this permission notice shall be included in all
38
+ copies or substantial portions of the Software.
39
+
40
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
41
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
42
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
43
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
44
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
45
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
46
+ SOFTWARE.
@@ -0,0 +1,119 @@
1
+ ---
2
+ name: hr-better-accessibility
3
+ description: "Accessibility rules for web UI: native elements, keyboard and focus, form labels and errors, accessible names, live regions, reduced motion, zoom and reflow. Use when building or reviewing web markup, forms, dialogs or interactive controls."
4
+ license: MIT
5
+ metadata:
6
+ upstream: https://github.com/jakubkrehel/skills
7
+ upstream_ref: 267330e1adfc66a718fb65fa6918c1f06d0a689e
8
+ upstream_path: skills/better-accessibility/
9
+ local_patches: "Use-when description; reduced-motion kill switch spares spinners; 320px reflow keeps the 2D-content exception; carousel pan-y, touch-callout and real-hardware check (from emilkowalski/skills mobile-native)"
10
+ house_rules_patches: "Portable provenance and review verdict; unique SVG title and desc ids"
11
+ mobile_native_source: https://github.com/emilkowalski/skills/blob/e8a175de22ae1e49370fc144c1f3bb9aeedf988d/skills/mobile-native/SKILL.md
12
+ ---
13
+
14
+ # Accessibility
15
+
16
+ Most accessibility is free if you use the platform. Native elements ship with keyboard support, real labels announce themselves and a visible focus ring is one CSS rule.
17
+
18
+ Write every fix in the project's styling system, and use the exact values below rather than familiar-looking substitutes.
19
+
20
+ Reviewing means two walks. Keyboard-only, where every flow completes without a mouse. Then screen-reader, where every control announces a name, a role and its state. When unsure, take the platform default over a custom rebuild, and remove ARIA rather than add it.
21
+
22
+ Contrast measurement and color fixes belong to `hr-better-colors`. Text sizing and iOS input zoom belong to `hr-better-typography`. Spatial RTL layout belongs to `hr-better-layout`.
23
+
24
+ ## Native elements first
25
+
26
+ The first rule of ARIA: don't use ARIA when a native element exists. `<button>` for actions, `<a href>` for navigation, never `<div onClick>`. A real link must support Cmd/Ctrl/middle-click. No ARIA is better than bad ARIA. See [semantics-and-aria.md](semantics-and-aria.md) for landmarks, button-vs-link and disabled states.
27
+
28
+ ## Visible focus rings
29
+
30
+ Style `:focus-visible`, not bare `:focus`. Keyboard users get a ring and mouse users usually don't. Prefer the browser's unmodified indicator.
31
+
32
+ A custom ring needs a project focus token or another explicit color. Verify the whole indicator against every adjacent color it crosses, `currentColor` included. Use at least a `2px` solid perimeter or an equivalent visible area. Never use `outline: none` without a verified replacement, and preserve system colors in forced-colors mode. Recipes are in [focus-and-keyboard.md](focus-and-keyboard.md).
33
+
34
+ ## Full keyboard support
35
+
36
+ Every pointer interaction needs a keyboard path. Follow the ARIA APG patterns: Escape closes overlays, arrow keys move within composite widgets, Tab moves between widgets, Enter and Space activate.
37
+
38
+ Use only `tabindex="0"` to join the natural tab order and `tabindex="-1"` for programmatic focus. Positive values break that order. Composite widgets use roving tabindex, where the active item is `0` and every other is `-1`.
39
+
40
+ ## Trap and restore focus
41
+
42
+ Modals set `inert` on the background content, move focus inside on open and return focus to the trigger on close. Add `overscroll-behavior: contain` so background content doesn't scroll.
43
+
44
+ ## Minimum hit area
45
+
46
+ WCAG 2.5.8's Level AA baseline is a 24×24 CSS-pixel target, or one of its spacing, equivalent-control, inline, user-agent and essential exceptions. Aim for 44×44px on touch and 40×40px on desktop where density permits. Extend with a pseudo-element when the visible element should stay smaller.
47
+
48
+ Never let extended hit areas overlap. Give decorative layers `pointer-events: none`, so a glow never swallows the clicks meant for the control beneath it. Sizes and collision rules are in [hit-areas.md](hit-areas.md).
49
+
50
+ ## Label and type every control
51
+
52
+ Every input gets a `<label for>` or a wrapping `<label>`. A placeholder is never a label. Label and control share one hit target, with no dead zone between a checkbox and its text.
53
+
54
+ Add `autocomplete` with a meaningful `name`, plus the `type` and `inputmode` that summon the right keyboard. Never block paste; users paste passwords and one-time codes. See [forms.md](forms.md).
55
+
56
+ ## Errors that announce
57
+
58
+ Keep submit enabled until the request starts, then disable with a spinner and the original label. Validate on submit. Mark failing fields `aria-invalid="true"`, point `aria-describedby` at the inline error text and focus the first invalid field.
59
+
60
+ Use native `disabled` when a control is genuinely unavailable. Reach for `aria-disabled="true"` only when it should stay focusable, then block pointer, keyboard and form behavior in code and style the state explicitly.
61
+
62
+ ## Accessible names everywhere
63
+
64
+ Icon-only buttons need a descriptive `aria-label`. Visible label text must appear in the accessible name. Decorative elements get `aria-hidden="true"`, never on a focusable element.
65
+
66
+ ## Don't rely on color alone
67
+
68
+ Status needs a redundant cue: an icon, text, or an underline alongside the color. Work out which WCAG contrast requirement applies, then use `hr-better-colors` to measure the rendered pair. When it fails, report the pair and the requirement it misses, and leave the colors alone unless asked.
69
+
70
+ ## Honor prefers-reduced-motion
71
+
72
+ Wrap motion in `@media (prefers-reduced-motion: no-preference)` so it is opt-in. Under reduced motion, replace slides and scales with opacity crossfades, and kill parallax and autoplay entirely.
73
+
74
+ Two rules hold regardless of the preference. Autoplaying media needs a visible pause control, and toasts carrying an action or an error stay until dismissed. See [motion-and-zoom.md](motion-and-zoom.md).
75
+
76
+ ## Announce dynamic content
77
+
78
+ Three mechanisms, three jobs. `aria-describedby` carries field-specific validation. A polite live region (`role="status"`) carries non-urgent updates not tied to a control, such as toasts and result counts. `role="alert"` carries urgent untied errors and nothing else.
79
+
80
+ Repeated polite announcements need a stable empty region rendered before its text updates. Dynamically inserted alerts vary in support, so test them on the screen readers you target. See [screen-readers.md](screen-readers.md).
81
+
82
+ ## Alt text by purpose
83
+
84
+ Decorative images get `alt=""`. Informative images describe the meaning. Functional images describe the action: a search icon button is `alt="Search"`, not `alt="magnifying glass"`.
85
+
86
+ ## Structure is navigation
87
+
88
+ Use headings that describe their sections and form a coherent outline. Give the page one `<h1>` and nest the levels below it without skipping. Expose one visible primary `<main>` landmark. When repeated navigation or chrome precedes it, make a "Skip to content" link the first focusable element. Anchored headings get `scroll-margin-top`.
89
+
90
+ ## Survive zoom and text resize
91
+
92
+ The page must work at 200% zoom and reflow at 320px width without horizontal scrolling. Tables, maps and code blocks may scroll inside their own container. Use `min-height` rather than fixed `height` on text containers. Prefer `rem` breakpoints where they fit the codebase, and never let the viewport meta cap how far the reader can zoom.
93
+
94
+ ## Before you finish
95
+
96
+ | Mistake | Fix |
97
+ | ----------------------------------------------- | ---------------------------------------------------------------- |
98
+ | Custom focus color assumed to work everywhere | Verify it against every adjacent color and in forced-colors mode |
99
+ | Repeated polite update inconsistently announced | Keep a stable empty status region and update its text |
100
+ | `assertive` live region for a routine toast | Use `polite`; reserve `assertive` for errors |
101
+ | `aria-hidden="true"` on a focusable element | Remove it or make the element non-focusable |
102
+ | Submit disabled until the form is valid | Keep it enabled; validate on submit and focus the first error |
103
+ | Hover treatment stuck after a tap on touch | Gate hover styling with `@media (hover: hover)` |
104
+ | Tooltip on a natively `disabled` control | Text beside it, or `aria-disabled` so it stays focusable |
105
+
106
+ ## Reporting
107
+
108
+ **Severity.** `HIGH` prevents a task, hides content from assistive technology, or creates a systemic failure. `MEDIUM` makes an interaction meaningfully harder. `LOW` is isolated polish.
109
+
110
+ **Verification.** Without a browser: accessible names on every interactive element, keyboard handlers on non-native controls, focus styles, `prefers-reduced-motion` guards and form labels bound to their inputs. With one: tab the flow in order, read computed names and roles from the accessibility tree, confirm a visible focus indicator at every stop and run an automated audit. Report every check you could not run as `Not verified`.
111
+
112
+ **Format.** Group findings under the principle each violates, ordered by severity, one row per root cause listing every location it appears in:
113
+
114
+ | Severity | Location | Before | After | Why |
115
+ | -------- | -------- | ------ | ----- | --- |
116
+
117
+ `Location` is `path/to/file:line`. `Why` names the principle and the user impact.
118
+
119
+ Report `Block` when any `HIGH` remains, `Approve` otherwise, leaving the rest in the table as work to do. Never `Approve` coverage you did not inspect. With nothing to report, state "No actionable accessibility findings" and report verification.
@@ -0,0 +1,3 @@
1
+ interface:
2
+ display_name: "HR: Better Accessibility"
3
+ short_description: "Focus, keyboard, ARIA, forms and screen readers"
@@ -0,0 +1,130 @@
1
+ # Focus and keyboard
2
+
3
+ Focus rings, skip links, tabindex, focus trapping and the APG keyboard patterns.
4
+
5
+ ## Focus rings
6
+
7
+ Style `:focus-visible`, not bare `:focus`. The browser shows it for keyboard and assistive-tech focus and suppresses it for mouse clicks, where focus is already obvious. Never write `outline: none` or `focus:outline-none` without a visible replacement, which removes keyboard navigation for sighted keyboard users.
8
+
9
+ Prefer the browser's unmodified focus indicator, which adapts to platform and forced-color settings without the author predicting every background. Adding only `outline-offset` preserves it. A custom `outline: 2px solid` with no color renders `currentColor`, which is not automatically accessible, because the outline may cross colors unlike the text's own background. The preference order:
10
+
11
+ ```css
12
+ /* Best: keep the browser ring, just give it breathing room */
13
+ :focus-visible {
14
+ outline-offset: 2px;
15
+ }
16
+
17
+ /* Custom ring when the design requires one: use the project's verified token */
18
+ :focus-visible {
19
+ outline: 2px solid var(--focus-ring);
20
+ outline-offset: 2px;
21
+ }
22
+ ```
23
+
24
+ ```tsx
25
+ // Tailwind: use the project's focus token or established focus-ring utility
26
+ <button className="focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:[outline-style:solid] focus-visible:outline-[var(--focus-ring)]">
27
+ Save
28
+ </button>
29
+ ```
30
+
31
+ A custom focus indicator must meet the applicable project or WCAG target for visible area and change of contrast. Inspect the whole perimeter against every adjacent color it crosses: component fills, page surfaces, images, gradients, hover and selected states. A token, brand color, or `currentColor` passes only when that rendered check does.
32
+
33
+ In `forced-colors: active` (Windows High Contrast), keep the default color adjustment or name a system color such as `Highlight`. `forced-color-adjust: none` freezes the authored color, so use it only where you have checked the control stays perceivable.
34
+
35
+ Group focus styles with `:focus-within` when a wrapper should light up while an inner input has focus (e.g. a search box with an icon inside the border).
36
+
37
+ ## Skip link
38
+
39
+ Target `<main id="main">` and visually hide the link until focused:
40
+
41
+ ```css
42
+ .skip-link {
43
+ position: absolute;
44
+ inset-inline-start: -999px;
45
+ }
46
+ .skip-link:focus {
47
+ inset-inline-start: 16px;
48
+ top: 16px;
49
+ }
50
+ ```
51
+
52
+ ```html
53
+ <body>
54
+ <a class="skip-link" href="#main">Skip to content</a>
55
+ <header>…</header>
56
+ <main id="main">…</main>
57
+ </body>
58
+ ```
59
+
60
+ Give in-page anchor targets `scroll-margin-top`, such as `80px` under a sticky header, so the target isn't hidden when jumped to.
61
+
62
+ ## tabindex rules
63
+
64
+ - `tabindex="0"`: adds an element to the natural tab order. Only for custom interactive elements that aren't natively focusable.
65
+ - `tabindex="-1"`: focusable via JavaScript only (`el.focus()`). Use for headings you move focus to, modal containers and roving-tabindex members.
66
+ - Positive `tabindex`: never. It hijacks the tab order for the whole page. Fix the DOM order instead.
67
+
68
+ ### Roving tabindex
69
+
70
+ Composite widgets, meaning tabs, menus, toolbars and radio groups, occupy one Tab stop. The active item has `tabindex="0"`, all others `tabindex="-1"`, and arrow keys move both focus and the `0`:
71
+
72
+ ```tsx
73
+ <div role="tablist">
74
+ {tabs.map((tab, i) => (
75
+ <button
76
+ role="tab"
77
+ tabIndex={i === activeIndex ? 0 : -1}
78
+ aria-selected={i === activeIndex}
79
+ onKeyDown={handleArrowKeys} // ArrowLeft/ArrowRight move activeIndex, wrapping
80
+ >
81
+ {tab.label}
82
+ </button>
83
+ ))}
84
+ </div>
85
+ ```
86
+
87
+ ## Focus trapping and restoration
88
+
89
+ Modals must trap focus. Prefer native `<dialog>` so the browser manages background inertness and initial focus:
90
+
91
+ ```tsx
92
+ // On open: the browser honors eligible autofocus targets and skips disabled controls.
93
+ dialogRef.current?.showModal();
94
+
95
+ // On close
96
+ dialogRef.current?.close();
97
+ triggerRef.current?.focus(); // return focus to the element that opened it
98
+ ```
99
+
100
+ For a custom modal, make the background `inert` and use the project's tested focus-management utility. Prefer an eligible autofocus target, then a visible, enabled focusable control. Skip hidden, disabled and inert candidates. If none exists, focus the dialog container with `tabindex="-1"`. Restore background interactivity when closing.
101
+
102
+ Prefer native `<dialog>` with `showModal()`, which gives you the trap, the `inert` background and Escape handling for free. A custom overlay that can't use it needs `role="dialog"`, `aria-modal="true"` and an accessible name via `aria-labelledby`. Either way:
103
+
104
+ - On open, prefer the eligible autofocus target, then a visible, enabled focusable control; use the container fallback when neither exists. For destructive confirmations, choose the least destructive action.
105
+ - On close, return focus to the trigger, or to the nearest logical container if the trigger is gone.
106
+ - Add `overscroll-behavior: contain` on the dialog so scrolling inside never scrolls the page behind it.
107
+
108
+ ## Keyboard patterns (ARIA APG)
109
+
110
+ Native elements come with these behaviors; custom widgets must implement them. A role is a promise. Give something `role="tab"` and users expect the full tab keyboard model.
111
+
112
+ | Widget | Keys |
113
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------- |
114
+ | Dialog | Tab/Shift+Tab cycle inside (wrap at ends); Escape closes |
115
+ | Tabs | Arrow keys move between tabs (wrapping); Tab exits to the panel; Home/End jump to first/last |
116
+ | Menu button | Enter/Space/ArrowDown opens and focuses first item; ArrowUp opens and focuses last; arrows navigate; Escape closes and refocuses the button |
117
+ | Disclosure / accordion | Header is a `<button aria-expanded>`; Enter and Space toggle |
118
+ | Combobox | ArrowDown opens/moves into the list; Enter accepts; Escape closes and returns to the input; typing filters |
119
+ | Listbox / radio group | Arrow keys move selection; one Tab stop for the whole group |
120
+
121
+ Universal rules:
122
+
123
+ - Escape dismisses whatever opened last: tooltip, then menu, then dialog.
124
+ - Arrow keys, not Tab, move within a composite widget; Tab moves between widgets.
125
+ - Tabs choose activation mode: automatic (panel switches on arrow focus) when panels render instantly, manual (Enter/Space to activate) when switching is expensive.
126
+ - Enter submits the focused input's form. In `<textarea>`, Enter inserts a newline and ⌘/Ctrl+Enter submits.
127
+
128
+ ## SPA route changes
129
+
130
+ Client-side navigation doesn't reset focus or announce anything. On route change, update `document.title` to match the new context, then move focus to the new view's `<h1>` (given `tabindex="-1"`) or to `<main>`. Restore scroll position on back and forward navigation, and scroll to top on forward navigation.
@@ -0,0 +1,82 @@
1
+ # Forms
2
+
3
+ Labels, autocomplete, error messaging, input types and submit behavior.
4
+
5
+ ## Labels
6
+
7
+ Every control needs a programmatic label: `<label for>` pointing at the input's `id`, or a wrapping `<label>`. A placeholder is never a label. It disappears the moment the user types and usually fails contrast.
8
+
9
+ ```html
10
+ <!-- Good: explicit association -->
11
+ <label for="email">Email</label>
12
+ <input id="email" type="email" autocomplete="email" />
13
+
14
+ <!-- Good: wrapping label, so label and control share one hit target -->
15
+ <label> <input type="checkbox" /> Send me updates </label>
16
+ ```
17
+
18
+ Label and control share one hit target, so clicking "Send me updates" toggles the checkbox with no dead zone between them. Mark required fields with native `required` plus a visible indicator explained once per form ("* required").
19
+
20
+ A placeholder used _in addition to_ a label shows an example of the expected format: `placeholder="name@company.com"`.
21
+
22
+ ## Error messaging
23
+
24
+ The complete pattern:
25
+
26
+ ```html
27
+ <label for="email">Email</label>
28
+ <input
29
+ id="email"
30
+ type="email"
31
+ autocomplete="email"
32
+ aria-invalid="true"
33
+ aria-describedby="email-error"
34
+ />
35
+ <p id="email-error">Enter a valid email address.</p>
36
+ ```
37
+
38
+ - `aria-invalid="true"` on the failing field, removed once fixed.
39
+ - `aria-describedby` links the field to its inline error so screen readers announce it with the field.
40
+ - Errors render inline beside their fields, with an icon or text. Never a red border alone, which is a color-only cue.
41
+ - On submit, focus the first invalid field.
42
+ - Allow incomplete submission so validation can surface. Never disable submit until valid (see below).
43
+ - Accept free text and validate after. Never block typing or filter characters as the user types. Trim values before validating, because autocomplete and text expansion add trailing spaces.
44
+
45
+ ## Autocomplete and input types
46
+
47
+ `autocomplete` with a meaningful `name` fills forms in one tap and is a WCAG requirement (1.3.5) for fields about the user. The common tokens:
48
+
49
+ | Field | `autocomplete` |
50
+ | -------------- | ----------------------------------------------------------- |
51
+ | Name | `name` (or `given-name` / `family-name`) |
52
+ | Email | `email` |
53
+ | Phone | `tel` |
54
+ | Address | `street-address`, `address-line1`, `postal-code`, `country` |
55
+ | Card | `cc-number`, `cc-exp`, `cc-csc`, `cc-name` |
56
+ | Login | `username`, `current-password` |
57
+ | Signup / reset | `new-password` |
58
+ | 2FA code | `one-time-code` |
59
+
60
+ Prefix with a section where relevant: `autocomplete="shipping street-address"`.
61
+
62
+ Correct `type` and `inputmode` pick the right mobile keyboard:
63
+
64
+ | Input | Use |
65
+ | ----------------------- | -------------------------------------------------------------------- |
66
+ | Email, URL, phone | `type="email"`, `type="url"`, `type="tel"` |
67
+ | OTP / PIN / card number | `type="text" inputmode="numeric"` (keeps text semantics, no spinner) |
68
+ | Money, decimals | `type="text" inputmode="decimal"` |
69
+ | True numeric quantity | `type="number"` |
70
+
71
+ Disable spellcheck on emails, codes and usernames: `spellcheck="false"`.
72
+
73
+ ## Never fight the user's tools
74
+
75
+ - Stay compatible with password managers and 2FA autofill: real `<form>`, correct `autocomplete`, no fake inputs.
76
+
77
+ ## Submit behavior
78
+
79
+ - Keep submit enabled until the request starts, then disable it and show a spinner _beside the original label_. "Save" with a spinner, not a bare spinner. The label is what tells assistive tech which button is busy.
80
+ - Announce results. Success goes through a polite live region. On failure, focus the first invalid field, which is itself the announcement. Reserve `role="alert"` for form-level errors not tied to a field ([screen-readers.md](screen-readers.md)).
81
+ - Warn on unsaved changes before navigation, and never lose typed input to a re-render. Hydration must preserve focus and value.
82
+ - Enter submits from any focused input; in `<textarea>`, ⌘/Ctrl+Enter submits.
@@ -0,0 +1,98 @@
1
+ # Hit areas
2
+
3
+ Target sizes, expanding hit areas without changing visual size and collision rules.
4
+
5
+ ## Target sizes
6
+
7
+ Separate the conformance baseline from larger usability targets:
8
+
9
+ | Standard | Minimum |
10
+ | ---------------- | ----------------------- |
11
+ | WCAG 2.5.8 (AA) | 24×24px, the hard floor |
12
+ | WCAG 2.5.5 (AAA) | 44×44px |
13
+ | Apple HIG | 44×44pt |
14
+ | Material Design | 48×48dp |
15
+
16
+ WCAG 2.5.8 Level AA requires a 24×24 CSS-pixel target or one of its exceptions. Treat 44px as the recommended touch target for primary controls and 40px as a useful desktop target where density permits. Smaller controls are not automatic failures. Check the spacing, equivalent-control, inline, user-agent and essential exceptions before reporting one.
17
+
18
+ Under the spacing exception, an undersized target passes when a 24px circle centered on its bounding box intersects no other target and no other undersized target's circle. In the simple case, 20px targets need a 4px gap.
19
+
20
+ The visible element can stay small; the hit area is what must be big. Anything that looks clickable must be clickable across its whole visual extent, with no dead zones. A checkbox and its label share one hit target.
21
+
22
+ ## Expanding the hit area
23
+
24
+ Where the visible element is smaller, say a 20×20 checkbox, extend the hit area with a pseudo-element. Put it on the wrapping `<label>` or `<button>`, never on the `<input>`, because replaced elements don't render `::before`/`::after` reliably.
25
+
26
+ ### CSS example
27
+
28
+ ```css
29
+ /* Small checkbox with expanded 44px hit area, on the wrapping label */
30
+ .checkbox-label {
31
+ position: relative;
32
+ width: 20px;
33
+ height: 20px;
34
+ }
35
+
36
+ .checkbox-label::after {
37
+ content: "";
38
+ position: absolute;
39
+ top: 50%;
40
+ left: 50%; /* physical centering: direction-independent */
41
+ transform: translate(-50%, -50%);
42
+ width: 44px;
43
+ height: 44px;
44
+ }
45
+ ```
46
+
47
+ ### Tailwind example
48
+
49
+ ```tsx
50
+ <button className="relative size-5 after:absolute after:top-1/2 after:left-1/2 after:size-11 after:-translate-1/2">
51
+ <CheckIcon />
52
+ </button>
53
+ ```
54
+
55
+ ### Layout alternative
56
+
57
+ Where the element can afford real box size, skip the pseudo-element and let the box be the target. That hands the browser real geometry for scrolling and gestures:
58
+
59
+ ```css
60
+ .icon-button {
61
+ min-width: 44px;
62
+ min-height: 44px;
63
+ display: inline-grid;
64
+ place-items: center;
65
+ }
66
+ ```
67
+
68
+ ## Collision rule
69
+
70
+ Where the extended hit area overlaps another interactive element, shrink the pseudo-element to the largest size that does not collide. Two interactive elements never have overlapping hit areas.
71
+
72
+ ## Decorative layers
73
+
74
+ A decorative layer painted over interactive content absorbs every pointer event its box covers: a gradient scrim, a glow, a blurred sheen, a full-bleed `::after`. The control underneath looks live and does nothing, and no hit-area sizing fixes it.
75
+
76
+ Give each one `pointer-events: none` (Tailwind `pointer-events-none`) so events reach the control below, plus `aria-hidden="true"` to keep it out of the accessibility tree:
77
+
78
+ ```css
79
+ .card-glow {
80
+ position: absolute;
81
+ inset: 0;
82
+ pointer-events: none;
83
+ }
84
+ ```
85
+
86
+ Keep pointer events on any layer the user is meant to hit: a modal scrim that dismisses on click is a control, not decoration.
87
+
88
+ ## Touch behavior
89
+
90
+ - Use `touch-action: manipulation` where removing the mobile tap delay is needed. It permits panning and pinch zoom but disables double-tap zoom on that element; keep the default when that gesture should remain available.
91
+ - Set `touch-action: none` on a surface implementing its own pan, zoom, or drag gestures, so the browser stops claiming them for scrolling and pinch-zoom. Scope it to that surface; at page level it removes scrolling.
92
+ - Set `touch-action: pan-y` on a horizontal carousel or swipe track that handles horizontal movement itself, so the browser keeps vertical page scrolling. A native `scroll-snap-type: x mandatory` track needs neither the gesture code nor the property.
93
+ - Set `-webkit-tap-highlight-color` to match the design instead of the default gray flash.
94
+ - Use `-webkit-touch-callout: none` only when a specific control needs to suppress Safari's long-press callout. Preserve it on links and images where users need native copy, share or link actions, including content links.
95
+ - Put hover-only styling behind `@media (hover: hover)`. On touch, `:hover` latches after a tap and holds until the user taps elsewhere, so it reads as a stuck selected state. Tailwind 4's `hover:` variant already compiles under this query.
96
+ - Prefer generous targets and clear affordances over finicky interactions such as tiny drag handles and precise hover zones.
97
+
98
+ Desktop emulation cannot verify all mobile browser behavior. Check hover, tap behavior, safe areas and keyboard interaction on supported devices, or report those checks as `Not verified`.