@nadicodeai/ui 12.1.164 → 12.1.166
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -9
- package/README.md +2 -17
- package/dist/components/accordion.d.ts.map +1 -1
- package/dist/components/accordion.js +2 -1
- package/dist/components/account-menu.d.ts.map +1 -1
- package/dist/components/account-menu.js +5 -3
- package/dist/components/activity-calendar.d.ts.map +1 -1
- package/dist/components/activity-calendar.js +3 -2
- package/dist/components/agent-card.js +1 -1
- package/dist/components/agent-orb.d.ts.map +1 -1
- package/dist/components/agent-orb.js +1 -0
- package/dist/components/agent-profile.d.ts.map +1 -1
- package/dist/components/agent-profile.js +7 -4
- package/dist/components/alert-dialog.d.ts +1 -1
- package/dist/components/alert-dialog.d.ts.map +1 -1
- package/dist/components/alert-dialog.js +3 -3
- package/dist/components/app-mark.js +1 -1
- package/dist/components/avatar.js +8 -8
- package/dist/components/badge.d.ts +11 -16
- package/dist/components/badge.d.ts.map +1 -1
- package/dist/components/badge.js +7 -43
- package/dist/components/breadcrumb.d.ts.map +1 -1
- package/dist/components/breadcrumb.js +3 -2
- package/dist/components/button.d.ts +5 -21
- package/dist/components/button.d.ts.map +1 -1
- package/dist/components/button.js +6 -76
- package/dist/components/calendar.d.ts +1 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +3 -3
- package/dist/components/card-row.js +1 -1
- package/dist/components/card.d.ts +4 -1
- package/dist/components/card.d.ts.map +1 -1
- package/dist/components/card.js +3 -2
- package/dist/components/catalog-drawer.d.ts.map +1 -1
- package/dist/components/catalog-drawer.js +3 -2
- package/dist/components/chart-axis.d.ts.map +1 -1
- package/dist/components/chart-axis.js +0 -2
- package/dist/components/chart-bar-horizontal.d.ts.map +1 -1
- package/dist/components/chart-bar-horizontal.js +16 -14
- package/dist/components/chart-card.d.ts.map +1 -1
- package/dist/components/chart-card.js +0 -1
- package/dist/components/chart.d.ts.map +1 -1
- package/dist/components/chart.js +18 -32
- package/dist/components/checkbox.d.ts +3 -2
- package/dist/components/checkbox.d.ts.map +1 -1
- package/dist/components/checkbox.js +4 -3
- package/dist/components/choicebox.d.ts +5 -5
- package/dist/components/choicebox.d.ts.map +1 -1
- package/dist/components/choicebox.js +6 -5
- package/dist/components/clearable-input.d.ts.map +1 -1
- package/dist/components/clearable-input.js +2 -2
- package/dist/components/code-block.d.ts.map +1 -1
- package/dist/components/code-block.js +4 -2
- package/dist/components/collapsible.js +1 -1
- package/dist/components/collection-filters.d.ts.map +1 -1
- package/dist/components/collection-filters.js +1 -0
- package/dist/components/collection-page.js +1 -1
- package/dist/components/collection-sort.d.ts.map +1 -1
- package/dist/components/collection-sort.js +1 -4
- package/dist/components/collection-toolbar.js +1 -1
- package/dist/components/combobox.js +4 -4
- package/dist/components/command.d.ts.map +1 -1
- package/dist/components/command.js +1 -1
- package/dist/components/confirm-dialog.js +2 -2
- package/dist/components/context-menu.d.ts +1 -1
- package/dist/components/context-menu.d.ts.map +1 -1
- package/dist/components/copy-button.js +1 -1
- package/dist/components/data-table.d.ts +11 -3
- package/dist/components/data-table.d.ts.map +1 -1
- package/dist/components/data-table.js +12 -4
- package/dist/components/dialog.d.ts +1 -1
- package/dist/components/dialog.d.ts.map +1 -1
- package/dist/components/dialog.js +2 -2
- package/dist/components/drawer.d.ts +4 -4
- package/dist/components/drawer.d.ts.map +1 -1
- package/dist/components/drawer.js +14 -15
- package/dist/components/dropdown-menu.d.ts +1 -1
- package/dist/components/dropdown-menu.d.ts.map +1 -1
- package/dist/components/dropdown-menu.js +9 -5
- package/dist/components/empty-art.js +1 -1
- package/dist/components/empty-state.js +2 -2
- package/dist/components/entry-scene.d.ts.map +1 -1
- package/dist/components/entry-scene.js +2 -3
- package/dist/components/field.js +3 -3
- package/dist/components/file-diff.d.ts.map +1 -1
- package/dist/components/file-diff.js +5 -2
- package/dist/components/footer.d.ts +1 -1
- package/dist/components/footer.d.ts.map +1 -1
- package/dist/components/footer.js +3 -3
- package/dist/components/form-dialog.d.ts.map +1 -1
- package/dist/components/form-dialog.js +2 -1
- package/dist/components/form-drawer.d.ts +7 -9
- package/dist/components/form-drawer.d.ts.map +1 -1
- package/dist/components/form-drawer.js +5 -5
- package/dist/components/form.d.ts +26 -0
- package/dist/components/form.d.ts.map +1 -0
- package/dist/components/form.js +29 -0
- package/dist/components/gauge.js +2 -2
- package/dist/components/heatmap/calendar-heatmap.d.ts.map +1 -1
- package/dist/components/heatmap/calendar-heatmap.js +15 -13
- package/dist/components/hive-scene.d.ts +3 -3
- package/dist/components/hive-scene.d.ts.map +1 -1
- package/dist/components/hive-scene.js +4 -2
- package/dist/components/hover-card.d.ts +1 -1
- package/dist/components/hover-card.d.ts.map +1 -1
- package/dist/components/hover-card.js +1 -1
- package/dist/components/input-group.d.ts +5 -6
- package/dist/components/input-group.d.ts.map +1 -1
- package/dist/components/input-group.js +8 -11
- package/dist/components/input-otp.d.ts.map +1 -1
- package/dist/components/input-otp.js +2 -2
- package/dist/components/item.d.ts +1 -1
- package/dist/components/item.js +2 -2
- package/dist/components/kbd.d.ts +6 -9
- package/dist/components/kbd.d.ts.map +1 -1
- package/dist/components/kbd.js +7 -3
- package/dist/components/message-notice.js +1 -1
- package/dist/components/middle-truncate.d.ts.map +1 -1
- package/dist/components/middle-truncate.js +1 -0
- package/dist/components/model-identity.d.ts +4 -4
- package/dist/components/model-identity.d.ts.map +1 -1
- package/dist/components/model-identity.js +2 -2
- package/dist/components/model-mark.d.ts.map +1 -1
- package/dist/components/model-mark.js +4 -2
- package/dist/components/multi-select.js +2 -2
- package/dist/components/native-select.d.ts.map +1 -1
- package/dist/components/native-select.js +2 -6
- package/dist/components/notice-banner.js +1 -1
- package/dist/components/object-page.js +2 -2
- package/dist/components/object-workspace.d.ts.map +1 -1
- package/dist/components/object-workspace.js +1 -3
- package/dist/components/overview-page.d.ts +1 -1
- package/dist/components/overview-page.d.ts.map +1 -1
- package/dist/components/overview-page.js +2 -1
- package/dist/components/page-actions.d.ts.map +1 -1
- package/dist/components/page-actions.js +9 -4
- package/dist/components/pairing-code.js +1 -1
- package/dist/components/pending-label.d.ts.map +1 -1
- package/dist/components/pending-label.js +0 -1
- package/dist/components/popover.d.ts +1 -1
- package/dist/components/popover.d.ts.map +1 -1
- package/dist/components/portal-sidebar-frame.d.ts.map +1 -1
- package/dist/components/portal-sidebar-frame.js +1 -1
- package/dist/components/product-message.d.ts.map +1 -1
- package/dist/components/product-message.js +1 -0
- package/dist/components/progress-checklist.d.ts.map +1 -1
- package/dist/components/progress-checklist.js +2 -2
- package/dist/components/progress.d.ts.map +1 -1
- package/dist/components/progress.js +5 -4
- package/dist/components/radio-group.js +2 -2
- package/dist/components/reading-column.js +1 -1
- package/dist/components/record-identity.d.ts.map +1 -1
- package/dist/components/record-identity.js +2 -5
- package/dist/components/record-list.d.ts.map +1 -1
- package/dist/components/record-list.js +4 -3
- package/dist/components/record-sheet.js +1 -1
- package/dist/components/record-time.d.ts.map +1 -1
- package/dist/components/record-time.js +2 -1
- package/dist/components/resizable.d.ts.map +1 -1
- package/dist/components/resizable.js +1 -1
- package/dist/components/row-edit-form.d.ts +2 -1
- package/dist/components/row-edit-form.d.ts.map +1 -1
- package/dist/components/row-edit-form.js +2 -1
- package/dist/components/run-timeline.js +1 -1
- package/dist/components/scope-switcher.js +1 -1
- package/dist/components/segmented-control.js +1 -1
- package/dist/components/select.js +1 -1
- package/dist/components/settings-facts.js +1 -1
- package/dist/components/settings-section.js +1 -1
- package/dist/components/sheet.d.ts +1 -1
- package/dist/components/sheet.d.ts.map +1 -1
- package/dist/components/sheet.js +1 -1
- package/dist/components/sidebar.d.ts +2 -2
- package/dist/components/sidebar.d.ts.map +1 -1
- package/dist/components/sidebar.js +16 -18
- package/dist/components/site-menu.d.ts +28 -0
- package/dist/components/site-menu.d.ts.map +1 -0
- package/dist/components/site-menu.js +23 -0
- package/dist/components/slider.d.ts.map +1 -1
- package/dist/components/slider.js +2 -1
- package/dist/components/snippet.d.ts +2 -0
- package/dist/components/snippet.d.ts.map +1 -1
- package/dist/components/snippet.js +13 -6
- package/dist/components/sonner.d.ts.map +1 -1
- package/dist/components/sonner.js +3 -4
- package/dist/components/status-badge.d.ts +1 -1
- package/dist/components/status-badge.d.ts.map +1 -1
- package/dist/components/switch.js +1 -1
- package/dist/components/table-card.js +1 -1
- package/dist/components/table.js +4 -4
- package/dist/components/tabs-nav.d.ts +52 -0
- package/dist/components/tabs-nav.d.ts.map +1 -0
- package/dist/components/tabs-nav.js +95 -0
- package/dist/components/tabs.d.ts +2 -56
- package/dist/components/tabs.d.ts.map +1 -1
- package/dist/components/tabs.js +2 -91
- package/dist/components/text-link.d.ts +1 -2
- package/dist/components/text-link.d.ts.map +1 -1
- package/dist/components/text-link.js +1 -2
- package/dist/components/textarea.d.ts +5 -2
- package/dist/components/textarea.d.ts.map +1 -1
- package/dist/components/textarea.js +12 -4
- package/dist/components/toast.d.ts +2 -0
- package/dist/components/toast.d.ts.map +1 -1
- package/dist/components/toast.js +4 -0
- package/dist/components/toggle-group.d.ts +3 -1
- package/dist/components/toggle-group.d.ts.map +1 -1
- package/dist/components/toggle-group.js +3 -6
- package/dist/components/toggle.js +1 -1
- package/dist/components/tooltip.d.ts +12 -12
- package/dist/components/tooltip.d.ts.map +1 -1
- package/dist/components/tooltip.js +9 -9
- package/dist/components/trend-badge.js +2 -2
- package/dist/components/update-card.js +1 -1
- package/dist/components/usage-meter.d.ts +1 -1
- package/dist/components/usage-meter.d.ts.map +1 -1
- package/dist/components/usage-meter.js +9 -7
- package/dist/components/waiting-line.js +1 -1
- package/dist/index.d.ts +3 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -7
- package/dist/internal/agent-orb-motion.d.ts +4 -1
- package/dist/internal/agent-orb-motion.d.ts.map +1 -1
- package/dist/internal/agent-orb-motion.js +9 -7
- package/dist/internal/agent-orb.d.ts +4 -1
- package/dist/internal/agent-orb.d.ts.map +1 -1
- package/dist/internal/agent-orb.js +7 -1
- package/dist/internal/assistant-ui/attachment.d.ts.map +1 -1
- package/dist/internal/assistant-ui/attachment.js +15 -15
- package/dist/internal/assistant-ui/follow-up-suggestions.js +1 -1
- package/dist/internal/assistant-ui/markdown-text.js +26 -25
- package/dist/internal/assistant-ui/reasoning.d.ts.map +1 -1
- package/dist/internal/assistant-ui/reasoning.js +13 -16
- package/dist/internal/assistant-ui/thread.d.ts.map +1 -1
- package/dist/internal/assistant-ui/thread.js +22 -16
- package/dist/internal/assistant-ui/tool-fallback.d.ts.map +1 -1
- package/dist/internal/assistant-ui/tool-fallback.js +29 -21
- package/dist/internal/assistant-ui/tool-group.d.ts.map +1 -1
- package/dist/internal/assistant-ui/tool-group.js +6 -7
- package/dist/internal/assistant-ui/tooltip-icon-button.js +1 -1
- package/dist/internal/badge-frame.d.ts +15 -0
- package/dist/internal/badge-frame.d.ts.map +1 -0
- package/dist/internal/badge-frame.js +44 -0
- package/dist/internal/button-classes.d.ts +20 -0
- package/dist/internal/button-classes.d.ts.map +1 -0
- package/dist/internal/button-classes.js +74 -0
- package/dist/internal/catalog-list.d.ts.map +1 -1
- package/dist/internal/catalog-list.js +8 -4
- package/dist/internal/collection-rail.js +3 -3
- package/dist/internal/form-drawer-frame.d.ts +10 -0
- package/dist/internal/form-drawer-frame.d.ts.map +1 -0
- package/dist/internal/form-drawer-frame.js +9 -0
- package/dist/internal/form-submit.d.ts +2 -1
- package/dist/internal/form-submit.d.ts.map +1 -1
- package/dist/internal/settings-save.d.ts.map +1 -1
- package/dist/internal/settings-save.js +4 -2
- package/dist/internal/status-mark.d.ts.map +1 -1
- package/dist/internal/status-mark.js +4 -1
- package/dist/internal/tab-row.d.ts +10 -0
- package/dist/internal/tab-row.d.ts.map +1 -0
- package/dist/internal/tab-row.js +2 -0
- package/dist/internal/table-compact-line.d.ts +1 -1
- package/dist/internal/table-compact-line.d.ts.map +1 -1
- package/dist/internal/table-compact-line.js +1 -1
- package/dist/internal/tooltip-content-frame.d.ts +14 -0
- package/dist/internal/tooltip-content-frame.d.ts.map +1 -0
- package/dist/internal/tooltip-content-frame.js +11 -0
- package/dist/lib/css-variables.d.ts +8 -0
- package/dist/lib/css-variables.d.ts.map +1 -0
- package/dist/lib/css-variables.js +4 -0
- package/dist/lib/merge-tokens.generated.js +1 -1
- package/dist/lib/use-copy-to-clipboard.d.ts.map +1 -1
- package/dist/lib/use-copy-to-clipboard.js +1 -2
- package/dist/showcase/samples.js +1 -1
- package/dist/showcase/specimens/choices.d.ts.map +1 -1
- package/dist/showcase/specimens/choices.js +3 -2
- package/dist/showcase/specimens/compositions.d.ts.map +1 -1
- package/dist/showcase/specimens/compositions.js +7 -5
- package/dist/showcase/specimens/data.d.ts.map +1 -1
- package/dist/showcase/specimens/data.js +1 -1
- package/dist/showcase/specimens/feedback.d.ts.map +1 -1
- package/dist/showcase/specimens/feedback.js +1 -2
- package/dist/showcase/specimens/index.d.ts +2 -0
- package/dist/showcase/specimens/index.d.ts.map +1 -1
- package/dist/showcase/specimens/inputs.d.ts +1 -0
- package/dist/showcase/specimens/inputs.d.ts.map +1 -1
- package/dist/showcase/specimens/inputs.js +5 -2
- package/dist/showcase/specimens/menus.d.ts +1 -0
- package/dist/showcase/specimens/menus.d.ts.map +1 -1
- package/dist/showcase/specimens/menus.js +7 -2
- package/dist/showcase/specimens/navigation.d.ts.map +1 -1
- package/dist/showcase/specimens/navigation.js +6 -5
- package/dist/showcase/specimens/status.d.ts.map +1 -1
- package/dist/showcase/specimens/status.js +4 -4
- package/dist/showcase/usage.generated.d.ts +1 -1
- package/dist/showcase/usage.generated.d.ts.map +1 -1
- package/dist/showcase/usage.generated.js +39 -17
- package/dist/standalone/index.js +1 -1
- package/dist/standalone/orb-page.js +10 -8
- package/dist/styles/components.css +14 -19
- 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,83 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: `no-restricted-syntax` can match a class token
|
|
3
|
-
* in a `className` literal (strict-design-system.js does), but not which file
|
|
4
|
-
* the token is in, so it cannot allow the files that render a button and refuse
|
|
5
|
-
* the rest, nor tell the small role's two files from the other four.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
const BUTTON_ROLE = /(?:^|:)!?(text-button(?:-lg|-xs)?|nc-type-button(?:-[a-z0-9]+)*)!?$/;
|
|
9
|
-
const SMALL_ROLE = /-xs$/;
|
|
10
|
-
const CLASS_FUNCTIONS = new Set(["cva", "cn"]);
|
|
11
|
-
|
|
12
|
-
const inFiles = (filename, files) => {
|
|
13
|
-
const path = filename.replaceAll("\\", "/");
|
|
14
|
-
return files.some((file) => path === file || path.endsWith(`/${file}`));
|
|
15
|
-
};
|
|
16
|
-
|
|
17
|
-
const classSite = (node) => {
|
|
18
|
-
for (let current = node.parent; current; current = current.parent) {
|
|
19
|
-
if (current.type === "JSXAttribute") return current.name.name === "className";
|
|
20
|
-
if (
|
|
21
|
-
current.type === "CallExpression" &&
|
|
22
|
-
current.callee.type === "Identifier" &&
|
|
23
|
-
CLASS_FUNCTIONS.has(current.callee.name)
|
|
24
|
-
) {
|
|
25
|
-
return true;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
return false;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const isVariantKey = (node) => node.parent.type === "Property" && node.parent.key === node;
|
|
32
|
-
|
|
33
|
-
export const buttonTypeRolesOnlyInButtons = {
|
|
34
|
-
meta: {
|
|
35
|
-
type: "problem",
|
|
36
|
-
docs: {
|
|
37
|
-
description:
|
|
38
|
-
"The button type roles label a component that renders a button, in the files that render one.",
|
|
39
|
-
},
|
|
40
|
-
schema: [
|
|
41
|
-
{
|
|
42
|
-
type: "object",
|
|
43
|
-
properties: {
|
|
44
|
-
allow: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
45
|
-
allowSmall: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
46
|
-
},
|
|
47
|
-
additionalProperties: false,
|
|
48
|
-
},
|
|
49
|
-
],
|
|
50
|
-
defaultOptions: [{ allow: [], allowSmall: [] }],
|
|
51
|
-
messages: {
|
|
52
|
-
"button-role":
|
|
53
|
-
"Button type roles label a component that renders a button. Use a body-* or label-* role for other text; text-button-xs is only for the small button inside a field.",
|
|
54
|
-
},
|
|
55
|
-
},
|
|
56
|
-
create(context) {
|
|
57
|
-
const [{ allow, allowSmall }] = context.options;
|
|
58
|
-
const inAllowed = inFiles(context.filename, allow);
|
|
59
|
-
const inSmallAllowed = inFiles(context.filename, allowSmall);
|
|
60
|
-
|
|
61
|
-
const refused = (value) =>
|
|
62
|
-
value
|
|
63
|
-
.split(/\s+/)
|
|
64
|
-
.map((utility) => BUTTON_ROLE.exec(utility)?.[1])
|
|
65
|
-
.filter(Boolean)
|
|
66
|
-
.some((role) => (SMALL_ROLE.test(role) ? !inSmallAllowed : !inAllowed));
|
|
67
|
-
|
|
68
|
-
function check(node, value) {
|
|
69
|
-
if (isVariantKey(node) || !classSite(node)) return;
|
|
70
|
-
if (refused(value)) context.report({ node, messageId: "button-role" });
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
Literal(node) {
|
|
75
|
-
// oxlint-disable-next-line anti-slop/no-runtime-typeof -- ESTree types Literal.value as string | number | boolean | RegExp | bigint | null; only typeof picks the string.
|
|
76
|
-
if (typeof node.value === "string") check(node, node.value);
|
|
77
|
-
},
|
|
78
|
-
TemplateElement(node) {
|
|
79
|
-
check(node, node.value.cooked ?? node.value.raw);
|
|
80
|
-
},
|
|
81
|
-
};
|
|
82
|
-
},
|
|
83
|
-
};
|
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: `jsx-a11y/label-has-associated-control` checks a
|
|
3
|
-
* Label for a control, not a control for a name, and a `no-restricted-syntax`
|
|
4
|
-
* selector cannot tell that the package's Checkbox is meant rather than a local
|
|
5
|
-
* one, that a Label elsewhere in the file points at its id through `htmlFor`,
|
|
6
|
-
* or that a labelling ancestor wraps it. Those need the import bindings, the
|
|
7
|
-
* ancestors and the file's labels read together.
|
|
8
|
-
*/
|
|
9
|
-
import { packageModules, partOf, renderedImports, staticAttribute } from "./rendered-import.js";
|
|
10
|
-
|
|
11
|
-
const partsIn = (module, names) =>
|
|
12
|
-
names.map((name) => [
|
|
13
|
-
name,
|
|
14
|
-
[...packageModules(module), `@/components/${module}`].map((from) => ({ from, part: name })),
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
const CONTROLS = new Map([
|
|
18
|
-
...partsIn("checkbox", ["Checkbox"]),
|
|
19
|
-
...partsIn("switch", ["Switch"]),
|
|
20
|
-
...partsIn("radio-group", ["RadioGroupItem"]),
|
|
21
|
-
]);
|
|
22
|
-
|
|
23
|
-
const LABELS = new Map([...partsIn("label", ["Label"]), ...partsIn("field", ["FieldLabel"])]);
|
|
24
|
-
|
|
25
|
-
const NAMING_ANCESTORS = new Map([...LABELS, ...partsIn("settings-section", ["SettingsField"])]);
|
|
26
|
-
|
|
27
|
-
const NAMING_ATTRIBUTES = new Set(["aria-label", "aria-labelledby"]);
|
|
28
|
-
|
|
29
|
-
const findAttribute = (openingElement, name) =>
|
|
30
|
-
openingElement.attributes.find(
|
|
31
|
-
(attribute) => attribute.type === "JSXAttribute" && attribute.name.name === name,
|
|
32
|
-
);
|
|
33
|
-
|
|
34
|
-
const hasSpread = (openingElement) =>
|
|
35
|
-
openingElement.attributes.some((attribute) => attribute.type === "JSXSpreadAttribute");
|
|
36
|
-
|
|
37
|
-
export const choiceControlHasAName = {
|
|
38
|
-
meta: {
|
|
39
|
-
type: "problem",
|
|
40
|
-
docs: {
|
|
41
|
-
description:
|
|
42
|
-
"A Checkbox, Switch or RadioGroupItem is named by an aria-label, an aria-labelledby, a Label with htmlFor, or a labelling ancestor.",
|
|
43
|
-
},
|
|
44
|
-
schema: [],
|
|
45
|
-
messages: {
|
|
46
|
-
unnamed:
|
|
47
|
-
"Name this control: give it an aria-label (a row's checkbox: Select <row name>), an aria-labelledby, or a Label with htmlFor.",
|
|
48
|
-
},
|
|
49
|
-
},
|
|
50
|
-
create(context) {
|
|
51
|
-
const importOf = renderedImports(context);
|
|
52
|
-
const isPart = (parts) => (openingElement) =>
|
|
53
|
-
Boolean(partOf(importOf(openingElement.name), parts, context.filename));
|
|
54
|
-
const isControl = isPart(CONTROLS);
|
|
55
|
-
const isLabel = isPart(LABELS);
|
|
56
|
-
const isNamingAncestor = isPart(NAMING_ANCESTORS);
|
|
57
|
-
|
|
58
|
-
const reference = (openingElement, attributeName) => {
|
|
59
|
-
const attribute = findAttribute(openingElement, attributeName);
|
|
60
|
-
if (!attribute) return null;
|
|
61
|
-
const stated = staticAttribute(openingElement, attributeName);
|
|
62
|
-
if (stated !== undefined) return String(stated);
|
|
63
|
-
return attribute.value ? context.sourceCode.getText(attribute.value) : null;
|
|
64
|
-
};
|
|
65
|
-
|
|
66
|
-
const controls = [];
|
|
67
|
-
const pointedAt = new Set();
|
|
68
|
-
|
|
69
|
-
const hasNamingAncestor = (element) => {
|
|
70
|
-
for (let current = element.parent; current; current = current.parent) {
|
|
71
|
-
if (current.type === "JSXElement" && isNamingAncestor(current.openingElement)) return true;
|
|
72
|
-
}
|
|
73
|
-
return false;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
return {
|
|
77
|
-
JSXElement(node) {
|
|
78
|
-
const { openingElement } = node;
|
|
79
|
-
if (isLabel(openingElement)) {
|
|
80
|
-
const target = reference(openingElement, "htmlFor");
|
|
81
|
-
if (target !== null) pointedAt.add(target);
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
if (!isControl(openingElement)) return;
|
|
85
|
-
if (hasSpread(openingElement)) return;
|
|
86
|
-
if (openingElement.attributes.some((a) => NAMING_ATTRIBUTES.has(a.name?.name))) return;
|
|
87
|
-
if (hasNamingAncestor(node)) return;
|
|
88
|
-
controls.push({ node: openingElement, id: reference(openingElement, "id") });
|
|
89
|
-
},
|
|
90
|
-
"Program:exit"() {
|
|
91
|
-
for (const { node, id } of controls) {
|
|
92
|
-
if (id !== null && pointedAt.has(id)) continue;
|
|
93
|
-
context.report({ node, messageId: "unnamed" });
|
|
94
|
-
}
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
},
|
|
98
|
-
};
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: a `no-restricted-syntax` selector can match a
|
|
3
|
-
* `RadioGroup` without a `FieldSet` ancestor, but not that the `FieldSet`
|
|
4
|
-
* holds a `FieldLegend`, nor that two `Checkbox` elements share a parent once
|
|
5
|
-
* each sits in its own `Field`, so it would miss the checkbox group.
|
|
6
|
-
*/
|
|
7
|
-
import { findBelow, renderedImports } from "./rendered-import.js";
|
|
8
|
-
|
|
9
|
-
const FROM_DESIGN_SYSTEM = /^(@nadicodeai\/ui(\/|$)|@\/|\.)/;
|
|
10
|
-
const ROW_WRAPPERS = new Set(["Field", "FieldContent", "FieldLabel", "Label"]);
|
|
11
|
-
|
|
12
|
-
const isRepeater = (node) =>
|
|
13
|
-
(node.type === "ArrowFunctionExpression" || node.type === "FunctionExpression") &&
|
|
14
|
-
node.parent.type === "CallExpression" &&
|
|
15
|
-
node.parent.callee.type === "MemberExpression" &&
|
|
16
|
-
node.parent.callee.property.name === "map";
|
|
17
|
-
|
|
18
|
-
export const choiceGroupInFieldset = {
|
|
19
|
-
meta: {
|
|
20
|
-
type: "problem",
|
|
21
|
-
docs: {
|
|
22
|
-
description: "A group of radios or checkboxes sits in a FieldSet with a FieldLegend.",
|
|
23
|
-
},
|
|
24
|
-
schema: [],
|
|
25
|
-
messages: {
|
|
26
|
-
"choice-group":
|
|
27
|
-
"A group of radios or checkboxes sits in a FieldSet with a FieldLegend, so the group is announced by its legend.",
|
|
28
|
-
},
|
|
29
|
-
},
|
|
30
|
-
create(context) {
|
|
31
|
-
const importOf = renderedImports(context);
|
|
32
|
-
const exportOf = (element) => {
|
|
33
|
-
const imported = importOf(element.openingElement.name);
|
|
34
|
-
return imported && FROM_DESIGN_SYSTEM.test(imported.source) ? imported.name : null;
|
|
35
|
-
};
|
|
36
|
-
const checkboxGroups = new Map();
|
|
37
|
-
|
|
38
|
-
function hasLegendedFieldSet(node) {
|
|
39
|
-
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
40
|
-
if (ancestor.type !== "JSXElement" || exportOf(ancestor) !== "FieldSet") continue;
|
|
41
|
-
const legend = findBelow(
|
|
42
|
-
ancestor,
|
|
43
|
-
context.sourceCode.visitorKeys,
|
|
44
|
-
(child) => child.type === "JSXElement" && exportOf(child) === "FieldLegend",
|
|
45
|
-
);
|
|
46
|
-
if (legend) return true;
|
|
47
|
-
}
|
|
48
|
-
return false;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function reportUnlessGrouped(element) {
|
|
52
|
-
if (!hasLegendedFieldSet(element)) {
|
|
53
|
-
context.report({ node: element.openingElement, messageId: "choice-group" });
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function recordCheckbox(element) {
|
|
58
|
-
let repeated = false;
|
|
59
|
-
let parent = element.parent;
|
|
60
|
-
while (parent) {
|
|
61
|
-
if (isRepeater(parent)) repeated = true;
|
|
62
|
-
const isRow = parent.type === "JSXElement" && ROW_WRAPPERS.has(exportOf(parent));
|
|
63
|
-
const isGroup = (parent.type === "JSXElement" || parent.type === "JSXFragment") && !isRow;
|
|
64
|
-
if (isGroup) break;
|
|
65
|
-
parent = parent.parent;
|
|
66
|
-
}
|
|
67
|
-
if (!parent) return;
|
|
68
|
-
const group = checkboxGroups.get(parent) ?? { first: element, count: 0 };
|
|
69
|
-
group.count += repeated ? 2 : 1;
|
|
70
|
-
checkboxGroups.set(parent, group);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
return {
|
|
74
|
-
JSXElement(node) {
|
|
75
|
-
const name = exportOf(node);
|
|
76
|
-
if (name === "RadioGroup") reportUnlessGrouped(node);
|
|
77
|
-
else if (name === "Checkbox") recordCheckbox(node);
|
|
78
|
-
},
|
|
79
|
-
"Program:exit"() {
|
|
80
|
-
for (const { first, count } of checkboxGroups.values()) {
|
|
81
|
-
if (count >= 2) reportUnlessGrouped(first);
|
|
82
|
-
}
|
|
83
|
-
},
|
|
84
|
-
};
|
|
85
|
-
},
|
|
86
|
-
};
|
|
@@ -1,118 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: a `no-restricted-syntax` selector can match the
|
|
3
|
-
* string `"mod+k"`, but not a handler that reads `key` and `metaKey` as two
|
|
4
|
-
* separate checks (`(e.metaKey || e.ctrlKey) && e.key === "k"`), because the
|
|
5
|
-
* two reads only mean a shortcut together, in the same function; and it cannot
|
|
6
|
-
* allow the files that own Find.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const SHORTCUT_STRING = /^(?:mod|meta|cmd|ctrl)\+k$/i;
|
|
10
|
-
const MODIFIER_PROPERTIES = new Set(["metaKey", "ctrlKey"]);
|
|
11
|
-
const CASE_METHODS = new Set(["toLowerCase", "toUpperCase"]);
|
|
12
|
-
const KEY_VALUES = { key: new Set(["k", "K"]), code: new Set(["KeyK"]) };
|
|
13
|
-
|
|
14
|
-
const inFiles = (filename, files) => {
|
|
15
|
-
const path = filename.replaceAll("\\", "/");
|
|
16
|
-
return files.some((file) => path === file || path.endsWith(`/${file}`));
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
const unwrapChain = (node) => (node.type === "ChainExpression" ? node.expression : node);
|
|
20
|
-
|
|
21
|
-
const propertyName = (member) =>
|
|
22
|
-
member.type === "MemberExpression" && !member.computed ? member.property.name : null;
|
|
23
|
-
|
|
24
|
-
function keyRead(node) {
|
|
25
|
-
const read = unwrapChain(node);
|
|
26
|
-
if (read.type === "CallExpression") {
|
|
27
|
-
const callee = unwrapChain(read.callee);
|
|
28
|
-
if (propertyName(callee) && CASE_METHODS.has(propertyName(callee))) {
|
|
29
|
-
return keyRead(callee.object);
|
|
30
|
-
}
|
|
31
|
-
return null;
|
|
32
|
-
}
|
|
33
|
-
const name = propertyName(read);
|
|
34
|
-
return name && name in KEY_VALUES ? name : null;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
const isLetterK = (read, literal) =>
|
|
38
|
-
literal.type === "Literal" &&
|
|
39
|
-
// 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.
|
|
40
|
-
typeof literal.value === "string" &&
|
|
41
|
-
KEY_VALUES[read].has(literal.value);
|
|
42
|
-
|
|
43
|
-
const COMPARISONS = new Set(["===", "==", "!==", "!="]);
|
|
44
|
-
|
|
45
|
-
export const commandShortcutSingleOwner = {
|
|
46
|
-
meta: {
|
|
47
|
-
type: "problem",
|
|
48
|
-
docs: {
|
|
49
|
-
description: "Only the files that own Find bind the command-and-K shortcut.",
|
|
50
|
-
},
|
|
51
|
-
schema: [
|
|
52
|
-
{
|
|
53
|
-
type: "object",
|
|
54
|
-
properties: {
|
|
55
|
-
owners: { type: "array", items: { type: "string" }, uniqueItems: true },
|
|
56
|
-
},
|
|
57
|
-
additionalProperties: false,
|
|
58
|
-
},
|
|
59
|
-
],
|
|
60
|
-
defaultOptions: [{ owners: [] }],
|
|
61
|
-
messages: {
|
|
62
|
-
"find-shortcut": "⌘K and Ctrl+K belong to Find. Bind an in-page field to another key.",
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
create(context) {
|
|
66
|
-
if (inFiles(context.filename, context.options[0].owners)) return {};
|
|
67
|
-
|
|
68
|
-
const frames = [{ letterKs: [], readsModifier: false }];
|
|
69
|
-
const frame = () => frames.at(-1);
|
|
70
|
-
|
|
71
|
-
const enter = () => frames.push({ letterKs: [], readsModifier: false });
|
|
72
|
-
const leave = () => {
|
|
73
|
-
const { letterKs, readsModifier } = frames.pop();
|
|
74
|
-
if (!readsModifier) return;
|
|
75
|
-
for (const node of letterKs) context.report({ node, messageId: "find-shortcut" });
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
const checkString = (node, value) => {
|
|
79
|
-
if (SHORTCUT_STRING.test(value.trim())) context.report({ node, messageId: "find-shortcut" });
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
const noteLetterK = (node, read, other) => {
|
|
83
|
-
const name = keyRead(read);
|
|
84
|
-
if (name && isLetterK(name, other)) frame().letterKs.push(node);
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
return {
|
|
88
|
-
FunctionDeclaration: enter,
|
|
89
|
-
FunctionExpression: enter,
|
|
90
|
-
ArrowFunctionExpression: enter,
|
|
91
|
-
"FunctionDeclaration:exit": leave,
|
|
92
|
-
"FunctionExpression:exit": leave,
|
|
93
|
-
"ArrowFunctionExpression:exit": leave,
|
|
94
|
-
"Program:exit"() {
|
|
95
|
-
leave();
|
|
96
|
-
},
|
|
97
|
-
MemberExpression(node) {
|
|
98
|
-
if (MODIFIER_PROPERTIES.has(propertyName(node))) frame().readsModifier = true;
|
|
99
|
-
},
|
|
100
|
-
BinaryExpression(node) {
|
|
101
|
-
if (!COMPARISONS.has(node.operator)) return;
|
|
102
|
-
noteLetterK(node, node.left, node.right);
|
|
103
|
-
noteLetterK(node, node.right, node.left);
|
|
104
|
-
},
|
|
105
|
-
SwitchCase(node) {
|
|
106
|
-
if (node.test) noteLetterK(node, node.parent.discriminant, node.test);
|
|
107
|
-
},
|
|
108
|
-
Literal(node) {
|
|
109
|
-
// 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.
|
|
110
|
-
if (typeof node.value === "string") checkString(node, node.value);
|
|
111
|
-
},
|
|
112
|
-
TemplateElement(node) {
|
|
113
|
-
if (node.parent.type === "TemplateLiteral" && node.parent.expressions.length === 0)
|
|
114
|
-
checkString(node, node.value.cooked ?? "");
|
|
115
|
-
},
|
|
116
|
-
};
|
|
117
|
-
},
|
|
118
|
-
};
|
|
@@ -1,63 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: `no-restricted-syntax` reads one attribute at a
|
|
3
|
-
* time, and this check compares two of them on the same element (the title's
|
|
4
|
-
* verb with the confirm label's), and must know the element is the package's
|
|
5
|
-
* `ConfirmDialog` or the Portal's wrapper of it, not a local component.
|
|
6
|
-
*/
|
|
7
|
-
import { stringAttribute } from "./literal-text.js";
|
|
8
|
-
import { renderedImports, writtenName } from "./rendered-import.js";
|
|
9
|
-
|
|
10
|
-
const DIALOGS = new Set(["ConfirmDialog", "ConfirmedActionForm"]);
|
|
11
|
-
const QUESTION_OPENING = /^(?:are you sure|do you (?:really )?want|would you like|should)\b/i;
|
|
12
|
-
|
|
13
|
-
const words = (text) => text.trim().split(/\s+/).filter(Boolean);
|
|
14
|
-
|
|
15
|
-
export const confirmDialogWords = {
|
|
16
|
-
meta: {
|
|
17
|
-
type: "problem",
|
|
18
|
-
docs: {
|
|
19
|
-
description:
|
|
20
|
-
"A ConfirmDialog's title is a statement, and its confirm label repeats the title's verb with the noun.",
|
|
21
|
-
},
|
|
22
|
-
schema: [],
|
|
23
|
-
messages: {
|
|
24
|
-
"title-is-a-question":
|
|
25
|
-
'The title of a confirmation is a statement that names the record, "Remove pec@acme.it", never a question.',
|
|
26
|
-
"confirm-label-bare":
|
|
27
|
-
'The confirm label repeats the verb and the noun, "Remove mailbox"; "{{label}}" alone does not say what is confirmed.',
|
|
28
|
-
"confirm-verb-differs":
|
|
29
|
-
'The confirm label repeats the title\'s verb: the title says "{{title}}", so the label starts with "{{verb}}", not "{{label}}".',
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
create(context) {
|
|
33
|
-
const importOf = renderedImports(context);
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
JSXOpeningElement(node) {
|
|
37
|
-
if (!importOf(node.name) || !DIALOGS.has(writtenName(node.name))) return;
|
|
38
|
-
const titleText = stringAttribute(node, "title");
|
|
39
|
-
const labelText = stringAttribute(node, "confirmLabel");
|
|
40
|
-
if (
|
|
41
|
-
titleText !== null &&
|
|
42
|
-
(titleText.trim().endsWith("?") || QUESTION_OPENING.test(titleText.trim()))
|
|
43
|
-
) {
|
|
44
|
-
context.report({ node, messageId: "title-is-a-question" });
|
|
45
|
-
}
|
|
46
|
-
if (labelText === null) return;
|
|
47
|
-
const labelWords = words(labelText);
|
|
48
|
-
if (labelWords.length < 2) {
|
|
49
|
-
context.report({ node, messageId: "confirm-label-bare", data: { label: labelText } });
|
|
50
|
-
return;
|
|
51
|
-
}
|
|
52
|
-
const verb = titleText === null ? undefined : words(titleText)[0];
|
|
53
|
-
if (verb !== undefined && verb.toLowerCase() !== labelWords[0].toLowerCase()) {
|
|
54
|
-
context.report({
|
|
55
|
-
node,
|
|
56
|
-
messageId: "confirm-verb-differs",
|
|
57
|
-
data: { title: titleText, verb, label: labelText },
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
},
|
|
61
|
-
};
|
|
62
|
-
},
|
|
63
|
-
};
|
|
@@ -1,96 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: a `no-restricted-syntax` selector matches one
|
|
3
|
-
* node, but this reads a whole `columns` array (how many of its objects lack a
|
|
4
|
-
* `size`, whether one sets `meta.wraps`, whether a spread or a call makes the
|
|
5
|
-
* list unknowable) and follows a variable to its array, which no selector can.
|
|
6
|
-
*/
|
|
7
|
-
import { renderedImports } from "./rendered-import.js";
|
|
8
|
-
|
|
9
|
-
const COMMANDS_COLUMN_ID = "actions";
|
|
10
|
-
|
|
11
|
-
const WRAPPERS = new Set(["TSAsExpression", "TSSatisfiesExpression", "TSNonNullExpression"]);
|
|
12
|
-
|
|
13
|
-
function unwrap(node) {
|
|
14
|
-
let current = node;
|
|
15
|
-
while (WRAPPERS.has(current.type)) current = current.expression;
|
|
16
|
-
return current;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const keyName = (property) => {
|
|
20
|
-
if (property.computed) return null;
|
|
21
|
-
return property.key.type === "Identifier" ? property.key.name : property.key.value;
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const propertyOf = (column, name) =>
|
|
25
|
-
column.properties.find((property) => property.type === "Property" && keyName(property) === name);
|
|
26
|
-
|
|
27
|
-
function setsWraps(column) {
|
|
28
|
-
const meta = propertyOf(column, "meta")?.value;
|
|
29
|
-
if (meta?.type !== "ObjectExpression") return false;
|
|
30
|
-
const wraps = propertyOf(meta, "wraps")?.value;
|
|
31
|
-
return wraps !== undefined && !(wraps.type === "Literal" && wraps.value === false);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
const isCommandsColumn = (column) => propertyOf(column, "id")?.value.value === COMMANDS_COLUMN_ID;
|
|
35
|
-
|
|
36
|
-
const isKnownColumn = (element) =>
|
|
37
|
-
element?.type === "ObjectExpression" &&
|
|
38
|
-
element.properties.every((property) => property.type === "Property");
|
|
39
|
-
|
|
40
|
-
function arrayOf(context, expression) {
|
|
41
|
-
const node = unwrap(expression);
|
|
42
|
-
if (node.type === "ArrayExpression") return node;
|
|
43
|
-
if (node.type !== "Identifier") return null;
|
|
44
|
-
for (let scope = context.sourceCode.getScope(node); scope; scope = scope.upper) {
|
|
45
|
-
const variable = scope.set.get(node.name);
|
|
46
|
-
if (!variable) continue;
|
|
47
|
-
const [definition] = variable.defs;
|
|
48
|
-
const initialisedOnce =
|
|
49
|
-
variable.defs.length === 1 && variable.references.filter((use) => use.isWrite()).length === 1;
|
|
50
|
-
if (!initialisedOnce || definition.node.type !== "VariableDeclarator") return null;
|
|
51
|
-
const init = definition.node.init ? unwrap(definition.node.init) : null;
|
|
52
|
-
return init?.type === "ArrayExpression" ? init : null;
|
|
53
|
-
}
|
|
54
|
-
return null;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function breaksTheRule(array) {
|
|
58
|
-
if (!array.elements.every(isKnownColumn)) return false;
|
|
59
|
-
if (array.elements.some(setsWraps)) return false;
|
|
60
|
-
const unsized = array.elements.filter((column) => !propertyOf(column, "size"));
|
|
61
|
-
if (unsized.length > 1) return true;
|
|
62
|
-
const content = array.elements.filter((column) => !isCommandsColumn(column));
|
|
63
|
-
return content.length > 0 && unsized.length === 0;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export const dataTableOneUnsizedColumn = {
|
|
67
|
-
meta: {
|
|
68
|
-
type: "problem",
|
|
69
|
-
docs: {
|
|
70
|
-
description:
|
|
71
|
-
"A DataTable's columns all have a size in pixels except the one that takes the spare width.",
|
|
72
|
-
},
|
|
73
|
-
schema: [],
|
|
74
|
-
messages: {
|
|
75
|
-
"one-unsized-column":
|
|
76
|
-
"Give every column a `size` in pixels except the one that takes the spare width. Columns without a size share what is left, so two of them make the widths depend on the data.",
|
|
77
|
-
},
|
|
78
|
-
},
|
|
79
|
-
create(context) {
|
|
80
|
-
const importOf = renderedImports(context);
|
|
81
|
-
return {
|
|
82
|
-
JSXOpeningElement(node) {
|
|
83
|
-
if (importOf(node.name)?.name !== "DataTable") return;
|
|
84
|
-
const attribute = node.attributes.find(
|
|
85
|
-
(candidate) => candidate.type === "JSXAttribute" && candidate.name.name === "columns",
|
|
86
|
-
);
|
|
87
|
-
const value = attribute?.value;
|
|
88
|
-
if (value?.type !== "JSXExpressionContainer") return;
|
|
89
|
-
const array = arrayOf(context, value.expression);
|
|
90
|
-
if (array && breaksTheRule(array)) {
|
|
91
|
-
context.report({ node: value.expression, messageId: "one-unsized-column" });
|
|
92
|
-
}
|
|
93
|
-
},
|
|
94
|
-
};
|
|
95
|
-
},
|
|
96
|
-
};
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: a `no-restricted-syntax` descendant selector can
|
|
3
|
-
* match a `Button` below `SheetContent`, but not that the button is the design
|
|
4
|
-
* system's, nor follow a renamed import, nor read `variant="destructive"` off
|
|
5
|
-
* the opening element or ignore a match that only sits in a `render` prop.
|
|
6
|
-
*/
|
|
7
|
-
import { renderedImports, staticAttribute } from "./rendered-import.js";
|
|
8
|
-
|
|
9
|
-
const BUTTONS = new Set(["Button", "ButtonLink"]);
|
|
10
|
-
const CONFIRMATIONS = new Set(["ConfirmDialog", "AlertDialog", "ConfirmActionDialog"]);
|
|
11
|
-
const CLOSED_SURFACES = new Set([
|
|
12
|
-
"SheetContent",
|
|
13
|
-
"DrawerContent",
|
|
14
|
-
"FormDrawer",
|
|
15
|
-
"CatalogDrawer",
|
|
16
|
-
"AccordionContent",
|
|
17
|
-
"CollapsibleContent",
|
|
18
|
-
"Disclosure",
|
|
19
|
-
"HoverCardContent",
|
|
20
|
-
]);
|
|
21
|
-
|
|
22
|
-
const FROM_DESIGN_SYSTEM = /^(@nadicodeai\/ui(\/|$)|@\/|\.)/;
|
|
23
|
-
|
|
24
|
-
export const destructiveActionPlacement = {
|
|
25
|
-
meta: {
|
|
26
|
-
type: "problem",
|
|
27
|
-
docs: {
|
|
28
|
-
description:
|
|
29
|
-
"A destructive action is confirmed in a ConfirmDialog opened from the page or a row's menu, never inside a sheet, drawer, hover card or collapsed section.",
|
|
30
|
-
},
|
|
31
|
-
schema: [],
|
|
32
|
-
messages: {
|
|
33
|
-
"destructive-placement":
|
|
34
|
-
"A destructive action is confirmed in a ConfirmDialog opened from the page or the row's menu: only a modal dims the whole page. Do not place it in a sheet, a drawer, a hover card or a section that starts closed.",
|
|
35
|
-
},
|
|
36
|
-
},
|
|
37
|
-
create(context) {
|
|
38
|
-
const importOf = renderedImports(context);
|
|
39
|
-
const exportOf = (element) => {
|
|
40
|
-
const imported = importOf(element.openingElement.name);
|
|
41
|
-
return imported && FROM_DESIGN_SYSTEM.test(imported.source) ? imported.name : null;
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const isDestructive = (element) => {
|
|
45
|
-
const name = exportOf(element);
|
|
46
|
-
if (CONFIRMATIONS.has(name)) return true;
|
|
47
|
-
return (
|
|
48
|
-
BUTTONS.has(name) && staticAttribute(element.openingElement, "variant") === "destructive"
|
|
49
|
-
);
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
function insideClosedSurface(element) {
|
|
53
|
-
let child = element;
|
|
54
|
-
for (let node = element.parent; node; child = node, node = node.parent) {
|
|
55
|
-
if (node.type !== "JSXElement" || !node.children.includes(child)) continue;
|
|
56
|
-
if (CLOSED_SURFACES.has(exportOf(node))) return true;
|
|
57
|
-
}
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
JSXElement(node) {
|
|
63
|
-
if (isDestructive(node) && insideClosedSurface(node)) {
|
|
64
|
-
context.report({ node: node.openingElement, messageId: "destructive-placement" });
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
},
|
|
69
|
-
};
|
|
@@ -1,71 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: a `no-restricted-syntax` selector can match a
|
|
3
|
-
* `disabled` attribute on a `Checkbox`, but not follow a renamed import to the
|
|
4
|
-
* component, nor read the other attributes of the same element (aria-describedby,
|
|
5
|
-
* a spread) or tell a wait (`disabled={pending}`) from a reason.
|
|
6
|
-
*/
|
|
7
|
-
import { PACKAGE_BARREL, partOf, renderedImports } from "./rendered-import.js";
|
|
8
|
-
|
|
9
|
-
const MODULES = [
|
|
10
|
-
["Checkbox", "checkbox"],
|
|
11
|
-
["Switch", "switch"],
|
|
12
|
-
["RadioGroupItem", "radio-group"],
|
|
13
|
-
];
|
|
14
|
-
|
|
15
|
-
const CHOICE = new Map(
|
|
16
|
-
MODULES.map(([part, module]) => [
|
|
17
|
-
part,
|
|
18
|
-
[`@nadicodeai/ui/components/${module}`, PACKAGE_BARREL].map((from) => ({ from, part })),
|
|
19
|
-
]),
|
|
20
|
-
);
|
|
21
|
-
|
|
22
|
-
const WAIT = /^(?:is)?(?:pending|saving|submitting|busy)$/i;
|
|
23
|
-
|
|
24
|
-
const unwrapChain = (node) => (node.type === "ChainExpression" ? node.expression : node);
|
|
25
|
-
|
|
26
|
-
function isWait(attribute) {
|
|
27
|
-
if (attribute.value?.type !== "JSXExpressionContainer") return false;
|
|
28
|
-
const read = unwrapChain(attribute.value.expression);
|
|
29
|
-
if (read.type === "Identifier") return WAIT.test(read.name);
|
|
30
|
-
if (read.type !== "MemberExpression" || read.computed) return false;
|
|
31
|
-
return WAIT.test(read.property.name);
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function isEnabledLiteral(attribute) {
|
|
35
|
-
const expression = attribute.value?.expression;
|
|
36
|
-
return expression?.type === "Literal" && expression.value === false;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const attributeNamed = (openingElement, name) =>
|
|
40
|
-
openingElement.attributes.find(
|
|
41
|
-
(attribute) => attribute.type === "JSXAttribute" && attribute.name.name === name,
|
|
42
|
-
);
|
|
43
|
-
|
|
44
|
-
export const disabledChoiceSaysWhy = {
|
|
45
|
-
meta: {
|
|
46
|
-
type: "problem",
|
|
47
|
-
docs: {
|
|
48
|
-
description:
|
|
49
|
-
"A disabled Checkbox, Switch or RadioGroupItem points aria-describedby at the line that names the reason.",
|
|
50
|
-
},
|
|
51
|
-
schema: [],
|
|
52
|
-
messages: {
|
|
53
|
-
"disabled-without-reason":
|
|
54
|
-
"Say why this is disabled: point aria-describedby at the line that names the reason, or use ChoiceboxGroupItem's disabledReason.",
|
|
55
|
-
},
|
|
56
|
-
},
|
|
57
|
-
create(context) {
|
|
58
|
-
const importOf = renderedImports(context);
|
|
59
|
-
|
|
60
|
-
return {
|
|
61
|
-
JSXOpeningElement(node) {
|
|
62
|
-
if (!partOf(importOf(node.name), CHOICE, context.filename)) return;
|
|
63
|
-
const disabled = attributeNamed(node, "disabled");
|
|
64
|
-
if (!disabled || isEnabledLiteral(disabled) || isWait(disabled)) return;
|
|
65
|
-
if (attributeNamed(node, "aria-describedby")) return;
|
|
66
|
-
if (node.attributes.some((attribute) => attribute.type === "JSXSpreadAttribute")) return;
|
|
67
|
-
context.report({ node: disabled, messageId: "disabled-without-reason" });
|
|
68
|
-
},
|
|
69
|
-
};
|
|
70
|
-
},
|
|
71
|
-
};
|