-
Notifications
You must be signed in to change notification settings - Fork 6k
Add axle — Accessibility Scanner #27308
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 6 commits
4e0bdfe
6c2d7d0
a9e94e4
b2c24f6
8cbabfe
eaeb6e2
9632889
313668e
5f94518
ee68eb6
eeadb01
7b16a63
b5cba7c
c36515f
48840c2
44c4828
a648ed1
21cbdd7
910a215
4bdb2f8
1720df1
7cddd6a
46b0744
eae1716
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # axle for Raycast | ||
|
|
||
| Scan any URL for WCAG 2.1 / 2.2 AA violations without leaving Raycast. | ||
|
|
||
| ## Commands | ||
|
|
||
| - **Scan URL for Accessibility** — prompt for a URL, run the axle scanner, show a filterable list of violations with severity and affected-element counts. Press Enter on any row for the offending HTML and a link to the WCAG reference. | ||
| - **Open Hebrew Accessibility Statement Generator** — one-click open of the free `תקנה 35`-aligned statement generator. | ||
|
|
||
| ## Under the hood | ||
|
|
||
| Commands call the public axle API at `https://axle-iota.vercel.app/api/scan`. No account required. Free tier is rate-limited — bring your own `ANTHROPIC_API_KEY` (via the web UI or the CLI) for unlimited AI fixes. | ||
|
|
||
| ## Install | ||
|
|
||
| Once listed in the Raycast Store: search "axle". | ||
|
|
||
| ## Dev | ||
|
|
||
| ``` | ||
| npm install | ||
| npm run dev | ||
| ``` |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,37 @@ | ||
| { | ||
| "$schema": "https://www.raycast.com/schemas/extension.json", | ||
| "name": "axle", | ||
| "title": "axle — Accessibility Scanner", | ||
| "description": "Scan any URL for WCAG 2.1 / 2.2 AA accessibility violations without leaving Raycast. Results appear as a list; pick any violation to see the offending element and suggested fix.", | ||
| "icon": "command-icon.png", | ||
| "author": "asafamos", | ||
| "categories": ["Developer Tools", "Web"], | ||
| "license": "MIT", | ||
| "commands": [ | ||
| { | ||
| "name": "scan", | ||
| "title": "Scan URL for Accessibility", | ||
| "subtitle": "axle", | ||
| "description": "Run an axe-core scan against any public URL and show violations.", | ||
| "mode": "view", | ||
| "arguments": [ | ||
| { | ||
| "name": "url", | ||
| "type": "text", | ||
| "placeholder": "https://example.com", | ||
| "required": true | ||
| } | ||
| ] | ||
| }, | ||
| { | ||
| "name": "statement", | ||
| "title": "Open Hebrew Accessibility Statement Generator", | ||
| "subtitle": "axle", | ||
| "description": "Opens the free Hebrew statement generator — aligned with Israeli תקנה 35.", | ||
| "mode": "no-view" | ||
| } | ||
| ], | ||
| "dependencies": { | ||
| "@raycast/api": "^1.78.0" | ||
| } | ||
| } | ||
|
Comment on lines
+1
to
+55
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Every PR to this repo must include a ## [Initial Release] - {PR_MERGE_DATE}
- Initial release of axle — Accessibility ScannerThe Rule Used: What: Ensure that CHANGELOG.md is created or updat... (source) Prompt To Fix With AIThis is a comment left during a code review.
Path: extensions/axle/package.json
Line: 1-37
Comment:
**Missing `CHANGELOG.md`**
Every PR to this repo must include a `CHANGELOG.md` file. For an initial release it should follow the standard pattern:
```markdown
## [Initial Release] - {PR_MERGE_DATE}
- Initial release of axle — Accessibility Scanner
```
The `{PR_MERGE_DATE}` placeholder is filled in automatically when the PR merges.
**Rule Used:** What: Ensure that CHANGELOG.md is created or updat... ([source](https://app.greptile.com/review/custom-context?memory=97cd51bc-963b-43f5-acc3-9ba85fe7bb2d))
How can I resolve this? If you propose a fix, please make it concise. |
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| /// <reference types="@raycast/api"> | ||
|
|
||
| /* 🚧 🚧 🚧 | ||
| * This file is auto-generated from the extension's manifest. | ||
| * Do not modify manually. Instead, update the `package.json` file. | ||
| * 🚧 🚧 🚧 */ | ||
|
|
||
| /* eslint-disable @typescript-eslint/ban-types */ | ||
|
|
||
| type ExtensionPreferences = {} | ||
|
|
||
| /** Preferences accessible in all the extension's commands */ | ||
| declare type Preferences = ExtensionPreferences | ||
|
|
||
| declare namespace Preferences { | ||
| /** Preferences accessible in the `scan` command */ | ||
| export type Scan = ExtensionPreferences & {} | ||
| /** Preferences accessible in the `statement` command */ | ||
| export type Statement = ExtensionPreferences & {} | ||
| } | ||
|
|
||
| declare namespace Arguments { | ||
| /** Arguments passed to the `scan` command */ | ||
| export type Scan = { | ||
| /** https://example.com */ | ||
| "url": string | ||
| } | ||
| /** Arguments passed to the `statement` command */ | ||
| export type Statement = {} | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,170 @@ | ||||||||||||||||||||||||||||||||||||||||
| import { | ||||||||||||||||||||||||||||||||||||||||
| ActionPanel, | ||||||||||||||||||||||||||||||||||||||||
| Action, | ||||||||||||||||||||||||||||||||||||||||
| List, | ||||||||||||||||||||||||||||||||||||||||
| Detail, | ||||||||||||||||||||||||||||||||||||||||
| Icon, | ||||||||||||||||||||||||||||||||||||||||
| showToast, | ||||||||||||||||||||||||||||||||||||||||
| Toast, | ||||||||||||||||||||||||||||||||||||||||
| useNavigation, | ||||||||||||||||||||||||||||||||||||||||
| } from "@raycast/api"; | ||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Prompt To Fix With AIThis is a comment left during a code review.
Path: extensions/axle/src/scan.tsx
Line: 1-10
Comment:
**Unused `useNavigation` import**
`useNavigation` is imported but never called in this file. `Action.Push` handles navigation internally and doesn't require it directly.
```suggestion
import {
ActionPanel,
Action,
List,
Detail,
Icon,
showToast,
Toast,
} from "@raycast/api";
```
How can I resolve this? If you propose a fix, please make it concise. |
||||||||||||||||||||||||||||||||||||||||
| import { useEffect, useState } from "react"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| type AxeViolation = { | ||||||||||||||||||||||||||||||||||||||||
| id: string; | ||||||||||||||||||||||||||||||||||||||||
| impact: "critical" | "serious" | "moderate" | "minor" | null; | ||||||||||||||||||||||||||||||||||||||||
| help: string; | ||||||||||||||||||||||||||||||||||||||||
| description: string; | ||||||||||||||||||||||||||||||||||||||||
| helpUrl: string; | ||||||||||||||||||||||||||||||||||||||||
| nodes: Array<{ | ||||||||||||||||||||||||||||||||||||||||
| html: string; | ||||||||||||||||||||||||||||||||||||||||
| target: string[]; | ||||||||||||||||||||||||||||||||||||||||
| failureSummary: string; | ||||||||||||||||||||||||||||||||||||||||
| }>; | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| type ScanResult = { | ||||||||||||||||||||||||||||||||||||||||
| url: string; | ||||||||||||||||||||||||||||||||||||||||
| title: string; | ||||||||||||||||||||||||||||||||||||||||
| violations: AxeViolation[]; | ||||||||||||||||||||||||||||||||||||||||
| summary: { | ||||||||||||||||||||||||||||||||||||||||
| critical: number; | ||||||||||||||||||||||||||||||||||||||||
| serious: number; | ||||||||||||||||||||||||||||||||||||||||
| moderate: number; | ||||||||||||||||||||||||||||||||||||||||
| minor: number; | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const AXLE_API = "https://axle-iota.vercel.app"; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| const IMPACT_ICON: Record<string, { source: Icon; tintColor?: string }> = { | ||||||||||||||||||||||||||||||||||||||||
| critical: { source: Icon.ExclamationMark, tintColor: "#dc2626" }, | ||||||||||||||||||||||||||||||||||||||||
| serious: { source: Icon.Warning, tintColor: "#ea580c" }, | ||||||||||||||||||||||||||||||||||||||||
| moderate: { source: Icon.Info, tintColor: "#d97706" }, | ||||||||||||||||||||||||||||||||||||||||
| minor: { source: Icon.Circle, tintColor: "#2563eb" }, | ||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| export default function Scan(props: { arguments: { url: string } }) { | ||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The
Suggested change
You'll also need to add Rule Used: What: Don't manually define Prompt To Fix With AIThis is a comment left during a code review.
Path: extensions/axle/src/scan.tsx
Line: 47
Comment:
**Use auto-generated argument types instead of manual typing**
The `raycast-env.d.ts` file already exports `Arguments.Scan` which covers the `url` argument. Prefer `LaunchProps` with the generated type to stay in sync with the manifest automatically.
```suggestion
export default function Scan(props: LaunchProps<{ arguments: Arguments.Scan }>) {
```
You'll also need to add `LaunchProps` to the `@raycast/api` import.
**Rule Used:** What: Don't manually define `Preferences` for `get... ([source](https://app.greptile.com/review/custom-context?memory=d93fc9fb-a45d-4479-a6a4-b1b4af98ebc8))
How can I resolve this? If you propose a fix, please make it concise.Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time! |
||||||||||||||||||||||||||||||||||||||||
| const { url } = props.arguments; | ||||||||||||||||||||||||||||||||||||||||
| const [result, setResult] = useState<ScanResult | null>(null); | ||||||||||||||||||||||||||||||||||||||||
| const [error, setError] = useState<string | null>(null); | ||||||||||||||||||||||||||||||||||||||||
| const [loading, setLoading] = useState(true); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||
| const normalized = /^https?:\/\//i.test(url) ? url : `https://${url}`; | ||||||||||||||||||||||||||||||||||||||||
| showToast({ style: Toast.Style.Animated, title: "Scanning", message: normalized }); | ||||||||||||||||||||||||||||||||||||||||
| fetch(`${AXLE_API}/api/scan`, { | ||||||||||||||||||||||||||||||||||||||||
| method: "POST", | ||||||||||||||||||||||||||||||||||||||||
| headers: { "Content-Type": "application/json" }, | ||||||||||||||||||||||||||||||||||||||||
| body: JSON.stringify({ url: normalized }), | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| .then(async (r) => { | ||||||||||||||||||||||||||||||||||||||||
| const data = (await r.json()) as ScanResult & { error?: string }; | ||||||||||||||||||||||||||||||||||||||||
| if (!r.ok || data.error) throw new Error(data.error || `HTTP ${r.status}`); | ||||||||||||||||||||||||||||||||||||||||
| setResult(data); | ||||||||||||||||||||||||||||||||||||||||
| showToast({ | ||||||||||||||||||||||||||||||||||||||||
| style: Toast.Style.Success, | ||||||||||||||||||||||||||||||||||||||||
| title: `${data.violations.length} violations`, | ||||||||||||||||||||||||||||||||||||||||
| message: normalized, | ||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| .catch((err) => { | ||||||||||||||||||||||||||||||||||||||||
| const msg = err instanceof Error ? err.message : "Scan failed"; | ||||||||||||||||||||||||||||||||||||||||
| setError(msg); | ||||||||||||||||||||||||||||||||||||||||
| showToast({ style: Toast.Style.Failure, title: "Scan failed", message: msg }); | ||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||
| .finally(() => setLoading(false)); | ||||||||||||||||||||||||||||||||||||||||
| }, [url]); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (loading) { | ||||||||||||||||||||||||||||||||||||||||
| return <List isLoading={true} searchBarPlaceholder="Scanning…" />; | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (error) { | ||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||
| <Detail markdown={`# Scan failed\n\n${error}`} /> | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| if (!result) return null; | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||
| <List | ||||||||||||||||||||||||||||||||||||||||
| navigationTitle={`${result.title || result.url} — ${result.violations.length} rules`} | ||||||||||||||||||||||||||||||||||||||||
| searchBarPlaceholder="Filter violations" | ||||||||||||||||||||||||||||||||||||||||
| > | ||||||||||||||||||||||||||||||||||||||||
| <List.Section title="Summary"> | ||||||||||||||||||||||||||||||||||||||||
| <List.Item | ||||||||||||||||||||||||||||||||||||||||
| title={`${result.summary.critical} critical · ${result.summary.serious} serious · ${result.summary.moderate} moderate · ${result.summary.minor} minor`} | ||||||||||||||||||||||||||||||||||||||||
| icon={Icon.BarChart} | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| </List.Section> | ||||||||||||||||||||||||||||||||||||||||
| <List.Section title="Violations"> | ||||||||||||||||||||||||||||||||||||||||
| {result.violations.map((v) => ( | ||||||||||||||||||||||||||||||||||||||||
| <List.Item | ||||||||||||||||||||||||||||||||||||||||
| key={v.id} | ||||||||||||||||||||||||||||||||||||||||
| title={v.help} | ||||||||||||||||||||||||||||||||||||||||
| subtitle={v.id} | ||||||||||||||||||||||||||||||||||||||||
| accessories={[ | ||||||||||||||||||||||||||||||||||||||||
| { text: `${v.nodes.length} element${v.nodes.length === 1 ? "" : "s"}` }, | ||||||||||||||||||||||||||||||||||||||||
| { tag: v.impact ?? "minor" }, | ||||||||||||||||||||||||||||||||||||||||
| ]} | ||||||||||||||||||||||||||||||||||||||||
| icon={IMPACT_ICON[v.impact ?? "minor"]} | ||||||||||||||||||||||||||||||||||||||||
| actions={ | ||||||||||||||||||||||||||||||||||||||||
| <ActionPanel> | ||||||||||||||||||||||||||||||||||||||||
| <Action.Push title="View Details" target={<ViolationDetail violation={v} />} /> | ||||||||||||||||||||||||||||||||||||||||
| <Action.OpenInBrowser title="Open WCAG Reference" url={v.helpUrl} /> | ||||||||||||||||||||||||||||||||||||||||
| <Action.CopyToClipboard | ||||||||||||||||||||||||||||||||||||||||
| title="Copy Rule ID" | ||||||||||||||||||||||||||||||||||||||||
| content={v.id} | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| </ActionPanel> | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| ))} | ||||||||||||||||||||||||||||||||||||||||
| </List.Section> | ||||||||||||||||||||||||||||||||||||||||
| </List> | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| function ViolationDetail({ violation }: { violation: AxeViolation }) { | ||||||||||||||||||||||||||||||||||||||||
| const first = violation.nodes[0]; | ||||||||||||||||||||||||||||||||||||||||
| const md = [ | ||||||||||||||||||||||||||||||||||||||||
| `# ${violation.help}`, | ||||||||||||||||||||||||||||||||||||||||
| "", | ||||||||||||||||||||||||||||||||||||||||
| `**Rule:** \`${violation.id}\` · **Impact:** ${violation.impact ?? "minor"}`, | ||||||||||||||||||||||||||||||||||||||||
| "", | ||||||||||||||||||||||||||||||||||||||||
| violation.description, | ||||||||||||||||||||||||||||||||||||||||
| "", | ||||||||||||||||||||||||||||||||||||||||
| `---`, | ||||||||||||||||||||||||||||||||||||||||
| "", | ||||||||||||||||||||||||||||||||||||||||
| `## First affected element`, | ||||||||||||||||||||||||||||||||||||||||
| "", | ||||||||||||||||||||||||||||||||||||||||
| `\`${first?.target.join(" ")}\``, | ||||||||||||||||||||||||||||||||||||||||
| "", | ||||||||||||||||||||||||||||||||||||||||
| "```html", | ||||||||||||||||||||||||||||||||||||||||
| first?.html ?? "", | ||||||||||||||||||||||||||||||||||||||||
| "```", | ||||||||||||||||||||||||||||||||||||||||
| "", | ||||||||||||||||||||||||||||||||||||||||
| first?.failureSummary ? `> ${first.failureSummary}` : "", | ||||||||||||||||||||||||||||||||||||||||
| "", | ||||||||||||||||||||||||||||||||||||||||
| `---`, | ||||||||||||||||||||||||||||||||||||||||
| "", | ||||||||||||||||||||||||||||||||||||||||
| `${violation.nodes.length} element(s) affected in total. [Open full report →](${AXLE_API}/)`, | ||||||||||||||||||||||||||||||||||||||||
| ].join("\n"); | ||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||
| <Detail | ||||||||||||||||||||||||||||||||||||||||
| markdown={md} | ||||||||||||||||||||||||||||||||||||||||
| actions={ | ||||||||||||||||||||||||||||||||||||||||
| <ActionPanel> | ||||||||||||||||||||||||||||||||||||||||
| <Action.OpenInBrowser title="Open WCAG Reference" url={violation.helpUrl} /> | ||||||||||||||||||||||||||||||||||||||||
| <Action.CopyToClipboard | ||||||||||||||||||||||||||||||||||||||||
| title="Copy Element HTML" | ||||||||||||||||||||||||||||||||||||||||
| content={first?.html ?? ""} | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| </ActionPanel> | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| import { open, showHUD } from "@raycast/api"; | ||
|
|
||
| export default async function Statement() { | ||
| await open("https://axle-iota.vercel.app/statement"); | ||
| await showHUD("Opened axle Hebrew statement generator"); | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
metadata/folder with screenshotsThe
scancommand has"mode": "view", so the store requires Raycast-styled screenshots in ametadata/directory. Without them the extension will be rejected from the store. See the Raycast docs for the expected format and dimensions.Rule Used: What: Extensions with view-type commands must incl... (source)
Prompt To Fix With AI