@nadicodeai/ui 12.1.165 → 12.1.167
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 +14 -7
- package/dist/components/data-table.d.ts.map +1 -1
- package/dist/components/data-table.js +24 -15
- 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 +5 -4
- package/dist/components/record-list.d.ts.map +1 -1
- package/dist/components/record-list.js +31 -17
- 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-layouts.d.ts +24 -9
- package/dist/components/table-layouts.d.ts.map +1 -1
- package/dist/components/table-layouts.js +27 -10
- 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 +40 -18
- package/dist/standalone/index.js +2 -2
- package/dist/standalone/orb-page.js +10 -8
- package/dist/styles/components.css +14 -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
package/dist/eslint/index.js
DELETED
|
@@ -1,224 +0,0 @@
|
|
|
1
|
-
import { createHash } from "node:crypto";
|
|
2
|
-
import { readdirSync, readFileSync } from "node:fs";
|
|
3
|
-
|
|
4
|
-
import { builtinRules } from "eslint/use-at-your-own-risk";
|
|
5
|
-
|
|
6
|
-
import { badgeIsStaticLabel } from "./rules/badge-is-static-label.js";
|
|
7
|
-
import { buttonIconSlot } from "./rules/button-icon-slot.js";
|
|
8
|
-
import { buttonLinkForNavigation } from "./rules/button-link-for-navigation.js";
|
|
9
|
-
import { buttonNoAriaLabelWithText } from "./rules/button-no-aria-label-with-text.js";
|
|
10
|
-
import { buttonTypeRolesOnlyInButtons } from "./rules/button-type-roles-only-in-buttons.js";
|
|
11
|
-
import { choiceControlHasAName } from "./rules/choice-control-has-a-name.js";
|
|
12
|
-
import { choiceGroupInFieldset } from "./rules/choice-group-in-fieldset.js";
|
|
13
|
-
import { commandShortcutSingleOwner } from "./rules/command-shortcut-single-owner.js";
|
|
14
|
-
import { confirmDialogWords } from "./rules/confirm-dialog-words.js";
|
|
15
|
-
import { dataTableOneUnsizedColumn } from "./rules/data-table-one-unsized-column.js";
|
|
16
|
-
import { destructiveActionPlacement } from "./rules/destructive-action-placement.js";
|
|
17
|
-
import { disabledChoiceSaysWhy } from "./rules/disabled-choice-says-why.js";
|
|
18
|
-
import { disabledControlAppearance } from "./rules/disabled-control-appearance.js";
|
|
19
|
-
import { settingsLimitInFooter } from "./rules/settings-limit-in-footer.js";
|
|
20
|
-
import { ellipsisOpensADialog } from "./rules/ellipsis-opens-a-dialog.js";
|
|
21
|
-
import { emptyStateOwner } from "./rules/empty-state-owner.js";
|
|
22
|
-
import { inputGroupOwners } from "./rules/input-group-owners.js";
|
|
23
|
-
import { kbdChildrenNoModifierText } from "./rules/kbd-children-no-modifier-text.js";
|
|
24
|
-
import { menuOwner } from "./rules/menu-owner.js";
|
|
25
|
-
import { middleTruncatePlacement } from "./rules/middle-truncate-placement.js";
|
|
26
|
-
import { noAppFeatureComponent } from "./rules/no-app-feature-component.js";
|
|
27
|
-
import { noBareButtonLabel } from "./rules/no-bare-button-label.js";
|
|
28
|
-
import { noChoiceControlWrapper } from "./rules/no-choice-control-wrapper.js";
|
|
29
|
-
import { noComment } from "./rules/no-comment.js";
|
|
30
|
-
import { noControlInReadOnlySurface } from "./rules/no-control-in-read-only-surface.js";
|
|
31
|
-
import { noDecorativeIcon } from "./rules/no-decorative-icon.js";
|
|
32
|
-
import { noDeprecatedZodApi } from "./rules/no-deprecated-zod-api.js";
|
|
33
|
-
import { noGlobalPatch } from "./rules/no-global-patch.js";
|
|
34
|
-
import { noHandBuiltBusy } from "./rules/no-hand-built-busy.js";
|
|
35
|
-
import { noHandBuiltThemePicker } from "./rules/no-hand-built-theme-picker.js";
|
|
36
|
-
import { noHandBuiltTile } from "./rules/no-hand-built-tile.js";
|
|
37
|
-
import { noHandFormattedDate } from "./rules/no-hand-formatted-date.js";
|
|
38
|
-
import { noHandStackedAvatars } from "./rules/no-hand-stacked-avatars.js";
|
|
39
|
-
import { noNestedCollapse } from "./rules/no-nested-collapse.js";
|
|
40
|
-
import { noPackageComponentRedefinition } from "./rules/no-package-component-redefinition.js";
|
|
41
|
-
import { noSearchInForm } from "./rules/no-search-in-form.js";
|
|
42
|
-
import { noShadcnAppearanceOverride } from "./rules/no-shadcn-appearance-override.js";
|
|
43
|
-
import { noShellPromptInSnippet } from "./rules/no-shell-prompt-in-snippet.js";
|
|
44
|
-
import { noStockErrorWords } from "./rules/no-stock-error-words.js";
|
|
45
|
-
import { noSubjectDerivedExpectation } from "./rules/no-subject-derived-expectation.js";
|
|
46
|
-
import { noTooltipAroundLabelledField } from "./rules/no-tooltip-around-labelled-field.js";
|
|
47
|
-
import { noWeightClassOnStrongInTypeRole } from "./rules/no-weight-class-on-strong-in-type-role.js";
|
|
48
|
-
import { noWriteInSubscription } from "./rules/no-write-in-subscription.js";
|
|
49
|
-
import { oneSubmenuLevel } from "./rules/one-submenu-level.js";
|
|
50
|
-
import { oneToastPerFlow } from "./rules/one-toast-per-flow.js";
|
|
51
|
-
import { optionCount } from "./rules/option-count.js";
|
|
52
|
-
import { pageCommandLinkNotRender } from "./rules/page-command-link-not-render.js";
|
|
53
|
-
import { pageComposition } from "./rules/page-composition.js";
|
|
54
|
-
import { progressNeedsName } from "./rules/progress-needs-name.js";
|
|
55
|
-
import { serverActionDiscipline } from "./rules/server-action-discipline.js";
|
|
56
|
-
import { skeletonInRegion } from "./rules/skeleton-in-region.js";
|
|
57
|
-
import { sortableHeadingUsesSort } from "./rules/sortable-heading-uses-sort.js";
|
|
58
|
-
import { tableCellInsetOwner } from "./rules/table-cell-inset-owner.js";
|
|
59
|
-
import { tabsSelectionIsControlled } from "./rules/tabs-selection-is-controlled.js";
|
|
60
|
-
import { testFilesFromOneDefinition } from "./rules/test-files-from-one-definition.js";
|
|
61
|
-
import { tooltipOnlyArrow } from "./rules/tooltip-only-arrow.js";
|
|
62
|
-
import { tooltipTriggerAccessibleName } from "./rules/tooltip-trigger-accessible-name.js";
|
|
63
|
-
import { workspaceDeclaresImports } from "./rules/workspace-declares-imports.js";
|
|
64
|
-
import { strictDesignSystemSelectors } from "./strict-design-system.js";
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* The plugin carries no version of its own.
|
|
68
|
-
* The published copy of this file sits at `<ui>/dist/eslint/index.js` and the
|
|
69
|
-
* source sits at `tools/lint/eslint/index.js`, which puts the
|
|
70
|
-
* manifest at one of two places relative to here; the `name` field says which
|
|
71
|
-
* candidate is the right one, so an unrelated manifest cannot answer by accident.
|
|
72
|
-
*/
|
|
73
|
-
const PUBLISHING_PACKAGE = "@nadicodeai/ui";
|
|
74
|
-
const MANIFEST_CANDIDATES = ["../../package.json", "../../../packages/ui/package.json"];
|
|
75
|
-
|
|
76
|
-
const version = readPublishedVersion();
|
|
77
|
-
|
|
78
|
-
function readPublishedVersion() {
|
|
79
|
-
for (const candidate of MANIFEST_CANDIDATES) {
|
|
80
|
-
let manifest;
|
|
81
|
-
try {
|
|
82
|
-
manifest = JSON.parse(readFileSync(new URL(candidate, import.meta.url), "utf8"));
|
|
83
|
-
} catch {
|
|
84
|
-
continue;
|
|
85
|
-
}
|
|
86
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- parses package.json at its boundary; this package has no schema library.
|
|
87
|
-
if (manifest?.name === PUBLISHING_PACKAGE && typeof manifest.version === "string") {
|
|
88
|
-
return manifest.version;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
throw new Error(
|
|
92
|
-
`@nadicodeai/eslint-plugin: no ${PUBLISHING_PACKAGE} manifest at ${MANIFEST_CANDIDATES.join(" or ")}, so the plugin cannot report the version it ships under.`,
|
|
93
|
-
);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* A digest of the plugin's own source. ESLint keys its lint cache on the resolved
|
|
98
|
-
* config, and a plugin enters that key only by name and version, which move on a
|
|
99
|
-
* release rather than on a rule edit. Configs carry this digest in `settings`, so
|
|
100
|
-
* a changed rule invalidates every cached result that ran under the old one.
|
|
101
|
-
*/
|
|
102
|
-
export const rulesDigest = digestOf([
|
|
103
|
-
"index.js",
|
|
104
|
-
"strict-design-system.js",
|
|
105
|
-
...readdirSync(new URL("./rules/", import.meta.url))
|
|
106
|
-
.filter((name) => name.endsWith(".js") && !name.endsWith(".test.js"))
|
|
107
|
-
.sort()
|
|
108
|
-
.map((name) => `rules/${name}`),
|
|
109
|
-
]);
|
|
110
|
-
|
|
111
|
-
function digestOf(paths) {
|
|
112
|
-
const hash = createHash("sha256");
|
|
113
|
-
for (const path of paths) {
|
|
114
|
-
hash
|
|
115
|
-
.update(path)
|
|
116
|
-
.update("\0")
|
|
117
|
-
.update(readFileSync(new URL(`./${path}`, import.meta.url)));
|
|
118
|
-
}
|
|
119
|
-
return hash.digest("hex").slice(0, 16);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
/*
|
|
123
|
-
* The strict design-system checks that a selector expresses are core
|
|
124
|
-
* `no-restricted-syntax`, registered here once per check under the check's own
|
|
125
|
-
* name, with the check's selectors as the rule's default options. A config that
|
|
126
|
-
* turns on one `nadicodeai/<check>` alone therefore runs that check, and
|
|
127
|
-
* `flat/strict` is self-contained for a consumer of `@nadicodeai/ui/eslint`.
|
|
128
|
-
*/
|
|
129
|
-
const restrictedSyntax = builtinRules.get("no-restricted-syntax");
|
|
130
|
-
const strictSelectorRules = Object.fromEntries(
|
|
131
|
-
Object.entries(strictDesignSystemSelectors).map(([name, entries]) => [
|
|
132
|
-
name,
|
|
133
|
-
{ ...restrictedSyntax, meta: { ...restrictedSyntax.meta, defaultOptions: entries } },
|
|
134
|
-
]),
|
|
135
|
-
);
|
|
136
|
-
|
|
137
|
-
export const strictDesignSystemRules = {
|
|
138
|
-
...Object.fromEntries(
|
|
139
|
-
Object.keys(strictDesignSystemSelectors).map((name) => [`nadicodeai/${name}`, "error"]),
|
|
140
|
-
),
|
|
141
|
-
"nadicodeai/no-shadcn-appearance-override": "error",
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
/** @type {import('eslint').ESLint.Plugin} */
|
|
145
|
-
const plugin = {
|
|
146
|
-
meta: {
|
|
147
|
-
name: "@nadicodeai/eslint-plugin",
|
|
148
|
-
version,
|
|
149
|
-
},
|
|
150
|
-
configs: {},
|
|
151
|
-
rules: {
|
|
152
|
-
...strictSelectorRules,
|
|
153
|
-
"badge-is-static-label": badgeIsStaticLabel,
|
|
154
|
-
"button-icon-slot": buttonIconSlot,
|
|
155
|
-
"button-link-for-navigation": buttonLinkForNavigation,
|
|
156
|
-
"button-no-aria-label-with-text": buttonNoAriaLabelWithText,
|
|
157
|
-
"button-type-roles-only-in-buttons": buttonTypeRolesOnlyInButtons,
|
|
158
|
-
"choice-control-has-a-name": choiceControlHasAName,
|
|
159
|
-
"choice-group-in-fieldset": choiceGroupInFieldset,
|
|
160
|
-
"command-shortcut-single-owner": commandShortcutSingleOwner,
|
|
161
|
-
"confirm-dialog-words": confirmDialogWords,
|
|
162
|
-
"data-table-one-unsized-column": dataTableOneUnsizedColumn,
|
|
163
|
-
"destructive-action-placement": destructiveActionPlacement,
|
|
164
|
-
"disabled-choice-says-why": disabledChoiceSaysWhy,
|
|
165
|
-
"disabled-control-appearance": disabledControlAppearance,
|
|
166
|
-
"settings-limit-in-footer": settingsLimitInFooter,
|
|
167
|
-
"ellipsis-opens-a-dialog": ellipsisOpensADialog,
|
|
168
|
-
"empty-state-owner": emptyStateOwner,
|
|
169
|
-
"input-group-owners": inputGroupOwners,
|
|
170
|
-
"kbd-children-no-modifier-text": kbdChildrenNoModifierText,
|
|
171
|
-
"menu-owner": menuOwner,
|
|
172
|
-
"middle-truncate-placement": middleTruncatePlacement,
|
|
173
|
-
"no-app-feature-component": noAppFeatureComponent,
|
|
174
|
-
"no-bare-button-label": noBareButtonLabel,
|
|
175
|
-
"no-choice-control-wrapper": noChoiceControlWrapper,
|
|
176
|
-
"no-comment": noComment,
|
|
177
|
-
"no-control-in-read-only-surface": noControlInReadOnlySurface,
|
|
178
|
-
"no-decorative-icon": noDecorativeIcon,
|
|
179
|
-
"no-deprecated-zod-api": noDeprecatedZodApi,
|
|
180
|
-
"no-global-patch": noGlobalPatch,
|
|
181
|
-
"no-hand-built-busy": noHandBuiltBusy,
|
|
182
|
-
"no-hand-built-theme-picker": noHandBuiltThemePicker,
|
|
183
|
-
"no-hand-built-tile": noHandBuiltTile,
|
|
184
|
-
"no-hand-formatted-date": noHandFormattedDate,
|
|
185
|
-
"no-hand-stacked-avatars": noHandStackedAvatars,
|
|
186
|
-
"no-nested-collapse": noNestedCollapse,
|
|
187
|
-
"no-package-component-redefinition": noPackageComponentRedefinition,
|
|
188
|
-
"no-search-in-form": noSearchInForm,
|
|
189
|
-
"no-shadcn-appearance-override": noShadcnAppearanceOverride,
|
|
190
|
-
"no-shell-prompt-in-snippet": noShellPromptInSnippet,
|
|
191
|
-
"no-stock-error-words": noStockErrorWords,
|
|
192
|
-
"no-subject-derived-expectation": noSubjectDerivedExpectation,
|
|
193
|
-
"no-tooltip-around-labelled-field": noTooltipAroundLabelledField,
|
|
194
|
-
"no-weight-class-on-strong-in-type-role": noWeightClassOnStrongInTypeRole,
|
|
195
|
-
"no-write-in-subscription": noWriteInSubscription,
|
|
196
|
-
"one-submenu-level": oneSubmenuLevel,
|
|
197
|
-
"one-toast-per-flow": oneToastPerFlow,
|
|
198
|
-
"option-count": optionCount,
|
|
199
|
-
"page-command-link-not-render": pageCommandLinkNotRender,
|
|
200
|
-
"page-composition": pageComposition,
|
|
201
|
-
"progress-needs-name": progressNeedsName,
|
|
202
|
-
"server-action-discipline": serverActionDiscipline,
|
|
203
|
-
"skeleton-in-region": skeletonInRegion,
|
|
204
|
-
"sortable-heading-uses-sort": sortableHeadingUsesSort,
|
|
205
|
-
"table-cell-inset-owner": tableCellInsetOwner,
|
|
206
|
-
"tabs-selection-is-controlled": tabsSelectionIsControlled,
|
|
207
|
-
"test-files-from-one-definition": testFilesFromOneDefinition,
|
|
208
|
-
"tooltip-only-arrow": tooltipOnlyArrow,
|
|
209
|
-
"tooltip-trigger-accessible-name": tooltipTriggerAccessibleName,
|
|
210
|
-
"workspace-declares-imports": workspaceDeclaresImports,
|
|
211
|
-
},
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
export const strictDesignSystemConfig = [
|
|
215
|
-
{
|
|
216
|
-
name: "nadicodeai/design-system-strict",
|
|
217
|
-
plugins: { nadicodeai: plugin },
|
|
218
|
-
rules: strictDesignSystemRules,
|
|
219
|
-
},
|
|
220
|
-
];
|
|
221
|
-
|
|
222
|
-
plugin.configs["flat/strict"] = strictDesignSystemConfig;
|
|
223
|
-
|
|
224
|
-
export default plugin;
|
|
@@ -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
|
-
};
|