@nadicodeai/ui 12.1.165 → 12.1.166
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/AGENTS.md +8 -9
- package/README.md +2 -17
- package/dist/components/accordion.d.ts.map +1 -1
- package/dist/components/accordion.js +2 -1
- package/dist/components/account-menu.d.ts.map +1 -1
- package/dist/components/account-menu.js +5 -3
- package/dist/components/activity-calendar.d.ts.map +1 -1
- package/dist/components/activity-calendar.js +3 -2
- package/dist/components/agent-card.js +1 -1
- package/dist/components/agent-orb.d.ts.map +1 -1
- package/dist/components/agent-orb.js +1 -0
- package/dist/components/agent-profile.d.ts.map +1 -1
- package/dist/components/agent-profile.js +7 -4
- package/dist/components/alert-dialog.d.ts +1 -1
- package/dist/components/alert-dialog.d.ts.map +1 -1
- package/dist/components/alert-dialog.js +3 -3
- package/dist/components/app-mark.js +1 -1
- package/dist/components/avatar.js +8 -8
- package/dist/components/badge.d.ts +11 -16
- package/dist/components/badge.d.ts.map +1 -1
- package/dist/components/badge.js +7 -43
- package/dist/components/breadcrumb.d.ts.map +1 -1
- package/dist/components/breadcrumb.js +3 -2
- package/dist/components/button.d.ts +5 -21
- package/dist/components/button.d.ts.map +1 -1
- package/dist/components/button.js +6 -76
- package/dist/components/calendar.d.ts +1 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +3 -3
- package/dist/components/card-row.js +1 -1
- package/dist/components/card.d.ts +4 -1
- package/dist/components/card.d.ts.map +1 -1
- package/dist/components/card.js +3 -2
- package/dist/components/catalog-drawer.d.ts.map +1 -1
- package/dist/components/catalog-drawer.js +3 -2
- package/dist/components/chart-axis.d.ts.map +1 -1
- package/dist/components/chart-axis.js +0 -2
- package/dist/components/chart-bar-horizontal.d.ts.map +1 -1
- package/dist/components/chart-bar-horizontal.js +16 -14
- package/dist/components/chart-card.d.ts.map +1 -1
- package/dist/components/chart-card.js +0 -1
- package/dist/components/chart.d.ts.map +1 -1
- package/dist/components/chart.js +18 -32
- package/dist/components/checkbox.d.ts +3 -2
- package/dist/components/checkbox.d.ts.map +1 -1
- package/dist/components/checkbox.js +4 -3
- package/dist/components/choicebox.d.ts +5 -5
- package/dist/components/choicebox.d.ts.map +1 -1
- package/dist/components/choicebox.js +6 -5
- package/dist/components/clearable-input.d.ts.map +1 -1
- package/dist/components/clearable-input.js +2 -2
- package/dist/components/code-block.d.ts.map +1 -1
- package/dist/components/code-block.js +4 -2
- package/dist/components/collapsible.js +1 -1
- package/dist/components/collection-filters.d.ts.map +1 -1
- package/dist/components/collection-filters.js +1 -0
- package/dist/components/collection-page.js +1 -1
- package/dist/components/collection-sort.d.ts.map +1 -1
- package/dist/components/collection-sort.js +1 -4
- package/dist/components/collection-toolbar.js +1 -1
- package/dist/components/combobox.js +4 -4
- package/dist/components/command.d.ts.map +1 -1
- package/dist/components/command.js +1 -1
- package/dist/components/confirm-dialog.js +2 -2
- package/dist/components/context-menu.d.ts +1 -1
- package/dist/components/context-menu.d.ts.map +1 -1
- package/dist/components/copy-button.js +1 -1
- package/dist/components/data-table.d.ts +11 -3
- package/dist/components/data-table.d.ts.map +1 -1
- package/dist/components/data-table.js +12 -4
- package/dist/components/dialog.d.ts +1 -1
- package/dist/components/dialog.d.ts.map +1 -1
- package/dist/components/dialog.js +2 -2
- package/dist/components/drawer.d.ts +4 -4
- package/dist/components/drawer.d.ts.map +1 -1
- package/dist/components/drawer.js +14 -15
- package/dist/components/dropdown-menu.d.ts +1 -1
- package/dist/components/dropdown-menu.d.ts.map +1 -1
- package/dist/components/dropdown-menu.js +9 -5
- package/dist/components/empty-art.js +1 -1
- package/dist/components/empty-state.js +2 -2
- package/dist/components/entry-scene.d.ts.map +1 -1
- package/dist/components/entry-scene.js +2 -3
- package/dist/components/field.js +3 -3
- package/dist/components/file-diff.d.ts.map +1 -1
- package/dist/components/file-diff.js +5 -2
- package/dist/components/footer.d.ts +1 -1
- package/dist/components/footer.d.ts.map +1 -1
- package/dist/components/footer.js +3 -3
- package/dist/components/form-dialog.d.ts.map +1 -1
- package/dist/components/form-dialog.js +2 -1
- package/dist/components/form-drawer.d.ts +7 -9
- package/dist/components/form-drawer.d.ts.map +1 -1
- package/dist/components/form-drawer.js +5 -5
- package/dist/components/form.d.ts +26 -0
- package/dist/components/form.d.ts.map +1 -0
- package/dist/components/form.js +29 -0
- package/dist/components/gauge.js +2 -2
- package/dist/components/heatmap/calendar-heatmap.d.ts.map +1 -1
- package/dist/components/heatmap/calendar-heatmap.js +15 -13
- package/dist/components/hive-scene.d.ts.map +1 -1
- package/dist/components/hive-scene.js +3 -1
- package/dist/components/hover-card.d.ts +1 -1
- package/dist/components/hover-card.d.ts.map +1 -1
- package/dist/components/hover-card.js +1 -1
- package/dist/components/input-group.d.ts +5 -6
- package/dist/components/input-group.d.ts.map +1 -1
- package/dist/components/input-group.js +8 -11
- package/dist/components/input-otp.d.ts.map +1 -1
- package/dist/components/input-otp.js +2 -2
- package/dist/components/item.d.ts +1 -1
- package/dist/components/item.js +2 -2
- package/dist/components/kbd.d.ts +6 -9
- package/dist/components/kbd.d.ts.map +1 -1
- package/dist/components/kbd.js +7 -3
- package/dist/components/message-notice.js +1 -1
- package/dist/components/middle-truncate.d.ts.map +1 -1
- package/dist/components/middle-truncate.js +1 -0
- package/dist/components/model-identity.d.ts +4 -4
- package/dist/components/model-identity.d.ts.map +1 -1
- package/dist/components/model-identity.js +2 -2
- package/dist/components/model-mark.d.ts.map +1 -1
- package/dist/components/model-mark.js +4 -2
- package/dist/components/multi-select.js +2 -2
- package/dist/components/native-select.d.ts.map +1 -1
- package/dist/components/native-select.js +2 -6
- package/dist/components/notice-banner.js +1 -1
- package/dist/components/object-page.js +2 -2
- package/dist/components/object-workspace.d.ts.map +1 -1
- package/dist/components/object-workspace.js +1 -3
- package/dist/components/overview-page.d.ts +1 -1
- package/dist/components/overview-page.d.ts.map +1 -1
- package/dist/components/overview-page.js +2 -1
- package/dist/components/page-actions.d.ts.map +1 -1
- package/dist/components/page-actions.js +9 -4
- package/dist/components/pairing-code.js +1 -1
- package/dist/components/pending-label.d.ts.map +1 -1
- package/dist/components/pending-label.js +0 -1
- package/dist/components/popover.d.ts +1 -1
- package/dist/components/popover.d.ts.map +1 -1
- package/dist/components/portal-sidebar-frame.d.ts.map +1 -1
- package/dist/components/portal-sidebar-frame.js +1 -1
- package/dist/components/product-message.d.ts.map +1 -1
- package/dist/components/product-message.js +1 -0
- package/dist/components/progress-checklist.d.ts.map +1 -1
- package/dist/components/progress-checklist.js +2 -2
- package/dist/components/progress.d.ts.map +1 -1
- package/dist/components/progress.js +5 -4
- package/dist/components/radio-group.js +2 -2
- package/dist/components/reading-column.js +1 -1
- package/dist/components/record-identity.d.ts.map +1 -1
- package/dist/components/record-identity.js +2 -5
- package/dist/components/record-list.d.ts.map +1 -1
- package/dist/components/record-list.js +4 -3
- package/dist/components/record-sheet.js +1 -1
- package/dist/components/record-time.d.ts.map +1 -1
- package/dist/components/record-time.js +2 -1
- package/dist/components/resizable.d.ts.map +1 -1
- package/dist/components/resizable.js +1 -1
- package/dist/components/row-edit-form.d.ts +2 -1
- package/dist/components/row-edit-form.d.ts.map +1 -1
- package/dist/components/row-edit-form.js +2 -1
- package/dist/components/run-timeline.js +1 -1
- package/dist/components/scope-switcher.js +1 -1
- package/dist/components/segmented-control.js +1 -1
- package/dist/components/select.js +1 -1
- package/dist/components/settings-facts.js +1 -1
- package/dist/components/settings-section.js +1 -1
- package/dist/components/sheet.d.ts +1 -1
- package/dist/components/sheet.d.ts.map +1 -1
- package/dist/components/sheet.js +1 -1
- package/dist/components/sidebar.d.ts +2 -2
- package/dist/components/sidebar.d.ts.map +1 -1
- package/dist/components/sidebar.js +16 -18
- package/dist/components/site-menu.d.ts +28 -0
- package/dist/components/site-menu.d.ts.map +1 -0
- package/dist/components/site-menu.js +23 -0
- package/dist/components/slider.d.ts.map +1 -1
- package/dist/components/slider.js +2 -1
- package/dist/components/snippet.d.ts +2 -0
- package/dist/components/snippet.d.ts.map +1 -1
- package/dist/components/snippet.js +13 -6
- package/dist/components/sonner.d.ts.map +1 -1
- package/dist/components/sonner.js +3 -4
- package/dist/components/status-badge.d.ts +1 -1
- package/dist/components/status-badge.d.ts.map +1 -1
- package/dist/components/switch.js +1 -1
- package/dist/components/table-card.js +1 -1
- package/dist/components/table.js +4 -4
- package/dist/components/tabs-nav.d.ts +52 -0
- package/dist/components/tabs-nav.d.ts.map +1 -0
- package/dist/components/tabs-nav.js +95 -0
- package/dist/components/tabs.d.ts +2 -56
- package/dist/components/tabs.d.ts.map +1 -1
- package/dist/components/tabs.js +2 -91
- package/dist/components/text-link.d.ts +1 -2
- package/dist/components/text-link.d.ts.map +1 -1
- package/dist/components/text-link.js +1 -2
- package/dist/components/textarea.d.ts +5 -2
- package/dist/components/textarea.d.ts.map +1 -1
- package/dist/components/textarea.js +12 -4
- package/dist/components/toast.d.ts +2 -0
- package/dist/components/toast.d.ts.map +1 -1
- package/dist/components/toast.js +4 -0
- package/dist/components/toggle-group.d.ts +3 -1
- package/dist/components/toggle-group.d.ts.map +1 -1
- package/dist/components/toggle-group.js +3 -6
- package/dist/components/toggle.js +1 -1
- package/dist/components/tooltip.d.ts +12 -12
- package/dist/components/tooltip.d.ts.map +1 -1
- package/dist/components/tooltip.js +9 -9
- package/dist/components/trend-badge.js +2 -2
- package/dist/components/update-card.js +1 -1
- package/dist/components/usage-meter.d.ts +1 -1
- package/dist/components/usage-meter.d.ts.map +1 -1
- package/dist/components/usage-meter.js +9 -7
- package/dist/components/waiting-line.js +1 -1
- package/dist/index.d.ts +3 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -7
- package/dist/internal/agent-orb-motion.d.ts +4 -1
- package/dist/internal/agent-orb-motion.d.ts.map +1 -1
- package/dist/internal/agent-orb-motion.js +9 -7
- package/dist/internal/agent-orb.d.ts +4 -1
- package/dist/internal/agent-orb.d.ts.map +1 -1
- package/dist/internal/agent-orb.js +7 -1
- package/dist/internal/assistant-ui/attachment.d.ts.map +1 -1
- package/dist/internal/assistant-ui/attachment.js +15 -15
- package/dist/internal/assistant-ui/follow-up-suggestions.js +1 -1
- package/dist/internal/assistant-ui/markdown-text.js +26 -25
- package/dist/internal/assistant-ui/reasoning.d.ts.map +1 -1
- package/dist/internal/assistant-ui/reasoning.js +13 -16
- package/dist/internal/assistant-ui/thread.d.ts.map +1 -1
- package/dist/internal/assistant-ui/thread.js +22 -16
- package/dist/internal/assistant-ui/tool-fallback.d.ts.map +1 -1
- package/dist/internal/assistant-ui/tool-fallback.js +29 -21
- package/dist/internal/assistant-ui/tool-group.d.ts.map +1 -1
- package/dist/internal/assistant-ui/tool-group.js +6 -7
- package/dist/internal/assistant-ui/tooltip-icon-button.js +1 -1
- package/dist/internal/badge-frame.d.ts +15 -0
- package/dist/internal/badge-frame.d.ts.map +1 -0
- package/dist/internal/badge-frame.js +44 -0
- package/dist/internal/button-classes.d.ts +20 -0
- package/dist/internal/button-classes.d.ts.map +1 -0
- package/dist/internal/button-classes.js +74 -0
- package/dist/internal/catalog-list.d.ts.map +1 -1
- package/dist/internal/catalog-list.js +8 -4
- package/dist/internal/collection-rail.js +3 -3
- package/dist/internal/form-drawer-frame.d.ts +10 -0
- package/dist/internal/form-drawer-frame.d.ts.map +1 -0
- package/dist/internal/form-drawer-frame.js +9 -0
- package/dist/internal/form-submit.d.ts +2 -1
- package/dist/internal/form-submit.d.ts.map +1 -1
- package/dist/internal/settings-save.d.ts.map +1 -1
- package/dist/internal/settings-save.js +4 -2
- package/dist/internal/status-mark.d.ts.map +1 -1
- package/dist/internal/status-mark.js +4 -1
- package/dist/internal/tab-row.d.ts +10 -0
- package/dist/internal/tab-row.d.ts.map +1 -0
- package/dist/internal/tab-row.js +2 -0
- package/dist/internal/table-compact-line.d.ts +1 -1
- package/dist/internal/table-compact-line.d.ts.map +1 -1
- package/dist/internal/table-compact-line.js +1 -1
- package/dist/internal/tooltip-content-frame.d.ts +14 -0
- package/dist/internal/tooltip-content-frame.d.ts.map +1 -0
- package/dist/internal/tooltip-content-frame.js +11 -0
- package/dist/lib/css-variables.d.ts +8 -0
- package/dist/lib/css-variables.d.ts.map +1 -0
- package/dist/lib/css-variables.js +4 -0
- package/dist/lib/merge-tokens.generated.js +1 -1
- package/dist/lib/use-copy-to-clipboard.d.ts.map +1 -1
- package/dist/lib/use-copy-to-clipboard.js +1 -2
- package/dist/showcase/samples.js +1 -1
- package/dist/showcase/specimens/choices.d.ts.map +1 -1
- package/dist/showcase/specimens/choices.js +3 -2
- package/dist/showcase/specimens/compositions.d.ts.map +1 -1
- package/dist/showcase/specimens/compositions.js +7 -5
- package/dist/showcase/specimens/data.d.ts.map +1 -1
- package/dist/showcase/specimens/data.js +1 -1
- package/dist/showcase/specimens/feedback.d.ts.map +1 -1
- package/dist/showcase/specimens/feedback.js +1 -2
- package/dist/showcase/specimens/index.d.ts +2 -0
- package/dist/showcase/specimens/index.d.ts.map +1 -1
- package/dist/showcase/specimens/inputs.d.ts +1 -0
- package/dist/showcase/specimens/inputs.d.ts.map +1 -1
- package/dist/showcase/specimens/inputs.js +5 -2
- package/dist/showcase/specimens/menus.d.ts +1 -0
- package/dist/showcase/specimens/menus.d.ts.map +1 -1
- package/dist/showcase/specimens/menus.js +7 -2
- package/dist/showcase/specimens/navigation.d.ts.map +1 -1
- package/dist/showcase/specimens/navigation.js +6 -5
- package/dist/showcase/specimens/status.d.ts.map +1 -1
- package/dist/showcase/specimens/status.js +4 -4
- package/dist/showcase/usage.generated.d.ts +1 -1
- package/dist/showcase/usage.generated.d.ts.map +1 -1
- package/dist/showcase/usage.generated.js +38 -16
- package/dist/standalone/index.js +1 -1
- package/dist/standalone/orb-page.js +10 -8
- package/dist/styles/components.css +1 -1
- package/package.json +32 -33
- package/dist/eslint/index.js +0 -224
- package/dist/eslint/rules/badge-is-static-label.js +0 -93
- package/dist/eslint/rules/button-icon-slot.js +0 -102
- package/dist/eslint/rules/button-link-for-navigation.js +0 -146
- package/dist/eslint/rules/button-no-aria-label-with-text.js +0 -75
- package/dist/eslint/rules/button-type-roles-only-in-buttons.js +0 -83
- package/dist/eslint/rules/choice-control-has-a-name.js +0 -98
- package/dist/eslint/rules/choice-group-in-fieldset.js +0 -86
- package/dist/eslint/rules/command-shortcut-single-owner.js +0 -118
- package/dist/eslint/rules/confirm-dialog-words.js +0 -63
- package/dist/eslint/rules/data-table-one-unsized-column.js +0 -96
- package/dist/eslint/rules/destructive-action-placement.js +0 -69
- package/dist/eslint/rules/disabled-choice-says-why.js +0 -71
- package/dist/eslint/rules/disabled-control-appearance.js +0 -75
- package/dist/eslint/rules/ellipsis-opens-a-dialog.js +0 -110
- package/dist/eslint/rules/empty-state-owner.js +0 -145
- package/dist/eslint/rules/input-group-owners.js +0 -155
- package/dist/eslint/rules/jsx-class-utility.js +0 -200
- package/dist/eslint/rules/kbd-children-no-modifier-text.js +0 -128
- package/dist/eslint/rules/literal-text.js +0 -41
- package/dist/eslint/rules/menu-owner.js +0 -75
- package/dist/eslint/rules/middle-truncate-placement.js +0 -69
- package/dist/eslint/rules/no-app-feature-component.js +0 -100
- package/dist/eslint/rules/no-bare-button-label.js +0 -55
- package/dist/eslint/rules/no-choice-control-wrapper.js +0 -97
- package/dist/eslint/rules/no-comment.js +0 -77
- package/dist/eslint/rules/no-control-in-read-only-surface.js +0 -106
- package/dist/eslint/rules/no-decorative-icon.js +0 -164
- package/dist/eslint/rules/no-deprecated-zod-api.js +0 -215
- package/dist/eslint/rules/no-global-patch.js +0 -115
- package/dist/eslint/rules/no-hand-built-busy.js +0 -104
- package/dist/eslint/rules/no-hand-built-theme-picker.js +0 -144
- package/dist/eslint/rules/no-hand-built-tile.js +0 -41
- package/dist/eslint/rules/no-hand-formatted-date.js +0 -87
- package/dist/eslint/rules/no-hand-stacked-avatars.js +0 -67
- package/dist/eslint/rules/no-nested-collapse.js +0 -60
- package/dist/eslint/rules/no-package-component-redefinition.js +0 -175
- package/dist/eslint/rules/no-search-in-form.js +0 -47
- package/dist/eslint/rules/no-shadcn-appearance-override.js +0 -305
- package/dist/eslint/rules/no-shell-prompt-in-snippet.js +0 -74
- package/dist/eslint/rules/no-stock-error-words.js +0 -56
- package/dist/eslint/rules/no-subject-derived-expectation.js +0 -183
- package/dist/eslint/rules/no-tooltip-around-labelled-field.js +0 -72
- package/dist/eslint/rules/no-weight-class-on-strong-in-type-role.js +0 -57
- package/dist/eslint/rules/no-write-in-subscription.js +0 -140
- package/dist/eslint/rules/one-submenu-level.js +0 -43
- package/dist/eslint/rules/one-toast-per-flow.js +0 -91
- package/dist/eslint/rules/option-count.js +0 -167
- package/dist/eslint/rules/page-command-link-not-render.js +0 -43
- package/dist/eslint/rules/page-composition.js +0 -232
- package/dist/eslint/rules/progress-needs-name.js +0 -57
- package/dist/eslint/rules/rendered-import.js +0 -121
- package/dist/eslint/rules/server-action-discipline.js +0 -233
- package/dist/eslint/rules/settings-limit-in-footer.js +0 -94
- package/dist/eslint/rules/skeleton-in-region.js +0 -80
- package/dist/eslint/rules/sortable-heading-uses-sort.js +0 -79
- package/dist/eslint/rules/table-cell-inset-owner.js +0 -107
- package/dist/eslint/rules/tabs-selection-is-controlled.js +0 -47
- package/dist/eslint/rules/test-files-from-one-definition.js +0 -67
- package/dist/eslint/rules/tooltip-only-arrow.js +0 -38
- package/dist/eslint/rules/tooltip-trigger-accessible-name.js +0 -114
- package/dist/eslint/rules/workspace-declares-imports.js +0 -163
- package/dist/eslint/strict-design-system.js +0 -250
|
@@ -1,93 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: a `no-restricted-syntax` selector such as
|
|
3
|
-
* `Badge Badge` matches a name written in the file, not the element bound to
|
|
4
|
-
* the package's Badge, so it misses a renamed import and flags a product's own
|
|
5
|
-
* `Badge`; and `Badge[icon]` cannot tell a label inside the `icon` prop from
|
|
6
|
-
* an icon. This rule joins each element to its import first.
|
|
7
|
-
*/
|
|
8
|
-
import { partOf, renderedImports, staticAttribute } from "./rendered-import.js";
|
|
9
|
-
|
|
10
|
-
const exportedBy = (module) => [
|
|
11
|
-
{ from: `@nadicodeai/ui/components/${module}`, part: module },
|
|
12
|
-
{ from: "@nadicodeai/ui", part: module },
|
|
13
|
-
{ from: `@/components/${module}`, part: module },
|
|
14
|
-
];
|
|
15
|
-
|
|
16
|
-
const PARTS = new Map([
|
|
17
|
-
["Badge", exportedBy("badge")],
|
|
18
|
-
[
|
|
19
|
-
"StatusBadge",
|
|
20
|
-
[
|
|
21
|
-
...exportedBy("status-badge"),
|
|
22
|
-
{ from: "@/components/kit/status-badge", part: "status-badge" },
|
|
23
|
-
],
|
|
24
|
-
],
|
|
25
|
-
["Tag", exportedBy("tag")],
|
|
26
|
-
["TrendBadge", exportedBy("trend-badge")],
|
|
27
|
-
]);
|
|
28
|
-
|
|
29
|
-
const HOSTS = new Set(["badge", "status-badge"]);
|
|
30
|
-
const BUTTON_ATTRIBUTES = new Set(["tabIndex", "aria-pressed", "onKeyDown", "onKeyUp"]);
|
|
31
|
-
|
|
32
|
-
const attributeName = (attribute) =>
|
|
33
|
-
attribute.type === "JSXAttribute" && attribute.name.type === "JSXIdentifier"
|
|
34
|
-
? attribute.name.name
|
|
35
|
-
: null;
|
|
36
|
-
|
|
37
|
-
export const badgeIsStaticLabel = {
|
|
38
|
-
meta: {
|
|
39
|
-
type: "problem",
|
|
40
|
-
docs: {
|
|
41
|
-
description:
|
|
42
|
-
"A Badge or StatusBadge is a static label: no label inside it, no button behaviour on it.",
|
|
43
|
-
},
|
|
44
|
-
schema: [],
|
|
45
|
-
messages: {
|
|
46
|
-
"static-label":
|
|
47
|
-
'A badge is a static label of text, or one icon and text. Put a second label beside it; a value that can be acted on is a Button, or a link with <Badge render={<a href=... />} variant="pill">.',
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
create(context) {
|
|
51
|
-
const importOf = renderedImports(context);
|
|
52
|
-
const partOfElement = (element) =>
|
|
53
|
-
partOf(importOf(element.openingElement.name), PARTS, context.filename);
|
|
54
|
-
const isHost = (opening) => HOSTS.has(partOf(importOf(opening.name), PARTS, context.filename));
|
|
55
|
-
|
|
56
|
-
function holdingHost(label) {
|
|
57
|
-
let child = label;
|
|
58
|
-
for (let current = label.parent; current; child = current, current = current.parent) {
|
|
59
|
-
const holdsAsChild =
|
|
60
|
-
current.type === "JSXElement" &&
|
|
61
|
-
current.children.includes(child) &&
|
|
62
|
-
isHost(current.openingElement);
|
|
63
|
-
if (holdsAsChild) return current;
|
|
64
|
-
if (
|
|
65
|
-
current.type === "JSXAttribute" &&
|
|
66
|
-
attributeName(current) === "icon" &&
|
|
67
|
-
isHost(current.parent)
|
|
68
|
-
) {
|
|
69
|
-
return current.parent;
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return {
|
|
76
|
-
JSXElement(node) {
|
|
77
|
-
if (partOfElement(node) && holdingHost(node)) {
|
|
78
|
-
context.report({ node, messageId: "static-label" });
|
|
79
|
-
}
|
|
80
|
-
},
|
|
81
|
-
JSXOpeningElement(node) {
|
|
82
|
-
if (!isHost(node)) return;
|
|
83
|
-
for (const attribute of node.attributes) {
|
|
84
|
-
const name = attributeName(attribute);
|
|
85
|
-
const refused =
|
|
86
|
-
BUTTON_ATTRIBUTES.has(name) ||
|
|
87
|
-
(name === "role" && staticAttribute(node, "role") === "button");
|
|
88
|
-
if (refused) context.report({ node: attribute, messageId: "static-label" });
|
|
89
|
-
}
|
|
90
|
-
},
|
|
91
|
-
};
|
|
92
|
-
},
|
|
93
|
-
};
|
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: the `repo/shadcn-composition` selector in the
|
|
3
|
-
* root config (`Button JSXOpeningElement[name.name=/Icon$/]` without
|
|
4
|
-
* `data-icon`) matches by element name under any `Button`, so it cannot tell a
|
|
5
|
-
* text Button from an icon-only one (the size decides where an icon goes), nor
|
|
6
|
-
* a `Button` imported from `@nadicodeai/ui` from a local one, nor an icon bound
|
|
7
|
-
* to an icon module from a component that only ends in `Icon`.
|
|
8
|
-
*/
|
|
9
|
-
import {
|
|
10
|
-
PACKAGE_BARREL,
|
|
11
|
-
partOf,
|
|
12
|
-
renderedImports,
|
|
13
|
-
sourceMatches,
|
|
14
|
-
staticAttribute,
|
|
15
|
-
writtenName,
|
|
16
|
-
} from "./rendered-import.js";
|
|
17
|
-
|
|
18
|
-
const BUTTONS = new Map(
|
|
19
|
-
["Button", "ButtonLink"].map((name) => [
|
|
20
|
-
name,
|
|
21
|
-
["@nadicodeai/ui/components/button", PACKAGE_BARREL].map((from) => ({ from, part: name })),
|
|
22
|
-
]),
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
const TEXT_SIZES = new Set(["default", "xs", "sm", "lg"]);
|
|
26
|
-
|
|
27
|
-
const isAttribute = (attribute, name) =>
|
|
28
|
-
attribute.type === "JSXAttribute" && attribute.name.name === name;
|
|
29
|
-
|
|
30
|
-
export const buttonIconSlot = {
|
|
31
|
-
meta: {
|
|
32
|
-
type: "problem",
|
|
33
|
-
docs: {
|
|
34
|
-
description:
|
|
35
|
-
"A text Button takes its icon as prefix or suffix; an icon-only Button takes the icon as its child with no data-icon.",
|
|
36
|
-
},
|
|
37
|
-
schema: [
|
|
38
|
-
{
|
|
39
|
-
type: "object",
|
|
40
|
-
properties: {
|
|
41
|
-
iconModules: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
42
|
-
},
|
|
43
|
-
additionalProperties: false,
|
|
44
|
-
},
|
|
45
|
-
],
|
|
46
|
-
defaultOptions: [{ iconModules: ["lucide-react", "@nadicodeai/ui/components/house-icons"] }],
|
|
47
|
-
messages: {
|
|
48
|
-
"icon-slot":
|
|
49
|
-
'Pass an icon in a text Button as prefix={<Icon />} or suffix={<Icon />}; an icon-only Button (size="icon") takes the icon as its child.',
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
create(context) {
|
|
53
|
-
const [{ iconModules }] = context.options;
|
|
54
|
-
const importOf = renderedImports(context);
|
|
55
|
-
const isButton = (openingElement) =>
|
|
56
|
-
partOf(importOf(openingElement.name), BUTTONS, context.filename) !== null;
|
|
57
|
-
|
|
58
|
-
const isIcon = (openingElement) => {
|
|
59
|
-
const imported = importOf(openingElement.name);
|
|
60
|
-
if (
|
|
61
|
-
imported &&
|
|
62
|
-
iconModules.some((module) => sourceMatches(imported.source, module, context.filename))
|
|
63
|
-
) {
|
|
64
|
-
return true;
|
|
65
|
-
}
|
|
66
|
-
return /Icon$/.test(writtenName(openingElement.name) ?? "");
|
|
67
|
-
};
|
|
68
|
-
|
|
69
|
-
function enclosingButton(node) {
|
|
70
|
-
for (let current = node.parent.parent; current; current = current.parent) {
|
|
71
|
-
if (current.type === "JSXAttribute") return null;
|
|
72
|
-
if (current.type === "JSXElement" && isButton(current.openingElement)) {
|
|
73
|
-
return current.openingElement;
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return null;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
function takesTextSize(button) {
|
|
80
|
-
if (button.attributes.some((attribute) => attribute.type === "JSXSpreadAttribute")) {
|
|
81
|
-
return false;
|
|
82
|
-
}
|
|
83
|
-
if (!button.attributes.some((attribute) => isAttribute(attribute, "size"))) return true;
|
|
84
|
-
return TEXT_SIZES.has(staticAttribute(button, "size"));
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
JSXOpeningElement(node) {
|
|
89
|
-
if (node.parent.type !== "JSXElement") return;
|
|
90
|
-
const carriesDataIcon = node.attributes.some((attribute) =>
|
|
91
|
-
isAttribute(attribute, "data-icon"),
|
|
92
|
-
);
|
|
93
|
-
if (!carriesDataIcon && !isIcon(node)) return;
|
|
94
|
-
const button = enclosingButton(node);
|
|
95
|
-
if (!button) return;
|
|
96
|
-
if (carriesDataIcon || takesTextSize(button)) {
|
|
97
|
-
context.report({ node, messageId: "icon-slot" });
|
|
98
|
-
}
|
|
99
|
-
},
|
|
100
|
-
};
|
|
101
|
-
},
|
|
102
|
-
};
|
|
@@ -1,146 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: `no-restricted-imports` can hold `buttonVariants`
|
|
3
|
-
* back, but not tell the Buzz tree's own `buttonVariants` from the package's, and
|
|
4
|
-
* a `no-restricted-syntax` selector cannot follow a renamed `Button` import to
|
|
5
|
-
* the `onClick` and `render` it reads.
|
|
6
|
-
*/
|
|
7
|
-
import { PACKAGE_BARREL, partOf, renderedImports, writtenName } from "./rendered-import.js";
|
|
8
|
-
|
|
9
|
-
const BUTTON_MODULES = ["@nadicodeai/ui/components/button", PACKAGE_BARREL];
|
|
10
|
-
|
|
11
|
-
const BUTTON = new Map([["Button", BUTTON_MODULES.map((from) => ({ from, part: "Button" }))]]);
|
|
12
|
-
|
|
13
|
-
const LINK_ELEMENTS = new Set(["a", "Link", "NextLink"]);
|
|
14
|
-
const ROUTER_METHODS = new Set(["push", "replace"]);
|
|
15
|
-
const FUNCTIONS = new Set(["FunctionExpression", "ArrowFunctionExpression"]);
|
|
16
|
-
|
|
17
|
-
function variantsBindings(context) {
|
|
18
|
-
const names = new Set();
|
|
19
|
-
const namespaces = new Set();
|
|
20
|
-
for (const statement of context.sourceCode.ast.body) {
|
|
21
|
-
if (statement.type !== "ImportDeclaration" || statement.importKind === "type") continue;
|
|
22
|
-
if (!BUTTON_MODULES.includes(statement.source.value)) continue;
|
|
23
|
-
for (const specifier of statement.specifiers) {
|
|
24
|
-
if (specifier.importKind === "type") continue;
|
|
25
|
-
if (specifier.type === "ImportNamespaceSpecifier") namespaces.add(specifier.local.name);
|
|
26
|
-
else if (
|
|
27
|
-
specifier.type === "ImportSpecifier" &&
|
|
28
|
-
(specifier.imported.name ?? specifier.imported.value) === "buttonVariants"
|
|
29
|
-
) {
|
|
30
|
-
names.add(specifier.local.name);
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return (callee) => {
|
|
35
|
-
if (callee.type === "Identifier") return names.has(callee.name);
|
|
36
|
-
return (
|
|
37
|
-
callee.type === "MemberExpression" &&
|
|
38
|
-
!callee.computed &&
|
|
39
|
-
callee.object.type === "Identifier" &&
|
|
40
|
-
namespaces.has(callee.object.name) &&
|
|
41
|
-
callee.property.name === "buttonVariants"
|
|
42
|
-
);
|
|
43
|
-
};
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const unwrap = (node) => {
|
|
47
|
-
if (node.type === "AwaitExpression") return unwrap(node.argument);
|
|
48
|
-
if (node.type === "UnaryExpression" && node.operator === "void") return unwrap(node.argument);
|
|
49
|
-
if (node.type === "ChainExpression") return unwrap(node.expression);
|
|
50
|
-
return node;
|
|
51
|
-
};
|
|
52
|
-
|
|
53
|
-
const isLocationTarget = (node) => {
|
|
54
|
-
if (node.type !== "MemberExpression" || node.computed) return false;
|
|
55
|
-
const { object, property } = node;
|
|
56
|
-
if (object.type === "Identifier") {
|
|
57
|
-
if (object.name === "window") return property.name === "location";
|
|
58
|
-
return object.name === "location" && property.name === "href";
|
|
59
|
-
}
|
|
60
|
-
return (
|
|
61
|
-
isLocationTarget(object) && object.property.name === "location" && property.name === "href"
|
|
62
|
-
);
|
|
63
|
-
};
|
|
64
|
-
|
|
65
|
-
function goesToAnAddress(expression) {
|
|
66
|
-
const node = unwrap(expression);
|
|
67
|
-
if (node.type === "AssignmentExpression") return isLocationTarget(node.left);
|
|
68
|
-
if (node.type !== "CallExpression") return false;
|
|
69
|
-
const { callee } = node;
|
|
70
|
-
if (callee.type === "Identifier") return callee.name === "navigate";
|
|
71
|
-
return (
|
|
72
|
-
callee.type === "MemberExpression" &&
|
|
73
|
-
!callee.computed &&
|
|
74
|
-
callee.object.type === "Identifier" &&
|
|
75
|
-
callee.object.name === "router" &&
|
|
76
|
-
ROUTER_METHODS.has(callee.property.name)
|
|
77
|
-
);
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function onlyGoesToAnAddress(fn) {
|
|
81
|
-
if (fn.body.type !== "BlockStatement") return goesToAnAddress(fn.body);
|
|
82
|
-
const { body } = fn.body;
|
|
83
|
-
return (
|
|
84
|
-
body.length > 0 &&
|
|
85
|
-
body.every(
|
|
86
|
-
(statement) =>
|
|
87
|
-
statement.type === "ExpressionStatement" && goesToAnAddress(statement.expression),
|
|
88
|
-
)
|
|
89
|
-
);
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
const attributeValue = (openingElement, attributeName) => {
|
|
93
|
-
const attribute = openingElement.attributes.find(
|
|
94
|
-
(candidate) => candidate.type === "JSXAttribute" && candidate.name.name === attributeName,
|
|
95
|
-
);
|
|
96
|
-
return attribute?.value?.type === "JSXExpressionContainer" ? attribute.value.expression : null;
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
const isLinkElement = (node) =>
|
|
100
|
-
node?.type === "JSXElement" && LINK_ELEMENTS.has(writtenName(node.openingElement.name) ?? "");
|
|
101
|
-
|
|
102
|
-
function enclosingAttribute(node) {
|
|
103
|
-
for (let current = node.parent; current; current = current.parent) {
|
|
104
|
-
if (current.type === "JSXAttribute") return current;
|
|
105
|
-
}
|
|
106
|
-
return null;
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
export const buttonLinkForNavigation = {
|
|
110
|
-
meta: {
|
|
111
|
-
type: "problem",
|
|
112
|
-
docs: {
|
|
113
|
-
description: "A control that goes to an address is a ButtonLink; a Button runs a command.",
|
|
114
|
-
},
|
|
115
|
-
schema: [],
|
|
116
|
-
messages: {
|
|
117
|
-
"navigating-button":
|
|
118
|
-
"A control that goes to an address is a ButtonLink; a Button runs a command.",
|
|
119
|
-
},
|
|
120
|
-
},
|
|
121
|
-
create(context) {
|
|
122
|
-
const importOf = renderedImports(context);
|
|
123
|
-
const isButtonVariants = variantsBindings(context);
|
|
124
|
-
return {
|
|
125
|
-
CallExpression(node) {
|
|
126
|
-
if (!isButtonVariants(node.callee)) return;
|
|
127
|
-
const attribute = enclosingAttribute(node);
|
|
128
|
-
if (attribute?.name.name !== "className") return;
|
|
129
|
-
if (isLinkElement(attribute.parent.parent)) {
|
|
130
|
-
context.report({ node, messageId: "navigating-button" });
|
|
131
|
-
}
|
|
132
|
-
},
|
|
133
|
-
JSXOpeningElement(node) {
|
|
134
|
-
if (partOf(importOf(node.name), BUTTON, context.filename) !== "Button") return;
|
|
135
|
-
const onClick = attributeValue(node, "onClick");
|
|
136
|
-
if (onClick && FUNCTIONS.has(onClick.type) && onlyGoesToAnAddress(onClick)) {
|
|
137
|
-
context.report({ node, messageId: "navigating-button" });
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
if (isLinkElement(attributeValue(node, "render"))) {
|
|
141
|
-
context.report({ node, messageId: "navigating-button" });
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
};
|
|
145
|
-
},
|
|
146
|
-
};
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: a `no-restricted-syntax` selector can match
|
|
3
|
-
* `JSXElement[openingElement.name.name='Button']:has(JSXAttribute[name.name='aria-label'])`,
|
|
4
|
-
* but not that the element is the package's Button rather than a local one with
|
|
5
|
-
* the same name, nor that a child is text rather than an icon, nor the `size`
|
|
6
|
-
* that makes a non-literal child an icon-only label. Those need the import
|
|
7
|
-
* binding and the children read together.
|
|
8
|
-
*/
|
|
9
|
-
import { PACKAGE_BARREL, partOf, renderedImports, staticAttribute } from "./rendered-import.js";
|
|
10
|
-
|
|
11
|
-
const BUTTONS = new Map(
|
|
12
|
-
["Button", "ButtonLink"].map((name) => [
|
|
13
|
-
name,
|
|
14
|
-
["@nadicodeai/ui/components/button", PACKAGE_BARREL].map((from) => ({ from, part: name })),
|
|
15
|
-
]),
|
|
16
|
-
);
|
|
17
|
-
|
|
18
|
-
const NAMING_ATTRIBUTES = new Set(["aria-label", "aria-labelledby"]);
|
|
19
|
-
|
|
20
|
-
const namesItself = (openingElement) =>
|
|
21
|
-
openingElement.attributes.some(
|
|
22
|
-
(attribute) => attribute.type === "JSXAttribute" && NAMING_ATTRIBUTES.has(attribute.name.name),
|
|
23
|
-
);
|
|
24
|
-
|
|
25
|
-
const isIconSize = (openingElement) =>
|
|
26
|
-
String(staticAttribute(openingElement, "size") ?? "").startsWith("icon");
|
|
27
|
-
|
|
28
|
-
function staticText(expression) {
|
|
29
|
-
if (expression.type === "Literal") {
|
|
30
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- ESTree types Literal.value as string | number | boolean | RegExp | bigint | null; only typeof picks the string.
|
|
31
|
-
return typeof expression.value === "string" ? expression.value : null;
|
|
32
|
-
}
|
|
33
|
-
if (expression.type === "TemplateLiteral" && expression.expressions.length === 0) {
|
|
34
|
-
return expression.quasis[0].value.cooked;
|
|
35
|
-
}
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
function childText(child) {
|
|
40
|
-
if (child.type === "JSXText") return child.value.trim() === "" ? "none" : "visible";
|
|
41
|
-
if (child.type !== "JSXExpressionContainer") return "none";
|
|
42
|
-
const { expression } = child;
|
|
43
|
-
if (expression.type === "JSXEmptyExpression") return "none";
|
|
44
|
-
const text = staticText(expression);
|
|
45
|
-
if (text !== null) return text.trim() === "" ? "none" : "visible";
|
|
46
|
-
return expression.type === "Literal" ? "none" : "computed";
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export const buttonNoAriaLabelWithText = {
|
|
50
|
-
meta: {
|
|
51
|
-
type: "problem",
|
|
52
|
-
docs: {
|
|
53
|
-
description: "A Button with visible text is named by its text, never by an aria-label.",
|
|
54
|
-
},
|
|
55
|
-
schema: [],
|
|
56
|
-
messages: {
|
|
57
|
-
"named-by-its-text":
|
|
58
|
-
"A Button with visible text is named by its text; remove aria-label. When the text alone does not name the target, use `accessibleName` on a PageCommand.",
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
create(context) {
|
|
62
|
-
const importOf = renderedImports(context);
|
|
63
|
-
return {
|
|
64
|
-
JSXElement(node) {
|
|
65
|
-
const { openingElement } = node;
|
|
66
|
-
if (!partOf(importOf(openingElement.name), BUTTONS, context.filename)) return;
|
|
67
|
-
if (!namesItself(openingElement)) return;
|
|
68
|
-
const kinds = node.children.map(childText);
|
|
69
|
-
const showsText =
|
|
70
|
-
kinds.includes("visible") || (kinds.includes("computed") && !isIconSize(openingElement));
|
|
71
|
-
if (showsText) context.report({ node: openingElement, messageId: "named-by-its-text" });
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
},
|
|
75
|
-
};
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: `no-restricted-syntax` can match a class token
|
|
3
|
-
* in a `className` literal (strict-design-system.js does), but not which file
|
|
4
|
-
* the token is in, so it cannot allow the files that render a button and refuse
|
|
5
|
-
* the rest, nor tell the small role's two files from the other four.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const BUTTON_ROLE = /(?:^|:)!?(text-button(?:-lg|-xs)?|nc-type-button(?:-[a-z0-9]+)*)!?$/;
|
|
9
|
-
const SMALL_ROLE = /-xs$/;
|
|
10
|
-
const CLASS_FUNCTIONS = new Set(["cva", "cn"]);
|
|
11
|
-
|
|
12
|
-
const inFiles = (filename, files) => {
|
|
13
|
-
const path = filename.replaceAll("\\", "/");
|
|
14
|
-
return files.some((file) => path === file || path.endsWith(`/${file}`));
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const classSite = (node) => {
|
|
18
|
-
for (let current = node.parent; current; current = current.parent) {
|
|
19
|
-
if (current.type === "JSXAttribute") return current.name.name === "className";
|
|
20
|
-
if (
|
|
21
|
-
current.type === "CallExpression" &&
|
|
22
|
-
current.callee.type === "Identifier" &&
|
|
23
|
-
CLASS_FUNCTIONS.has(current.callee.name)
|
|
24
|
-
) {
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return false;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const isVariantKey = (node) => node.parent.type === "Property" && node.parent.key === node;
|
|
32
|
-
|
|
33
|
-
export const buttonTypeRolesOnlyInButtons = {
|
|
34
|
-
meta: {
|
|
35
|
-
type: "problem",
|
|
36
|
-
docs: {
|
|
37
|
-
description:
|
|
38
|
-
"The button type roles label a component that renders a button, in the files that render one.",
|
|
39
|
-
},
|
|
40
|
-
schema: [
|
|
41
|
-
{
|
|
42
|
-
type: "object",
|
|
43
|
-
properties: {
|
|
44
|
-
allow: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
45
|
-
allowSmall: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
46
|
-
},
|
|
47
|
-
additionalProperties: false,
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
defaultOptions: [{ allow: [], allowSmall: [] }],
|
|
51
|
-
messages: {
|
|
52
|
-
"button-role":
|
|
53
|
-
"Button type roles label a component that renders a button. Use a body-* or label-* role for other text; text-button-xs is only for the small button inside a field.",
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
create(context) {
|
|
57
|
-
const [{ allow, allowSmall }] = context.options;
|
|
58
|
-
const inAllowed = inFiles(context.filename, allow);
|
|
59
|
-
const inSmallAllowed = inFiles(context.filename, allowSmall);
|
|
60
|
-
|
|
61
|
-
const refused = (value) =>
|
|
62
|
-
value
|
|
63
|
-
.split(/\s+/)
|
|
64
|
-
.map((utility) => BUTTON_ROLE.exec(utility)?.[1])
|
|
65
|
-
.filter(Boolean)
|
|
66
|
-
.some((role) => (SMALL_ROLE.test(role) ? !inSmallAllowed : !inAllowed));
|
|
67
|
-
|
|
68
|
-
function check(node, value) {
|
|
69
|
-
if (isVariantKey(node) || !classSite(node)) return;
|
|
70
|
-
if (refused(value)) context.report({ node, messageId: "button-role" });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
Literal(node) {
|
|
75
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- ESTree types Literal.value as string | number | boolean | RegExp | bigint | null; only typeof picks the string.
|
|
76
|
-
if (typeof node.value === "string") check(node, node.value);
|
|
77
|
-
},
|
|
78
|
-
TemplateElement(node) {
|
|
79
|
-
check(node, node.value.cooked ?? node.value.raw);
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
},
|
|
83
|
-
};
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: `jsx-a11y/label-has-associated-control` checks a
|
|
3
|
-
* Label for a control, not a control for a name, and a `no-restricted-syntax`
|
|
4
|
-
* selector cannot tell that the package's Checkbox is meant rather than a local
|
|
5
|
-
* one, that a Label elsewhere in the file points at its id through `htmlFor`,
|
|
6
|
-
* or that a labelling ancestor wraps it. Those need the import bindings, the
|
|
7
|
-
* ancestors and the file's labels read together.
|
|
8
|
-
*/
|
|
9
|
-
import { packageModules, partOf, renderedImports, staticAttribute } from "./rendered-import.js";
|
|
10
|
-
|
|
11
|
-
const partsIn = (module, names) =>
|
|
12
|
-
names.map((name) => [
|
|
13
|
-
name,
|
|
14
|
-
[...packageModules(module), `@/components/${module}`].map((from) => ({ from, part: name })),
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
const CONTROLS = new Map([
|
|
18
|
-
...partsIn("checkbox", ["Checkbox"]),
|
|
19
|
-
...partsIn("switch", ["Switch"]),
|
|
20
|
-
...partsIn("radio-group", ["RadioGroupItem"]),
|
|
21
|
-
]);
|
|
22
|
-
|
|
23
|
-
const LABELS = new Map([...partsIn("label", ["Label"]), ...partsIn("field", ["FieldLabel"])]);
|
|
24
|
-
|
|
25
|
-
const NAMING_ANCESTORS = new Map([...LABELS, ...partsIn("settings-section", ["SettingsField"])]);
|
|
26
|
-
|
|
27
|
-
const NAMING_ATTRIBUTES = new Set(["aria-label", "aria-labelledby"]);
|
|
28
|
-
|
|
29
|
-
const findAttribute = (openingElement, name) =>
|
|
30
|
-
openingElement.attributes.find(
|
|
31
|
-
(attribute) => attribute.type === "JSXAttribute" && attribute.name.name === name,
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const hasSpread = (openingElement) =>
|
|
35
|
-
openingElement.attributes.some((attribute) => attribute.type === "JSXSpreadAttribute");
|
|
36
|
-
|
|
37
|
-
export const choiceControlHasAName = {
|
|
38
|
-
meta: {
|
|
39
|
-
type: "problem",
|
|
40
|
-
docs: {
|
|
41
|
-
description:
|
|
42
|
-
"A Checkbox, Switch or RadioGroupItem is named by an aria-label, an aria-labelledby, a Label with htmlFor, or a labelling ancestor.",
|
|
43
|
-
},
|
|
44
|
-
schema: [],
|
|
45
|
-
messages: {
|
|
46
|
-
unnamed:
|
|
47
|
-
"Name this control: give it an aria-label (a row's checkbox: Select <row name>), an aria-labelledby, or a Label with htmlFor.",
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
create(context) {
|
|
51
|
-
const importOf = renderedImports(context);
|
|
52
|
-
const isPart = (parts) => (openingElement) =>
|
|
53
|
-
Boolean(partOf(importOf(openingElement.name), parts, context.filename));
|
|
54
|
-
const isControl = isPart(CONTROLS);
|
|
55
|
-
const isLabel = isPart(LABELS);
|
|
56
|
-
const isNamingAncestor = isPart(NAMING_ANCESTORS);
|
|
57
|
-
|
|
58
|
-
const reference = (openingElement, attributeName) => {
|
|
59
|
-
const attribute = findAttribute(openingElement, attributeName);
|
|
60
|
-
if (!attribute) return null;
|
|
61
|
-
const stated = staticAttribute(openingElement, attributeName);
|
|
62
|
-
if (stated !== undefined) return String(stated);
|
|
63
|
-
return attribute.value ? context.sourceCode.getText(attribute.value) : null;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const controls = [];
|
|
67
|
-
const pointedAt = new Set();
|
|
68
|
-
|
|
69
|
-
const hasNamingAncestor = (element) => {
|
|
70
|
-
for (let current = element.parent; current; current = current.parent) {
|
|
71
|
-
if (current.type === "JSXElement" && isNamingAncestor(current.openingElement)) return true;
|
|
72
|
-
}
|
|
73
|
-
return false;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
JSXElement(node) {
|
|
78
|
-
const { openingElement } = node;
|
|
79
|
-
if (isLabel(openingElement)) {
|
|
80
|
-
const target = reference(openingElement, "htmlFor");
|
|
81
|
-
if (target !== null) pointedAt.add(target);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (!isControl(openingElement)) return;
|
|
85
|
-
if (hasSpread(openingElement)) return;
|
|
86
|
-
if (openingElement.attributes.some((a) => NAMING_ATTRIBUTES.has(a.name?.name))) return;
|
|
87
|
-
if (hasNamingAncestor(node)) return;
|
|
88
|
-
controls.push({ node: openingElement, id: reference(openingElement, "id") });
|
|
89
|
-
},
|
|
90
|
-
"Program:exit"() {
|
|
91
|
-
for (const { node, id } of controls) {
|
|
92
|
-
if (id !== null && pointedAt.has(id)) continue;
|
|
93
|
-
context.report({ node, messageId: "unnamed" });
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
},
|
|
98
|
-
};
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: a `no-restricted-syntax` selector can match a
|
|
3
|
-
* `RadioGroup` without a `FieldSet` ancestor, but not that the `FieldSet`
|
|
4
|
-
* holds a `FieldLegend`, nor that two `Checkbox` elements share a parent once
|
|
5
|
-
* each sits in its own `Field`, so it would miss the checkbox group.
|
|
6
|
-
*/
|
|
7
|
-
import { findBelow, renderedImports } from "./rendered-import.js";
|
|
8
|
-
|
|
9
|
-
const FROM_DESIGN_SYSTEM = /^(@nadicodeai\/ui(\/|$)|@\/|\.)/;
|
|
10
|
-
const ROW_WRAPPERS = new Set(["Field", "FieldContent", "FieldLabel", "Label"]);
|
|
11
|
-
|
|
12
|
-
const isRepeater = (node) =>
|
|
13
|
-
(node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") &&
|
|
14
|
-
node.parent.type === "CallExpression" &&
|
|
15
|
-
node.parent.callee.type === "MemberExpression" &&
|
|
16
|
-
node.parent.callee.property.name === "map";
|
|
17
|
-
|
|
18
|
-
export const choiceGroupInFieldset = {
|
|
19
|
-
meta: {
|
|
20
|
-
type: "problem",
|
|
21
|
-
docs: {
|
|
22
|
-
description: "A group of radios or checkboxes sits in a FieldSet with a FieldLegend.",
|
|
23
|
-
},
|
|
24
|
-
schema: [],
|
|
25
|
-
messages: {
|
|
26
|
-
"choice-group":
|
|
27
|
-
"A group of radios or checkboxes sits in a FieldSet with a FieldLegend, so the group is announced by its legend.",
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
create(context) {
|
|
31
|
-
const importOf = renderedImports(context);
|
|
32
|
-
const exportOf = (element) => {
|
|
33
|
-
const imported = importOf(element.openingElement.name);
|
|
34
|
-
return imported && FROM_DESIGN_SYSTEM.test(imported.source) ? imported.name : null;
|
|
35
|
-
};
|
|
36
|
-
const checkboxGroups = new Map();
|
|
37
|
-
|
|
38
|
-
function hasLegendedFieldSet(node) {
|
|
39
|
-
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
40
|
-
if (ancestor.type !== "JSXElement" || exportOf(ancestor) !== "FieldSet") continue;
|
|
41
|
-
const legend = findBelow(
|
|
42
|
-
ancestor,
|
|
43
|
-
context.sourceCode.visitorKeys,
|
|
44
|
-
(child) => child.type === "JSXElement" && exportOf(child) === "FieldLegend",
|
|
45
|
-
);
|
|
46
|
-
if (legend) return true;
|
|
47
|
-
}
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function reportUnlessGrouped(element) {
|
|
52
|
-
if (!hasLegendedFieldSet(element)) {
|
|
53
|
-
context.report({ node: element.openingElement, messageId: "choice-group" });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function recordCheckbox(element) {
|
|
58
|
-
let repeated = false;
|
|
59
|
-
let parent = element.parent;
|
|
60
|
-
while (parent) {
|
|
61
|
-
if (isRepeater(parent)) repeated = true;
|
|
62
|
-
const isRow = parent.type === "JSXElement" && ROW_WRAPPERS.has(exportOf(parent));
|
|
63
|
-
const isGroup = (parent.type === "JSXElement" || parent.type === "JSXFragment") && !isRow;
|
|
64
|
-
if (isGroup) break;
|
|
65
|
-
parent = parent.parent;
|
|
66
|
-
}
|
|
67
|
-
if (!parent) return;
|
|
68
|
-
const group = checkboxGroups.get(parent) ?? { first: element, count: 0 };
|
|
69
|
-
group.count += repeated ? 2 : 1;
|
|
70
|
-
checkboxGroups.set(parent, group);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
JSXElement(node) {
|
|
75
|
-
const name = exportOf(node);
|
|
76
|
-
if (name === "RadioGroup") reportUnlessGrouped(node);
|
|
77
|
-
else if (name === "Checkbox") recordCheckbox(node);
|
|
78
|
-
},
|
|
79
|
-
"Program:exit"() {
|
|
80
|
-
for (const { first, count } of checkboxGroups.values()) {
|
|
81
|
-
if (count >= 2) reportUnlessGrouped(first);
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
},
|
|
86
|
-
};
|