taste-lint 0.0.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.
Files changed (138) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +141 -0
  3. package/data/config.schema.json +123 -0
  4. package/data/corpus/blode-co-review.jsonl +80 -0
  5. package/data/corpus/copywriting.jsonl +139 -0
  6. package/data/corpus/typography.jsonl +53 -0
  7. package/data/rule-drafts/README.md +9 -0
  8. package/data/rule-drafts/interaction/interaction-layout-shift.yaml +54 -0
  9. package/data/rule-drafts/typography/typography-long-content-safety.yaml +60 -0
  10. package/data/rules/authoring/authoring-approval-every-step.yaml +44 -0
  11. package/data/rules/authoring/authoring-fixed-agent-quota.yaml +44 -0
  12. package/data/rules/authoring/authoring-generic-coding-advice.yaml +44 -0
  13. package/data/rules/authoring/authoring-generic-verification.yaml +45 -0
  14. package/data/rules/authoring/authoring-plan-vague-acceptance.yaml +43 -0
  15. package/data/rules/copywriting/copywriting-announced-honesty.yaml +43 -0
  16. package/data/rules/copywriting/copywriting-anthropomorphism.yaml +29 -0
  17. package/data/rules/copywriting/copywriting-artificial-balance.yaml +35 -0
  18. package/data/rules/copywriting/copywriting-bare-confirm-label.yaml +23 -0
  19. package/data/rules/copywriting/copywriting-boilerplate-offer.yaml +38 -0
  20. package/data/rules/copywriting/copywriting-canned-phrasing.yaml +53 -0
  21. package/data/rules/copywriting/copywriting-changed-supplied-facts.yaml +31 -0
  22. package/data/rules/copywriting/copywriting-claim-without-evidence.yaml +59 -0
  23. package/data/rules/copywriting/copywriting-click-here-links.yaml +22 -0
  24. package/data/rules/copywriting/copywriting-conditions-first.yaml +50 -0
  25. package/data/rules/copywriting/copywriting-confirmshaming.yaml +43 -0
  26. package/data/rules/copywriting/copywriting-cta-names-outcome.yaml +54 -0
  27. package/data/rules/copywriting/copywriting-dead-weight-hedges.yaml +58 -0
  28. package/data/rules/copywriting/copywriting-drafting-instruction-echo.yaml +31 -0
  29. package/data/rules/copywriting/copywriting-emoji-in-ui.yaml +20 -0
  30. package/data/rules/copywriting/copywriting-empty-state-no-action.yaml +51 -0
  31. package/data/rules/copywriting/copywriting-error-descriptions.yaml +53 -0
  32. package/data/rules/copywriting/copywriting-excessive-hedging.yaml +48 -0
  33. package/data/rules/copywriting/copywriting-fabricated-specificity.yaml +35 -0
  34. package/data/rules/copywriting/copywriting-filler-words.yaml +55 -0
  35. package/data/rules/copywriting/copywriting-flat-description.yaml +37 -0
  36. package/data/rules/copywriting/copywriting-friction-cta.yaml +53 -0
  37. package/data/rules/copywriting/copywriting-generic-audience.yaml +55 -0
  38. package/data/rules/copywriting/copywriting-generic-framing.yaml +35 -0
  39. package/data/rules/copywriting/copywriting-generic-language.yaml +35 -0
  40. package/data/rules/copywriting/copywriting-generic-validation.yaml +46 -0
  41. package/data/rules/copywriting/copywriting-idea-repetition.yaml +34 -0
  42. package/data/rules/copywriting/copywriting-latinisms.yaml +25 -0
  43. package/data/rules/copywriting/copywriting-leaked-error-message.yaml +23 -0
  44. package/data/rules/copywriting/copywriting-live-region-feedback.yaml +24 -0
  45. package/data/rules/copywriting/copywriting-machine-prose.yaml +73 -0
  46. package/data/rules/copywriting/copywriting-mechanism-over-outcome.yaml +33 -0
  47. package/data/rules/copywriting/copywriting-meta-commentary.yaml +48 -0
  48. package/data/rules/copywriting/copywriting-missing-reasoning-link.yaml +33 -0
  49. package/data/rules/copywriting/copywriting-nominalisations.yaml +45 -0
  50. package/data/rules/copywriting/copywriting-over-explanation.yaml +34 -0
  51. package/data/rules/copywriting/copywriting-passive-voice.yaml +53 -0
  52. package/data/rules/copywriting/copywriting-permission-benefit.yaml +43 -0
  53. package/data/rules/copywriting/copywriting-profile-register-mismatch.yaml +31 -0
  54. package/data/rules/copywriting/copywriting-promotional-docs-voice.yaml +58 -0
  55. package/data/rules/copywriting/copywriting-prompt-restatement.yaml +35 -0
  56. package/data/rules/copywriting/copywriting-punctuation-tics.yaml +35 -0
  57. package/data/rules/copywriting/copywriting-reader-first.yaml +59 -0
  58. package/data/rules/copywriting/copywriting-register-shift.yaml +58 -0
  59. package/data/rules/copywriting/copywriting-repetitive-transitions.yaml +48 -0
  60. package/data/rules/copywriting/copywriting-requirements-language.yaml +50 -0
  61. package/data/rules/copywriting/copywriting-searchable-headings.yaml +52 -0
  62. package/data/rules/copywriting/copywriting-semantic-links.yaml +23 -0
  63. package/data/rules/copywriting/copywriting-structural-symmetry.yaml +35 -0
  64. package/data/rules/copywriting/copywriting-success-state-specific.yaml +43 -0
  65. package/data/rules/copywriting/copywriting-temporal-words.yaml +55 -0
  66. package/data/rules/copywriting/copywriting-toast-successfully.yaml +22 -0
  67. package/data/rules/copywriting/copywriting-unexplained-action.yaml +33 -0
  68. package/data/rules/copywriting/copywriting-unnatural-polish.yaml +48 -0
  69. package/data/rules/copywriting/copywriting-unraised-objection.yaml +43 -0
  70. package/data/rules/copywriting/copywriting-unverifiable-proof.yaml +23 -0
  71. package/data/rules/copywriting/copywriting-vague-error.yaml +58 -0
  72. package/data/rules/copywriting/copywriting-vague-personal-request.yaml +43 -0
  73. package/data/rules/copywriting/copywriting-visual-direction-instruction.yaml +43 -0
  74. package/data/rules/craft/craft-affordance-mismatch.yaml +23 -0
  75. package/data/rules/craft/craft-decoration-no-role.yaml +27 -0
  76. package/data/rules/craft/craft-faux-product-chrome.yaml +22 -0
  77. package/data/rules/craft/craft-hover-only-affordance.yaml +23 -0
  78. package/data/rules/craft/craft-i18n-rtl-untested.yaml +23 -0
  79. package/data/rules/craft/craft-i18n-untested.yaml +28 -0
  80. package/data/rules/craft/craft-image-dimensions-and-priority.yaml +23 -0
  81. package/data/rules/craft/craft-inline-styles.yaml +24 -0
  82. package/data/rules/craft/craft-lazy-load-offscreen.yaml +28 -0
  83. package/data/rules/craft/craft-near-duplicate-scale.yaml +29 -0
  84. package/data/rules/craft/craft-token-drift.yaml +22 -0
  85. package/data/rules/craft/craft-viewport-scaling.yaml +25 -0
  86. package/data/rules/craft/craft-virtualize-large-lists.yaml +24 -0
  87. package/data/rules/interaction/interaction-a11y-color-only-meaning.yaml +23 -0
  88. package/data/rules/interaction/interaction-a11y-data-table-semantics.yaml +24 -0
  89. package/data/rules/interaction/interaction-a11y-disabled-control-tooltip.yaml +23 -0
  90. package/data/rules/interaction/interaction-a11y-document-language.yaml +24 -0
  91. package/data/rules/interaction/interaction-a11y-icon-controls-labeled.yaml +23 -0
  92. package/data/rules/interaction/interaction-a11y-image-alt-text.yaml +24 -0
  93. package/data/rules/interaction/interaction-a11y-media-captions.yaml +24 -0
  94. package/data/rules/interaction/interaction-a11y-semantic-html-first.yaml +23 -0
  95. package/data/rules/interaction/interaction-a11y-skip-link-heading-order.yaml +24 -0
  96. package/data/rules/interaction/interaction-a11y-tooltip-no-interactive.yaml +23 -0
  97. package/data/rules/interaction/interaction-broken-focus-trap.yaml +25 -0
  98. package/data/rules/interaction/interaction-dont-block-paste-ime.yaml +23 -0
  99. package/data/rules/interaction/interaction-error-association.yaml +24 -0
  100. package/data/rules/interaction/interaction-focus-visible.yaml +25 -0
  101. package/data/rules/interaction/interaction-inline-errors-first-focus.yaml +24 -0
  102. package/data/rules/interaction/interaction-input-affix-hit-target.yaml +23 -0
  103. package/data/rules/interaction/interaction-keyboard-operable.yaml +23 -0
  104. package/data/rules/interaction/interaction-labels-and-autocomplete.yaml +23 -0
  105. package/data/rules/interaction/interaction-lost-data-on-error.yaml +23 -0
  106. package/data/rules/interaction/interaction-mobile-input-font-size.yaml +24 -0
  107. package/data/rules/interaction/interaction-no-disable-while-submitting.yaml +23 -0
  108. package/data/rules/interaction/interaction-no-empty-state.yaml +26 -0
  109. package/data/rules/interaction/interaction-no-error-boundary.yaml +27 -0
  110. package/data/rules/interaction/interaction-no-error-state.yaml +26 -0
  111. package/data/rules/interaction/interaction-no-suspense-boundary.yaml +23 -0
  112. package/data/rules/interaction/interaction-not-restored.yaml +25 -0
  113. package/data/rules/interaction/interaction-on-dynamic-content.yaml +25 -0
  114. package/data/rules/interaction/interaction-optimistic-without-rollback.yaml +23 -0
  115. package/data/rules/interaction/interaction-out-of-order-responses.yaml +27 -0
  116. package/data/rules/interaction/interaction-target-size.yaml +26 -0
  117. package/data/rules/interaction/interaction-use-form-status-misuse.yaml +23 -0
  118. package/data/rules/product/product-nested-modals.yaml +26 -0
  119. package/data/rules/product/product-select-for-few-options.yaml +24 -0
  120. package/data/rules/schema/rule.schema.json +262 -0
  121. package/data/rules/tuning.json +1 -0
  122. package/data/rules/typography/typography-dashes.yaml +28 -0
  123. package/data/rules/typography/typography-ellipsis.yaml +27 -0
  124. package/data/rules/typography/typography-hover-weight-shift.yaml +24 -0
  125. package/data/rules/typography/typography-midpoint-separators.yaml +21 -0
  126. package/data/rules/typography/typography-multiplication-sign.yaml +27 -0
  127. package/data/rules/typography/typography-nbsp-value-unit.yaml +27 -0
  128. package/data/rules/typography/typography-readable-scale.yaml +23 -0
  129. package/data/rules/typography/typography-straight-quotes.yaml +37 -0
  130. package/data/rules/typography/typography-title-case-heading.yaml +49 -0
  131. package/dist/cli.d.ts +2 -0
  132. package/dist/cli.js +6317 -0
  133. package/dist/cli.js.map +1 -0
  134. package/dist/index.d.ts +736 -0
  135. package/dist/index.d.ts.map +1 -0
  136. package/dist/index.js +5330 -0
  137. package/dist/index.js.map +1 -0
  138. package/package.json +87 -0
