@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
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: a `no-restricted-syntax` selector can match a
|
|
3
|
-
* conditional `variant`, but not that its condition reads a value the same
|
|
4
|
-
* element's `disabled` reads. That needs the two expressions compared.
|
|
5
|
-
*/
|
|
6
|
-
|
|
7
|
-
import { walk } from "./rendered-import.js";
|
|
8
|
-
|
|
9
|
-
const APPEARANCE_ATTRIBUTES = ["variant", "triggerVariant", "size"];
|
|
10
|
-
|
|
11
|
-
function valuePathsAndPrefixes(root, sourceCode) {
|
|
12
|
-
const names = new Set();
|
|
13
|
-
const collect = (node) => {
|
|
14
|
-
const pathTail = node.parent?.type === "MemberExpression" && !node.parent.computed;
|
|
15
|
-
if (node.type === "Identifier" && !pathTail) names.add(node.name);
|
|
16
|
-
if (node.type === "MemberExpression" && !node.computed) names.add(sourceCode.getText(node));
|
|
17
|
-
};
|
|
18
|
-
collect(root);
|
|
19
|
-
walk(root, sourceCode.visitorKeys, collect);
|
|
20
|
-
return names;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const attribute = (openingElement, name) =>
|
|
24
|
-
openingElement.attributes.find(
|
|
25
|
-
(candidate) => candidate.type === "JSXAttribute" && candidate.name.name === name,
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
export const disabledControlAppearance = {
|
|
29
|
-
meta: {
|
|
30
|
-
type: "problem",
|
|
31
|
-
docs: {
|
|
32
|
-
description:
|
|
33
|
-
"A control never chooses its variant from the condition that disables it, so a disabled control has one appearance.",
|
|
34
|
-
},
|
|
35
|
-
schema: [
|
|
36
|
-
{
|
|
37
|
-
type: "object",
|
|
38
|
-
properties: {
|
|
39
|
-
controls: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
40
|
-
},
|
|
41
|
-
additionalProperties: false,
|
|
42
|
-
},
|
|
43
|
-
],
|
|
44
|
-
defaultOptions: [{ controls: ["Button"] }],
|
|
45
|
-
messages: {
|
|
46
|
-
"variant-follows-disabled":
|
|
47
|
-
"{{control}} turns its {{attribute}} on `{{condition}}`, which also disables it, so its disabled state looks unlike every other disabled {{control}}. Keep one {{attribute}} and let `disabled` draw the state.",
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
create(context) {
|
|
51
|
-
const [{ controls }] = context.options;
|
|
52
|
-
const { sourceCode } = context;
|
|
53
|
-
return {
|
|
54
|
-
JSXOpeningElement(node) {
|
|
55
|
-
if (node.name.type !== "JSXIdentifier" || !controls.includes(node.name.name)) return;
|
|
56
|
-
const disabled = attribute(node, "disabled")?.value;
|
|
57
|
-
if (disabled?.type !== "JSXExpressionContainer") return;
|
|
58
|
-
const disabling = valuePathsAndPrefixes(disabled.expression, sourceCode);
|
|
59
|
-
for (const name of APPEARANCE_ATTRIBUTES) {
|
|
60
|
-
const chosen = attribute(node, name)?.value;
|
|
61
|
-
if (chosen?.type !== "JSXExpressionContainer") continue;
|
|
62
|
-
if (chosen.expression.type !== "ConditionalExpression") continue;
|
|
63
|
-
const { test } = chosen.expression;
|
|
64
|
-
if (![...valuePathsAndPrefixes(test, sourceCode)].some((read) => disabling.has(read)))
|
|
65
|
-
continue;
|
|
66
|
-
context.report({
|
|
67
|
-
node: chosen,
|
|
68
|
-
messageId: "variant-follows-disabled",
|
|
69
|
-
data: { control: node.name.name, attribute: name, condition: sourceCode.getText(test) },
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
},
|
|
75
|
-
};
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: `no-restricted-syntax` can match a label that
|
|
3
|
-
* ends in an ellipsis, but not what its command does: whether the handler
|
|
4
|
-
* opens something (a call that opens or passes `true`), runs, or navigates.
|
|
5
|
-
*/
|
|
6
|
-
import { childText, keyName, literalText } from "./literal-text.js";
|
|
7
|
-
import { renderedImports } from "./rendered-import.js";
|
|
8
|
-
|
|
9
|
-
const ENDS_IN_ELLIPSIS = /(?:…|\.\.\.)\s*$/;
|
|
10
|
-
const COMMAND_KEYS = new Set(["onClick", "onSelect", "link", "render", "icon"]);
|
|
11
|
-
const HANDLER_KEYS = new Set(["onClick", "onSelect"]);
|
|
12
|
-
const MENU_ROWS = new Set([
|
|
13
|
-
"DropdownMenuItem",
|
|
14
|
-
"DropdownMenuCommand",
|
|
15
|
-
"ContextMenuItem",
|
|
16
|
-
"MenubarItem",
|
|
17
|
-
]);
|
|
18
|
-
const MENU_LINKS = new Set(["DropdownMenuLink", "ContextMenuLink"]);
|
|
19
|
-
const FROM_DESIGN_SYSTEM = /^(@nadicodeai\/ui(\/|$)|@\/|\.)/;
|
|
20
|
-
const OPENING_CALL = /^(?:open|show)(?:[A-Z]|$)/;
|
|
21
|
-
const FUNCTIONS = new Set(["ArrowFunctionExpression", "FunctionExpression"]);
|
|
22
|
-
|
|
23
|
-
const calleeName = (callee) => {
|
|
24
|
-
if (callee.type === "Identifier") return callee.name;
|
|
25
|
-
if (callee.type === "MemberExpression" && !callee.computed) return callee.property.name;
|
|
26
|
-
return null;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
function opensSomething(node, visitorKeys) {
|
|
30
|
-
const calls = [];
|
|
31
|
-
const stack = [node.body];
|
|
32
|
-
while (stack.length > 0) {
|
|
33
|
-
const current = stack.pop();
|
|
34
|
-
if (current.type === "CallExpression") calls.push(current);
|
|
35
|
-
for (const key of visitorKeys[current.type] ?? []) {
|
|
36
|
-
const value = current[key];
|
|
37
|
-
for (const child of Array.isArray(value) ? value : [value]) {
|
|
38
|
-
if (child?.type) stack.push(child);
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
return calls.some(
|
|
43
|
-
(call) =>
|
|
44
|
-
OPENING_CALL.test(calleeName(call.callee) ?? "") ||
|
|
45
|
-
call.arguments.some((argument) => argument.type === "Literal" && argument.value === true),
|
|
46
|
-
);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export const ellipsisOpensADialog = {
|
|
50
|
-
meta: {
|
|
51
|
-
type: "problem",
|
|
52
|
-
docs: {
|
|
53
|
-
description: "A command's label ends in an ellipsis only when the command opens a dialog.",
|
|
54
|
-
},
|
|
55
|
-
schema: [],
|
|
56
|
-
messages: {
|
|
57
|
-
"ellipsis-without-dialog":
|
|
58
|
-
'An ellipsis says "this asks for more before it acts". "{{label}}" navigates or runs at once, so drop the ellipsis; keep it only on a command that opens a dialog.',
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
|
-
create(context) {
|
|
62
|
-
const importOf = renderedImports(context);
|
|
63
|
-
const { visitorKeys } = context.sourceCode;
|
|
64
|
-
|
|
65
|
-
function reportsNoDialog(handler) {
|
|
66
|
-
return (
|
|
67
|
-
handler !== undefined &&
|
|
68
|
-
FUNCTIONS.has(handler.type) &&
|
|
69
|
-
!opensSomething(handler, visitorKeys)
|
|
70
|
-
);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
ObjectExpression(node) {
|
|
75
|
-
const keys = new Map(node.properties.map((property) => [keyName(property), property]));
|
|
76
|
-
const label = literalText(keys.get("label")?.value);
|
|
77
|
-
if (label === null || !ENDS_IN_ELLIPSIS.test(label)) return;
|
|
78
|
-
if (![...keys.keys()].some((key) => COMMAND_KEYS.has(key))) return;
|
|
79
|
-
if (keys.has("render")) return;
|
|
80
|
-
const handlerProperty = [...keys].find(([key]) => HANDLER_KEYS.has(key))?.[1];
|
|
81
|
-
const navigates = keys.has("link");
|
|
82
|
-
if (navigates || reportsNoDialog(handlerProperty?.value)) {
|
|
83
|
-
context.report({
|
|
84
|
-
node: keys.get("label").value,
|
|
85
|
-
messageId: "ellipsis-without-dialog",
|
|
86
|
-
data: { label },
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
},
|
|
90
|
-
JSXElement(node) {
|
|
91
|
-
const imported = importOf(node.openingElement.name);
|
|
92
|
-
if (!imported || !FROM_DESIGN_SYSTEM.test(imported.source)) return;
|
|
93
|
-
const row = MENU_ROWS.has(imported.name);
|
|
94
|
-
if (!row && !MENU_LINKS.has(imported.name)) return;
|
|
95
|
-
const label = childText(node);
|
|
96
|
-
if (label === null || !ENDS_IN_ELLIPSIS.test(label)) return;
|
|
97
|
-
const handler = node.openingElement.attributes.find(
|
|
98
|
-
(attribute) => attribute.type === "JSXAttribute" && HANDLER_KEYS.has(attribute.name.name),
|
|
99
|
-
)?.value?.expression;
|
|
100
|
-
if (!row || reportsNoDialog(handler)) {
|
|
101
|
-
context.report({
|
|
102
|
-
node: node.openingElement,
|
|
103
|
-
messageId: "ellipsis-without-dialog",
|
|
104
|
-
data: { label },
|
|
105
|
-
});
|
|
106
|
-
}
|
|
107
|
-
},
|
|
108
|
-
};
|
|
109
|
-
},
|
|
110
|
-
};
|
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: a `no-restricted-syntax` selector can match
|
|
3
|
-
* `items.length === 0 ? <X/> : …`, but not which branch is the empty one once
|
|
4
|
-
* the test is turned around (`items.length > 0 ? <List/> : <p/>`, an early
|
|
5
|
-
* `if` whose empty branch is the next `return`), so it would either miss those
|
|
6
|
-
* or report the list branch.
|
|
7
|
-
*/
|
|
8
|
-
import { renderedImports } from "./rendered-import.js";
|
|
9
|
-
|
|
10
|
-
const OWNERS = [
|
|
11
|
-
"EmptyState",
|
|
12
|
-
"CollectionPage",
|
|
13
|
-
"RecordList",
|
|
14
|
-
"ActivityFeed",
|
|
15
|
-
"LibraryShelf",
|
|
16
|
-
"CatalogDrawer",
|
|
17
|
-
"DataTable",
|
|
18
|
-
"CommandEmpty",
|
|
19
|
-
"ComboboxEmpty",
|
|
20
|
-
];
|
|
21
|
-
|
|
22
|
-
const SIZE_PROPERTIES = new Set(["length", "size"]);
|
|
23
|
-
const COUNT = /^count$|[a-z0-9]Count$/;
|
|
24
|
-
|
|
25
|
-
const unwrapChain = (node) => (node.type === "ChainExpression" ? node.expression : node);
|
|
26
|
-
|
|
27
|
-
const isSize = (node) => {
|
|
28
|
-
const read = unwrapChain(node);
|
|
29
|
-
if (read.type === "Identifier") return COUNT.test(read.name);
|
|
30
|
-
if (read.type !== "MemberExpression" || read.computed) return false;
|
|
31
|
-
return SIZE_PROPERTIES.has(read.property.name) || COUNT.test(read.property.name);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const MIRRORED = { "<": ">", ">": "<", "<=": ">=", ">=": "<=" };
|
|
35
|
-
|
|
36
|
-
const EMPTY_WHEN = {
|
|
37
|
-
"===": { 0: "empty" },
|
|
38
|
-
"==": { 0: "empty" },
|
|
39
|
-
"!==": { 0: "present" },
|
|
40
|
-
"!=": { 0: "present" },
|
|
41
|
-
"<": { 1: "empty" },
|
|
42
|
-
"<=": { 0: "empty" },
|
|
43
|
-
">": { 0: "present" },
|
|
44
|
-
">=": { 1: "present" },
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
function emptiness(test) {
|
|
48
|
-
const node = unwrapChain(test);
|
|
49
|
-
if (isSize(node)) return "present";
|
|
50
|
-
if (node.type === "UnaryExpression" && node.operator === "!") {
|
|
51
|
-
const inner = emptiness(node.argument);
|
|
52
|
-
if (inner === "empty") return "present";
|
|
53
|
-
if (inner === "present") return "empty";
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
if (node.type === "LogicalExpression") {
|
|
57
|
-
const sides = [emptiness(node.left), emptiness(node.right)];
|
|
58
|
-
if (node.operator === "&&" && sides.includes("empty")) return "empty";
|
|
59
|
-
if (node.operator === "||" && sides.includes("present")) return "present";
|
|
60
|
-
return null;
|
|
61
|
-
}
|
|
62
|
-
if (node.type !== "BinaryExpression") return null;
|
|
63
|
-
let { operator, left, right } = node;
|
|
64
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- ESTree types Literal.value as string | number | boolean | RegExp | bigint | null; only typeof picks the number.
|
|
65
|
-
if (isSize(right) && typeof left.value === "number") {
|
|
66
|
-
[left, right] = [right, left];
|
|
67
|
-
operator = MIRRORED[operator] ?? operator;
|
|
68
|
-
}
|
|
69
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- ESTree types Literal.value as string | number | boolean | RegExp | bigint | null; only typeof picks the number.
|
|
70
|
-
if (!isSize(left) || typeof right.value !== "number") return null;
|
|
71
|
-
return EMPTY_WHEN[operator]?.[right.value] ?? null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
function returned(statement) {
|
|
75
|
-
if (!statement) return null;
|
|
76
|
-
if (statement.type === "ReturnStatement") return statement.argument;
|
|
77
|
-
if (statement.type === "BlockStatement") {
|
|
78
|
-
const found = statement.body.find((inner) => inner.type === "ReturnStatement");
|
|
79
|
-
return found?.argument ?? null;
|
|
80
|
-
}
|
|
81
|
-
return null;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function nextStatement(statement) {
|
|
85
|
-
const body = statement.parent.body;
|
|
86
|
-
if (!Array.isArray(body)) return null;
|
|
87
|
-
return body[body.indexOf(statement) + 1] ?? null;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
export const emptyStateOwner = {
|
|
91
|
-
meta: {
|
|
92
|
-
type: "problem",
|
|
93
|
-
docs: {
|
|
94
|
-
description: "An absence renders EmptyState, or a composition that draws its own.",
|
|
95
|
-
},
|
|
96
|
-
schema: [
|
|
97
|
-
{
|
|
98
|
-
type: "object",
|
|
99
|
-
properties: {
|
|
100
|
-
owners: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
101
|
-
},
|
|
102
|
-
additionalProperties: false,
|
|
103
|
-
},
|
|
104
|
-
],
|
|
105
|
-
defaultOptions: [{ owners: [] }],
|
|
106
|
-
messages: {
|
|
107
|
-
"absence-branch":
|
|
108
|
-
"An absence is EmptyState: firstUse with the section's picture, nothingYet, noResults with its clear, or failed with its retry.",
|
|
109
|
-
},
|
|
110
|
-
},
|
|
111
|
-
create(context) {
|
|
112
|
-
const owners = new Set([...OWNERS, ...context.options[0].owners]);
|
|
113
|
-
const importOf = renderedImports(context);
|
|
114
|
-
const drawnByAnOwner = (element) =>
|
|
115
|
-
owners.has(importOf(element.openingElement.name)?.name ?? "");
|
|
116
|
-
|
|
117
|
-
function checkEmptyBranch(branch) {
|
|
118
|
-
const drawsItsOwn =
|
|
119
|
-
branch?.type === "JSXFragment" ||
|
|
120
|
-
(branch?.type === "JSXElement" && !drawnByAnOwner(branch));
|
|
121
|
-
if (drawsItsOwn) context.report({ node: branch, messageId: "absence-branch" });
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return {
|
|
125
|
-
ConditionalExpression(node) {
|
|
126
|
-
const verdict = emptiness(node.test);
|
|
127
|
-
if (verdict === "empty") checkEmptyBranch(node.consequent);
|
|
128
|
-
else if (verdict === "present") checkEmptyBranch(node.alternate);
|
|
129
|
-
},
|
|
130
|
-
LogicalExpression(node) {
|
|
131
|
-
if (node.operator === "&&" && emptiness(node.left) === "empty") {
|
|
132
|
-
checkEmptyBranch(node.right);
|
|
133
|
-
}
|
|
134
|
-
},
|
|
135
|
-
IfStatement(node) {
|
|
136
|
-
const verdict = emptiness(node.test);
|
|
137
|
-
if (verdict === "empty") {
|
|
138
|
-
checkEmptyBranch(returned(node.consequent));
|
|
139
|
-
} else if (verdict === "present" && returned(node.consequent)) {
|
|
140
|
-
checkEmptyBranch(returned(node.alternate ?? nextStatement(node)));
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
};
|
|
144
|
-
},
|
|
145
|
-
};
|
|
@@ -1,155 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: a `no-restricted-syntax` selector can match an
|
|
3
|
-
* `InputGroupAddon` holding `<Search/>`, but not an element joined to its import
|
|
4
|
-
* (a renamed or namespaced one), nor the shape of a whole group: only addons of
|
|
5
|
-
* fixed text or one icon around one input, a group with another button, a
|
|
6
|
-
* block addon or a textarea allowed. That needs the group's children together.
|
|
7
|
-
*/
|
|
8
|
-
import {
|
|
9
|
-
PACKAGE_BARREL,
|
|
10
|
-
findBelow,
|
|
11
|
-
partOf,
|
|
12
|
-
renderedImports,
|
|
13
|
-
staticAttribute,
|
|
14
|
-
walk,
|
|
15
|
-
} from "./rendered-import.js";
|
|
16
|
-
|
|
17
|
-
const GROUP_PARTS = [
|
|
18
|
-
"InputGroup",
|
|
19
|
-
"InputGroupAddon",
|
|
20
|
-
"InputGroupButton",
|
|
21
|
-
"InputGroupInput",
|
|
22
|
-
"InputGroupText",
|
|
23
|
-
"InputGroupTextarea",
|
|
24
|
-
];
|
|
25
|
-
|
|
26
|
-
const GROUP_MODULES = [
|
|
27
|
-
"@nadicodeai/ui/components/input-group",
|
|
28
|
-
PACKAGE_BARREL,
|
|
29
|
-
"@/components/input-group",
|
|
30
|
-
];
|
|
31
|
-
|
|
32
|
-
const PARTS = new Map(
|
|
33
|
-
GROUP_PARTS.map((part) => [part, GROUP_MODULES.map((from) => ({ from, part }))]),
|
|
34
|
-
);
|
|
35
|
-
|
|
36
|
-
const ICON_MODULES = new Set(["lucide-react", "@nadicodeai/ui/components/house-icons"]);
|
|
37
|
-
const SEARCH_ICONS = new Set(["Search", "SearchIcon"]);
|
|
38
|
-
const CLEAR_ICONS = new Set(["X", "XIcon"]);
|
|
39
|
-
const INLINE_ALIGNS = new Set(["inline-start", "inline-end"]);
|
|
40
|
-
|
|
41
|
-
const significant = (element) =>
|
|
42
|
-
element.children.filter(
|
|
43
|
-
(child) =>
|
|
44
|
-
!(child.type === "JSXText" && child.value.trim() === "") &&
|
|
45
|
-
!(child.type === "JSXExpressionContainer" && child.expression.type === "JSXEmptyExpression"),
|
|
46
|
-
);
|
|
47
|
-
|
|
48
|
-
const isFixedText = (child) =>
|
|
49
|
-
child.type === "JSXText" ||
|
|
50
|
-
(child.type === "JSXExpressionContainer" && child.expression.type === "Literal");
|
|
51
|
-
|
|
52
|
-
export const inputGroupOwners = {
|
|
53
|
-
meta: {
|
|
54
|
-
type: "problem",
|
|
55
|
-
docs: {
|
|
56
|
-
description:
|
|
57
|
-
"A search box, a box that clears and fixed text or an icon in a box are drawn by the component that owns them, not by hand from InputGroup parts.",
|
|
58
|
-
},
|
|
59
|
-
schema: [],
|
|
60
|
-
messages: {
|
|
61
|
-
"hand-built-field":
|
|
62
|
-
"Use the component that owns this field: CollectionSearch for a collection's search, ClearableInput for a box that clears, Input's prefix and suffix for fixed text or an icon in the box.",
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
create(context) {
|
|
66
|
-
const importOf = renderedImports(context);
|
|
67
|
-
const { visitorKeys } = context.sourceCode;
|
|
68
|
-
|
|
69
|
-
const partName = (element) =>
|
|
70
|
-
partOf(importOf(element.openingElement.name), PARTS, context.filename);
|
|
71
|
-
|
|
72
|
-
const iconName = (element) => {
|
|
73
|
-
const imported = importOf(element.openingElement.name);
|
|
74
|
-
return imported && ICON_MODULES.has(imported.source) ? imported.name : null;
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
const holdsIcon = (element, names) =>
|
|
78
|
-
findBelow(
|
|
79
|
-
element,
|
|
80
|
-
visitorKeys,
|
|
81
|
-
(node) => node.type === "JSXElement" && names.has(iconName(node)),
|
|
82
|
-
) !== null;
|
|
83
|
-
|
|
84
|
-
function holdsTextOrOneIcon(element) {
|
|
85
|
-
const content = significant(element);
|
|
86
|
-
if (content.length === 0) return false;
|
|
87
|
-
if (content.length === 1 && content[0].type === "JSXElement") {
|
|
88
|
-
const [only] = content;
|
|
89
|
-
if (iconName(only)) return true;
|
|
90
|
-
return partName(only) === "InputGroupText" && holdsTextOrOneIcon(only);
|
|
91
|
-
}
|
|
92
|
-
return content.every(isFixedText);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function readGroup(group) {
|
|
96
|
-
const facts = {
|
|
97
|
-
inputs: 0,
|
|
98
|
-
hasTextarea: false,
|
|
99
|
-
hasBlockAddon: false,
|
|
100
|
-
hasSearchAddon: false,
|
|
101
|
-
hasClearButton: false,
|
|
102
|
-
hasOtherButton: false,
|
|
103
|
-
};
|
|
104
|
-
walk(group, visitorKeys, (node) => {
|
|
105
|
-
if (node.type !== "JSXElement") return true;
|
|
106
|
-
const part = partName(node);
|
|
107
|
-
if (part === "InputGroup") return false;
|
|
108
|
-
if (part === "InputGroupInput") facts.inputs += 1;
|
|
109
|
-
if (part === "InputGroupTextarea") facts.hasTextarea = true;
|
|
110
|
-
if (part === "InputGroupAddon") {
|
|
111
|
-
const { openingElement } = node;
|
|
112
|
-
const hasAlign = openingElement.attributes.some(
|
|
113
|
-
(attribute) => attribute.type === "JSXAttribute" && attribute.name.name === "align",
|
|
114
|
-
);
|
|
115
|
-
if (hasAlign && !INLINE_ALIGNS.has(staticAttribute(openingElement, "align"))) {
|
|
116
|
-
facts.hasBlockAddon = true;
|
|
117
|
-
}
|
|
118
|
-
if (holdsIcon(node, SEARCH_ICONS)) facts.hasSearchAddon = true;
|
|
119
|
-
}
|
|
120
|
-
if (part === "InputGroupButton") {
|
|
121
|
-
if (holdsIcon(node, CLEAR_ICONS)) facts.hasClearButton = true;
|
|
122
|
-
else facts.hasOtherButton = true;
|
|
123
|
-
}
|
|
124
|
-
return true;
|
|
125
|
-
});
|
|
126
|
-
return facts;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
function isFixedAdornments(group) {
|
|
130
|
-
const children = significant(group);
|
|
131
|
-
if (!children.every((child) => child.type === "JSXElement")) return false;
|
|
132
|
-
const parts = children.map(partName);
|
|
133
|
-
const addons = children.filter((_, index) => parts[index] === "InputGroupAddon");
|
|
134
|
-
const inputs = parts.filter((part) => part === "InputGroupInput");
|
|
135
|
-
return (
|
|
136
|
-
inputs.length === 1 &&
|
|
137
|
-
addons.length > 0 &&
|
|
138
|
-
addons.length + inputs.length === children.length &&
|
|
139
|
-
addons.every(holdsTextOrOneIcon)
|
|
140
|
-
);
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
return {
|
|
144
|
-
JSXElement(node) {
|
|
145
|
-
if (partName(node) !== "InputGroup") return;
|
|
146
|
-
const facts = readGroup(node);
|
|
147
|
-
if (facts.hasTextarea || facts.hasBlockAddon || facts.hasOtherButton) return;
|
|
148
|
-
const isSearch = facts.inputs > 0 && facts.hasSearchAddon;
|
|
149
|
-
if (isSearch || facts.hasClearButton || isFixedAdornments(node)) {
|
|
150
|
-
context.report({ node: node.openingElement, messageId: "hand-built-field" });
|
|
151
|
-
}
|
|
152
|
-
},
|
|
153
|
-
};
|
|
154
|
-
},
|
|
155
|
-
};
|
|
@@ -1,200 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* The className reader of no-shadcn-appearance-override.
|
|
3
|
-
*
|
|
4
|
-
* It reads LITERALS: string literals and template quasis anywhere inside a
|
|
5
|
-
* `className` expression. On top
|
|
6
|
-
* of that it takes EXACTLY ONE HOP: an identifier resolves to a same-file
|
|
7
|
-
* `const` string literal, and an identifier or a `TABLE.key` lookup resolves to
|
|
8
|
-
* a same-file `const` object whose string values are then read as classes. A
|
|
9
|
-
* `{...props}` spread resolves the same way and contributes that object's
|
|
10
|
-
* `className` value alone.
|
|
11
|
-
*
|
|
12
|
-
* One hop, and no further: nothing chains through a second binding, nothing
|
|
13
|
-
* crosses a file, nothing evaluates a condition or a template, and no value the
|
|
14
|
-
* hop cannot see is guessed at. That is the minimum that holds the package
|
|
15
|
-
* repaint ban (`packages/design-system/DESIGN.md`, "Components") against the way
|
|
16
|
-
* this repository actually authors classes — a tone table beside its component
|
|
17
|
-
* and a lookup at the call site, as `VALUE_TONE_CLASS` is in
|
|
18
|
-
* `packages/ui/src/components/kpi-card.tsx`. Without the
|
|
19
|
-
* hop, `<Card className={tones.warning} />` repaints a primitive and no rule
|
|
20
|
-
* sees it; with it, the table's own values are read.
|
|
21
|
-
*
|
|
22
|
-
* Standard tool checked first: `eslint-plugin-better-tailwindcss`
|
|
23
|
-
* reads lookup tables through a `variable` selector with an `objectValues`
|
|
24
|
-
* matcher, and the root config uses exactly that so a table's classes are
|
|
25
|
-
* checked for existence. What it cannot do is say WHERE a value lands: its
|
|
26
|
-
* rules judge a class on its own, never against the JSX element it is applied
|
|
27
|
-
* to. "Not on an imported primitive" is a relationship between a class and an
|
|
28
|
-
* element, so it stays here.
|
|
29
|
-
*
|
|
30
|
-
* Anything past one hop remains out of reach by design and is a review finding,
|
|
31
|
-
* not a lint finding.
|
|
32
|
-
*/
|
|
33
|
-
|
|
34
|
-
/** The same-file binding an identifier refers to, or undefined. */
|
|
35
|
-
function findVariable(scope, name) {
|
|
36
|
-
for (let current = scope; current; current = current.upper) {
|
|
37
|
-
const variable = current.set.get(name);
|
|
38
|
-
if (variable) return variable;
|
|
39
|
-
}
|
|
40
|
-
return undefined;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* The initializer of the same-file `const` an identifier names. Undefined for
|
|
45
|
-
* imports, parameters, `let`/`var`, and anything declared more than once —
|
|
46
|
-
* every shape whose value is not fixed by this file alone.
|
|
47
|
-
*/
|
|
48
|
-
function constInitializer(identifier, context) {
|
|
49
|
-
if (!context?.sourceCode) return undefined;
|
|
50
|
-
const variable = findVariable(context.sourceCode.getScope(identifier), identifier.name);
|
|
51
|
-
if (!variable || variable.defs.length !== 1) return undefined;
|
|
52
|
-
const [definition] = variable.defs;
|
|
53
|
-
if (definition.type !== "Variable" || definition.parent?.kind !== "const") return undefined;
|
|
54
|
-
return definition.node.init ?? undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** Every string-literal value of an object literal; a lookup can select any of them. */
|
|
58
|
-
function objectStringValues(objectExpression) {
|
|
59
|
-
const values = [];
|
|
60
|
-
for (const property of objectExpression.properties) {
|
|
61
|
-
if (property.type !== "Property") continue;
|
|
62
|
-
// 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.
|
|
63
|
-
if (property.value.type === "Literal" && typeof property.value.value === "string") {
|
|
64
|
-
values.push(property.value.value);
|
|
65
|
-
}
|
|
66
|
-
}
|
|
67
|
-
return values;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/** The one hop: the class strings a resolved `const` contributes, if any. */
|
|
71
|
-
function resolvedFragments(identifier, context, { objectsOnly = false } = {}) {
|
|
72
|
-
const initializer = constInitializer(identifier, context);
|
|
73
|
-
if (!initializer) return [];
|
|
74
|
-
if (initializer.type === "ObjectExpression") return objectStringValues(initializer);
|
|
75
|
-
if (objectsOnly) return [];
|
|
76
|
-
const fragments = [];
|
|
77
|
-
// Read the local composition, but do not resolve another binding from it.
|
|
78
|
-
literalFragments(initializer, fragments, { skipPropertyKeys: false, context: undefined });
|
|
79
|
-
return fragments;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** Every string literal and template chunk in the subtree, in source order. */
|
|
83
|
-
function literalFragments(node, fragments, { skipPropertyKeys, context }) {
|
|
84
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- walks the generic AST, where only an object whose type is a string is a node.
|
|
85
|
-
if (!node || typeof node.type !== "string") return;
|
|
86
|
-
|
|
87
|
-
if (node.type === "Literal") {
|
|
88
|
-
// 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.
|
|
89
|
-
if (typeof node.value === "string") fragments.push(node.value);
|
|
90
|
-
return;
|
|
91
|
-
}
|
|
92
|
-
if (node.type === "TemplateElement") {
|
|
93
|
-
fragments.push(node.value.cooked ?? node.value.raw);
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
if (
|
|
97
|
-
node.type === "CallExpression" &&
|
|
98
|
-
node.callee.type === "MemberExpression" &&
|
|
99
|
-
!node.callee.computed &&
|
|
100
|
-
(node.callee.property.name === "join" ||
|
|
101
|
-
(node.callee.property.name === "filter" &&
|
|
102
|
-
node.arguments.length === 1 &&
|
|
103
|
-
node.arguments[0].type === "Identifier" &&
|
|
104
|
-
node.arguments[0].name === "Boolean"))
|
|
105
|
-
) {
|
|
106
|
-
literalFragments(node.callee.object, fragments, { skipPropertyKeys, context });
|
|
107
|
-
return;
|
|
108
|
-
}
|
|
109
|
-
// `VALUE_TONE_CLASS[tone]` selects a class string, it does
|
|
110
|
-
// not contain one: the names inside the lookup are keys. The hop reads the
|
|
111
|
-
// table instead — any of its values can be the one selected — and the
|
|
112
|
-
// property is never descended into.
|
|
113
|
-
if (node.type === "MemberExpression") {
|
|
114
|
-
if (node.object.type === "Identifier") {
|
|
115
|
-
fragments.push(...resolvedFragments(node.object, context, { objectsOnly: true }));
|
|
116
|
-
}
|
|
117
|
-
return;
|
|
118
|
-
}
|
|
119
|
-
if (node.type === "Identifier") {
|
|
120
|
-
fragments.push(...resolvedFragments(node, context));
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
for (const [key, value] of Object.entries(node)) {
|
|
125
|
-
if (key === "parent" || key === "loc" || key === "range") continue;
|
|
126
|
-
// A cva variant map's keys name variants, not classes.
|
|
127
|
-
if (skipPropertyKeys && key === "key" && node.type === "Property" && !node.computed) continue;
|
|
128
|
-
if (Array.isArray(value)) {
|
|
129
|
-
for (const child of value) literalFragments(child, fragments, { skipPropertyKeys, context });
|
|
130
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- walks the generic AST, where only an object whose type is a string is a node.
|
|
131
|
-
} else if (value && typeof value === "object") {
|
|
132
|
-
literalFragments(value, fragments, { skipPropertyKeys, context });
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function utilities(node, { skipPropertyKeys = false, context } = {}) {
|
|
138
|
-
const fragments = [];
|
|
139
|
-
literalFragments(node, fragments, { skipPropertyKeys, context });
|
|
140
|
-
return fragments.flatMap((fragment) => fragment.split(/\s+/).filter(Boolean));
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* The class utilities written in one `className` JSX attribute. Pass `context`
|
|
145
|
-
* to take the one hop; without it the reader stays literal-only.
|
|
146
|
-
*/
|
|
147
|
-
export function classUtilities(attribute, context, includeSlots = false) {
|
|
148
|
-
if (attribute?.type !== "JSXAttribute" || !isClassProperty(attribute.name.name, includeSlots))
|
|
149
|
-
return [];
|
|
150
|
-
return utilities(attribute.value, { context });
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
function isClassProperty(name, includeSlots) {
|
|
154
|
-
return name === "className" || (includeSlots && /ClassName$/.test(name));
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** The `className` a spread object carries — its own key only, not its other values. */
|
|
158
|
-
function spreadClassUtilities(attribute, context, includeSlots) {
|
|
159
|
-
if (attribute.argument.type !== "Identifier") return [];
|
|
160
|
-
const initializer = constInitializer(attribute.argument, context);
|
|
161
|
-
if (initializer?.type !== "ObjectExpression") return [];
|
|
162
|
-
const values = [];
|
|
163
|
-
for (const property of initializer.properties) {
|
|
164
|
-
if (property.type !== "Property" || property.computed) continue;
|
|
165
|
-
const name = property.key.type === "Identifier" ? property.key.name : property.key.value;
|
|
166
|
-
if (!isClassProperty(name, includeSlots) && name !== "class") continue;
|
|
167
|
-
// 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.
|
|
168
|
-
if (property.value.type !== "Literal" || typeof property.value.value !== "string") continue;
|
|
169
|
-
values.push(...property.value.value.split(/\s+/).filter(Boolean));
|
|
170
|
-
}
|
|
171
|
-
return values;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
/** The class utilities written across every attribute of one element, spreads included. */
|
|
175
|
-
export function openingElementClassUtilities(node, context, includeSlots = false) {
|
|
176
|
-
return node.attributes.flatMap((attribute) =>
|
|
177
|
-
attribute.type === "JSXSpreadAttribute"
|
|
178
|
-
? spreadClassUtilities(attribute, context, includeSlots)
|
|
179
|
-
: classUtilities(attribute, context, includeSlots),
|
|
180
|
-
);
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
/*
|
|
184
|
-
* Package ownership as a per-rule option (#821, #822). `@nadicodeai/ui`'s own
|
|
185
|
-
* source is the place the primitives are BUILT, so a rule that polices what
|
|
186
|
-
* callers do to a primitive has nothing to say there — that is the "exempt"
|
|
187
|
-
* default. A rule whose invariant holds inside the package too selects "lint".
|
|
188
|
-
*/
|
|
189
|
-
const PACKAGE_PRIMITIVE_PATH = "packages/ui/src/";
|
|
190
|
-
|
|
191
|
-
export const packageOwnedPrimitivesSchema = {
|
|
192
|
-
type: "object",
|
|
193
|
-
properties: { packageOwnedPrimitives: { enum: ["exempt", "lint"] } },
|
|
194
|
-
additionalProperties: false,
|
|
195
|
-
};
|
|
196
|
-
|
|
197
|
-
export function skipsPackageOwnedPrimitive(context) {
|
|
198
|
-
if (context.options?.[0]?.packageOwnedPrimitives === "lint") return false;
|
|
199
|
-
return context.filename.replaceAll("\\", "/").includes(PACKAGE_PRIMITIVE_PATH);
|
|
200
|
-
}
|