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