@@ -0,0 +1,23 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/a11y-semantic-html-first.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-a11y-semantic-html-first
4
+ title: Prefer Native Semantics Before ARIA
5
+ categoryId: focus-and-a11y
6
+ source:
7
+ line: 9
8
+ path: skills/ui-design/rules/a11y-semantic-html-first.md
9
+ repo: mblode/agent-skills
10
+ ruleId: a11y-semantic-html-first
11
+ scope:
12
+ include:
13
+ - "**/*.tsx"
14
+ - "**/*.jsx"
15
+ unit:
16
+ - source
17
+ mechanical:
18
+ flags: gus
19
+ regex: <(div|span|li|td)\b[^>]*\bonClick=
20
+ severity: major
21
+ fix:
22
+ hint: Use semantic HTML controls first; only add ARIA when native elements cannot express intent. Rebuilding those semantics in ARIA reimplements keyboard behaviour, role, and state by hand, and any gap leaves assistive tech reporting the wrong thing.
23
+ status: review-only
@@ -0,0 +1,24 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/a11y-skip-link-heading-order.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-a11y-skip-link-heading-order
4
+ title: Provide Skip Link and Logical Heading Order
5
+ categoryId: focus-and-a11y
6
+ source:
7
+ line: 9
8
+ path: skills/ui-design/rules/a11y-skip-link-heading-order.md
9
+ repo: mblode/agent-skills
10
+ ruleId: a11y-skip-link-heading-order
11
+ scope:
12
+ include:
13
+ - "**/*.tsx"
14
+ - "**/*.jsx"
15
+ unit:
16
+ - source
17
+ mechanical:
18
+ absent: href="#
19
+ flags: gu
20
+ regex: <main\b
21
+ severity: minor
22
+ fix:
23
+ hint: Include a skip link and keep heading levels sequential. Without both, keyboard and screen reader users walk the whole header on every page and lose the outline they navigate by.
24
+ status: review-only
@@ -0,0 +1,23 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/a11y-tooltip-no-interactive.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-a11y-tooltip-no-interactive
4
+ title: Hover tooltips must not contain interactive content
5
+ categoryId: focus-and-a11y
6
+ source:
7
+ line: 10
8
+ path: skills/ui-design/rules/a11y-tooltip-no-interactive.md
9
+ repo: mblode/agent-skills
10
+ ruleId: a11y-tooltip-no-interactive
11
+ scope:
12
+ include:
13
+ - "**/*.tsx"
14
+ - "**/*.jsx"
15
+ unit:
16
+ - source
17
+ mechanical:
18
+ flags: gus
19
+ regex: <(Tooltip(Content)?|HoverCard(Content)?)[^>]*>[\s\S]{0,800}<(button|a |input|select|textarea)\b
20
+ severity: minor
21
+ fix:
22
+ hint: A hover tooltip that contains a link, button, or input cannot be used with a keyboard or a touch screen, and it vanishes when the pointer moves to reach the inner control. Users see an action they cannot activate reliably.
23
+ status: review-only
@@ -0,0 +1,25 @@
1
+ # Hand-ported from mblode/agent-skills skills/ui-design/rules/focus-broken-focus-trap.md. The detection there is a
2
+ # shell loop or pipeline; regex plus absent is the same check over one file.
3
+ id: interaction-broken-focus-trap
4
+ title: Modal without working focus trap
5
+ categoryId: focus-and-a11y
6
+ source:
7
+ repo: mblode/agent-skills
8
+ path: skills/ui-design/rules/focus-broken-focus-trap.md
9
+ line: 10
10
+ ruleId: focus-broken-focus-trap
11
+ scope:
12
+ include:
13
+ - "**/*.ts"
14
+ - "**/*.tsx"
15
+ unit: [source]
16
+ mechanical:
17
+ flags: gu
18
+ regex: 'role="(dialog|alertdialog)"'
19
+ absent: "@radix-ui/react-dialog|react-aria|focus-trap-react|@headlessui/react"
20
+ severity: major
21
+ fix:
22
+ hint: >-
23
+ A hand-rolled dialog: Tab and Shift+Tab must cycle inside it and Esc must close it. Use a primitive that traps focus (Radix Dialog, React Aria, Headless UI, focus-trap-react) or wire the trap yourself.
24
+ status: review-only
25
+ handWritten: [mechanical, fix, status]
@@ -0,0 +1,23 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/forms-dont-block-paste-ime.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-dont-block-paste-ime
4
+ title: Do Not Block Paste or IME Input
5
+ categoryId: form-usability
6
+ source:
7
+ line: 9
8
+ path: skills/ui-design/rules/forms-dont-block-paste-ime.md
9
+ repo: mblode/agent-skills
10
+ ruleId: forms-dont-block-paste-ime
11
+ scope:
12
+ include:
13
+ - "**/*.tsx"
14
+ - "**/*.jsx"
15
+ unit:
16
+ - source
17
+ mechanical:
18
+ flags: gus
19
+ regex: onPaste=\{[^}]*preventDefault
20
+ severity: minor
21
+ fix:
22
+ hint: Avoid handlers that prevent paste or aggressively filter keystrokes. Blocking paste breaks password managers and assistive input, and keystroke filters swallow the composition events IME users type with.
23
+ status: review-only
@@ -0,0 +1,24 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/forms-error-association.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-error-association
4
+ title: Associate and Announce Form Errors
5
+ categoryId: form-usability
6
+ source:
7
+ line: 9
8
+ path: skills/ui-design/rules/forms-error-association.md
9
+ repo: mblode/agent-skills
10
+ ruleId: forms-error-association
11
+ scope:
12
+ include:
13
+ - "**/*.tsx"
14
+ - "**/*.jsx"
15
+ unit:
16
+ - source
17
+ mechanical:
18
+ absent: aria-describedby
19
+ flags: gu
20
+ regex: \{\s*[\w.?]*[Ee]rror[\w.?]*\s*&&
21
+ severity: major
22
+ fix:
23
+ hint: Tie each error to its input via `aria-describedby`, mark the field `aria-invalid`, and announce it through a live region (`role="alert"`). A message only visually near the field is invisible to screen readers. Complements `forms-inline-errors-first-focus`, which covers placement and focus.
24
+ status: review-only
@@ -0,0 +1,25 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/interaction-focus-visible.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-focus-visible
4
+ title: Preserve Visible Focus States
5
+ categoryId: focus-and-a11y
6
+ source:
7
+ line: 9
8
+ path: skills/ui-design/rules/interaction-focus-visible.md
9
+ repo: mblode/agent-skills
10
+ ruleId: interaction-focus-visible
11
+ scope:
12
+ include:
13
+ - "**/*.css"
14
+ - "**/*.tsx"
15
+ - "**/*.jsx"
16
+ unit:
17
+ - source
18
+ mechanical:
19
+ absent: focus-visible
20
+ flags: gu
21
+ regex: outline:\s*(none|0)|\boutline-(none|hidden)\b
22
+ severity: major
23
+ fix:
24
+ hint: Never remove outlines without a clear `:focus-visible` replacement. With the outline gone, keyboard users cannot see where they are and the interface stops being navigable.
25
+ status: review-only
@@ -0,0 +1,24 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/forms-inline-errors-first-focus.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-inline-errors-first-focus
4
+ title: Show Inline Errors and Focus the First Invalid Field
5
+ categoryId: form-usability
6
+ source:
7
+ line: 9
8
+ path: skills/ui-design/rules/forms-inline-errors-first-focus.md
9
+ repo: mblode/agent-skills
10
+ ruleId: forms-inline-errors-first-focus
11
+ scope:
12
+ include:
13
+ - "**/*.tsx"
14
+ - "**/*.jsx"
15
+ unit:
16
+ - source
17
+ mechanical:
18
+ absent: \.focus\(\)|autoFocus|shouldFocusError
19
+ flags: gu
20
+ regex: <form\b
21
+ severity: minor
22
+ fix:
23
+ hint: On submit, reveal all relevant errors and move focus to the first failing field. Errors the user has to hunt for are the point where they abandon the form.
24
+ status: review-only
@@ -0,0 +1,23 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/forms-input-affix-hit-target.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-input-affix-hit-target
4
+ title: Overlay input affixes on the field
5
+ categoryId: form-usability
6
+ source:
7
+ line: 10
8
+ path: skills/ui-design/rules/forms-input-affix-hit-target.md
9
+ repo: mblode/agent-skills
10
+ ruleId: forms-input-affix-hit-target
11
+ scope:
12
+ include:
13
+ - "**/*.tsx"
14
+ - "**/*.jsx"
15
+ unit:
16
+ - source
17
+ mechanical:
18
+ flags: gus
19
+ regex: <div[^>]*(flex|grid)[^>]*>\s*<(svg|Icon|Search|span)[^>]*>[\s\S]{0,400}<input\b
20
+ severity: minor
21
+ fix:
22
+ hint: "A search icon, currency mark, or unit sitting beside the field as a sibling splits the hit target: clicks on the decoration do not focus the input, and the control reads as two widgets. Prefix and suffix decorations belong on top of the text input, with padding so the value does not collide, and the"
23
+ status: review-only
@@ -0,0 +1,23 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/interaction-keyboard-operable.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-keyboard-operable
4
+ title: Ensure Full Keyboard Operability
5
+ categoryId: focus-and-a11y
6
+ source:
7
+ line: 9
8
+ path: skills/ui-design/rules/interaction-keyboard-operable.md
9
+ repo: mblode/agent-skills
10
+ ruleId: interaction-keyboard-operable
11
+ scope:
12
+ include:
13
+ - "**/*.tsx"
14
+ - "**/*.jsx"
15
+ unit:
16
+ - source
17
+ mechanical:
18
+ flags: gus
19
+ regex: <[A-Za-z][^>]*?\b(?:role="button"|tabIndex=\{[0-9])(?![^>]*?(?:onKeyDown|onKeyUp|onKeyPress))[^>]*?>
20
+ severity: major
21
+ fix:
22
+ hint: Pointer-only handlers are not acceptable for critical actions. Anything reachable only by pointer cannot be completed by keyboard, switch, or screen reader users at all.
23
+ status: review-only
@@ -0,0 +1,23 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/forms-labels-and-autocomplete.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-labels-and-autocomplete
4
+ title: Label Inputs and Set Autocomplete Metadata
5
+ categoryId: form-usability
6
+ source:
7
+ line: 9
8
+ path: skills/ui-design/rules/forms-labels-and-autocomplete.md
9
+ repo: mblode/agent-skills
10
+ ruleId: forms-labels-and-autocomplete
11
+ scope:
12
+ include:
13
+ - "**/*.tsx"
14
+ - "**/*.jsx"
15
+ unit:
16
+ - source
17
+ mechanical:
18
+ flags: gus
19
+ regex: <input\b(?![^>]*\bid=)(?![^>]*aria-label)[^>]*placeholder=
20
+ severity: major
21
+ fix:
22
+ hint: Inputs require explicit labels and appropriate `type`, `name`, and `autocomplete` values. Without them users retype data the browser already has, and assistive tech has no name to announce.
23
+ status: review-only
@@ -0,0 +1,23 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/forms-lost-data-on-error.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-lost-data-on-error
4
+ title: Form data lost on validation error
5
+ categoryId: form-usability
6
+ source:
7
+ line: 10
8
+ path: skills/ui-design/rules/forms-lost-data-on-error.md
9
+ repo: mblode/agent-skills
10
+ ruleId: forms-lost-data-on-error
11
+ scope:
12
+ include:
13
+ - "**/*.ts"
14
+ - "**/*.tsx"
15
+ unit:
16
+ - source
17
+ mechanical:
18
+ flags: gu
19
+ regex: <form
20
+ severity: minor
21
+ fix:
22
+ hint: "When a form fails server validation, typed values must survive the round-trip. Clearing fields on error is one of the highest-cost UX bugs in production: users abandon checkout, retype passwords wrong, lose multi-paragraph inputs. React 19's `useActionState` makes preservation the default, but only "
23
+ status: review-only
@@ -0,0 +1,24 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/forms-mobile-input-font-size.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-mobile-input-font-size
4
+ title: Keep Mobile Input Text at Readable Size
5
+ categoryId: form-usability
6
+ source:
7
+ line: 9
8
+ path: skills/ui-design/rules/forms-mobile-input-font-size.md
9
+ repo: mblode/agent-skills
10
+ ruleId: forms-mobile-input-font-size
11
+ scope:
12
+ include:
13
+ - "**/*.css"
14
+ - "**/*.tsx"
15
+ - "**/*.jsx"
16
+ unit:
17
+ - source
18
+ mechanical:
19
+ flags: gus
20
+ regex: \b(?:input|textarea|select)\b[^{}/]*\{[^}]*font-size:\s*(?:0?\.\d+rem|(?<![\d.])(?:\d|1[0-5])(?:\.\d+)?px)|<(?:input|textarea|select|Input|Textarea)\b[^>]*className="[^"]*\btext-(?:xs|sm)\b
21
+ severity: minor
22
+ fix:
23
+ hint: "Set input text to at least 16px on mobile and avoid autofocus on touch-first flows. iOS zooms the viewport in on any field below 16px and does not zoom back out, leaving the user panned into a form they now have to scroll sideways. Do not stop that zoom with `maximum-scale=1` or `user-scalable=no`: "
24
+ status: review-only
@@ -0,0 +1,23 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/forms-no-disable-while-submitting.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-no-disable-while-submitting
4
+ title: Submit button not disabled while pending
5
+ categoryId: form-usability
6
+ source:
7
+ line: 10
8
+ path: skills/ui-design/rules/forms-no-disable-while-submitting.md
9
+ repo: mblode/agent-skills
10
+ ruleId: forms-no-disable-while-submitting
11
+ scope:
12
+ include:
13
+ - "**/*.ts"
14
+ - "**/*.tsx"
15
+ unit:
16
+ - source
17
+ mechanical:
18
+ flags: gu
19
+ regex: <form
20
+ severity: major
21
+ fix:
22
+ hint: "A double-clickable submit button creates duplicate accounts, double-charges cards, and posts the same comment twice. React 19's `useFormStatus` makes the fix mechanical: a child component reads `pending` from the surrounding `<form>`, disables itself, and exposes a busy state. Keep the label stable "
23
+ status: review-only
@@ -0,0 +1,26 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/states-no-empty-state.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-no-empty-state
4
+ title: Empty state has no call to action
5
+ categoryId: state-coverage
6
+ source:
7
+ line: 10
8
+ path: skills/ui-design/rules/states-no-empty-state.md
9
+ repo: mblode/agent-skills
10
+ ruleId: states-no-empty-state
11
+ scope:
12
+ include:
13
+ - "**/*.ts"
14
+ - "**/*.tsx"
15
+ unit:
16
+ - source
17
+ # The source's real check is its loop: an empty branch with no action element.
18
+ mechanical:
19
+ flags: gu
20
+ regex: \.length === 0|isEmpty
21
+ absent: Button|Link|<a |onClick
22
+ severity: minor
23
+ fix:
24
+ hint: 'When a list, table, or feed is empty, "No items" alone is a dead end: the user can''t tell whether nothing exists, the filter is too narrow, or they must act to populate it. A good empty state names the situation, explains why, and offers exactly one primary action. Onboarding empty states (first-run'
25
+ status: review-only
26
+ handWritten: [mechanical]
@@ -0,0 +1,27 @@
1
+ # Hand-ported from mblode/agent-skills skills/ui-design/rules/async-no-error-boundary.md. The detection there is a
2
+ # shell loop or pipeline; regex plus absent is the same check over one file.
3
+ # The source also lists every 'use client' file; this port keeps the async part
4
+ # (useQuery, useSWR) so a plain client component is not a finding.
5
+ id: interaction-no-error-boundary
6
+ title: Client async tree without a local error boundary
7
+ categoryId: state-coverage
8
+ source:
9
+ repo: mblode/agent-skills
10
+ path: skills/ui-design/rules/async-no-error-boundary.md
11
+ line: 10
12
+ ruleId: async-no-error-boundary
13
+ scope:
14
+ include:
15
+ - "**/*.ts"
16
+ - "**/*.tsx"
17
+ unit: [source]
18
+ mechanical:
19
+ flags: gu
20
+ regex: "useQuery|useSWR"
21
+ absent: "ErrorBoundary"
22
+ severity: major
23
+ fix:
24
+ hint: >-
25
+ A thrown error in an async tree with no ancestor boundary unmounts the whole route. Wrap the fetching subtree in an ErrorBoundary (or add error.tsx to the route segment) so a failed query degrades in place.
26
+ status: review-only
27
+ handWritten: [mechanical, fix, status]
@@ -0,0 +1,26 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/states-no-error-state.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-no-error-state
4
+ title: Async fetch has no inline error state
5
+ categoryId: state-coverage
6
+ source:
7
+ line: 10
8
+ path: skills/ui-design/rules/states-no-error-state.md
9
+ repo: mblode/agent-skills
10
+ ruleId: states-no-error-state
11
+ scope:
12
+ include:
13
+ - "**/*.ts"
14
+ - "**/*.tsx"
15
+ unit:
16
+ - source
17
+ # The source's real check is its loop: a fetcher with no inline error branch.
18
+ mechanical:
19
+ flags: gu
20
+ regex: useQuery|useSWR
21
+ absent: isError|hasError|onError|catch
22
+ severity: major
23
+ fix:
24
+ hint: "Every async op can fail. A happy-path-only component turns a transient 500 into a blank screen, an infinite spinner, or a silent stale render. This rule owns the component's own error branch: the failure renders a message naming what broke plus a way out (retry, go back, contact support), in place, "
25
+ status: review-only
26
+ handWritten: [mechanical]
@@ -0,0 +1,23 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/async-no-suspense-boundary.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-no-suspense-boundary
4
+ title: Async server component without Suspense boundary
5
+ categoryId: state-coverage
6
+ source:
7
+ line: 10
8
+ path: skills/ui-design/rules/async-no-suspense-boundary.md
9
+ repo: mblode/agent-skills
10
+ ruleId: async-no-suspense-boundary
11
+ scope:
12
+ include:
13
+ - "**/*.ts"
14
+ - "**/*.tsx"
15
+ unit:
16
+ - source
17
+ mechanical:
18
+ flags: gu
19
+ regex: ^export (default )?async function
20
+ severity: minor
21
+ fix:
22
+ hint: "In the App Router and React 19, an async server component with no enclosing `<Suspense>` boundary blocks the whole route from streaming. Users wait for the slowest fetch before seeing anything: no shell, no skeleton, no progressive paint. Wrap the slow async tree in `<Suspense fallback={...}>` to un"
23
+ status: review-only
@@ -0,0 +1,25 @@
1
+ # Hand-ported from mblode/agent-skills skills/ui-design/rules/focus-not-restored.md. The detection there is a
2
+ # shell loop or pipeline; regex plus absent is the same check over one file.
3
+ id: interaction-not-restored
4
+ title: Focus not restored after a modal, sheet or popover closes
5
+ categoryId: focus-and-a11y
6
+ source:
7
+ repo: mblode/agent-skills
8
+ path: skills/ui-design/rules/focus-not-restored.md
9
+ line: 10
10
+ ruleId: focus-not-restored
11
+ scope:
12
+ include:
13
+ - "**/*.ts"
14
+ - "**/*.tsx"
15
+ unit: [source]
16
+ mechanical:
17
+ flags: gu
18
+ regex: "role=\"dialog\"|<Dialog\\b|<Sheet\\b"
19
+ absent: "onCloseAutoFocus|triggerRef|finalFocus|returnFocusOnDeactivate"
20
+ severity: major
21
+ fix:
22
+ hint: >-
23
+ When a dialog closes, focus returns to the element that opened it. Keep the trigger ref and focus it on close (onCloseAutoFocus, finalFocus, returnFocusOnDeactivate) instead of leaving focus on body.
24
+ status: review-only
25
+ handWritten: [mechanical, fix, status]
@@ -0,0 +1,25 @@
1
+ # Hand-ported from mblode/agent-skills skills/ui-design/rules/focus-on-dynamic-content.md. The detection there is a
2
+ # shell loop or pipeline; regex plus absent is the same check over one file.
3
+ id: interaction-on-dynamic-content
4
+ title: Route change without focus management or a live region
5
+ categoryId: focus-and-a11y
6
+ source:
7
+ repo: mblode/agent-skills
8
+ path: skills/ui-design/rules/focus-on-dynamic-content.md
9
+ line: 10
10
+ ruleId: focus-on-dynamic-content
11
+ scope:
12
+ include:
13
+ - "**/*.ts"
14
+ - "**/*.tsx"
15
+ unit: [source]
16
+ mechanical:
17
+ flags: gu
18
+ regex: "router\\.(push|replace)"
19
+ absent: "focus\\(\\)|aria-live"
20
+ severity: minor
21
+ fix:
22
+ hint: >-
23
+ A client-side navigation never moves focus or announces the new content. After router.push, focus the new page heading or main region, or announce the change in an aria-live region.
24
+ status: review-only
25
+ handWritten: [mechanical, fix, status]
@@ -0,0 +1,23 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/async-optimistic-without-rollback.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-optimistic-without-rollback
4
+ title: Optimistic update without rollback on server reject
5
+ categoryId: state-coverage
6
+ source:
7
+ line: 10
8
+ path: skills/ui-design/rules/async-optimistic-without-rollback.md
9
+ repo: mblode/agent-skills
10
+ ruleId: async-optimistic-without-rollback
11
+ scope:
12
+ include:
13
+ - "**/*.ts"
14
+ - "**/*.tsx"
15
+ unit:
16
+ - source
17
+ mechanical:
18
+ flags: gu
19
+ regex: useOptimistic\b
20
+ severity: major
21
+ fix:
22
+ hint: "`useOptimistic` shows the post-action state immediately (liked post, cart item added, renamed file). Contract: on server reject (422, 500, network error) the UI must roll back to real server state, or it lies and a reload reveals the truth, costing trust. Second half of the contract: optimistic upda"
23
+ status: review-only
@@ -0,0 +1,27 @@
1
+ # Hand-ported from mblode/agent-skills skills/ui-design/rules/async-out-of-order-responses.md. The detection there is a
2
+ # shell loop or pipeline; regex plus absent is the same check over one file.
3
+ # The first `-A 8` window in the source becomes a 400-character span here; a handler
4
+ # that fetches further down still counts, a fetch elsewhere in the file does not.
5
+ id: interaction-out-of-order-responses
6
+ title: Input handler fetches with no cancellation
7
+ categoryId: state-coverage
8
+ source:
9
+ repo: mblode/agent-skills
10
+ path: skills/ui-design/rules/async-out-of-order-responses.md
11
+ line: 10
12
+ ruleId: async-out-of-order-responses
13
+ scope:
14
+ include:
15
+ - "**/*.ts"
16
+ - "**/*.tsx"
17
+ unit: [source]
18
+ mechanical:
19
+ flags: gu
20
+ regex: "on(?:Change|Input)=[\\s\\S]{0,400}?(?:fetch\\(|useQuery\\(|axios\\.)"
21
+ absent: "AbortController|useDeferredValue|signal:"
22
+ severity: major
23
+ fix:
24
+ hint: >-
25
+ Two keystrokes fire two requests and the slow one can land last, showing stale results under fresh input. Cancel the in-flight request with an AbortController (or pass signal), or debounce through useDeferredValue.
26
+ status: review-only
27
+ handWritten: [mechanical, fix, status]
@@ -0,0 +1,26 @@
1
+ # Hand-ported from mblode/agent-skills skills/ui-design/rules/interaction-target-size.md. The detection there is a
2
+ # shell loop or pipeline; regex plus absent is the same check over one file.
3
+ # Both halves of the source pipeline apply to one line: a small h-, w- or size- step
4
+ # and a button on the same line.
5
+ id: interaction-target-size
6
+ title: Button sized below the touch target floor
7
+ categoryId: focus-and-a11y
8
+ source:
9
+ repo: mblode/agent-skills
10
+ path: skills/ui-design/rules/interaction-target-size.md
11
+ line: 9
12
+ ruleId: interaction-target-size
13
+ scope:
14
+ include:
15
+ - "**/*.tsx"
16
+ - "**/*.jsx"
17
+ unit: [source]
18
+ mechanical:
19
+ flags: gum
20
+ regex: "^(?=.*\\b(?:h|w|size)-(?:[1-9]|10)\\b).*(?:button|role=\"button\").*$"
21
+ severity: minor
22
+ fix:
23
+ hint: >-
24
+ Touch targets need 44 by 44px; 24px is the floor only for dense desktop UI under pointer: fine. Give the button at least size-11, or pad the hit area while keeping the icon small.
25
+ status: review-only
26
+ handWritten: [mechanical, fix, status]
@@ -0,0 +1,23 @@
1
+ # Ported from mblode/agent-skills skills/ui-design/rules/forms-use-form-status-misuse.md by scripts/port-rules.ts.
2
+ # Keys listed in handWritten are preserved on re-run; everything else is regenerated.
3
+ id: interaction-use-form-status-misuse
4
+ title: useFormStatus called in same component as form (silent bug)
5
+ categoryId: form-usability
6
+ source:
7
+ line: 10
8
+ path: skills/ui-design/rules/forms-use-form-status-misuse.md
9
+ repo: mblode/agent-skills
10
+ ruleId: forms-use-form-status-misuse
11
+ scope:
12
+ include:
13
+ - "**/*.ts"
14
+ - "**/*.tsx"
15
+ unit:
16
+ - source
17
+ mechanical:
18
+ flags: gu
19
+ regex: useFormStatus
20
+ severity: major
21
+ fix:
22
+ hint: "`useFormStatus` returns the **parent** `<form>`'s status. Called in the same component that renders the `<form>`, it returns `{ pending: false }` forever (no parent form to inspect). It compiles, runs, never warns, and silently breaks everything depending on it (disabled submit, pending label, optim"
23
+ status: review-only
@@ -0,0 +1,26 @@
1
+ # Hand-authored from mblode/agent-skills skills/product-design/references/rules.md (rule/no-nested-modals), a deterministic
2
+ # slice named in skills/product-design/references/lint-patterns.md.
3
+ id: product-nested-modals
4
+ title: A modal that opens inside another modal
5
+ categoryId: control-choice
6
+ source:
7
+ repo: mblode/agent-skills
8
+ path: skills/product-design/references/rules.md
9
+ line: 86
10
+ ruleId: rule/no-nested-modals
11
+ scope:
12
+ include: ["**/*.tsx", "**/*.jsx"]
13
+ unit: [source]
14
+ mechanical:
15
+ flags: gu
16
+ regex:
17
+ "<(Dialog|AlertDialog|Modal|Sheet|Drawer)\\b[^>]*>(?:(?!</\\1>)[\\s\\S])*?<(?:Dialog|AlertDi\
18
+ alog|Modal|Sheet|Drawer)\\b[^>]*>"
19
+ severity: minor
20
+ fix:
21
+ hint: >-
22
+ Resolve the first modal, use one multi-step surface, or move the second step inline. Stacked modals break focus trapping, escape order and layering.
23
+ # review-only: a regex over markup cannot see dynamically rendered children or
24
+ # a modal opened from a handler; the finding is a candidate for a person.
25
+ status: review-only
26
+ handWritten: [mechanical, fix, status]