a11y-lint 0.14.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c80dbcc5dec24d2850b42b84b2bbf512dc792f9c77e41ba4fd778d792c0d750a
4
- data.tar.gz: aff2c4e3002a785f2f8fbfc5cfe9b115466cc3850299114b8724a82b5c71e52a
3
+ metadata.gz: 17ce590d5fc702bdcf3feb01606767c2aaa0938ca6a40b3f456737d7e9f519f2
4
+ data.tar.gz: b1ec19b360ccbc592fd42a9848cfdb351b924f3752b3041afa063f64bfd3e380
5
5
  SHA512:
6
- metadata.gz: c6431784a0c4704f7040953c2ef881678b13e61fee513f639c1ff4de019a4bb1599633638902764dc0cf9f9b25fe553349262639e0f30029033e5498f252f23a
7
- data.tar.gz: 5b140955d0e2eadc1fefe2314bf85a16f0129d0c2cf569deb507c829ca0cf1aeb633ed601d670e4eef814d1b825a0576a5ad5d41e733bf4c8fceae7b6040f4fe
6
+ metadata.gz: fa1db2cd985c446515c75808512e3663859af93d254949e51fb3638e0ee1fe7cf2172a55686f66b937476e8739a9f24910b8662ef03e285445dfab9079983be3
7
+ data.tar.gz: 045c3ec7a7e4442a47517020aa2cf3da15496a3e304a1768e8e067d7767f2cd0693db025711f25c05e02736c6d3f0095378e9dfb68ed1a3b19ebcb3e8232a49e
@@ -0,0 +1,37 @@
1
+ ---
2
+ description: Capture a free-form idea as a GitHub issue in the current repo
3
+ argument-hint: <idea>
4
+ allowed-tools: Bash(gh issue create:*), Bash(gh repo view:*), Bash(grep:*), Bash(rg:*), Bash(ls:*), Bash(find:*), Read, Glob, Grep
5
+ ---
6
+
7
+ Take the free-form idea below and turn it into a well-formed GitHub issue in the current repo.
8
+
9
+ Idea: $ARGUMENTS
10
+
11
+ ## What to do
12
+
13
+ 1. **Refine** the idea into:
14
+ - A clear **title** (≤ ~70 chars, sentence case, no trailing period)
15
+ - A **body** with these sections (omit any that don't apply):
16
+ - **Problem / idea** — restate the idea clearly in 1–3 sentences
17
+ - **Proposed approach** — only include if the approach is obvious from the idea
18
+ - **Files likely involved** — only include if you can quickly identify them from the repo (e.g. a new rule idea should point at the right WCAG principle directory under `lib/a11y/lint/rules/`)
19
+
20
+ 2. **Clarify only if necessary** — one round, max. Voice input drops context, but too many questions defeat frictionless capture. Skip clarification when the idea is unambiguous. Typical clarifications: bug vs. feature, which rule/file, which template pipeline (Slim/ERB/Phlex).
21
+
22
+ 3. **Create the issue** with `gh issue create --title "..." --body "$(cat <<'EOF' ... EOF)"` against the current repo. Do **not** pass `--repo`. Do **not** add labels.
23
+
24
+ 4. **Return the issue URL** that `gh issue create` prints. Keep your final reply to one line — just the URL (and a one-line summary if helpful).
25
+
26
+ ## Out of scope
27
+
28
+ - Auto-suggesting labels
29
+ - Cross-repo support
30
+ - Duplicate detection / linking related issues
31
+ - Editing or closing existing issues
32
+
33
+ ## Style notes
34
+
35
+ - Bias toward **fast capture** over thoroughness — the issue can always be edited on GitHub.
36
+ - Don't pad the body with boilerplate. If "Proposed approach" or "Files likely involved" isn't obvious, leave it out.
37
+ - Match the tone of recent issues in this repo if you can quickly check one.
data/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.14.1] - 2026-05-03
11
+
12
+ ### Fixed
13
+
14
+ - `AnchorMissingAccessibleName` and `ButtonMissingAccessibleName` no longer report Phlex `a`/`button` tags whose accessible text comes from a phlex-rails value helper (`t`, `translate`, `l`, `localize`, `pluralize`, `truncate`, the `number_to_*` / `number_with_*` helpers, `highlight`, `excerpt`) or Phlex's built-in `text`. Previously only `plain` was recognized
15
+
10
16
  ## [0.14.0] - 2026-04-28
