`/`` markup. Suppress the report
whenever a bespoke static table genuinely reads better as markup.
## Incorrect
@@ -26,4 +26,12 @@ whenever a bespoke static table genuinely reads better as markup.
```
-Use `data-raw` as a local escape hatch when a raw native table is intentional.
+Suppress an intentional raw native table with
+``.
+
+Only genuine native elements are reported: a component whose name merely lowercases to the
+tag (``) is left alone.
+
+## Further reading
+
+- [Nuxt UI Table](https://ui.nuxt.com/docs/components/table)
diff --git a/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-table/index.test.ts b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-table/index.test.ts
index 6f38dff..8843610 100644
--- a/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-table/index.test.ts
+++ b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-table/index.test.ts
@@ -1,21 +1,17 @@
-import { RuleTester } from 'eslint'
import { describe, it } from 'vitest'
-import vueParser from 'vue-eslint-parser'
-import plugin from '../../../index.js'
+import { ruleTester as tester } from '../../../../tests/rule-tester.js'
+import { nuxtUiPlugin } from '../../../index.js'
-const rule = plugin.rules?.['prefer-u-table']
+const rule = nuxtUiPlugin.rules['prefer-u-table']
describe('prefer-u-table', () => {
it('reports raw tables in Vue templates', () => {
- const tester = new RuleTester({ languageOptions: { ecmaVersion: 'latest', sourceType: 'module', parser: vueParser } })
-
- tester.run('prefer-u-table', rule as never, {
+ tester.run('prefer-u-table', rule, {
valid: [
- { filename: 'component.vue', code: '' },
- { filename: 'component.vue', code: '' },
+ { code: '' },
],
invalid: [
- { filename: 'component.vue', code: '', errors: [{ messageId: 'preferUTable' }] },
+ { code: '', errors: [{ messageId: 'preferUTable' }] },
],
})
})
diff --git a/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-table/index.ts b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-table/index.ts
index b58bb9f..0bfb5fe 100644
--- a/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-table/index.ts
+++ b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-table/index.ts
@@ -2,7 +2,7 @@ import { preferUComponent } from '../prefer-u-component.js'
export const preferUTable = preferUComponent({
nativeTag: 'table',
- component: 'UTable',
+ component: 'Table',
ruleName: 'prefer-u-table',
messageId: 'preferUTable',
})
diff --git a/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-tree/index.md b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-tree/index.md
new file mode 100644
index 0000000..319ccbe
--- /dev/null
+++ b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-tree/index.md
@@ -0,0 +1,30 @@
+# `@nustack/nuxt-ui/prefer-u-tree`
+
+Prefer Nuxt UI's `` over a hand-written tree root.
+
+This first implementation reports only an explicit static `role="tree"` on components
+other than `UTree`. It does not infer trees from recursion, list structure, dynamic role
+bindings, drag and drop, or expansion controls.
+
+## Incorrect
+
+```vue
+
+
+
+```
+
+## Correct
+
+```vue
+
+
+
+```
+
+Suppress the report with ``
+when a custom implementation is required. A custom tree
+remains responsible for arrow-key navigation, Home/End behavior, expansion state, focus
+movement, and keyboard-equivalent reordering.
diff --git a/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-tree/index.test.ts b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-tree/index.test.ts
new file mode 100644
index 0000000..8616112
--- /dev/null
+++ b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-tree/index.test.ts
@@ -0,0 +1,39 @@
+import { describe, it } from 'vitest'
+import { ruleTester as tester } from '../../../../tests/rule-tester.js'
+import { preferUTree } from './index.js'
+
+describe('prefer-u-tree', () => {
+ it('reports only explicit static tree roots', () => {
+ tester.run('prefer-u-tree', preferUTree, {
+ valid: [
+ { code: '' },
+ {
+ code: '',
+ },
+ { code: '' },
+ {
+ code: '',
+ },
+ { code: '' },
+ {
+ code: '',
+ },
+ ],
+ invalid: [
+ {
+ code: '',
+ errors: [{ messageId: 'preferUTree' }],
+ },
+ {
+ code: '',
+ errors: [{ messageId: 'preferUTree', data: { component: 'UTree' } }],
+ },
+ {
+ code: '',
+ settings: { '@nustack/nuxt-ui': { enabled: true, prefix: 'Nu' } },
+ errors: [{ messageId: 'preferUTree', data: { component: 'NuTree' } }],
+ },
+ ],
+ })
+ })
+})
diff --git a/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-tree/index.ts b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-tree/index.ts
new file mode 100644
index 0000000..8a32701
--- /dev/null
+++ b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/prefer-u-tree/index.ts
@@ -0,0 +1,42 @@
+import type { Context, Rule, Visitor } from '@oxlint/plugins'
+import type { AST as VueAST } from 'vue-eslint-parser'
+import { docsUrl } from '../../../utils/docs-url.js'
+import { defineTemplateVisitor } from '../../../utils/template.js'
+import { createNuxtUiMatcher } from '../components.js'
+
+function hasStaticTreeRole(node: any): boolean {
+ return node.startTag.attributes.some(
+ (attribute: any) =>
+ !attribute.directive && attribute.key.name === 'role' && attribute.value?.value === 'tree',
+ )
+}
+
+export const preferUTree: Rule = {
+ meta: {
+ type: 'suggestion',
+ docs: {
+ description: 'Prefer UTree over hand-written elements with an explicit tree role.',
+ url: docsUrl('nuxt-ui/prefer-u-tree'),
+ },
+ schema: [],
+ messages: {
+ preferUTree:
+ 'Use `` instead of a custom `role="tree"`; custom trees must implement the complete tree keyboard and focus model.',
+ },
+ },
+ create(context: Context): Visitor {
+ const matcher = createNuxtUiMatcher(context)
+
+ return defineTemplateVisitor(context, {
+ VElement(node: VueAST.VElement) {
+ if (matcher.is(node, 'Tree') || !hasStaticTreeRole(node))
+ return
+
+ context.report({
+ loc: node.startTag.loc,
+ messageId: 'preferUTree',
+ })
+ },
+ })
+ },
+}
diff --git a/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-avatar-alt/index.md b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-avatar-alt/index.md
new file mode 100644
index 0000000..84836f1
--- /dev/null
+++ b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-avatar-alt/index.md
@@ -0,0 +1,29 @@
+# `@nustack/nuxt-ui/require-avatar-alt`
+
+Require `alt` on image-backed `UAvatar` components. Use `aria-hidden` for an explicitly
+decorative avatar. An empty `alt=""` counts as a deliberate decorative marker, but a binding
+known to be nullish (`:alt="undefined"`) renders no alternative text and is reported.
+
+## Incorrect
+
+```vue
+
+
+
+```
+
+## Correct
+
+```vue
+
+
+
+
+
+
+
+```
+
+## Further reading
+
+- [Nuxt UI Avatar](https://ui.nuxt.com/docs/components/avatar)
diff --git a/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-avatar-alt/index.test.ts b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-avatar-alt/index.test.ts
new file mode 100644
index 0000000..321fa04
--- /dev/null
+++ b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-avatar-alt/index.test.ts
@@ -0,0 +1,24 @@
+import { describe, it } from 'vitest'
+import { ruleTester as tester } from '../../../../tests/rule-tester.js'
+import { nuxtUiPlugin } from '../../../index.js'
+
+describe('require-avatar-alt', () => {
+ it('requires alt for image avatars', () => {
+ tester.run('require-avatar-alt', nuxtUiPlugin.rules['require-avatar-alt'], {
+ valid: [
+ { code: '' },
+ { code: '' },
+ { code: '' },
+ { code: '' },
+ { code: '' },
+ ],
+ invalid: [
+ { code: '', errors: [{ messageId: 'missingAlt' }] },
+ { code: '', errors: [{ messageId: 'missingAlt' }] },
+ { code: '', errors: [{ messageId: 'missingAlt' }] },
+ { code: '', errors: [{ messageId: 'missingAlt' }] },
+ { code: '', errors: [{ messageId: 'missingAlt' }] },
+ ],
+ })
+ })
+})
diff --git a/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-avatar-alt/index.ts b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-avatar-alt/index.ts
new file mode 100644
index 0000000..6c34471
--- /dev/null
+++ b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-avatar-alt/index.ts
@@ -0,0 +1,34 @@
+import type { Context, Rule, Visitor } from '@oxlint/plugins'
+import type { AST as VueAST } from 'vue-eslint-parser'
+import { docsUrl } from '../../../utils/docs-url.js'
+import { defineTemplateVisitor, getStaticBoolean, hasDefiniteAttribute } from '../../../utils/template.js'
+import { createNuxtUiMatcher } from '../components.js'
+
+export const requireAvatarAlt: Rule = {
+ meta: {
+ type: 'problem',
+ docs: {
+ description: 'Require alternative text for Nuxt UI avatars that render an image.',
+ url: docsUrl('nuxt-ui/require-avatar-alt'),
+ },
+ schema: [],
+ messages: {
+ missingAlt: 'Add `alt` to `` (or explicitly mark a decorative avatar with `aria-hidden`).',
+ },
+ },
+ create(context: Context): Visitor {
+ const matcher = createNuxtUiMatcher(context)
+
+ return defineTemplateVisitor(context, {
+ VElement(node: VueAST.VElement) {
+ if (!matcher.is(node, 'Avatar') || !hasDefiniteAttribute(node, 'src'))
+ return
+ if (hasDefiniteAttribute(node, 'alt'))
+ return
+ if (getStaticBoolean(node, 'aria-hidden') === true)
+ return
+ context.report({ loc: node.startTag.loc, messageId: 'missingAlt' })
+ },
+ })
+ },
+}
diff --git a/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-form-control-label/index.md b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-form-control-label/index.md
new file mode 100644
index 0000000..d74e959
--- /dev/null
+++ b/packages/lint-plugin-nuxt-ecosystem/src/rules/nuxt-ui/require-form-control-label/index.md
@@ -0,0 +1,56 @@
+# `@nustack/nuxt-ui/require-form-control-label`
+
+Require an accessible name for Nuxt UI form controls. The rule understands each component's
+supported `label` or `legend` API, ARIA naming, native ` |