@nadicodeai/ui 12.1.165 → 12.1.166
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +8 -9
- package/README.md +2 -17
- package/dist/components/accordion.d.ts.map +1 -1
- package/dist/components/accordion.js +2 -1
- package/dist/components/account-menu.d.ts.map +1 -1
- package/dist/components/account-menu.js +5 -3
- package/dist/components/activity-calendar.d.ts.map +1 -1
- package/dist/components/activity-calendar.js +3 -2
- package/dist/components/agent-card.js +1 -1
- package/dist/components/agent-orb.d.ts.map +1 -1
- package/dist/components/agent-orb.js +1 -0
- package/dist/components/agent-profile.d.ts.map +1 -1
- package/dist/components/agent-profile.js +7 -4
- package/dist/components/alert-dialog.d.ts +1 -1
- package/dist/components/alert-dialog.d.ts.map +1 -1
- package/dist/components/alert-dialog.js +3 -3
- package/dist/components/app-mark.js +1 -1
- package/dist/components/avatar.js +8 -8
- package/dist/components/badge.d.ts +11 -16
- package/dist/components/badge.d.ts.map +1 -1
- package/dist/components/badge.js +7 -43
- package/dist/components/breadcrumb.d.ts.map +1 -1
- package/dist/components/breadcrumb.js +3 -2
- package/dist/components/button.d.ts +5 -21
- package/dist/components/button.d.ts.map +1 -1
- package/dist/components/button.js +6 -76
- package/dist/components/calendar.d.ts +1 -1
- package/dist/components/calendar.d.ts.map +1 -1
- package/dist/components/calendar.js +3 -3
- package/dist/components/card-row.js +1 -1
- package/dist/components/card.d.ts +4 -1
- package/dist/components/card.d.ts.map +1 -1
- package/dist/components/card.js +3 -2
- package/dist/components/catalog-drawer.d.ts.map +1 -1
- package/dist/components/catalog-drawer.js +3 -2
- package/dist/components/chart-axis.d.ts.map +1 -1
- package/dist/components/chart-axis.js +0 -2
- package/dist/components/chart-bar-horizontal.d.ts.map +1 -1
- package/dist/components/chart-bar-horizontal.js +16 -14
- package/dist/components/chart-card.d.ts.map +1 -1
- package/dist/components/chart-card.js +0 -1
- package/dist/components/chart.d.ts.map +1 -1
- package/dist/components/chart.js +18 -32
- package/dist/components/checkbox.d.ts +3 -2
- package/dist/components/checkbox.d.ts.map +1 -1
- package/dist/components/checkbox.js +4 -3
- package/dist/components/choicebox.d.ts +5 -5
- package/dist/components/choicebox.d.ts.map +1 -1
- package/dist/components/choicebox.js +6 -5
- package/dist/components/clearable-input.d.ts.map +1 -1
- package/dist/components/clearable-input.js +2 -2
- package/dist/components/code-block.d.ts.map +1 -1
- package/dist/components/code-block.js +4 -2
- package/dist/components/collapsible.js +1 -1
- package/dist/components/collection-filters.d.ts.map +1 -1
- package/dist/components/collection-filters.js +1 -0
- package/dist/components/collection-page.js +1 -1
- package/dist/components/collection-sort.d.ts.map +1 -1
- package/dist/components/collection-sort.js +1 -4
- package/dist/components/collection-toolbar.js +1 -1
- package/dist/components/combobox.js +4 -4
- package/dist/components/command.d.ts.map +1 -1
- package/dist/components/command.js +1 -1
- package/dist/components/confirm-dialog.js +2 -2
- package/dist/components/context-menu.d.ts +1 -1
- package/dist/components/context-menu.d.ts.map +1 -1
- package/dist/components/copy-button.js +1 -1
- package/dist/components/data-table.d.ts +11 -3
- package/dist/components/data-table.d.ts.map +1 -1
- package/dist/components/data-table.js +12 -4
- package/dist/components/dialog.d.ts +1 -1
- package/dist/components/dialog.d.ts.map +1 -1
- package/dist/components/dialog.js +2 -2
- package/dist/components/drawer.d.ts +4 -4
- package/dist/components/drawer.d.ts.map +1 -1
- package/dist/components/drawer.js +14 -15
- package/dist/components/dropdown-menu.d.ts +1 -1
- package/dist/components/dropdown-menu.d.ts.map +1 -1
- package/dist/components/dropdown-menu.js +9 -5
- package/dist/components/empty-art.js +1 -1
- package/dist/components/empty-state.js +2 -2
- package/dist/components/entry-scene.d.ts.map +1 -1
- package/dist/components/entry-scene.js +2 -3
- package/dist/components/field.js +3 -3
- package/dist/components/file-diff.d.ts.map +1 -1
- package/dist/components/file-diff.js +5 -2
- package/dist/components/footer.d.ts +1 -1
- package/dist/components/footer.d.ts.map +1 -1
- package/dist/components/footer.js +3 -3
- package/dist/components/form-dialog.d.ts.map +1 -1
- package/dist/components/form-dialog.js +2 -1
- package/dist/components/form-drawer.d.ts +7 -9
- package/dist/components/form-drawer.d.ts.map +1 -1
- package/dist/components/form-drawer.js +5 -5
- package/dist/components/form.d.ts +26 -0
- package/dist/components/form.d.ts.map +1 -0
- package/dist/components/form.js +29 -0
- package/dist/components/gauge.js +2 -2
- package/dist/components/heatmap/calendar-heatmap.d.ts.map +1 -1
- package/dist/components/heatmap/calendar-heatmap.js +15 -13
- package/dist/components/hive-scene.d.ts.map +1 -1
- package/dist/components/hive-scene.js +3 -1
- package/dist/components/hover-card.d.ts +1 -1
- package/dist/components/hover-card.d.ts.map +1 -1
- package/dist/components/hover-card.js +1 -1
- package/dist/components/input-group.d.ts +5 -6
- package/dist/components/input-group.d.ts.map +1 -1
- package/dist/components/input-group.js +8 -11
- package/dist/components/input-otp.d.ts.map +1 -1
- package/dist/components/input-otp.js +2 -2
- package/dist/components/item.d.ts +1 -1
- package/dist/components/item.js +2 -2
- package/dist/components/kbd.d.ts +6 -9
- package/dist/components/kbd.d.ts.map +1 -1
- package/dist/components/kbd.js +7 -3
- package/dist/components/message-notice.js +1 -1
- package/dist/components/middle-truncate.d.ts.map +1 -1
- package/dist/components/middle-truncate.js +1 -0
- package/dist/components/model-identity.d.ts +4 -4
- package/dist/components/model-identity.d.ts.map +1 -1
- package/dist/components/model-identity.js +2 -2
- package/dist/components/model-mark.d.ts.map +1 -1
- package/dist/components/model-mark.js +4 -2
- package/dist/components/multi-select.js +2 -2
- package/dist/components/native-select.d.ts.map +1 -1
- package/dist/components/native-select.js +2 -6
- package/dist/components/notice-banner.js +1 -1
- package/dist/components/object-page.js +2 -2
- package/dist/components/object-workspace.d.ts.map +1 -1
- package/dist/components/object-workspace.js +1 -3
- package/dist/components/overview-page.d.ts +1 -1
- package/dist/components/overview-page.d.ts.map +1 -1
- package/dist/components/overview-page.js +2 -1
- package/dist/components/page-actions.d.ts.map +1 -1
- package/dist/components/page-actions.js +9 -4
- package/dist/components/pairing-code.js +1 -1
- package/dist/components/pending-label.d.ts.map +1 -1
- package/dist/components/pending-label.js +0 -1
- package/dist/components/popover.d.ts +1 -1
- package/dist/components/popover.d.ts.map +1 -1
- package/dist/components/portal-sidebar-frame.d.ts.map +1 -1
- package/dist/components/portal-sidebar-frame.js +1 -1
- package/dist/components/product-message.d.ts.map +1 -1
- package/dist/components/product-message.js +1 -0
- package/dist/components/progress-checklist.d.ts.map +1 -1
- package/dist/components/progress-checklist.js +2 -2
- package/dist/components/progress.d.ts.map +1 -1
- package/dist/components/progress.js +5 -4
- package/dist/components/radio-group.js +2 -2
- package/dist/components/reading-column.js +1 -1
- package/dist/components/record-identity.d.ts.map +1 -1
- package/dist/components/record-identity.js +2 -5
- package/dist/components/record-list.d.ts.map +1 -1
- package/dist/components/record-list.js +4 -3
- package/dist/components/record-sheet.js +1 -1
- package/dist/components/record-time.d.ts.map +1 -1
- package/dist/components/record-time.js +2 -1
- package/dist/components/resizable.d.ts.map +1 -1
- package/dist/components/resizable.js +1 -1
- package/dist/components/row-edit-form.d.ts +2 -1
- package/dist/components/row-edit-form.d.ts.map +1 -1
- package/dist/components/row-edit-form.js +2 -1
- package/dist/components/run-timeline.js +1 -1
- package/dist/components/scope-switcher.js +1 -1
- package/dist/components/segmented-control.js +1 -1
- package/dist/components/select.js +1 -1
- package/dist/components/settings-facts.js +1 -1
- package/dist/components/settings-section.js +1 -1
- package/dist/components/sheet.d.ts +1 -1
- package/dist/components/sheet.d.ts.map +1 -1
- package/dist/components/sheet.js +1 -1
- package/dist/components/sidebar.d.ts +2 -2
- package/dist/components/sidebar.d.ts.map +1 -1
- package/dist/components/sidebar.js +16 -18
- package/dist/components/site-menu.d.ts +28 -0
- package/dist/components/site-menu.d.ts.map +1 -0
- package/dist/components/site-menu.js +23 -0
- package/dist/components/slider.d.ts.map +1 -1
- package/dist/components/slider.js +2 -1
- package/dist/components/snippet.d.ts +2 -0
- package/dist/components/snippet.d.ts.map +1 -1
- package/dist/components/snippet.js +13 -6
- package/dist/components/sonner.d.ts.map +1 -1
- package/dist/components/sonner.js +3 -4
- package/dist/components/status-badge.d.ts +1 -1
- package/dist/components/status-badge.d.ts.map +1 -1
- package/dist/components/switch.js +1 -1
- package/dist/components/table-card.js +1 -1
- package/dist/components/table.js +4 -4
- package/dist/components/tabs-nav.d.ts +52 -0
- package/dist/components/tabs-nav.d.ts.map +1 -0
- package/dist/components/tabs-nav.js +95 -0
- package/dist/components/tabs.d.ts +2 -56
- package/dist/components/tabs.d.ts.map +1 -1
- package/dist/components/tabs.js +2 -91
- package/dist/components/text-link.d.ts +1 -2
- package/dist/components/text-link.d.ts.map +1 -1
- package/dist/components/text-link.js +1 -2
- package/dist/components/textarea.d.ts +5 -2
- package/dist/components/textarea.d.ts.map +1 -1
- package/dist/components/textarea.js +12 -4
- package/dist/components/toast.d.ts +2 -0
- package/dist/components/toast.d.ts.map +1 -1
- package/dist/components/toast.js +4 -0
- package/dist/components/toggle-group.d.ts +3 -1
- package/dist/components/toggle-group.d.ts.map +1 -1
- package/dist/components/toggle-group.js +3 -6
- package/dist/components/toggle.js +1 -1
- package/dist/components/tooltip.d.ts +12 -12
- package/dist/components/tooltip.d.ts.map +1 -1
- package/dist/components/tooltip.js +9 -9
- package/dist/components/trend-badge.js +2 -2
- package/dist/components/update-card.js +1 -1
- package/dist/components/usage-meter.d.ts +1 -1
- package/dist/components/usage-meter.d.ts.map +1 -1
- package/dist/components/usage-meter.js +9 -7
- package/dist/components/waiting-line.js +1 -1
- package/dist/index.d.ts +3 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -7
- package/dist/internal/agent-orb-motion.d.ts +4 -1
- package/dist/internal/agent-orb-motion.d.ts.map +1 -1
- package/dist/internal/agent-orb-motion.js +9 -7
- package/dist/internal/agent-orb.d.ts +4 -1
- package/dist/internal/agent-orb.d.ts.map +1 -1
- package/dist/internal/agent-orb.js +7 -1
- package/dist/internal/assistant-ui/attachment.d.ts.map +1 -1
- package/dist/internal/assistant-ui/attachment.js +15 -15
- package/dist/internal/assistant-ui/follow-up-suggestions.js +1 -1
- package/dist/internal/assistant-ui/markdown-text.js +26 -25
- package/dist/internal/assistant-ui/reasoning.d.ts.map +1 -1
- package/dist/internal/assistant-ui/reasoning.js +13 -16
- package/dist/internal/assistant-ui/thread.d.ts.map +1 -1
- package/dist/internal/assistant-ui/thread.js +22 -16
- package/dist/internal/assistant-ui/tool-fallback.d.ts.map +1 -1
- package/dist/internal/assistant-ui/tool-fallback.js +29 -21
- package/dist/internal/assistant-ui/tool-group.d.ts.map +1 -1
- package/dist/internal/assistant-ui/tool-group.js +6 -7
- package/dist/internal/assistant-ui/tooltip-icon-button.js +1 -1
- package/dist/internal/badge-frame.d.ts +15 -0
- package/dist/internal/badge-frame.d.ts.map +1 -0
- package/dist/internal/badge-frame.js +44 -0
- package/dist/internal/button-classes.d.ts +20 -0
- package/dist/internal/button-classes.d.ts.map +1 -0
- package/dist/internal/button-classes.js +74 -0
- package/dist/internal/catalog-list.d.ts.map +1 -1
- package/dist/internal/catalog-list.js +8 -4
- package/dist/internal/collection-rail.js +3 -3
- package/dist/internal/form-drawer-frame.d.ts +10 -0
- package/dist/internal/form-drawer-frame.d.ts.map +1 -0
- package/dist/internal/form-drawer-frame.js +9 -0
- package/dist/internal/form-submit.d.ts +2 -1
- package/dist/internal/form-submit.d.ts.map +1 -1
- package/dist/internal/settings-save.d.ts.map +1 -1
- package/dist/internal/settings-save.js +4 -2
- package/dist/internal/status-mark.d.ts.map +1 -1
- package/dist/internal/status-mark.js +4 -1
- package/dist/internal/tab-row.d.ts +10 -0
- package/dist/internal/tab-row.d.ts.map +1 -0
- package/dist/internal/tab-row.js +2 -0
- package/dist/internal/table-compact-line.d.ts +1 -1
- package/dist/internal/table-compact-line.d.ts.map +1 -1
- package/dist/internal/table-compact-line.js +1 -1
- package/dist/internal/tooltip-content-frame.d.ts +14 -0
- package/dist/internal/tooltip-content-frame.d.ts.map +1 -0
- package/dist/internal/tooltip-content-frame.js +11 -0
- package/dist/lib/css-variables.d.ts +8 -0
- package/dist/lib/css-variables.d.ts.map +1 -0
- package/dist/lib/css-variables.js +4 -0
- package/dist/lib/merge-tokens.generated.js +1 -1
- package/dist/lib/use-copy-to-clipboard.d.ts.map +1 -1
- package/dist/lib/use-copy-to-clipboard.js +1 -2
- package/dist/showcase/samples.js +1 -1
- package/dist/showcase/specimens/choices.d.ts.map +1 -1
- package/dist/showcase/specimens/choices.js +3 -2
- package/dist/showcase/specimens/compositions.d.ts.map +1 -1
- package/dist/showcase/specimens/compositions.js +7 -5
- package/dist/showcase/specimens/data.d.ts.map +1 -1
- package/dist/showcase/specimens/data.js +1 -1
- package/dist/showcase/specimens/feedback.d.ts.map +1 -1
- package/dist/showcase/specimens/feedback.js +1 -2
- package/dist/showcase/specimens/index.d.ts +2 -0
- package/dist/showcase/specimens/index.d.ts.map +1 -1
- package/dist/showcase/specimens/inputs.d.ts +1 -0
- package/dist/showcase/specimens/inputs.d.ts.map +1 -1
- package/dist/showcase/specimens/inputs.js +5 -2
- package/dist/showcase/specimens/menus.d.ts +1 -0
- package/dist/showcase/specimens/menus.d.ts.map +1 -1
- package/dist/showcase/specimens/menus.js +7 -2
- package/dist/showcase/specimens/navigation.d.ts.map +1 -1
- package/dist/showcase/specimens/navigation.js +6 -5
- package/dist/showcase/specimens/status.d.ts.map +1 -1
- package/dist/showcase/specimens/status.js +4 -4
- package/dist/showcase/usage.generated.d.ts +1 -1
- package/dist/showcase/usage.generated.d.ts.map +1 -1
- package/dist/showcase/usage.generated.js +38 -16
- package/dist/standalone/index.js +1 -1
- package/dist/standalone/orb-page.js +10 -8
- package/dist/styles/components.css +1 -1
- package/package.json +32 -33
- package/dist/eslint/index.js +0 -224
- package/dist/eslint/rules/badge-is-static-label.js +0 -93
- package/dist/eslint/rules/button-icon-slot.js +0 -102
- package/dist/eslint/rules/button-link-for-navigation.js +0 -146
- package/dist/eslint/rules/button-no-aria-label-with-text.js +0 -75
- package/dist/eslint/rules/button-type-roles-only-in-buttons.js +0 -83
- package/dist/eslint/rules/choice-control-has-a-name.js +0 -98
- package/dist/eslint/rules/choice-group-in-fieldset.js +0 -86
- package/dist/eslint/rules/command-shortcut-single-owner.js +0 -118
- package/dist/eslint/rules/confirm-dialog-words.js +0 -63
- package/dist/eslint/rules/data-table-one-unsized-column.js +0 -96
- package/dist/eslint/rules/destructive-action-placement.js +0 -69
- package/dist/eslint/rules/disabled-choice-says-why.js +0 -71
- package/dist/eslint/rules/disabled-control-appearance.js +0 -75
- package/dist/eslint/rules/ellipsis-opens-a-dialog.js +0 -110
- package/dist/eslint/rules/empty-state-owner.js +0 -145
- package/dist/eslint/rules/input-group-owners.js +0 -155
- package/dist/eslint/rules/jsx-class-utility.js +0 -200
- package/dist/eslint/rules/kbd-children-no-modifier-text.js +0 -128
- package/dist/eslint/rules/literal-text.js +0 -41
- package/dist/eslint/rules/menu-owner.js +0 -75
- package/dist/eslint/rules/middle-truncate-placement.js +0 -69
- package/dist/eslint/rules/no-app-feature-component.js +0 -100
- package/dist/eslint/rules/no-bare-button-label.js +0 -55
- package/dist/eslint/rules/no-choice-control-wrapper.js +0 -97
- package/dist/eslint/rules/no-comment.js +0 -77
- package/dist/eslint/rules/no-control-in-read-only-surface.js +0 -106
- package/dist/eslint/rules/no-decorative-icon.js +0 -164
- package/dist/eslint/rules/no-deprecated-zod-api.js +0 -215
- package/dist/eslint/rules/no-global-patch.js +0 -115
- package/dist/eslint/rules/no-hand-built-busy.js +0 -104
- package/dist/eslint/rules/no-hand-built-theme-picker.js +0 -144
- package/dist/eslint/rules/no-hand-built-tile.js +0 -41
- package/dist/eslint/rules/no-hand-formatted-date.js +0 -87
- package/dist/eslint/rules/no-hand-stacked-avatars.js +0 -67
- package/dist/eslint/rules/no-nested-collapse.js +0 -60
- package/dist/eslint/rules/no-package-component-redefinition.js +0 -175
- package/dist/eslint/rules/no-search-in-form.js +0 -47
- package/dist/eslint/rules/no-shadcn-appearance-override.js +0 -305
- package/dist/eslint/rules/no-shell-prompt-in-snippet.js +0 -74
- package/dist/eslint/rules/no-stock-error-words.js +0 -56
- package/dist/eslint/rules/no-subject-derived-expectation.js +0 -183
- package/dist/eslint/rules/no-tooltip-around-labelled-field.js +0 -72
- package/dist/eslint/rules/no-weight-class-on-strong-in-type-role.js +0 -57
- package/dist/eslint/rules/no-write-in-subscription.js +0 -140
- package/dist/eslint/rules/one-submenu-level.js +0 -43
- package/dist/eslint/rules/one-toast-per-flow.js +0 -91
- package/dist/eslint/rules/option-count.js +0 -167
- package/dist/eslint/rules/page-command-link-not-render.js +0 -43
- package/dist/eslint/rules/page-composition.js +0 -232
- package/dist/eslint/rules/progress-needs-name.js +0 -57
- package/dist/eslint/rules/rendered-import.js +0 -121
- package/dist/eslint/rules/server-action-discipline.js +0 -233
- package/dist/eslint/rules/settings-limit-in-footer.js +0 -94
- package/dist/eslint/rules/skeleton-in-region.js +0 -80
- package/dist/eslint/rules/sortable-heading-uses-sort.js +0 -79
- package/dist/eslint/rules/table-cell-inset-owner.js +0 -107
- package/dist/eslint/rules/tabs-selection-is-controlled.js +0 -47
- package/dist/eslint/rules/test-files-from-one-definition.js +0 -67
- package/dist/eslint/rules/tooltip-only-arrow.js +0 -38
- package/dist/eslint/rules/tooltip-trigger-accessible-name.js +0 -114
- package/dist/eslint/rules/workspace-declares-imports.js +0 -163
- package/dist/eslint/strict-design-system.js +0 -250
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: `no-restricted-imports` holds the menu modules
|
|
3
|
-
* and the ellipsis icons (`repo/menu-owner-imports` in the root config's
|
|
4
|
-
* product-composition blocks), but not a destructive `Button` rendered once per
|
|
5
|
-
* row: that is an import binding joined to where its element renders, which no
|
|
6
|
-
* import rule sees and a selector cannot follow through a renamed import.
|
|
7
|
-
*/
|
|
8
|
-
import { PACKAGE_BARREL, partOf, renderedImports, staticAttribute } from "./rendered-import.js";
|
|
9
|
-
|
|
10
|
-
const BUTTON = new Map([
|
|
11
|
-
[
|
|
12
|
-
"Button",
|
|
13
|
-
["@nadicodeai/ui/components/button", PACKAGE_BARREL].map((from) => ({ from, part: "Button" })),
|
|
14
|
-
],
|
|
15
|
-
]);
|
|
16
|
-
|
|
17
|
-
const FUNCTIONS = new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
|
|
18
|
-
const ROW_CALLBACKS = new Set(["map", "flatMap"]);
|
|
19
|
-
const ROW_RENDERERS = new Set(["cell", "command"]);
|
|
20
|
-
const ROW_COMPONENT = /Row$/;
|
|
21
|
-
|
|
22
|
-
const nameOf = (key) => (key?.type === "Identifier" ? key.name : key?.value);
|
|
23
|
-
|
|
24
|
-
function drawsOneRecord(fn) {
|
|
25
|
-
const { parent } = fn;
|
|
26
|
-
if (
|
|
27
|
-
parent.type === "CallExpression" &&
|
|
28
|
-
parent.arguments[0] === fn &&
|
|
29
|
-
parent.callee.type === "MemberExpression" &&
|
|
30
|
-
!parent.callee.computed &&
|
|
31
|
-
ROW_CALLBACKS.has(parent.callee.property.name)
|
|
32
|
-
) {
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
if (parent.type === "Property" && parent.value === fn) {
|
|
36
|
-
return ROW_RENDERERS.has(nameOf(parent.key));
|
|
37
|
-
}
|
|
38
|
-
if (parent.type === "JSXExpressionContainer" && parent.parent.type === "JSXAttribute") {
|
|
39
|
-
return ROW_RENDERERS.has(parent.parent.name.name);
|
|
40
|
-
}
|
|
41
|
-
if (fn.type === "FunctionDeclaration") return ROW_COMPONENT.test(fn.id?.name ?? "");
|
|
42
|
-
return parent.type === "VariableDeclarator" && ROW_COMPONENT.test(nameOf(parent.id) ?? "");
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function insideARow(node) {
|
|
46
|
-
for (let current = node.parent; current; current = current.parent) {
|
|
47
|
-
if (FUNCTIONS.has(current.type) && drawsOneRecord(current)) return true;
|
|
48
|
-
}
|
|
49
|
-
return false;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export const menuOwner = {
|
|
53
|
-
meta: {
|
|
54
|
-
type: "problem",
|
|
55
|
-
docs: {
|
|
56
|
-
description:
|
|
57
|
-
"A record's destructive command is the last item of its ActionMenu, never a destructive button in its row.",
|
|
58
|
-
},
|
|
59
|
-
schema: [],
|
|
60
|
-
messages: {
|
|
61
|
-
"row-destructive-button":
|
|
62
|
-
"A destructive command is the last item of the row's ActionMenu, after a divider.",
|
|
63
|
-
},
|
|
64
|
-
},
|
|
65
|
-
create(context) {
|
|
66
|
-
const importOf = renderedImports(context);
|
|
67
|
-
return {
|
|
68
|
-
JSXOpeningElement(node) {
|
|
69
|
-
if (partOf(importOf(node.name), BUTTON, context.filename) !== "Button") return;
|
|
70
|
-
if (staticAttribute(node, "variant") !== "destructive") return;
|
|
71
|
-
if (insideARow(node)) context.report({ node, messageId: "row-destructive-button" });
|
|
72
|
-
},
|
|
73
|
-
};
|
|
74
|
-
},
|
|
75
|
-
};
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: a `no-restricted-syntax` descendant selector can
|
|
3
|
-
* match `MiddleTruncate` under an element by name, but not that the name is the
|
|
4
|
-
* design system's import, that the class sits in a `cn(...)` call or a
|
|
5
|
-
* same-file table, nor that the nearest control carries an `aria-label`
|
|
6
|
-
* without a second selector per spelling.
|
|
7
|
-
*/
|
|
8
|
-
import { openingElementClassUtilities } from "./jsx-class-utility.js";
|
|
9
|
-
import { renderedImports } from "./rendered-import.js";
|
|
10
|
-
|
|
11
|
-
const FROM_DESIGN_SYSTEM = /^(@nadicodeai\/ui(\/|$)|@\/|\.)/;
|
|
12
|
-
const CONTROLS = new Set(["Button", "ButtonLink", "TextLink"]);
|
|
13
|
-
const HOST_CONTROLS = new Set(["a", "button"]);
|
|
14
|
-
const NAMING_ATTRIBUTES = new Set(["aria-label", "aria-labelledby"]);
|
|
15
|
-
const TRUNCATING = /^(truncate|text-ellipsis|overflow-ellipsis|line-clamp-(?!none$).+)$/;
|
|
16
|
-
|
|
17
|
-
const withoutVariants = (utility) => utility.split(":").pop().replace(/^!/, "");
|
|
18
|
-
|
|
19
|
-
const namesItself = (openingElement) =>
|
|
20
|
-
openingElement.attributes.some(
|
|
21
|
-
(attribute) => attribute.type === "JSXAttribute" && NAMING_ATTRIBUTES.has(attribute.name.name),
|
|
22
|
-
);
|
|
23
|
-
|
|
24
|
-
export const middleTruncatePlacement = {
|
|
25
|
-
meta: {
|
|
26
|
-
type: "problem",
|
|
27
|
-
docs: {
|
|
28
|
-
description: "MiddleTruncate cuts its own ellipsis and a control around it carries the name.",
|
|
29
|
-
},
|
|
30
|
-
schema: [],
|
|
31
|
-
messages: {
|
|
32
|
-
"cuts-its-own-ellipsis":
|
|
33
|
-
"MiddleTruncate cuts its own ellipsis: take it out of the element that also truncates. Inside a control, give the control an aria-label that names its target, since the cut value is not a name.",
|
|
34
|
-
},
|
|
35
|
-
},
|
|
36
|
-
create(context) {
|
|
37
|
-
const importOf = renderedImports(context);
|
|
38
|
-
const designSystemExport = (elementName) => {
|
|
39
|
-
const imported = importOf(elementName);
|
|
40
|
-
return imported && FROM_DESIGN_SYSTEM.test(imported.source) ? imported.name : null;
|
|
41
|
-
};
|
|
42
|
-
const isControl = ({ name }) =>
|
|
43
|
-
CONTROLS.has(designSystemExport(name)) ||
|
|
44
|
-
(name.type === "JSXIdentifier" && HOST_CONTROLS.has(name.name) && !importOf(name));
|
|
45
|
-
const truncates = (openingElement) =>
|
|
46
|
-
openingElementClassUtilities(openingElement, context).some((utility) =>
|
|
47
|
-
TRUNCATING.test(withoutVariants(utility)),
|
|
48
|
-
);
|
|
49
|
-
|
|
50
|
-
return {
|
|
51
|
-
JSXElement(node) {
|
|
52
|
-
if (designSystemExport(node.openingElement.name) !== "MiddleTruncate") return;
|
|
53
|
-
let nearestControl = null;
|
|
54
|
-
for (let ancestor = node.parent; ancestor; ancestor = ancestor.parent) {
|
|
55
|
-
if (ancestor.type !== "JSXElement") continue;
|
|
56
|
-
const { openingElement } = ancestor;
|
|
57
|
-
if (truncates(openingElement)) {
|
|
58
|
-
context.report({ node: node.openingElement, messageId: "cuts-its-own-ellipsis" });
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
if (!nearestControl && isControl(openingElement)) nearestControl = openingElement;
|
|
62
|
-
}
|
|
63
|
-
if (nearestControl && !namesItself(nearestControl)) {
|
|
64
|
-
context.report({ node: node.openingElement, messageId: "cuts-its-own-ellipsis" });
|
|
65
|
-
}
|
|
66
|
-
},
|
|
67
|
-
};
|
|
68
|
-
},
|
|
69
|
-
};
|
|
@@ -1,100 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* `@nadicodeai/ui` ships standard components. An operator login, a portal
|
|
3
|
-
* activation screen, a customer-credential panel — those are use cases, and a
|
|
4
|
-
* use case in the package is a screen two consumers can never both want. The
|
|
5
|
-
* boundary is named in the app's vocabulary, so the rule reads names: the
|
|
6
|
-
* module's own file name and everything it exports.
|
|
7
|
-
*
|
|
8
|
-
* Standard tool checked first: core `no-restricted-syntax` matches
|
|
9
|
-
* an exported identifier against a regex through esquery, but it is given one
|
|
10
|
-
* node at a time and never the file that node is in — and a module called
|
|
11
|
-
* `activation.tsx` exporting `Banner` is the case the file name is there to
|
|
12
|
-
* catch. `context.filename` is why this is a rule rather than a selector list.
|
|
13
|
-
*
|
|
14
|
-
* The vocabulary itself is configuration, not code: the banned words live in the
|
|
15
|
-
* workspace's eslint config where the package boundary is drawn, and this file
|
|
16
|
-
* holds only the matching. Replaces the @nadicodeai/ui no-app-feature-components
|
|
17
|
-
* scanner (#822), which built a TypeScript AST over the package's components to
|
|
18
|
-
* ask the same question.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
/** `OperatorLoginCard` and `operator-login.tsx` both normalise to `operator-login`. */
|
|
22
|
-
function normalizeName(value) {
|
|
23
|
-
return value
|
|
24
|
-
.replace(/\.[jt]sx?$/, "")
|
|
25
|
-
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
26
|
-
.replace(/[\s_]+/g, "-")
|
|
27
|
-
.toLowerCase();
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** The declared names one `export` statement introduces. */
|
|
31
|
-
function exportedNames(node) {
|
|
32
|
-
if (!node.declaration) {
|
|
33
|
-
return node.specifiers.map((specifier) => specifier.exported.name ?? specifier.exported.value);
|
|
34
|
-
}
|
|
35
|
-
const declaration = node.declaration;
|
|
36
|
-
if (declaration.type === "VariableDeclaration") {
|
|
37
|
-
return declaration.declarations
|
|
38
|
-
.filter((declarator) => declarator.id.type === "Identifier")
|
|
39
|
-
.map((declarator) => declarator.id.name);
|
|
40
|
-
}
|
|
41
|
-
return declaration.id ? [declaration.id.name] : [];
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export const noAppFeatureComponent = {
|
|
45
|
-
meta: {
|
|
46
|
-
type: "problem",
|
|
47
|
-
docs: {
|
|
48
|
-
description: "Disallow app-owned feature and use-case names in a shared component package",
|
|
49
|
-
},
|
|
50
|
-
schema: [
|
|
51
|
-
{
|
|
52
|
-
type: "object",
|
|
53
|
-
properties: {
|
|
54
|
-
names: {
|
|
55
|
-
type: "array",
|
|
56
|
-
items: {
|
|
57
|
-
type: "object",
|
|
58
|
-
properties: { feature: { type: "string" }, pattern: { type: "string" } },
|
|
59
|
-
required: ["feature", "pattern"],
|
|
60
|
-
additionalProperties: false,
|
|
61
|
-
},
|
|
62
|
-
},
|
|
63
|
-
},
|
|
64
|
-
additionalProperties: false,
|
|
65
|
-
},
|
|
66
|
-
],
|
|
67
|
-
messages: {
|
|
68
|
-
appFeature:
|
|
69
|
-
"{{what}} names the {{feature}} use case. This package ships standard components; compose an app feature inside the consuming app.",
|
|
70
|
-
},
|
|
71
|
-
},
|
|
72
|
-
create(context) {
|
|
73
|
-
const names = (context.options?.[0]?.names ?? []).map(({ feature, pattern }) => ({
|
|
74
|
-
feature,
|
|
75
|
-
pattern: new RegExp(pattern),
|
|
76
|
-
}));
|
|
77
|
-
if (names.length === 0) return {};
|
|
78
|
-
|
|
79
|
-
const feature = (candidate) =>
|
|
80
|
-
names.find(({ pattern }) => pattern.test(normalizeName(candidate)))?.feature;
|
|
81
|
-
|
|
82
|
-
const report = (node, candidate, what) => {
|
|
83
|
-
const matched = feature(candidate);
|
|
84
|
-
if (matched)
|
|
85
|
-
context.report({ node, messageId: "appFeature", data: { what, feature: matched } });
|
|
86
|
-
};
|
|
87
|
-
|
|
88
|
-
return {
|
|
89
|
-
Program(node) {
|
|
90
|
-
const file = context.filename.replaceAll("\\", "/").split("/").at(-1) ?? "";
|
|
91
|
-
report(node, file, `The module name "${file}"`);
|
|
92
|
-
},
|
|
93
|
-
ExportNamedDeclaration(node) {
|
|
94
|
-
for (const name of exportedNames(node)) {
|
|
95
|
-
report(node, name, `The export "${name}"`);
|
|
96
|
-
}
|
|
97
|
-
},
|
|
98
|
-
};
|
|
99
|
-
},
|
|
100
|
-
};
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: `no-restricted-syntax` can match a `Button`
|
|
3
|
-
* whose text is "OK", but not whether that `Button` is the package's (an
|
|
4
|
-
* import binding, so a renamed or local one is judged correctly), nor read the
|
|
5
|
-
* same word off a form's `submit={{ label }}`.
|
|
6
|
-
*/
|
|
7
|
-
import { keyName, childText, literalText } from "./literal-text.js";
|
|
8
|
-
import { renderedImports } from "./rendered-import.js";
|
|
9
|
-
|
|
10
|
-
const BUTTONS = new Set(["Button", "ButtonLink"]);
|
|
11
|
-
const BARE = /^(?:submit|ok|okay|confirm)[.!…]*$/i;
|
|
12
|
-
const FROM_DESIGN_SYSTEM = /^(@nadicodeai\/ui(\/|$)|@\/|\.)/;
|
|
13
|
-
|
|
14
|
-
export const noBareButtonLabel = {
|
|
15
|
-
meta: {
|
|
16
|
-
type: "problem",
|
|
17
|
-
docs: {
|
|
18
|
-
description:
|
|
19
|
-
"A button says its result as a verb and a noun, never a bare Submit, OK or Confirm.",
|
|
20
|
-
},
|
|
21
|
-
schema: [],
|
|
22
|
-
messages: {
|
|
23
|
-
"bare-label":
|
|
24
|
-
'A button says its result: "Create customer", "Save changes", "Remove mailbox". "{{label}}" says nothing about what happens.',
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
create(context) {
|
|
28
|
-
const importOf = renderedImports(context);
|
|
29
|
-
|
|
30
|
-
function report(node, label) {
|
|
31
|
-
context.report({ node, messageId: "bare-label", data: { label } });
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
return {
|
|
35
|
-
JSXElement(node) {
|
|
36
|
-
const imported = importOf(node.openingElement.name);
|
|
37
|
-
if (!imported || !BUTTONS.has(imported.name) || !FROM_DESIGN_SYSTEM.test(imported.source)) {
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
const label = childText(node);
|
|
41
|
-
if (label !== null && BARE.test(label)) report(node.openingElement, label);
|
|
42
|
-
},
|
|
43
|
-
JSXAttribute(node) {
|
|
44
|
-
if (node.name.name !== "submit" || node.value?.type !== "JSXExpressionContainer") return;
|
|
45
|
-
const { expression } = node.value;
|
|
46
|
-
if (expression.type !== "ObjectExpression") return;
|
|
47
|
-
for (const property of expression.properties) {
|
|
48
|
-
if (keyName(property) !== "label") continue;
|
|
49
|
-
const label = literalText(property.value);
|
|
50
|
-
if (label !== null && BARE.test(label.trim())) report(property.value, label.trim());
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
};
|
|
54
|
-
},
|
|
55
|
-
};
|
|
@@ -1,97 +0,0 @@
|
|
|
1
|
-
const CHOICE_CONTROLS = new Set(["Select", "ToggleGroup", "RadioGroup", "Tabs"]);
|
|
2
|
-
|
|
3
|
-
const FUNCTIONS = new Set(["FunctionDeclaration", "FunctionExpression", "ArrowFunctionExpression"]);
|
|
4
|
-
|
|
5
|
-
function enclosingFunction(node) {
|
|
6
|
-
let current = node.parent;
|
|
7
|
-
while (current && !FUNCTIONS.has(current.type)) current = current.parent;
|
|
8
|
-
return current;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
/** A component's own props: a destructured or typed parameter. */
|
|
12
|
-
const isComponent = (fn) =>
|
|
13
|
-
fn.params.some((param) => param.type === "ObjectPattern" || param.typeAnnotation);
|
|
14
|
-
|
|
15
|
-
/** Whether `owner` handed the element its value: the element's own component, or a callback inside it. */
|
|
16
|
-
function handedTo(element, owner) {
|
|
17
|
-
for (let fn = enclosingFunction(element); fn; fn = enclosingFunction(fn)) {
|
|
18
|
-
if (fn === owner) return true;
|
|
19
|
-
if (isComponent(fn)) return false;
|
|
20
|
-
}
|
|
21
|
-
return false;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
/** The identifiers in a handler that invoke what they name: the handler itself, a callee, or a method's receiver. */
|
|
25
|
-
function invoked(handler) {
|
|
26
|
-
const found = [];
|
|
27
|
-
const visit = (node) => {
|
|
28
|
-
// 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.
|
|
29
|
-
if (!node || typeof node.type !== "string") return;
|
|
30
|
-
if (node.type === "Identifier") {
|
|
31
|
-
const parent = node.parent;
|
|
32
|
-
const isCallee = parent?.type === "CallExpression" && parent.callee === node;
|
|
33
|
-
const isReceiver =
|
|
34
|
-
parent?.type === "MemberExpression" &&
|
|
35
|
-
parent.object === node &&
|
|
36
|
-
parent.parent?.type === "CallExpression" &&
|
|
37
|
-
parent.parent.callee === parent;
|
|
38
|
-
if (node === handler || isCallee || isReceiver) found.push(node);
|
|
39
|
-
}
|
|
40
|
-
for (const [key, value] of Object.entries(node)) {
|
|
41
|
-
if (key === "parent") continue;
|
|
42
|
-
for (const child of Array.isArray(value) ? value : [value]) {
|
|
43
|
-
// 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.
|
|
44
|
-
if (child && typeof child.type === "string") visit(child);
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
};
|
|
48
|
-
visit(handler);
|
|
49
|
-
return found;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* A kit choice control whose onValueChange forwards to a callback the enclosing
|
|
54
|
-
* component was handed. `ToolbarToggle` flattened Base UI's array into a string
|
|
55
|
-
* there, and "Split by" stopped working while every check stayed green.
|
|
56
|
-
*/
|
|
57
|
-
export const noChoiceControlWrapper = {
|
|
58
|
-
meta: {
|
|
59
|
-
type: "problem",
|
|
60
|
-
docs: {
|
|
61
|
-
description:
|
|
62
|
-
"Render a kit choice control where its value is used, never behind a wrapper that forwards it.",
|
|
63
|
-
},
|
|
64
|
-
schema: [],
|
|
65
|
-
messages: {
|
|
66
|
-
wrapper:
|
|
67
|
-
"Render the kit's {{control}} where the value is used, with its own API: this one forwards onValueChange to \"{{name}}\", which the component was handed, and a wrapper is where the value's shape gets lost.",
|
|
68
|
-
},
|
|
69
|
-
},
|
|
70
|
-
create(context) {
|
|
71
|
-
return {
|
|
72
|
-
"JSXAttribute[name.name='onValueChange'] > JSXExpressionContainer"(container) {
|
|
73
|
-
const opening = container.parent.parent;
|
|
74
|
-
const control = opening.name.type === "JSXIdentifier" ? opening.name.name : null;
|
|
75
|
-
if (!CHOICE_CONTROLS.has(control)) return;
|
|
76
|
-
const handler = container.expression;
|
|
77
|
-
for (const identifier of invoked(handler)) {
|
|
78
|
-
const scope = context.sourceCode.getScope(identifier);
|
|
79
|
-
let variable = null;
|
|
80
|
-
for (let current = scope; current && !variable; current = current.upper) {
|
|
81
|
-
variable = current.set.get(identifier.name) ?? null;
|
|
82
|
-
}
|
|
83
|
-
const definition = variable?.defs[0];
|
|
84
|
-
if (definition?.type !== "Parameter") continue;
|
|
85
|
-
if (definition.node === handler) continue;
|
|
86
|
-
if (!handedTo(opening, definition.node)) continue;
|
|
87
|
-
context.report({
|
|
88
|
-
node: container,
|
|
89
|
-
messageId: "wrapper",
|
|
90
|
-
data: { control, name: identifier.name },
|
|
91
|
-
});
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
},
|
|
95
|
-
};
|
|
96
|
-
},
|
|
97
|
-
};
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Code says what it does in its names and its structure, and a test says what
|
|
3
|
-
* it proves in its name and its assertions; a comment restates one of them and
|
|
4
|
-
* nothing keeps it true when the code changes. A directive to a tool is not
|
|
5
|
-
* prose for the reader: ESLint's, Oxlint's, TypeScript's, Biome's, Prettier's,
|
|
6
|
-
* coverage's and Vitest's stay, and so does the marker the seam guard requires.
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
const DIRECTIVE =
|
|
10
|
-
/^[*/]?\s*(?:@ts-|eslint-|eslint\s+[\w@/-]+\s*:|oxlint-|biome-ignore|<reference\b|istanbul\b|c8\s|prettier-ignore|@vitest-environment|@jsx)/;
|
|
11
|
-
|
|
12
|
-
const SEAM_MARKER = /^\s*NADIA\((?:added|changed|removed)\):/;
|
|
13
|
-
|
|
14
|
-
function moduleHeader(comments, firstTokenStart) {
|
|
15
|
-
const [first, ...rest] = comments;
|
|
16
|
-
if (first === undefined || first.range[1] > firstTokenStart) return [];
|
|
17
|
-
if (first.type === "Block") return [first];
|
|
18
|
-
const run = [first];
|
|
19
|
-
for (const next of rest) {
|
|
20
|
-
if (
|
|
21
|
-
next.type !== "Line" ||
|
|
22
|
-
next.range[1] > firstTokenStart ||
|
|
23
|
-
next.loc.start.line !== run.at(-1).loc.end.line + 1
|
|
24
|
-
) {
|
|
25
|
-
break;
|
|
26
|
-
}
|
|
27
|
-
run.push(next);
|
|
28
|
-
}
|
|
29
|
-
return run;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export const noComment = {
|
|
33
|
-
meta: {
|
|
34
|
-
type: "problem",
|
|
35
|
-
docs: {
|
|
36
|
-
description:
|
|
37
|
-
"Disallow comments in code, but for directives to a tool, the marker the seam guard requires and, when allowed, the one comment that opens a module",
|
|
38
|
-
},
|
|
39
|
-
schema: [
|
|
40
|
-
{
|
|
41
|
-
type: "object",
|
|
42
|
-
properties: { allowModuleHeader: { type: "boolean" } },
|
|
43
|
-
additionalProperties: false,
|
|
44
|
-
},
|
|
45
|
-
],
|
|
46
|
-
defaultOptions: [{ allowModuleHeader: false }],
|
|
47
|
-
messages: {
|
|
48
|
-
comment:
|
|
49
|
-
"Code says what it does in its names and its structure, never in a comment. Rename or restructure, or remove the comment.",
|
|
50
|
-
},
|
|
51
|
-
},
|
|
52
|
-
|
|
53
|
-
create(context) {
|
|
54
|
-
const [{ allowModuleHeader }] = context.options;
|
|
55
|
-
const { sourceCode } = context;
|
|
56
|
-
return {
|
|
57
|
-
Program() {
|
|
58
|
-
const prose = sourceCode
|
|
59
|
-
.getAllComments()
|
|
60
|
-
.filter(
|
|
61
|
-
(comment) =>
|
|
62
|
-
comment.type !== "Shebang" &&
|
|
63
|
-
!DIRECTIVE.test(comment.value) &&
|
|
64
|
-
!SEAM_MARKER.test(comment.value),
|
|
65
|
-
);
|
|
66
|
-
const header = allowModuleHeader
|
|
67
|
-
? moduleHeader(prose, sourceCode.ast.tokens[0]?.range[0] ?? Infinity)
|
|
68
|
-
: [];
|
|
69
|
-
for (const comment of prose) {
|
|
70
|
-
if (!header.includes(comment)) {
|
|
71
|
-
context.report({ loc: comment.loc, messageId: "comment" });
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
};
|
|
76
|
-
},
|
|
77
|
-
};
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* Standard tool checked first: a `no-restricted-syntax` selector can match a
|
|
3
|
-
* `Button` below `TooltipContent`, but not follow a renamed import to the part,
|
|
4
|
-
* count the links across a HoverCardContent's children and its summary's
|
|
5
|
-
* `action`.
|
|
6
|
-
*/
|
|
7
|
-
import { renderedImports, walk } from "./rendered-import.js";
|
|
8
|
-
|
|
9
|
-
const OVERLAYS = new Set(["Tooltip", "HoverCard"]);
|
|
10
|
-
const CONTROLS = new Set(["Button", "ButtonLink", "TextLink", "Link", "a"]);
|
|
11
|
-
const NATIVE_CONTROLS = new Set(["input", "select", "textarea"]);
|
|
12
|
-
const HOVER_CARD_SUMMARY = "HoverCardSummary";
|
|
13
|
-
|
|
14
|
-
const hasAttribute = (openingElement, attributeName) =>
|
|
15
|
-
openingElement.attributes.some(
|
|
16
|
-
(attribute) => attribute.type === "JSXAttribute" && attribute.name.name === attributeName,
|
|
17
|
-
);
|
|
18
|
-
|
|
19
|
-
export const noControlInReadOnlySurface = {
|
|
20
|
-
meta: {
|
|
21
|
-
type: "problem",
|
|
22
|
-
docs: {
|
|
23
|
-
description:
|
|
24
|
-
"Tooltip text and a Choicebox tile are for reading; a HoverCard holds at most one link.",
|
|
25
|
-
},
|
|
26
|
-
schema: [],
|
|
27
|
-
messages: {
|
|
28
|
-
"read-only-surface":
|
|
29
|
-
"Tooltip text and a Choicebox tile are for reading: move the control outside. A HoverCard holds at most one link, through HoverCardSummary's `action`, and never a second Tooltip or HoverCard; for more, open a Popover, a menu or the record's page.",
|
|
30
|
-
},
|
|
31
|
-
},
|
|
32
|
-
create(context) {
|
|
33
|
-
const importOf = renderedImports(context);
|
|
34
|
-
const { visitorKeys } = context.sourceCode;
|
|
35
|
-
|
|
36
|
-
const nameOf = (element) => {
|
|
37
|
-
const { name } = element.openingElement;
|
|
38
|
-
if (name.type !== "JSXIdentifier" && name.type !== "JSXMemberExpression") return null;
|
|
39
|
-
if (name.type === "JSXIdentifier" && /^[a-z]/.test(name.name)) return name.name;
|
|
40
|
-
const imported = importOf(name);
|
|
41
|
-
if (!imported) return null;
|
|
42
|
-
return imported.name === "default" ? name.name : imported.name;
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
const isInteractive = (element, name) =>
|
|
46
|
-
CONTROLS.has(name) ||
|
|
47
|
-
NATIVE_CONTROLS.has(name) ||
|
|
48
|
-
hasAttribute(element.openingElement, "onClick") ||
|
|
49
|
-
hasAttribute(element.openingElement, "tabIndex");
|
|
50
|
-
|
|
51
|
-
function visitChildren(container, visit) {
|
|
52
|
-
for (const child of container.children) {
|
|
53
|
-
if (visit(child) === false) continue;
|
|
54
|
-
walk(child, visitorKeys, visit);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function checkTooltipContent(container) {
|
|
59
|
-
visitChildren(container, (child) => {
|
|
60
|
-
if (child.type !== "JSXElement") return true;
|
|
61
|
-
const name = nameOf(child);
|
|
62
|
-
if (!OVERLAYS.has(name) && !isInteractive(child, name)) return true;
|
|
63
|
-
context.report({ node: child.openingElement, messageId: "read-only-surface" });
|
|
64
|
-
return false;
|
|
65
|
-
});
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function checkChoiceboxItem(container) {
|
|
69
|
-
visitChildren(container, (child) => {
|
|
70
|
-
if (child.type !== "JSXElement" || !isInteractive(child, nameOf(child))) return true;
|
|
71
|
-
context.report({ node: child.openingElement, messageId: "read-only-surface" });
|
|
72
|
-
return false;
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
function checkHoverCardContent(container) {
|
|
77
|
-
let links = 0;
|
|
78
|
-
visitChildren(container, (child) => {
|
|
79
|
-
if (child.type !== "JSXElement") return true;
|
|
80
|
-
const name = nameOf(child);
|
|
81
|
-
const { openingElement } = child;
|
|
82
|
-
if (OVERLAYS.has(name)) {
|
|
83
|
-
context.report({ node: openingElement, messageId: "read-only-surface" });
|
|
84
|
-
return false;
|
|
85
|
-
}
|
|
86
|
-
const isLink =
|
|
87
|
-
CONTROLS.has(name) ||
|
|
88
|
-
hasAttribute(openingElement, "onClick") ||
|
|
89
|
-
(name === HOVER_CARD_SUMMARY && hasAttribute(openingElement, "action"));
|
|
90
|
-
if (!isLink) return true;
|
|
91
|
-
links += 1;
|
|
92
|
-
if (links > 1) context.report({ node: openingElement, messageId: "read-only-surface" });
|
|
93
|
-
return true;
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return {
|
|
98
|
-
JSXElement(node) {
|
|
99
|
-
const name = nameOf(node);
|
|
100
|
-
if (name === "TooltipContent") checkTooltipContent(node);
|
|
101
|
-
else if (name === "ChoiceboxGroupItem") checkChoiceboxItem(node);
|
|
102
|
-
else if (name === "HoverCardContent") checkHoverCardContent(node);
|
|
103
|
-
},
|
|
104
|
-
};
|
|
105
|
-
},
|
|
106
|
-
};
|