@morya-ui/setup 0.2.8 → 0.2.9

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/README.md CHANGED
@@ -9,7 +9,7 @@ Docs (zh/en):
9
9
  - [Agent Skill](https://morya-space.github.io/morya-ui/docs/agent-skill) — `morya-ui-pages` behavior
10
10
  - [Quick start](https://morya-space.github.io/morya-ui/docs/quick-start) — install and first component
11
11
 
12
- Installs the UI library, copies Agent skill / Cursor rules / design docs from `design-kit`, merges Cursor MCP for [`@morya-ui/mcp`](https://www.npmjs.com/package/@morya-ui/mcp), and injects `import 'morya-ui/styles.css'`.
12
+ Installs the UI library, copies Agent skills / Cursor rules / design docs from `design-kit`, merges Cursor MCP for [`@morya-ui/mcp`](https://www.npmjs.com/package/@morya-ui/mcp), and injects `import 'morya-ui/styles.css'`.
13
13
 
14
14
  ## Usage
15
15
 
@@ -19,6 +19,8 @@ In your app project root:
19
19
  npx @morya-ui/setup
20
20
  ```
21
21
 
22
+ On a TTY, `full` / `ai` will prompt for optional Agent skills (required `morya-ui-pages` is always included).
23
+
22
24
  Other common commands:
23
25
 
24
26
  ```bash
@@ -27,6 +29,11 @@ npx @morya-ui/setup ai
27
29
 
28
30
  # Library + styles only
29
31
  npx @morya-ui/setup app
32
+
33
+ # Non-interactive skill selection
34
+ npx @morya-ui/setup ai --yes
35
+ npx @morya-ui/setup ai --skills=morya-ui-pages,frontend-design
36
+ npx @morya-ui/setup ai --skills=all
30
37
  ```
31
38
 
32
39
  ### Options
@@ -35,20 +42,26 @@ npx @morya-ui/setup app
35
42
  | --- | --- |
36
43
  | `--cwd <dir>` | Target project root (default: current directory) |
37
44
  | `--pm pnpm\|yarn\|npm` | Package manager (default: detect from lockfile, else `pnpm`) |
45
+ | `--skills <list>` | Comma-separated skill ids, or `all` (skips the prompt) |
46
+ | `--yes` / `-y` | Use default skills without prompting |
38
47
  | `--force` | Overwrite existing template files and the `morya-ui` MCP entry |
39
48
  | `--dry-run` | Print actions without writing or installing |
40
49
  | `--skip-install` | Do not install `morya-ui` |
41
- | `--skip-template` | Do not copy AI skill / rules / docs / tokens |
50
+ | `--skip-template` | Do not copy AI skill / rules / docs |
42
51
  | `--skip-mcp` | Do not write `.cursor/mcp.json` |
43
52
  | `--skip-styles` | Do not inject `styles.css` |
44
53
  | `--skip-scripts` | Do not add `check:colors` to `package.json` |
45
54
  | `-h`, `--help` | Show help |
46
55
 
47
- Example: MCP only:
56
+ ### Skills
48
57
 
49
- ```bash
50
- npx @morya-ui/setup ai --skip-template --skip-scripts
51
- ```
58
+ | Id | Default | Role |
59
+ | --- | --- | --- |
60
+ | `morya-ui-pages` | required | Page generation with `M*` + golden layouts |
61
+ | `frontend-design` | optional | Express / brand visual taste |
62
+ | `fixing-accessibility` | optional | A11y audit and targeted fixes |
63
+
64
+ Catalog: [`catalog/skills.json`](./catalog/skills.json).
52
65
 
53
66
  ### Conflict policy
54
67
 
@@ -61,8 +74,8 @@ npx @morya-ui/setup ai --skip-template --skip-scripts
61
74
 
62
75
  From the package `template/` (synced from repo `design-kit/`):
63
76
 
64
- - `DESIGN.md`
65
- - `.agents/skills/morya-ui-pages/`
77
+ - `DESIGN.md` — core design contract (principles, tokens, bans)
78
+ - `.agents/skills/<selected>/` — at least `morya-ui-pages`
66
79
  - `.cursor/rules/`
67
80
  - `scripts/check-raw-colors.mjs`
68
81
 
@@ -81,28 +94,4 @@ Writes / merges:
81
94
  }
82
95
  ```
83
96
 
84
- Restart Cursor (or reload MCP) after setup.
85
-
86
- Other clients can use the same stdio command; this CLI only writes `.cursor/mcp.json`.
87
-
88
- ### Styles
89
-
90
- Looks for `src/main.ts` / `.js` / `.tsx` / `.jsx`, root `main.*`, `src/app.ts` / `.js`, or the module script in `index.html`. If none match, add manually:
91
-
92
- ```ts
93
- import 'morya-ui/styles.css'
94
- ```
95
-
96
- The CLI does **not** call `app.use(MoryaUI)` or edit `App.vue`.
97
-
98
- ## Local development (this monorepo)
99
-
100
- ```bash
101
- pnpm setup:sync-template # refresh packages/setup/template from design-kit
102
- pnpm setup:build
103
- node packages/setup/bin/morya-ui-setup.js --cwd /path/to/app --dry-run
104
- ```
105
-
106
- ## License
107
-
108
- MIT
97
+ Restart Cursor (or reload MCP) after install.
@@ -0,0 +1,28 @@
1
+ {
2
+ "skills": [
3
+ {
4
+ "id": "morya-ui-pages",
5
+ "name": "Morya UI Pages",
6
+ "description": "Build admin and product pages with M* components, golden layouts, and tokens.",
7
+ "required": true,
8
+ "default": true,
9
+ "path": ".agents/skills/morya-ui-pages"
10
+ },
11
+ {
12
+ "id": "frontend-design",
13
+ "name": "Frontend Design",
14
+ "description": "Distinctive visual direction for Express / brand surfaces (taste only; still remediate with M*).",
15
+ "required": false,
16
+ "default": false,
17
+ "path": ".agents/skills/frontend-design"
18
+ },
19
+ {
20
+ "id": "fixing-accessibility",
21
+ "name": "Fixing Accessibility",
22
+ "description": "Audit and fix names, keyboard, focus, forms, and WCAG basics.",
23
+ "required": false,
24
+ "default": false,
25
+ "path": ".agents/skills/fixing-accessibility"
26
+ }
27
+ ]
28
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@morya-ui/setup",
3
3
  "type": "module",
4
- "version": "0.2.8",
4
+ "version": "0.2.9",
5
5
  "description": "One-shot setup for morya-ui: install the library, Cursor MCP, Agent skills, and design config.",
6
6
  "license": "MIT",
7
7
  "homepage": "https://github.com/morya-space/morya-ui/tree/main/packages/setup#readme",
@@ -22,6 +22,7 @@
22
22
  "files": [
23
23
  "README.md",
24
24
  "bin",
25
+ "catalog",
25
26
  "src",
26
27
  "template"
27
28
  ],
package/src/cli.mjs CHANGED
@@ -6,6 +6,12 @@ import { readJson } from './fs-utils.mjs'
6
6
  import { installMoryaUi } from './install.mjs'
7
7
  import { mergeMcpConfig } from './mcp.mjs'
8
8
  import { ensureCheckColorsScript } from './package-json.mjs'
9
+ import {
10
+ buildAiInclude,
11
+ loadSkillsCatalog,
12
+ parseSkillsFlag,
13
+ resolveSkillSelection,
14
+ } from './skills.mjs'
9
15
  import { ensureStylesImport } from './styles.mjs'
10
16
 
11
17
  const __dirname = dirname(fileURLToPath(import.meta.url))
@@ -14,14 +20,6 @@ const TEMPLATE_ROOT = join(PKG_ROOT, 'template')
14
20
 
15
21
  const MODES = new Set(['app', 'ai', 'full'])
16
22
 
17
- /** AI pack paths under template/. */
18
- const AI_TEMPLATE_INCLUDE = [
19
- 'DESIGN.md',
20
- '.agents/skills/morya-ui-pages',
21
- '.cursor/rules',
22
- 'scripts/check-raw-colors.mjs',
23
- ]
24
-
25
23
  /** Mode → default skips (user --skip-* can only add more skips). */
26
24
  const MODE_DEFAULTS = {
27
25
  full: {},
@@ -38,17 +36,23 @@ const SKIP_FLAGS = {
38
36
  }
39
37
 
40
38
  export function printHelp() {
39
+ const { skills } = loadSkillsCatalog()
40
+ const skillList = skills.map((s) => {
41
+ const tag = s.required ? 'required' : s.default ? 'default' : 'optional'
42
+ return ` ${s.id.padEnd(24)} (${tag}) ${s.description}`
43
+ }).join('\n')
44
+
41
45
  console.log(`Usage: morya-ui-setup [command] [options]
42
46
 
43
47
  Commands:
44
48
  (default) / full Install morya-ui, AI template, MCP, styles, check:colors
45
49
  app Install morya-ui and inject styles.css
46
- ai Copy Agent skill / rules / DESIGN,
50
+ ai Copy Agent skills / rules / DESIGN,
47
51
  merge Cursor MCP, add check:colors
48
52
 
49
53
  Default command:
50
54
  - install morya-ui
51
- - copy DESIGN.md, Agent skill, Cursor rules
55
+ - copy DESIGN.md, selected Agent skills, Cursor rules
52
56
  - merge .cursor/mcp.json for @morya-ui/mcp
53
57
  - inject import 'morya-ui/styles.css' into the app entry when found
54
58
  - add check:colors script when missing
@@ -56,6 +60,8 @@ Default command:
56
60
  Options:
57
61
  --cwd <dir> Target project root (default: process.cwd())
58
62
  --pm <name> Package manager: pnpm | yarn | npm (auto-detect by lockfile)
63
+ --skills <list> Comma-separated skill ids, or "all" (skips interactive prompt)
64
+ --yes Use default skills without prompting (CI / non-interactive)
59
65
  --force Overwrite existing template files and MCP server entry
60
66
  --dry-run Print actions without writing or installing
61
67
  --skip-install Skip dependency install
@@ -64,6 +70,9 @@ Options:
64
70
  --skip-styles Skip injecting styles.css
65
71
  --skip-scripts Skip adding check:colors to package.json
66
72
  -h, --help Show this help
73
+
74
+ Skills:
75
+ ${skillList}
67
76
  `)
68
77
  }
69
78
 
@@ -75,6 +84,8 @@ export function parseArgs(argv) {
75
84
  mode: 'full',
76
85
  cwd: process.cwd(),
77
86
  pm: undefined,
87
+ skills: undefined,
88
+ yes: false,
78
89
  force: false,
79
90
  dryRun: false,
80
91
  skipInstall: false,
@@ -103,6 +114,10 @@ export function parseArgs(argv) {
103
114
  options.dryRun = true
104
115
  continue
105
116
  }
117
+ if (arg === '--yes' || arg === '-y') {
118
+ options.yes = true
119
+ continue
120
+ }
106
121
  if (SKIP_FLAGS[arg]) {
107
122
  options[SKIP_FLAGS[arg]] = true
108
123
  continue
@@ -127,6 +142,16 @@ export function parseArgs(argv) {
127
142
  options.pm = arg.slice('--pm='.length)
128
143
  continue
129
144
  }
145
+ if (arg === '--skills') {
146
+ const value = argv[++i]
147
+ if (!value) throw new Error('--skills requires a comma-separated list or "all"')
148
+ options.skills = value
149
+ continue
150
+ }
151
+ if (arg.startsWith('--skills=')) {
152
+ options.skills = arg.slice('--skills='.length)
153
+ continue
154
+ }
130
155
  throw new Error(`Unknown argument: ${arg}`)
131
156
  }
132
157
 
@@ -176,6 +201,8 @@ export async function runSetup(options) {
176
201
  skipStyles,
177
202
  skipScripts,
178
203
  pm,
204
+ skills: skillsFlag,
205
+ yes,
179
206
  } = options
180
207
 
181
208
  console.log(`@morya-ui/setup [${mode}] → ${cwd}${dryRun ? ' (dry-run)' : ''}`)
@@ -188,6 +215,24 @@ export async function runSetup(options) {
188
215
  console.log('')
189
216
  }
190
217
 
218
+ const catalog = loadSkillsCatalog()
219
+ let selectedSkills = []
220
+ let aiInclude
221
+
222
+ const needsTemplate = !skipTemplate && (mode === 'ai' || mode === 'full')
223
+ if (needsTemplate) {
224
+ if (skillsFlag != null) {
225
+ selectedSkills = parseSkillsFlag(skillsFlag, catalog.skills)
226
+ } else {
227
+ selectedSkills = await resolveSkillSelection(catalog.skills, {
228
+ skipPrompt: yes || dryRun,
229
+ })
230
+ }
231
+ aiInclude = buildAiInclude(selectedSkills, catalog.skills)
232
+ console.log(`Skills: ${selectedSkills.join(', ')}`)
233
+ console.log('')
234
+ }
235
+
191
236
  const install = installMoryaUi(cwd, { pm, dryRun, skipInstall })
192
237
 
193
238
  const template = skipTemplate
@@ -195,7 +240,7 @@ export async function runSetup(options) {
195
240
  : copyTemplate(TEMPLATE_ROOT, cwd, {
196
241
  force,
197
242
  dryRun,
198
- include: mode === 'ai' || mode === 'full' ? AI_TEMPLATE_INCLUDE : undefined,
243
+ include: aiInclude,
199
244
  })
200
245
 
201
246
  const mcp = skipMcp
@@ -223,6 +268,7 @@ export async function runSetup(options) {
223
268
  console.log(
224
269
  `Template: ${template.copied.length} copied, ${template.forced.length} overwritten, ${template.skipped.length} skipped`,
225
270
  )
271
+ if (selectedSkills.length) console.log(` skills: ${selectedSkills.join(', ')}`)
226
272
  if (template.skipped.length && template.skipped.length <= 8) {
227
273
  for (const file of template.skipped) console.log(` skip ${file}`)
228
274
  } else if (template.skipped.length > 8) {
@@ -270,5 +316,5 @@ export async function runSetup(options) {
270
316
  console.log(' 4. Optional: pnpm check:colors')
271
317
  }
272
318
 
273
- return { mode, install, template, mcp, styles, scripts }
319
+ return { mode, install, template, mcp, styles, scripts, skills: selectedSkills }
274
320
  }
package/src/skills.mjs ADDED
@@ -0,0 +1,154 @@
1
+ import { createInterface } from 'node:readline'
2
+ import { dirname, join } from 'node:path'
3
+ import { fileURLToPath } from 'node:url'
4
+ import { readJson } from './fs-utils.mjs'
5
+
6
+ const __dirname = dirname(fileURLToPath(import.meta.url))
7
+ const CATALOG_PATH = join(__dirname, '..', 'catalog', 'skills.json')
8
+
9
+ /**
10
+ * @typedef {{
11
+ * id: string
12
+ * name: string
13
+ * description: string
14
+ * required?: boolean
15
+ * default?: boolean
16
+ * path: string
17
+ * }} SkillEntry
18
+ */
19
+
20
+ /**
21
+ * @returns {{ skills: SkillEntry[] }}
22
+ */
23
+ export function loadSkillsCatalog() {
24
+ return readJson(CATALOG_PATH)
25
+ }
26
+
27
+ /**
28
+ * @param {SkillEntry[]} skills
29
+ */
30
+ export function defaultSkillIds(skills) {
31
+ return skills.filter((s) => s.required || s.default).map((s) => s.id)
32
+ }
33
+
34
+ /**
35
+ * @param {string | undefined} raw
36
+ * @param {SkillEntry[]} skills
37
+ * @returns {string[]}
38
+ */
39
+ export function parseSkillsFlag(raw, skills) {
40
+ if (raw == null || raw === '') return defaultSkillIds(skills)
41
+ const known = new Map(skills.map((s) => [s.id, s]))
42
+ if (raw.trim() === 'all') return skills.map((s) => s.id)
43
+
44
+ const ids = raw
45
+ .split(',')
46
+ .map((part) => part.trim())
47
+ .filter(Boolean)
48
+
49
+ if (ids.length === 0) return defaultSkillIds(skills)
50
+
51
+ const missing = ids.filter((id) => !known.has(id))
52
+ if (missing.length) {
53
+ throw new Error(
54
+ `Unknown skill id(s): ${missing.join(', ')}. Available: ${skills.map((s) => s.id).join(', ')}, or all`,
55
+ )
56
+ }
57
+
58
+ const selected = new Set(ids)
59
+ for (const skill of skills) {
60
+ if (skill.required) selected.add(skill.id)
61
+ }
62
+ return skills.map((s) => s.id).filter((id) => selected.has(id))
63
+ }
64
+
65
+ /**
66
+ * @param {string[]} skillIds
67
+ * @param {SkillEntry[]} skills
68
+ * @returns {string[]}
69
+ */
70
+ export function skillPathsForIds(skillIds, skills) {
71
+ const byId = new Map(skills.map((s) => [s.id, s]))
72
+ return skillIds.map((id) => {
73
+ const entry = byId.get(id)
74
+ if (!entry) throw new Error(`Unknown skill id: ${id}`)
75
+ return entry.path
76
+ })
77
+ }
78
+
79
+ /**
80
+ * Build template include prefixes for the AI pack.
81
+ * @param {string[]} skillIds
82
+ * @param {SkillEntry[]} skills
83
+ */
84
+ export function buildAiInclude(skillIds, skills) {
85
+ return [
86
+ 'DESIGN.md',
87
+ ...skillPathsForIds(skillIds, skills),
88
+ '.cursor/rules',
89
+ 'scripts/check-raw-colors.mjs',
90
+ ]
91
+ }
92
+
93
+ /**
94
+ * Prompt for optional skills when stdin is a TTY.
95
+ * Required skills are always included.
96
+ * @param {SkillEntry[]} skills
97
+ * @param {{ skipPrompt?: boolean }} [options]
98
+ * @returns {Promise<string[]>}
99
+ */
100
+ export async function resolveSkillSelection(skills, { skipPrompt = false } = {}) {
101
+ const required = skills.filter((s) => s.required).map((s) => s.id)
102
+ const optional = skills.filter((s) => !s.required)
103
+
104
+ if (skipPrompt || optional.length === 0 || !process.stdin.isTTY) {
105
+ return defaultSkillIds(skills)
106
+ }
107
+
108
+ console.log('Select optional Agent skills (in addition to required):')
109
+ optional.forEach((skill, index) => {
110
+ const mark = skill.default ? 'x' : ' '
111
+ console.log(` ${index + 1}. [${mark}] ${skill.id} — ${skill.description}`)
112
+ })
113
+ console.log('')
114
+ console.log('Enter comma-separated numbers or ids (empty = defaults only).')
115
+ console.log(`Required: ${required.join(', ') || '(none)'}`)
116
+
117
+ const answer = await question('> ')
118
+ const trimmed = answer.trim()
119
+ if (!trimmed) return defaultSkillIds(skills)
120
+
121
+ const selected = new Set(required)
122
+ const tokens = trimmed.split(/[\s,]+/).filter(Boolean)
123
+ for (const token of tokens) {
124
+ const asIndex = Number(token)
125
+ if (Number.isInteger(asIndex) && asIndex >= 1 && asIndex <= optional.length) {
126
+ selected.add(optional[asIndex - 1].id)
127
+ continue
128
+ }
129
+ if (token === 'all') {
130
+ for (const skill of optional) selected.add(skill.id)
131
+ continue
132
+ }
133
+ if (!skills.some((s) => s.id === token)) {
134
+ throw new Error(`Unknown skill selection: ${token}`)
135
+ }
136
+ selected.add(token)
137
+ }
138
+
139
+ return skills.map((s) => s.id).filter((id) => selected.has(id))
140
+ }
141
+
142
+ /**
143
+ * @param {string} prompt
144
+ * @returns {Promise<string>}
145
+ */
146
+ function question(prompt) {
147
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
148
+ return new Promise((resolve) => {
149
+ rl.question(prompt, (answer) => {
150
+ rl.close()
151
+ resolve(answer)
152
+ })
153
+ })
154
+ }
@@ -0,0 +1,138 @@
1
+ ---
2
+ name: fixing-accessibility
3
+ description: Audit and fix HTML accessibility issues including ARIA labels, keyboard navigation, focus management, color contrast, and form errors. Use when adding interactive controls, forms, dialogs, or reviewing WCAG compliance.
4
+ ---
5
+
6
+ # fixing-accessibility
7
+
8
+ > **morya-ui consumer note:** Prefer library defaults (`label`, `aria-label`, Esc-dismiss overlays). Do not replace `M*` widgets with custom ARIA hacks. When guidance conflicts with `DESIGN.md` or `morya-ui-pages`, **those win**.
9
+
10
+ Fix accessibility issues.
11
+
12
+ ## how to use
13
+
14
+ - `/fixing-accessibility`
15
+ Apply these constraints to any UI work in this conversation.
16
+
17
+ - `/fixing-accessibility <file>`
18
+ Review the file against all rules below and report:
19
+ - violations (quote the exact line or snippet)
20
+ - why it matters (one short sentence)
21
+ - a concrete fix (code-level suggestion)
22
+
23
+ Do not rewrite large parts of the UI. Prefer minimal, targeted fixes.
24
+
25
+ ## when to apply
26
+
27
+ Reference these guidelines when:
28
+ - adding or changing buttons, links, inputs, menus, dialogs, tabs, dropdowns
29
+ - building forms, validation, error states, helper text
30
+ - implementing keyboard shortcuts or custom interactions
31
+ - working on focus states, focus trapping, or modal behavior
32
+ - rendering icon-only controls
33
+ - adding hover-only interactions or hidden content
34
+
35
+ ## rule categories by priority
36
+
37
+ | priority | category | impact |
38
+ |----------|----------|--------|
39
+ | 1 | accessible names | critical |
40
+ | 2 | keyboard access | critical |
41
+ | 3 | focus and dialogs | critical |
42
+ | 4 | semantics | high |
43
+ | 5 | forms and errors | high |
44
+ | 6 | announcements | medium-high |
45
+ | 7 | contrast and states | medium |
46
+ | 8 | media and motion | low-medium |
47
+ | 9 | tool boundaries | critical |
48
+
49
+ ## quick reference
50
+
51
+ ### 1. accessible names (critical)
52
+
53
+ - every interactive control must have an accessible name
54
+ - icon-only buttons must have aria-label or aria-labelledby
55
+ - every input, select, and textarea must be labeled
56
+ - links must have meaningful text (no “click here”)
57
+ - decorative icons must be aria-hidden
58
+
59
+ ### 2. keyboard access (critical)
60
+
61
+ - do not use div or span as buttons without full keyboard support
62
+ - all interactive elements must be reachable by Tab
63
+ - focus must be visible for keyboard users
64
+ - do not use tabindex greater than 0
65
+ - Escape must close dialogs or overlays when applicable
66
+
67
+ ### 3. focus and dialogs (critical)
68
+
69
+ - modals must trap focus while open
70
+ - restore focus to the trigger on close
71
+ - set initial focus inside dialogs
72
+ - opening a dialog should not scroll the page unexpectedly
73
+
74
+ ### 4. semantics (high)
75
+
76
+ - prefer native elements (button, a, input) over role-based hacks
77
+ - if a role is used, required aria attributes must be present
78
+ - lists must use ul or ol with li
79
+ - do not skip heading levels
80
+ - tables must use th for headers when applicable
81
+
82
+ ### 5. forms and errors (high)
83
+
84
+ - errors must be linked to fields using aria-describedby
85
+ - required fields must be announced
86
+ - invalid fields must set aria-invalid
87
+ - helper text must be associated with inputs
88
+ - disabled submit actions must explain why
89
+
90
+ ### 6. announcements (medium-high)
91
+
92
+ - critical form errors should use aria-live
93
+ - loading states should use aria-busy or status text
94
+ - toasts must not be the only way to convey critical information
95
+ - expandable controls must use aria-expanded and aria-controls
96
+
97
+ ### 7. contrast and states (medium)
98
+
99
+ - ensure sufficient contrast for text and icons
100
+ - hover-only interactions must have keyboard equivalents
101
+ - disabled states must not rely on color alone
102
+ - do not remove focus outlines without a visible replacement
103
+
104
+ ### 8. media and motion (low-medium)
105
+
106
+ - images must have correct alt text (meaningful or empty)
107
+ - videos with speech should provide captions when relevant
108
+ - respect prefers-reduced-motion for non-essential motion
109
+ - avoid autoplaying media with sound
110
+
111
+ ### 9. tool boundaries (critical)
112
+
113
+ - prefer minimal changes, do not refactor unrelated code
114
+ - do not add aria when native semantics already solve the problem
115
+ - do not migrate UI libraries unless requested
116
+
117
+ ## common fixes
118
+
119
+ ```html
120
+ <!-- icon-only button: add aria-label -->
121
+ <!-- before --> <button><svg>...</svg></button>
122
+ <!-- after --> <button aria-label="Close"><svg aria-hidden="true">...</svg></button>
123
+
124
+ <!-- div as button: use native element -->
125
+ <!-- before --> <div onclick="save()">Save</div>
126
+ <!-- after --> <button onclick="save()">Save</button>
127
+
128
+ <!-- form error: link with aria-describedby -->
129
+ <!-- before --> <input id="email" /> <span>Invalid email</span>
130
+ <!-- after --> <input id="email" aria-describedby="email-err" aria-invalid="true" /> <span id="email-err">Invalid email</span>
131
+ ```
132
+
133
+ ## review guidance
134
+
135
+ - fix critical issues first (names, keyboard, focus, tool boundaries)
136
+ - prefer native HTML before adding aria
137
+ - quote the exact snippet, state the failure, propose a small fix
138
+ - for complex widgets (menu, dialog, combobox), prefer established accessible primitives over custom behavior