11
17
 
12
18
  ### Changed
@@ -0,0 +1,35 @@
1
+ # Contributing to the a11y-lint docs site
2
+
3
+ ## Authoring rule reference pages
4
+
5
+ Rule pages live in `src/rules/`, one Markdown file per WCAG concept. Each rule class is an anchor section within that page.
6
+
7
+ ### Showing example code in multiple template languages
8
+
9
+ a11y-lint supports ERB, Slim, and Phlex. When a rule page shows example code, render it through the `Shared::CodeTabs` component so all three pipelines collapse into one tabbed block instead of three stacked code blocks.
10
+
11
+ ERB is required and is the default tab. Slim and Phlex are optional — omit either kwarg and the tab won't render.
12
+
13
+ ```erb
14
+ <%= render Shared::CodeTabs.new(
15
+ erb: <<~ERB,
16
+ <img src="hero.jpg" alt="Team celebrating after a product launch">
17
+ ERB
18
+ slim: <<~SLIM,
19
+ img src="hero.jpg" alt="Team celebrating after a product launch"
20
+ SLIM
21
+ phlex: <<~PHLEX,
22
+ img(src: "hero.jpg", alt: "Team celebrating after a product launch")
23
+ PHLEX
24
+ ) %>
25
+ ```
26
+
27
+ The component takes care of:
28
+
29
+ - Rouge syntax highlighting per tab (using the `erb`, `slim`, and `ruby` lexers).
30
+ - WAI-ARIA tab pattern: `role="tablist"` / `role="tab"` / `role="tabpanel"` with full `aria-*` wiring.
31
+ - Keyboard navigation: Left/Right arrows move between tabs, Home/End jump to the first/last tab, Enter/Space activate, Tab moves into the panel.
32
+ - A no-JavaScript fallback that shows the ERB panel only.
33
+ - `prefers-reduced-motion`.
34
+
35
+ Multiple instances on the same page get unique IDs automatically.
@@ -0,0 +1,57 @@
1
+ .code-tabs {
2
+ margin: 1.5rem 0;
3
+ }
4
+
5
+ .code-tabs__tablist {
6
+ display: none;
7
+ gap: 0.25rem;
8
+ border-bottom: 1px solid #ddd;
9
+ }
10
+
11
+ .code-tabs.is-enhanced .code-tabs__tablist {
12
+ display: flex;
13
+ }
14
+
15
+ .code-tabs__tab {
16
+ background: transparent;
17
+ border: 1px solid transparent;
18
+ border-bottom: none;
19
+ border-radius: 4px 4px 0 0;
20
+ padding: 0.5rem 1rem;
21
+ font: inherit;
22
+ font-weight: 600;
23
+ color: var(--body-color);
24
+ cursor: pointer;
25
+ margin-bottom: -1px;
26
+ }
27
+
28
+ .code-tabs__tab[aria-selected="true"] {
29
+ background: white;
30
+ border-color: #ddd;
31
+ color: var(--heading-color);
32
+ }
33
+
34
+ .code-tabs__tab:focus-visible,
35
+ .code-tabs__panel:focus-visible {
36
+ outline: 2px solid var(--action-color);
37
+ outline-offset: 2px;
38
+ }
39
+
40
+ .code-tabs__panel {
41
+ border: 1px solid #ddd;
42
+ border-radius: 0 4px 4px 4px;
43
+ }
44
+
45
+ .code-tabs.is-enhanced .code-tabs__panel {
46
+ border-top-left-radius: 0;
47
+ }
48
+
49
+ .code-tabs__panel pre {
50
+ margin: 0;
51
+ }
52
+
53
+ @media (prefers-reduced-motion: reduce) {
54
+ .code-tabs__tab {
55
+ transition: none;
56
+ }
57
+ }
@@ -0,0 +1,30 @@
1
+ <div class="code-tabs">
2
+ <div class="code-tabs__tablist" role="tablist" aria-label="Template language">
3
+ <% tabs.each_with_index do |tab, i| %>
4
+ <button
5
+ type="button"
6
+ role="tab"
7
+ id="<%= tab_id(tab[:key]) %>"
8
+ aria-controls="<%= panel_id(tab[:key]) %>"
9
+ aria-selected="<%= i.zero? %>"
10
+ tabindex="<%= i.zero? ? 0 : -1 %>"
11
+ class="code-tabs__tab"
12
+ >
13
+ <%= tab[:label] %>
14
+ </button>
15
+ <% end %>
16
+ </div>
17
+
18
+ <% tabs.each_with_index do |tab, i| %>
19
+ <div
20
+ role="tabpanel"
21
+ id="<%= panel_id(tab[:key]) %>"
22
+ aria-labelledby="<%= tab_id(tab[:key]) %>"
23
+ class="code-tabs__panel"
24
+ tabindex="0"
25
+ <%= "hidden" unless i.zero? %>
26
+ >
27
+ <%= raw highlighted(tab[:key], tab[:lexer]) %>
28
+ </div>
29
+ <% end %>
30
+ </div>
@@ -0,0 +1,54 @@
1
+ function initCodeTabs(root) {
2
+ if (root.classList.contains("is-enhanced")) return
3
+
4
+ const tabs = Array.from(root.querySelectorAll('[role="tab"]'))
5
+ const panels = Array.from(root.querySelectorAll('[role="tabpanel"]'))
6
+ if (tabs.length === 0) return
7
+
8
+ root.classList.add("is-enhanced")
9
+
10
+ function activate(index, { focus = true } = {}) {
11
+ tabs.forEach((tab, i) => {
12
+ const selected = i === index
13
+ tab.setAttribute("aria-selected", String(selected))
14
+ tab.setAttribute("tabindex", selected ? "0" : "-1")
15
+ panels[i].hidden = !selected
16
+ })
17
+ if (focus) tabs[index].focus()
18
+ }
19
+
20
+ tabs.forEach((tab, i) => {
21
+ tab.addEventListener("click", () => activate(i))
22
+ tab.addEventListener("keydown", (event) => {
23
+ let next = null
24
+ switch (event.key) {
25
+ case "ArrowRight":
26
+ next = (i + 1) % tabs.length
27
+ break
28
+ case "ArrowLeft":
29
+ next = (i - 1 + tabs.length) % tabs.length
30
+ break
31
+ case "Home":
32
+ next = 0
33
+ break
34
+ case "End":
35
+ next = tabs.length - 1
36
+ break
37
+ default:
38
+ return
39
+ }
40
+ event.preventDefault()
41
+ activate(next)
42
+ })
43
+ })
44
+ }
45
+
46
+ function initAllCodeTabs() {
47
+ document.querySelectorAll(".code-tabs").forEach(initCodeTabs)
48
+ }
49
+
50
+ if (document.readyState === "loading") {
51
+ document.addEventListener("DOMContentLoaded", initAllCodeTabs)
52
+ } else {
53
+ initAllCodeTabs()
54
+ }
@@ -0,0 +1,36 @@
1
+ require "rouge"
2
+ require "securerandom"
3
+
4
+ class Shared::CodeTabs < Bridgetown::Component
5
+ TABS = [
6
+ { key: :erb, label: "ERB", lexer: "erb" },
7
+ { key: :slim, label: "Slim", lexer: "slim" },
8
+ { key: :phlex, label: "Phlex", lexer: "ruby" }
9
+ ].freeze
10
+
11
+ attr_reader :id
12
+
13
+ def initialize(erb:, slim: nil, phlex: nil)
14
+ @samples = { erb: erb, slim: slim, phlex: phlex }
15
+ @id = "code-tabs-#{SecureRandom.hex(4)}"
16
+ end
17
+
18
+ def tabs
19
+ TABS.select { |tab| @samples[tab[:key]] }
20
+ end
21
+
22
+ def tab_id(key)
23
+ "#{id}-tab-#{key}"
24
+ end
25
+
26
+ def panel_id(key)
27
+ "#{id}-panel-#{key}"
28
+ end
29
+
30
+ def highlighted(key, lexer_name)
31
+ formatter = Rouge::Formatters::HTML.new
32
+ lexer = Rouge::Lexer.find(lexer_name) || Rouge::Lexers::PlainText.new
33
+ inner = formatter.format(lexer.lex(@samples[key].to_s))
34
+ %(<pre class="highlight"><code>#{inner}</code></pre>)
35
+ end
36
+ end
@@ -67,15 +67,31 @@ Applies to: HTML `<img>` elements.
67
67
 
68
68
  ### Bad
69
69
 
70
- ```erb
71
- <img src="hero.jpg">
72
- ```
70
+ <%= render Shared::CodeTabs.new(
71
+ erb: <<~ERB,
72
+ <img src="hero.jpg">
73
+ ERB
74
+ slim: <<~SLIM,
75
+ img src="hero.jpg"
76
+ SLIM
77
+ phlex: <<~PHLEX,
78
+ img(src: "hero.jpg")
79
+ PHLEX
80
+ ) %>
73
81
 
74
82
  ### Good
75
83
 
76
- ```erb
77
- <img src="hero.jpg" alt="Team celebrating after a product launch">
78
- ```
84
+ <%= render Shared::CodeTabs.new(
85
+ erb: <<~ERB,
86
+ <img src="hero.jpg" alt="Team celebrating after a product launch">
87
+ ERB
88
+ slim: <<~SLIM,
89
+ img src="hero.jpg" alt="Team celebrating after a product launch"
90
+ SLIM
91
+ phlex: <<~PHLEX,
92
+ img(src: "hero.jpg", alt: "Team celebrating after a product launch")
93
+ PHLEX
94
+ ) %>
79
95
 
80
96
  For a decorative image, use an empty `alt` so assistive tech skips it:
81
97
 
@@ -9,6 +9,19 @@ module A11y
9
9
  class PhlexRunner
10
10
  PHLEX_PATTERN = /\bdef\s+view_template\b/
11
11
 
12
+ # Phlex auto-emits the return value of these calls into the document:
13
+ # `plain` / `text` are built-in; the rest are registered as value
14
+ # helpers by phlex-rails via `register_value_helper`.
15
+ TEXT_CALLS = %w[
16
+ plain text
17
+ t translate l localize
18
+ pluralize truncate
19
+ number_to_currency number_to_human number_to_human_size
20
+ number_to_percentage number_to_phone
21
+ number_with_delimiter number_with_precision
22
+ highlight excerpt
23
+ ].to_set.freeze
24
+
12
25
  def initialize(rules = nil, configuration: Configuration.new)
13
26
  @rules = rules || configuration.enabled_rules
14
27
  @configuration = configuration
@@ -161,7 +174,7 @@ module A11y
161
174
  end
162
175
 
163
176
  def text_call?(node)
164
- receiverless_call?(node) && node.name.to_s == "plain"
177
+ receiverless_call?(node) && TEXT_CALLS.include?(node.name.to_s)
165
178
  end
166
179
 
167
180
  def receiverless_call?(node)
@@ -2,6 +2,6 @@
2
2
 
3
3
  module A11y
4
4
  module Lint
5
- VERSION = "0.14.0"
5
+ VERSION = "0.14.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: a11y-lint
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.0
4
+ version: 0.14.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Abdullah Hashim
@@ -45,6 +45,7 @@ executables:
45
45
  extensions: []
46
46
  extra_rdoc_files: []
47
47
  files:
48
+ - ".claude/commands/task.md"
48
49
  - ".claude/rules/testing.md"
49
50
  - ".rubocop.yml"
50
51
  - CHANGELOG.md
@@ -55,6 +56,7 @@ files:
55
56
  - Rakefile
56
57
  - docs/.gitignore
57
58
  - docs/.ruby-version
59
+ - docs/CONTRIBUTING.md
58
60
  - docs/Gemfile
59
61
  - docs/Gemfile.lock
60
62
  - docs/README.md
@@ -80,6 +82,10 @@ files:
80
82
  - docs/src/404.html
81
83
  - docs/src/500.html
82
84
  - docs/src/CNAME
85
+ - docs/src/_components/shared/code_tabs.css
86
+ - docs/src/_components/shared/code_tabs.erb
87
+ - docs/src/_components/shared/code_tabs.js
88
+ - docs/src/_components/shared/code_tabs.rb
83
89
  - docs/src/_components/shared/navbar.erb
84
90
  - docs/src/_components/shared/navbar.rb
85
91
  - docs/src/_data/site_metadata.yml