@setsquare/ui 0.1.0
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/CHANGELOG.md +70 -0
- package/LICENSE +202 -0
- package/NOTICE +49 -0
- package/README.md +313 -0
- package/dist/index.d.ts +129 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/cn.d.ts +4 -0
- package/dist/lib/cn.d.ts.map +1 -0
- package/dist/lib/cn.js +59 -0
- package/dist/lib/cn.js.map +1 -0
- package/dist/lib/tokens.d.ts +5 -0
- package/dist/lib/tokens.d.ts.map +1 -0
- package/dist/lib/tokens.js +22 -0
- package/dist/lib/tokens.js.map +1 -0
- package/dist/lib/use-element-width.d.ts +4 -0
- package/dist/lib/use-element-width.d.ts.map +1 -0
- package/dist/lib/use-element-width.js +17 -0
- package/dist/lib/use-element-width.js.map +1 -0
- package/dist/lint/index.d.ts +41 -0
- package/dist/lint/index.d.ts.map +1 -0
- package/dist/lint/index.js +648 -0
- package/dist/lint/index.js.map +1 -0
- package/dist/shell/app-header.d.ts +13 -0
- package/dist/shell/app-header.d.ts.map +1 -0
- package/dist/shell/app-header.js +11 -0
- package/dist/shell/app-header.js.map +1 -0
- package/dist/shell/dashboard-shell.d.ts +67 -0
- package/dist/shell/dashboard-shell.d.ts.map +1 -0
- package/dist/shell/dashboard-shell.js +168 -0
- package/dist/shell/dashboard-shell.js.map +1 -0
- package/dist/shell/page-header.d.ts +19 -0
- package/dist/shell/page-header.d.ts.map +1 -0
- package/dist/shell/page-header.js +11 -0
- package/dist/shell/page-header.js.map +1 -0
- package/dist/shell/right-panel.d.ts +25 -0
- package/dist/shell/right-panel.d.ts.map +1 -0
- package/dist/shell/right-panel.js +37 -0
- package/dist/shell/right-panel.js.map +1 -0
- package/dist/shell/sidebar.d.ts +68 -0
- package/dist/shell/sidebar.d.ts.map +1 -0
- package/dist/shell/sidebar.js +68 -0
- package/dist/shell/sidebar.js.map +1 -0
- package/dist/shell/site-footer.d.ts +28 -0
- package/dist/shell/site-footer.d.ts.map +1 -0
- package/dist/shell/site-footer.js +15 -0
- package/dist/shell/site-footer.js.map +1 -0
- package/dist/shell/site-header-padding.d.ts +7 -0
- package/dist/shell/site-header-padding.d.ts.map +1 -0
- package/dist/shell/site-header-padding.js +27 -0
- package/dist/shell/site-header-padding.js.map +1 -0
- package/dist/shell/site-header.d.ts +23 -0
- package/dist/shell/site-header.d.ts.map +1 -0
- package/dist/shell/site-header.js +13 -0
- package/dist/shell/site-header.js.map +1 -0
- package/dist/shell/site-menu.d.ts +27 -0
- package/dist/shell/site-menu.d.ts.map +1 -0
- package/dist/shell/site-menu.js +56 -0
- package/dist/shell/site-menu.js.map +1 -0
- package/dist/shell/site-nav-link.d.ts +24 -0
- package/dist/shell/site-nav-link.d.ts.map +1 -0
- package/dist/shell/site-nav-link.js +35 -0
- package/dist/shell/site-nav-link.js.map +1 -0
- package/dist/shell/site-nav.d.ts +12 -0
- package/dist/shell/site-nav.d.ts.map +1 -0
- package/dist/shell/site-nav.js +9 -0
- package/dist/shell/site-nav.js.map +1 -0
- package/dist/shell/site-path.d.ts +8 -0
- package/dist/shell/site-path.d.ts.map +1 -0
- package/dist/shell/site-path.js +11 -0
- package/dist/shell/site-path.js.map +1 -0
- package/dist/shell/site-shell.d.ts +25 -0
- package/dist/shell/site-shell.d.ts.map +1 -0
- package/dist/shell/site-shell.js +17 -0
- package/dist/shell/site-shell.js.map +1 -0
- package/dist/styles/styles.css +10 -0
- package/dist/styles/theme.css +579 -0
- package/dist/styles/tokens.css +295 -0
- package/dist/testing/index.d.ts +321 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +1175 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/tokens.json +982 -0
- package/dist/ui/alert.d.ts +35 -0
- package/dist/ui/alert.d.ts.map +1 -0
- package/dist/ui/alert.js +65 -0
- package/dist/ui/alert.js.map +1 -0
- package/dist/ui/avatar.d.ts +16 -0
- package/dist/ui/avatar.d.ts.map +1 -0
- package/dist/ui/avatar.js +13 -0
- package/dist/ui/avatar.js.map +1 -0
- package/dist/ui/badge.d.ts +13 -0
- package/dist/ui/badge.d.ts.map +1 -0
- package/dist/ui/badge.js +24 -0
- package/dist/ui/badge.js.map +1 -0
- package/dist/ui/breadcrumbs.d.ts +13 -0
- package/dist/ui/breadcrumbs.d.ts.map +1 -0
- package/dist/ui/breadcrumbs.js +12 -0
- package/dist/ui/breadcrumbs.js.map +1 -0
- package/dist/ui/button-styles.d.ts +24 -0
- package/dist/ui/button-styles.d.ts.map +1 -0
- package/dist/ui/button-styles.js +50 -0
- package/dist/ui/button-styles.js.map +1 -0
- package/dist/ui/button.d.ts +27 -0
- package/dist/ui/button.d.ts.map +1 -0
- package/dist/ui/button.js +30 -0
- package/dist/ui/button.js.map +1 -0
- package/dist/ui/calendar.d.ts +19 -0
- package/dist/ui/calendar.d.ts.map +1 -0
- package/dist/ui/calendar.js +52 -0
- package/dist/ui/calendar.js.map +1 -0
- package/dist/ui/card.d.ts +30 -0
- package/dist/ui/card.d.ts.map +1 -0
- package/dist/ui/card.js +18 -0
- package/dist/ui/card.js.map +1 -0
- package/dist/ui/checkbox.d.ts +36 -0
- package/dist/ui/checkbox.d.ts.map +1 -0
- package/dist/ui/checkbox.js +38 -0
- package/dist/ui/checkbox.js.map +1 -0
- package/dist/ui/code-block.d.ts +20 -0
- package/dist/ui/code-block.d.ts.map +1 -0
- package/dist/ui/code-block.js +22 -0
- package/dist/ui/code-block.js.map +1 -0
- package/dist/ui/combo-box.d.ts +17 -0
- package/dist/ui/combo-box.d.ts.map +1 -0
- package/dist/ui/combo-box.js +14 -0
- package/dist/ui/combo-box.js.map +1 -0
- package/dist/ui/copy-button.d.ts +14 -0
- package/dist/ui/copy-button.d.ts.map +1 -0
- package/dist/ui/copy-button.js +36 -0
- package/dist/ui/copy-button.js.map +1 -0
- package/dist/ui/date-field.d.ts +38 -0
- package/dist/ui/date-field.d.ts.map +1 -0
- package/dist/ui/date-field.js +78 -0
- package/dist/ui/date-field.js.map +1 -0
- package/dist/ui/dialog.d.ts +41 -0
- package/dist/ui/dialog.d.ts.map +1 -0
- package/dist/ui/dialog.js +69 -0
- package/dist/ui/dialog.js.map +1 -0
- package/dist/ui/disclosure.d.ts +18 -0
- package/dist/ui/disclosure.d.ts.map +1 -0
- package/dist/ui/disclosure.js +15 -0
- package/dist/ui/disclosure.js.map +1 -0
- package/dist/ui/empty-state.d.ts +17 -0
- package/dist/ui/empty-state.d.ts.map +1 -0
- package/dist/ui/empty-state.js +9 -0
- package/dist/ui/empty-state.js.map +1 -0
- package/dist/ui/error-state.d.ts +45 -0
- package/dist/ui/error-state.d.ts.map +1 -0
- package/dist/ui/error-state.js +88 -0
- package/dist/ui/error-state.js.map +1 -0
- package/dist/ui/field.d.ts +46 -0
- package/dist/ui/field.d.ts.map +1 -0
- package/dist/ui/field.js +79 -0
- package/dist/ui/field.js.map +1 -0
- package/dist/ui/file-upload.d.ts +54 -0
- package/dist/ui/file-upload.d.ts.map +1 -0
- package/dist/ui/file-upload.js +170 -0
- package/dist/ui/file-upload.js.map +1 -0
- package/dist/ui/form.d.ts +56 -0
- package/dist/ui/form.d.ts.map +1 -0
- package/dist/ui/form.js +137 -0
- package/dist/ui/form.js.map +1 -0
- package/dist/ui/grid-list.d.ts +24 -0
- package/dist/ui/grid-list.d.ts.map +1 -0
- package/dist/ui/grid-list.js +21 -0
- package/dist/ui/grid-list.js.map +1 -0
- package/dist/ui/hero.d.ts +36 -0
- package/dist/ui/hero.d.ts.map +1 -0
- package/dist/ui/hero.js +19 -0
- package/dist/ui/hero.js.map +1 -0
- package/dist/ui/icon-button.d.ts +18 -0
- package/dist/ui/icon-button.d.ts.map +1 -0
- package/dist/ui/icon-button.js +13 -0
- package/dist/ui/icon-button.js.map +1 -0
- package/dist/ui/icon.d.ts +20 -0
- package/dist/ui/icon.d.ts.map +1 -0
- package/dist/ui/icon.js +26 -0
- package/dist/ui/icon.js.map +1 -0
- package/dist/ui/kbd.d.ts +7 -0
- package/dist/ui/kbd.d.ts.map +1 -0
- package/dist/ui/kbd.js +7 -0
- package/dist/ui/kbd.js.map +1 -0
- package/dist/ui/link-card.d.ts +23 -0
- package/dist/ui/link-card.d.ts.map +1 -0
- package/dist/ui/link-card.js +19 -0
- package/dist/ui/link-card.js.map +1 -0
- package/dist/ui/list-box.d.ts +17 -0
- package/dist/ui/list-box.d.ts.map +1 -0
- package/dist/ui/list-box.js +18 -0
- package/dist/ui/list-box.js.map +1 -0
- package/dist/ui/menu.d.ts +31 -0
- package/dist/ui/menu.d.ts.map +1 -0
- package/dist/ui/menu.js +40 -0
- package/dist/ui/menu.js.map +1 -0
- package/dist/ui/number-field.d.ts +19 -0
- package/dist/ui/number-field.d.ts.map +1 -0
- package/dist/ui/number-field.js +19 -0
- package/dist/ui/number-field.js.map +1 -0
- package/dist/ui/prose.d.ts +12 -0
- package/dist/ui/prose.d.ts.map +1 -0
- package/dist/ui/prose.js +11 -0
- package/dist/ui/prose.js.map +1 -0
- package/dist/ui/radio-group.d.ts +20 -0
- package/dist/ui/radio-group.d.ts.map +1 -0
- package/dist/ui/radio-group.js +16 -0
- package/dist/ui/radio-group.js.map +1 -0
- package/dist/ui/scroll-region.d.ts +18 -0
- package/dist/ui/scroll-region.d.ts.map +1 -0
- package/dist/ui/scroll-region.js +29 -0
- package/dist/ui/scroll-region.js.map +1 -0
- package/dist/ui/search-field.d.ts +12 -0
- package/dist/ui/search-field.d.ts.map +1 -0
- package/dist/ui/search-field.js +12 -0
- package/dist/ui/search-field.js.map +1 -0
- package/dist/ui/search.d.ts +43 -0
- package/dist/ui/search.d.ts.map +1 -0
- package/dist/ui/search.js +45 -0
- package/dist/ui/search.js.map +1 -0
- package/dist/ui/section.d.ts +50 -0
- package/dist/ui/section.d.ts.map +1 -0
- package/dist/ui/section.js +25 -0
- package/dist/ui/section.js.map +1 -0
- package/dist/ui/segmented-control.d.ts +18 -0
- package/dist/ui/segmented-control.d.ts.map +1 -0
- package/dist/ui/segmented-control.js +17 -0
- package/dist/ui/segmented-control.js.map +1 -0
- package/dist/ui/select.d.ts +18 -0
- package/dist/ui/select.d.ts.map +1 -0
- package/dist/ui/select.js +13 -0
- package/dist/ui/select.js.map +1 -0
- package/dist/ui/selection-bar.d.ts +28 -0
- package/dist/ui/selection-bar.d.ts.map +1 -0
- package/dist/ui/selection-bar.js +35 -0
- package/dist/ui/selection-bar.js.map +1 -0
- package/dist/ui/sheet.d.ts +21 -0
- package/dist/ui/sheet.d.ts.map +1 -0
- package/dist/ui/sheet.js +16 -0
- package/dist/ui/sheet.js.map +1 -0
- package/dist/ui/skeleton.d.ts +31 -0
- package/dist/ui/skeleton.d.ts.map +1 -0
- package/dist/ui/skeleton.js +38 -0
- package/dist/ui/skeleton.js.map +1 -0
- package/dist/ui/slider.d.ts +31 -0
- package/dist/ui/slider.d.ts.map +1 -0
- package/dist/ui/slider.js +33 -0
- package/dist/ui/slider.js.map +1 -0
- package/dist/ui/spinner.d.ts +27 -0
- package/dist/ui/spinner.d.ts.map +1 -0
- package/dist/ui/spinner.js +21 -0
- package/dist/ui/spinner.js.map +1 -0
- package/dist/ui/split.d.ts +14 -0
- package/dist/ui/split.d.ts.map +1 -0
- package/dist/ui/split.js +11 -0
- package/dist/ui/split.js.map +1 -0
- package/dist/ui/stat-list.d.ts +25 -0
- package/dist/ui/stat-list.d.ts.map +1 -0
- package/dist/ui/stat-list.js +14 -0
- package/dist/ui/stat-list.js.map +1 -0
- package/dist/ui/steps.d.ts +23 -0
- package/dist/ui/steps.d.ts.map +1 -0
- package/dist/ui/steps.js +16 -0
- package/dist/ui/steps.js.map +1 -0
- package/dist/ui/switch.d.ts +13 -0
- package/dist/ui/switch.d.ts.map +1 -0
- package/dist/ui/switch.js +12 -0
- package/dist/ui/switch.js.map +1 -0
- package/dist/ui/table.d.ts +25 -0
- package/dist/ui/table.d.ts.map +1 -0
- package/dist/ui/table.js +29 -0
- package/dist/ui/table.js.map +1 -0
- package/dist/ui/tag.d.ts +14 -0
- package/dist/ui/tag.d.ts.map +1 -0
- package/dist/ui/tag.js +11 -0
- package/dist/ui/tag.js.map +1 -0
- package/dist/ui/text-field.d.ts +27 -0
- package/dist/ui/text-field.d.ts.map +1 -0
- package/dist/ui/text-field.js +17 -0
- package/dist/ui/text-field.js.map +1 -0
- package/dist/ui/text-link.d.ts +17 -0
- package/dist/ui/text-link.d.ts.map +1 -0
- package/dist/ui/text-link.js +15 -0
- package/dist/ui/text-link.js.map +1 -0
- package/dist/ui/theme-switcher.d.ts +26 -0
- package/dist/ui/theme-switcher.d.ts.map +1 -0
- package/dist/ui/theme-switcher.js +32 -0
- package/dist/ui/theme-switcher.js.map +1 -0
- package/dist/ui/theme.d.ts +54 -0
- package/dist/ui/theme.d.ts.map +1 -0
- package/dist/ui/theme.js +135 -0
- package/dist/ui/theme.js.map +1 -0
- package/dist/ui/toast.d.ts +34 -0
- package/dist/ui/toast.d.ts.map +1 -0
- package/dist/ui/toast.js +69 -0
- package/dist/ui/toast.js.map +1 -0
- package/dist/ui/tooltip.d.ts +10 -0
- package/dist/ui/tooltip.d.ts.map +1 -0
- package/dist/ui/tooltip.js +10 -0
- package/dist/ui/tooltip.js.map +1 -0
- package/package.json +123 -0
- package/templates/next/error.tsx +32 -0
- package/templates/next/loading.tsx +19 -0
- package/templates/next/not-found.tsx +18 -0
- package/templates/next/page-transition.tsx +33 -0
- package/templates/next/portal-layout.tsx +70 -0
- package/templates/next/prefetch-on-intent.ts +31 -0
- package/templates/next/root-layout.tsx +16 -0
- package/templates/next/site-layout.tsx +58 -0
- package/templates/next/site-nav.ts +16 -0
- package/templates/next/site-not-found.tsx +21 -0
- package/templates/next/site-router.tsx +17 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
const RAC = 'react-aria-components';
|
|
2
|
+
const PKG = '@setsquare/ui';
|
|
3
|
+
/** React Aria components that render a VisuallyHidden input or button, and what to use instead. */
|
|
4
|
+
const WRAPPED = {
|
|
5
|
+
Checkbox: 'Checkbox (or SelectionCheckbox in a Table or GridList)',
|
|
6
|
+
CheckboxGroup: 'CheckboxGroup',
|
|
7
|
+
Radio: 'Radio',
|
|
8
|
+
RadioGroup: 'RadioGroup',
|
|
9
|
+
Switch: 'Switch',
|
|
10
|
+
Slider: 'Slider or RangeSlider',
|
|
11
|
+
SliderTrack: 'Slider or RangeSlider',
|
|
12
|
+
SliderThumb: 'Slider or RangeSlider',
|
|
13
|
+
DateField: 'DateField',
|
|
14
|
+
TimeField: 'TimeField',
|
|
15
|
+
DatePicker: 'DatePicker',
|
|
16
|
+
DateRangePicker: 'DateRangePicker',
|
|
17
|
+
Calendar: 'Calendar',
|
|
18
|
+
RangeCalendar: 'RangeCalendar',
|
|
19
|
+
DropZone: 'FileUpload',
|
|
20
|
+
GridList: 'GridList',
|
|
21
|
+
GridListItem: 'GridListItem',
|
|
22
|
+
};
|
|
23
|
+
/** Also render hidden inputs, but have no Setsquare version yet. */
|
|
24
|
+
const UNWRAPPED = ['ColorArea', 'ColorSlider', 'ColorWheel'];
|
|
25
|
+
/** Setsquare components whose root must stay positioned. */
|
|
26
|
+
const POSITIONED_ROOTS = new Set([
|
|
27
|
+
'Checkbox',
|
|
28
|
+
'CheckboxGroup',
|
|
29
|
+
'SelectionCheckbox',
|
|
30
|
+
'Radio',
|
|
31
|
+
'RadioGroup',
|
|
32
|
+
'Switch',
|
|
33
|
+
'Slider',
|
|
34
|
+
'RangeSlider',
|
|
35
|
+
'DateField',
|
|
36
|
+
'TimeField',
|
|
37
|
+
'DatePicker',
|
|
38
|
+
'DateRangePicker',
|
|
39
|
+
'Calendar',
|
|
40
|
+
'RangeCalendar',
|
|
41
|
+
'FileUpload',
|
|
42
|
+
'GridList',
|
|
43
|
+
'GridListItem',
|
|
44
|
+
'Table',
|
|
45
|
+
'ShellMain',
|
|
46
|
+
'SidebarContent',
|
|
47
|
+
'ListBox',
|
|
48
|
+
'Menu',
|
|
49
|
+
'TextField',
|
|
50
|
+
'TextArea',
|
|
51
|
+
'NumberField',
|
|
52
|
+
'Select',
|
|
53
|
+
'ComboBox',
|
|
54
|
+
'SearchField',
|
|
55
|
+
]);
|
|
56
|
+
/** Setsquare components whose `className` lands on the target (Layer 1). Their paint takes `visualClassName`. */
|
|
57
|
+
const TARGET_COMPONENTS = new Set(['Button', 'LinkButton', 'IconButton', 'Segment']);
|
|
58
|
+
/**
|
|
59
|
+
* min-h-* and min-w-* in a target's className replace the component's own min-h-target / min-w-target
|
|
60
|
+
* (tailwind-merge keeps the later of two classes for the same property), so they are the one way a className
|
|
61
|
+
* can take the target below --target-min. A height, width or max-* can't: min-width and min-height win.
|
|
62
|
+
*/
|
|
63
|
+
const TARGET_FLOOR = /^(?:[\w-]+:|\[[^\]]*\]:)*!?min-[hw]-.+$/;
|
|
64
|
+
const DEFAULT_HELPERS = [
|
|
65
|
+
'cn',
|
|
66
|
+
'clsx',
|
|
67
|
+
'cx',
|
|
68
|
+
'classnames',
|
|
69
|
+
'classNames',
|
|
70
|
+
'twMerge',
|
|
71
|
+
'twJoin',
|
|
72
|
+
'cva',
|
|
73
|
+
'tv',
|
|
74
|
+
];
|
|
75
|
+
const SCROLL = /^(?:[\w-]+:|\[[^\]]*\]:)*!?overflow-(?:x-|y-)?(?:auto|scroll)!?$/;
|
|
76
|
+
const POSITIONED = /^(?:[\w-]+:|\[[^\]]*\]:)*!?(?:relative|absolute|fixed|sticky)!?$/;
|
|
77
|
+
const STATIC = /^(?:[\w-]+:|\[[^\]]*\]:)*!?static!?$/;
|
|
78
|
+
const tokens = (s) => s.split(/\s+/).filter(Boolean);
|
|
79
|
+
/** Every string that can end up in a class list: literals, template text, helper arguments, both branches of ?: and &&. */
|
|
80
|
+
function collectClassStrings(expr, helpers, out = []) {
|
|
81
|
+
if (!expr)
|
|
82
|
+
return out;
|
|
83
|
+
switch (expr.type) {
|
|
84
|
+
case 'Literal':
|
|
85
|
+
if (typeof expr.value === 'string')
|
|
86
|
+
out.push({ node: expr, text: expr.value });
|
|
87
|
+
break;
|
|
88
|
+
case 'TemplateLiteral':
|
|
89
|
+
for (const q of expr.quasis)
|
|
90
|
+
out.push({
|
|
91
|
+
node: q,
|
|
92
|
+
text: String(q.value.cooked ??
|
|
93
|
+
q.value.raw),
|
|
94
|
+
});
|
|
95
|
+
for (const e of expr.expressions)
|
|
96
|
+
collectClassStrings(e, helpers, out);
|
|
97
|
+
break;
|
|
98
|
+
case 'JSXExpressionContainer':
|
|
99
|
+
collectClassStrings(expr.expression, helpers, out);
|
|
100
|
+
break;
|
|
101
|
+
case 'ConditionalExpression':
|
|
102
|
+
collectClassStrings(expr.consequent, helpers, out);
|
|
103
|
+
collectClassStrings(expr.alternate, helpers, out);
|
|
104
|
+
break;
|
|
105
|
+
case 'LogicalExpression':
|
|
106
|
+
collectClassStrings(expr.left, helpers, out);
|
|
107
|
+
collectClassStrings(expr.right, helpers, out);
|
|
108
|
+
break;
|
|
109
|
+
case 'ArrayExpression':
|
|
110
|
+
for (const e of expr.elements)
|
|
111
|
+
collectClassStrings(e, helpers, out);
|
|
112
|
+
break;
|
|
113
|
+
case 'ObjectExpression':
|
|
114
|
+
// clsx({ 'overflow-auto': cond }) — the keys are the classes.
|
|
115
|
+
for (const p of expr.properties) {
|
|
116
|
+
if (p.type === 'Property') {
|
|
117
|
+
const key = p.key;
|
|
118
|
+
if (key.type === 'Literal' && typeof key.value === 'string')
|
|
119
|
+
out.push({ node: key, text: key.value });
|
|
120
|
+
else if (key.type === 'Identifier' && !p.computed)
|
|
121
|
+
out.push({ node: key, text: String(key.name) });
|
|
122
|
+
collectClassStrings(p.value, helpers, out);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
break;
|
|
126
|
+
case 'CallExpression': {
|
|
127
|
+
const callee = expr.callee;
|
|
128
|
+
const name = callee.type === 'Identifier'
|
|
129
|
+
? String(callee.name)
|
|
130
|
+
: callee.type === 'MemberExpression'
|
|
131
|
+
? String(callee.property.name)
|
|
132
|
+
: '';
|
|
133
|
+
if (helpers.has(name))
|
|
134
|
+
for (const a of expr.arguments)
|
|
135
|
+
collectClassStrings(a, helpers, out);
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
case 'TSAsExpression':
|
|
139
|
+
case 'TSSatisfiesExpression':
|
|
140
|
+
case 'TSNonNullExpression':
|
|
141
|
+
collectClassStrings(expr.expression, helpers, out);
|
|
142
|
+
break;
|
|
143
|
+
default:
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function attr(opening, name) {
|
|
149
|
+
return opening.attributes.find((a) => a.type === 'JSXAttribute' &&
|
|
150
|
+
a.name.type === 'JSXIdentifier' &&
|
|
151
|
+
a.name.name === name);
|
|
152
|
+
}
|
|
153
|
+
/** overflow / overflowX / overflowY: 'auto' | 'scroll' in an inline style object, and whether it sets position. */
|
|
154
|
+
function styleScroll(styleAttr) {
|
|
155
|
+
const value = styleAttr?.value;
|
|
156
|
+
const obj = value?.type === 'JSXExpressionContainer' ? value.expression : null;
|
|
157
|
+
if (!obj || obj.type !== 'ObjectExpression')
|
|
158
|
+
return { scrolls: null, positioned: false };
|
|
159
|
+
let scrolls = null;
|
|
160
|
+
let positioned = false;
|
|
161
|
+
for (const p of obj.properties) {
|
|
162
|
+
if (p.type !== 'Property')
|
|
163
|
+
continue;
|
|
164
|
+
const key = p.key;
|
|
165
|
+
const k = key.type === 'Identifier' ? key.name : key.type === 'Literal' ? key.value : null;
|
|
166
|
+
const v = p.value;
|
|
167
|
+
if ((k === 'overflow' || k === 'overflowX' || k === 'overflowY') &&
|
|
168
|
+
v.type === 'Literal' &&
|
|
169
|
+
(v.value === 'auto' || v.value === 'scroll'))
|
|
170
|
+
scrolls = p;
|
|
171
|
+
if (k === 'position' &&
|
|
172
|
+
v.type === 'Literal' &&
|
|
173
|
+
['relative', 'absolute', 'fixed', 'sticky'].includes(String(v.value)))
|
|
174
|
+
positioned = true;
|
|
175
|
+
}
|
|
176
|
+
return { scrolls, positioned };
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* Where `relative ` can go without changing any other class: the start of a string literal, or the start of a
|
|
180
|
+
* template literal (straight after the backtick). The string holding the overflow class is preferred.
|
|
181
|
+
*/
|
|
182
|
+
function fixTarget(preferred, all, source) {
|
|
183
|
+
const start = (s) => {
|
|
184
|
+
const range = s.node.range;
|
|
185
|
+
if (!range)
|
|
186
|
+
return null;
|
|
187
|
+
if (s.node.type === 'Literal')
|
|
188
|
+
return range[0] + 1;
|
|
189
|
+
if (s.node.type === 'TemplateElement' && source.text[range[0]] === '`')
|
|
190
|
+
return range[0] + 1;
|
|
191
|
+
return null;
|
|
192
|
+
};
|
|
193
|
+
for (const s of [preferred, ...all]) {
|
|
194
|
+
const at = start(s);
|
|
195
|
+
if (at != null)
|
|
196
|
+
return at;
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
const positionedScrollContainer = {
|
|
201
|
+
meta: {
|
|
202
|
+
type: 'problem',
|
|
203
|
+
fixable: 'code',
|
|
204
|
+
docs: {
|
|
205
|
+
description: 'Require every scroll container to be positioned, so absolutely positioned descendants scroll with it',
|
|
206
|
+
},
|
|
207
|
+
schema: [
|
|
208
|
+
{
|
|
209
|
+
type: 'object',
|
|
210
|
+
properties: { helpers: { type: 'array', items: { type: 'string' } } },
|
|
211
|
+
additionalProperties: false,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
messages: {
|
|
215
|
+
missing: "'{{cls}}' makes this a scroll container, but it isn't positioned. Add 'relative': otherwise sr-only text and React Aria's hidden inputs inside it are placed against an outer element, don't scroll with the content, and shift the page when they are focused or read.",
|
|
216
|
+
missingStyle: "This inline style makes the element a scroll container, but it isn't positioned. Add position: 'relative' (or the 'relative' class) so absolutely positioned descendants scroll with it.",
|
|
217
|
+
},
|
|
218
|
+
},
|
|
219
|
+
create(context) {
|
|
220
|
+
const opts = (context.options[0] ?? {});
|
|
221
|
+
const helpers = new Set(opts.helpers ?? DEFAULT_HELPERS);
|
|
222
|
+
const check = (strings, reportOn) => {
|
|
223
|
+
const all = strings.flatMap((s) => tokens(s.text).map((t) => ({ t, s })));
|
|
224
|
+
const scroll = all.find((x) => SCROLL.test(x.t));
|
|
225
|
+
if (!scroll || all.some((x) => POSITIONED.test(x.t)))
|
|
226
|
+
return;
|
|
227
|
+
context.report({
|
|
228
|
+
node: (scroll.s.node ?? reportOn),
|
|
229
|
+
messageId: 'missing',
|
|
230
|
+
data: { cls: scroll.t },
|
|
231
|
+
fix: (fixer) => {
|
|
232
|
+
const at = fixTarget(scroll.s, strings, context.sourceCode);
|
|
233
|
+
return at == null ? null : fixer.insertTextBeforeRange([at, at], 'relative ');
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
};
|
|
237
|
+
return {
|
|
238
|
+
JSXOpeningElement(node) {
|
|
239
|
+
const n = node;
|
|
240
|
+
const classAttr = attr(n, 'className') ?? attr(n, 'class');
|
|
241
|
+
const strings = classAttr ? collectClassStrings(classAttr.value, helpers) : [];
|
|
242
|
+
const style = styleScroll(attr(n, 'style'));
|
|
243
|
+
const classPositioned = strings.some((s) => tokens(s.text).some((t) => POSITIONED.test(t)));
|
|
244
|
+
if (style.scrolls && !style.positioned && !classPositioned) {
|
|
245
|
+
context.report({
|
|
246
|
+
node: style.scrolls,
|
|
247
|
+
messageId: 'missingStyle',
|
|
248
|
+
});
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
if (style.positioned)
|
|
252
|
+
return;
|
|
253
|
+
if (strings.length)
|
|
254
|
+
check(strings, n);
|
|
255
|
+
},
|
|
256
|
+
CallExpression(node) {
|
|
257
|
+
const n = node;
|
|
258
|
+
// Inside a className attribute, the JSX handler checks the whole attribute as one class list.
|
|
259
|
+
for (let p = n.parent; p; p = p.parent) {
|
|
260
|
+
if (p.type === 'JSXAttribute' &&
|
|
261
|
+
['className', 'class'].includes(String(p.name.name)))
|
|
262
|
+
return;
|
|
263
|
+
if (p.type === 'JSXOpeningElement' || p.type === 'Program')
|
|
264
|
+
break;
|
|
265
|
+
}
|
|
266
|
+
const callee = n.callee;
|
|
267
|
+
const name = callee.type === 'Identifier' ? String(callee.name) : '';
|
|
268
|
+
if (!helpers.has(name))
|
|
269
|
+
return;
|
|
270
|
+
// Only the outermost helper call is one class list.
|
|
271
|
+
const parent = n.parent;
|
|
272
|
+
if (parent?.type === 'CallExpression' &&
|
|
273
|
+
helpers.has(String(parent.callee?.name)))
|
|
274
|
+
return;
|
|
275
|
+
const strings = collectClassStrings(n, helpers);
|
|
276
|
+
if (name === 'cva' || name === 'tv') {
|
|
277
|
+
// Variant helpers: each variant string is applied on its own, so check the base alone and each string.
|
|
278
|
+
const base = collectClassStrings(n.arguments[0], helpers);
|
|
279
|
+
if (base.length)
|
|
280
|
+
check(base, n);
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
check(strings, n);
|
|
284
|
+
},
|
|
285
|
+
};
|
|
286
|
+
},
|
|
287
|
+
};
|
|
288
|
+
const noRawAriaControls = {
|
|
289
|
+
meta: {
|
|
290
|
+
type: 'problem',
|
|
291
|
+
docs: {
|
|
292
|
+
description: "Use @setsquare/ui's versions of React Aria components that render a visually hidden input",
|
|
293
|
+
},
|
|
294
|
+
schema: [
|
|
295
|
+
{
|
|
296
|
+
type: 'object',
|
|
297
|
+
properties: { allow: { type: 'array', items: { type: 'string' } } },
|
|
298
|
+
additionalProperties: false,
|
|
299
|
+
},
|
|
300
|
+
],
|
|
301
|
+
messages: {
|
|
302
|
+
wrapped: "Use {{replacement}} from '@setsquare/ui' instead of React Aria's {{name}}. React Aria renders its input in a visually hidden, absolutely positioned span; the Setsquare version keeps a positioned root so the page doesn't shift when it is focused, and brings the conformance tokens with it.",
|
|
303
|
+
unwrapped: "React Aria's {{name}} renders a visually hidden, absolutely positioned input and has no Setsquare version yet. Give its root 'relative', then add it to this rule's allow list.",
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
create(context) {
|
|
307
|
+
const opts = (context.options[0] ?? {});
|
|
308
|
+
const allow = new Set(opts.allow ?? []);
|
|
309
|
+
const namespaces = new Set();
|
|
310
|
+
const report = (node, name) => {
|
|
311
|
+
if (allow.has(name))
|
|
312
|
+
return;
|
|
313
|
+
if (WRAPPED[name])
|
|
314
|
+
context.report({
|
|
315
|
+
node: node,
|
|
316
|
+
messageId: 'wrapped',
|
|
317
|
+
data: { name, replacement: WRAPPED[name] },
|
|
318
|
+
});
|
|
319
|
+
else if (UNWRAPPED.includes(name))
|
|
320
|
+
context.report({
|
|
321
|
+
node: node,
|
|
322
|
+
messageId: 'unwrapped',
|
|
323
|
+
data: { name },
|
|
324
|
+
});
|
|
325
|
+
};
|
|
326
|
+
const importedName = (s) => {
|
|
327
|
+
const imp = s.imported;
|
|
328
|
+
return imp ? String(imp.type === 'Literal' ? imp.value : imp.name) : '';
|
|
329
|
+
};
|
|
330
|
+
return {
|
|
331
|
+
ImportDeclaration(node) {
|
|
332
|
+
const n = node;
|
|
333
|
+
if (n.source.value !== RAC || n.importKind === 'type')
|
|
334
|
+
return;
|
|
335
|
+
for (const s of n.specifiers) {
|
|
336
|
+
if (s.type === 'ImportSpecifier' && s.importKind !== 'type')
|
|
337
|
+
report(s, importedName(s));
|
|
338
|
+
if (s.type === 'ImportNamespaceSpecifier')
|
|
339
|
+
namespaces.add(String(s.local.name));
|
|
340
|
+
}
|
|
341
|
+
},
|
|
342
|
+
ExportNamedDeclaration(node) {
|
|
343
|
+
const n = node;
|
|
344
|
+
if (n.source?.value !== RAC || n.exportKind === 'type')
|
|
345
|
+
return;
|
|
346
|
+
for (const s of n.specifiers) {
|
|
347
|
+
const local = s.local;
|
|
348
|
+
report(s, String(local.type === 'Literal' ? local.value : local.name));
|
|
349
|
+
}
|
|
350
|
+
},
|
|
351
|
+
JSXMemberExpression(node) {
|
|
352
|
+
const n = node;
|
|
353
|
+
const obj = n.object;
|
|
354
|
+
if (obj.type === 'JSXIdentifier' && namespaces.has(String(obj.name)))
|
|
355
|
+
report(n, String(n.property.name));
|
|
356
|
+
},
|
|
357
|
+
MemberExpression(node) {
|
|
358
|
+
const n = node;
|
|
359
|
+
const obj = n.object;
|
|
360
|
+
if (obj.type === 'Identifier' && namespaces.has(String(obj.name)) && !n.computed)
|
|
361
|
+
report(n, String(n.property.name));
|
|
362
|
+
},
|
|
363
|
+
};
|
|
364
|
+
},
|
|
365
|
+
};
|
|
366
|
+
const noStaticControlRoot = {
|
|
367
|
+
meta: {
|
|
368
|
+
type: 'problem',
|
|
369
|
+
docs: {
|
|
370
|
+
description: 'Disallow `static` on @setsquare/ui components whose root must stay positioned',
|
|
371
|
+
},
|
|
372
|
+
schema: [],
|
|
373
|
+
messages: {
|
|
374
|
+
static: "'{{cls}}' removes the positioning {{name}} depends on: its root is 'relative' so React Aria's hidden input (or the sr-only text inside it) stays with it. Remove '{{cls}}'.",
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
create(context) {
|
|
378
|
+
const local = new Map();
|
|
379
|
+
return {
|
|
380
|
+
ImportDeclaration(node) {
|
|
381
|
+
const n = node;
|
|
382
|
+
const src = String(n.source.value);
|
|
383
|
+
if (src !== PKG && !src.startsWith(`${PKG}/`))
|
|
384
|
+
return;
|
|
385
|
+
for (const s of n.specifiers) {
|
|
386
|
+
if (s.type !== 'ImportSpecifier')
|
|
387
|
+
continue;
|
|
388
|
+
const imp = s.imported;
|
|
389
|
+
const name = String(imp.type === 'Literal' ? imp.value : imp.name);
|
|
390
|
+
if (POSITIONED_ROOTS.has(name))
|
|
391
|
+
local.set(String(s.local.name), name);
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
JSXOpeningElement(node) {
|
|
395
|
+
const n = node;
|
|
396
|
+
const nameNode = n.name;
|
|
397
|
+
if (nameNode.type !== 'JSXIdentifier' || !local.has(String(nameNode.name)))
|
|
398
|
+
return;
|
|
399
|
+
const classAttr = attr(n, 'className');
|
|
400
|
+
if (!classAttr)
|
|
401
|
+
return;
|
|
402
|
+
for (const s of collectClassStrings(classAttr.value, new Set(DEFAULT_HELPERS))) {
|
|
403
|
+
const hit = tokens(s.text).find((t) => STATIC.test(t));
|
|
404
|
+
if (hit)
|
|
405
|
+
context.report({
|
|
406
|
+
node: s.node,
|
|
407
|
+
messageId: 'static',
|
|
408
|
+
data: { cls: hit, name: local.get(String(nameNode.name)) },
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
},
|
|
412
|
+
};
|
|
413
|
+
},
|
|
414
|
+
};
|
|
415
|
+
/** The imported names of @setsquare/ui components in this file, local name → exported name. */
|
|
416
|
+
function trackPackageImports(local, wanted) {
|
|
417
|
+
return (node) => {
|
|
418
|
+
const n = node;
|
|
419
|
+
const src = String(n.source.value);
|
|
420
|
+
if (src !== PKG && !src.startsWith(`${PKG}/`))
|
|
421
|
+
return;
|
|
422
|
+
for (const s of n.specifiers) {
|
|
423
|
+
if (s.type !== 'ImportSpecifier')
|
|
424
|
+
continue;
|
|
425
|
+
const imp = s.imported;
|
|
426
|
+
const name = String(imp.type === 'Literal' ? imp.value : imp.name);
|
|
427
|
+
if (wanted.has(name))
|
|
428
|
+
local.set(String(s.local.name), name);
|
|
429
|
+
}
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
const noTargetShrink = {
|
|
433
|
+
meta: {
|
|
434
|
+
type: 'problem',
|
|
435
|
+
docs: {
|
|
436
|
+
description: 'Disallow min-h-* and min-w-* in the className of a three-layer control: it is the target, and they replace its --target-min floor',
|
|
437
|
+
},
|
|
438
|
+
schema: [],
|
|
439
|
+
messages: {
|
|
440
|
+
shrink: "'{{cls}}' is on {{name}}'s target (Layer 1), the element a pointer has to hit, and replaces its --target-min floor: at AAA the target could be smaller than 44px. Size the painted box with visualSize, or put the class on visualClassName.",
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
create(context) {
|
|
444
|
+
const local = new Map();
|
|
445
|
+
return {
|
|
446
|
+
ImportDeclaration: trackPackageImports(local, TARGET_COMPONENTS),
|
|
447
|
+
JSXOpeningElement(node) {
|
|
448
|
+
const n = node;
|
|
449
|
+
const nameNode = n.name;
|
|
450
|
+
if (nameNode.type !== 'JSXIdentifier' || !local.has(String(nameNode.name)))
|
|
451
|
+
return;
|
|
452
|
+
const classAttr = attr(n, 'className');
|
|
453
|
+
if (!classAttr)
|
|
454
|
+
return;
|
|
455
|
+
for (const s of collectClassStrings(classAttr.value, new Set(DEFAULT_HELPERS))) {
|
|
456
|
+
const hit = tokens(s.text).find((t) => TARGET_FLOOR.test(t));
|
|
457
|
+
if (hit)
|
|
458
|
+
context.report({
|
|
459
|
+
node: s.node,
|
|
460
|
+
messageId: 'shrink',
|
|
461
|
+
data: { cls: hit, name: local.get(String(nameNode.name)) },
|
|
462
|
+
});
|
|
463
|
+
}
|
|
464
|
+
},
|
|
465
|
+
};
|
|
466
|
+
},
|
|
467
|
+
};
|
|
468
|
+
/** Does this expression call `name(…)` anywhere inside it? */
|
|
469
|
+
function callsFunction(expr, name) {
|
|
470
|
+
if (!expr || typeof expr !== 'object')
|
|
471
|
+
return false;
|
|
472
|
+
if (expr.type === 'CallExpression' &&
|
|
473
|
+
expr.callee.type === 'Identifier' &&
|
|
474
|
+
expr.callee.name === name)
|
|
475
|
+
return true;
|
|
476
|
+
for (const [key, value] of Object.entries(expr)) {
|
|
477
|
+
if (key === 'parent' || key === 'loc' || key === 'range')
|
|
478
|
+
continue;
|
|
479
|
+
if (Array.isArray(value)) {
|
|
480
|
+
if (value.some((v) => v && typeof v === 'object' && callsFunction(v, name)))
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
else if (value && typeof value === 'object' && 'type' in value) {
|
|
484
|
+
if (callsFunction(value, name))
|
|
485
|
+
return true;
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
/** <a>, <button>, and components named like links and buttons (Link, NextLink, RACButton, ToggleButton…). */
|
|
491
|
+
function isInteractiveElement(nameNode) {
|
|
492
|
+
const name = nameNode.type === 'JSXIdentifier'
|
|
493
|
+
? String(nameNode.name)
|
|
494
|
+
: nameNode.type === 'JSXMemberExpression'
|
|
495
|
+
? String(nameNode.property.name)
|
|
496
|
+
: '';
|
|
497
|
+
if (name === 'a' || name === 'button')
|
|
498
|
+
return name;
|
|
499
|
+
if (/(?:Link|Button)$/.test(name))
|
|
500
|
+
return name;
|
|
501
|
+
return null;
|
|
502
|
+
}
|
|
503
|
+
const noVisualOnlyTarget = {
|
|
504
|
+
meta: {
|
|
505
|
+
type: 'problem',
|
|
506
|
+
docs: {
|
|
507
|
+
description: 'Disallow buttonVisualStyles() on an interactive element: it is the painted box, and on its own it makes the paint the pointer target',
|
|
508
|
+
},
|
|
509
|
+
schema: [],
|
|
510
|
+
messages: {
|
|
511
|
+
visualOnly: 'buttonVisualStyles() paints Layer 2 only. On <{{name}}> it makes the painted box the pointer target, so a small button is a small target and fails 2.5.5 at AAA. Put buttonTargetStyles() on <{{name}}> and buttonVisualStyles() on a span inside it, or use LinkButton.',
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
create(context) {
|
|
515
|
+
return {
|
|
516
|
+
JSXOpeningElement(node) {
|
|
517
|
+
const n = node;
|
|
518
|
+
const name = isInteractiveElement(n.name);
|
|
519
|
+
if (!name)
|
|
520
|
+
return;
|
|
521
|
+
const classAttr = attr(n, 'className');
|
|
522
|
+
if (!classAttr)
|
|
523
|
+
return;
|
|
524
|
+
if (callsFunction(classAttr.value, 'buttonVisualStyles'))
|
|
525
|
+
context.report({
|
|
526
|
+
node: classAttr,
|
|
527
|
+
messageId: 'visualOnly',
|
|
528
|
+
data: { name },
|
|
529
|
+
});
|
|
530
|
+
},
|
|
531
|
+
};
|
|
532
|
+
},
|
|
533
|
+
};
|
|
534
|
+
/** Hooks React's server build exports (react.react-server in React 19): a server component may call these. */
|
|
535
|
+
const SERVER_HOOKS = new Set(['use', 'useCallback', 'useDebugValue', 'useId', 'useMemo']);
|
|
536
|
+
/** Packages that only work in the client graph: React Aria's entry imports `client-only`. */
|
|
537
|
+
const CLIENT_PACKAGES = /^(react-aria-components|react-aria|react-stately|@react-aria\/|@react-stately\/)/;
|
|
538
|
+
const clientDirective = {
|
|
539
|
+
meta: {
|
|
540
|
+
type: 'problem',
|
|
541
|
+
docs: {
|
|
542
|
+
description: "Require 'use client' exactly when a module needs it: React Aria, a hook, a context, an event handler or a function passed as a prop",
|
|
543
|
+
},
|
|
544
|
+
schema: [],
|
|
545
|
+
messages: {
|
|
546
|
+
missing: "This module {{reason}}, which only a client module can do. Add 'use client' as its first line.",
|
|
547
|
+
unneeded: "'use client' isn't needed here: nothing in this module needs the client (React Aria, a hook other than use, useCallback, useDebugValue, useId or useMemo, createContext, an event handler, or a function passed as a prop or child). Remove it, so server components can use this module without shipping it to the browser.",
|
|
548
|
+
},
|
|
549
|
+
},
|
|
550
|
+
create(context) {
|
|
551
|
+
let directive = null;
|
|
552
|
+
let reason = null;
|
|
553
|
+
const need = (node, text) => {
|
|
554
|
+
reason ??= { node, text };
|
|
555
|
+
};
|
|
556
|
+
const isFunction = (n) => !!n && (n.type === 'ArrowFunctionExpression' || n.type === 'FunctionExpression');
|
|
557
|
+
return {
|
|
558
|
+
Program(node) {
|
|
559
|
+
// The directive prologue: string-literal statements at the very start of the file.
|
|
560
|
+
for (const stmt of node.body) {
|
|
561
|
+
const expr = stmt.expression;
|
|
562
|
+
if (stmt.type !== 'ExpressionStatement' || expr?.type !== 'Literal')
|
|
563
|
+
break;
|
|
564
|
+
if (typeof expr.value === 'string' && expr.value === 'use client')
|
|
565
|
+
directive = stmt;
|
|
566
|
+
}
|
|
567
|
+
},
|
|
568
|
+
ImportDeclaration(node) {
|
|
569
|
+
const n = node;
|
|
570
|
+
if (n.importKind === 'type')
|
|
571
|
+
return;
|
|
572
|
+
const source = String(n.source.value);
|
|
573
|
+
const specifiers = n.specifiers ?? [];
|
|
574
|
+
const valueImport = !specifiers.length || specifiers.some((s) => s.importKind !== 'type');
|
|
575
|
+
if (CLIENT_PACKAGES.test(source) && valueImport)
|
|
576
|
+
need(n, `imports ${source}`);
|
|
577
|
+
},
|
|
578
|
+
CallExpression(node) {
|
|
579
|
+
const n = node;
|
|
580
|
+
const callee = n.callee;
|
|
581
|
+
const name = callee.type === 'Identifier'
|
|
582
|
+
? String(callee.name)
|
|
583
|
+
: callee.type === 'MemberExpression' &&
|
|
584
|
+
callee.property.type === 'Identifier'
|
|
585
|
+
? String(callee.property.name)
|
|
586
|
+
: '';
|
|
587
|
+
if (name === 'createContext')
|
|
588
|
+
need(n, 'creates a context');
|
|
589
|
+
else if (/^use[A-Z]/.test(name) && !SERVER_HOOKS.has(name))
|
|
590
|
+
need(n, `calls ${name}`);
|
|
591
|
+
},
|
|
592
|
+
JSXAttribute(node) {
|
|
593
|
+
const n = node;
|
|
594
|
+
const attrName = String(n.name.name);
|
|
595
|
+
const value = n.value;
|
|
596
|
+
const expr = value?.type === 'JSXExpressionContainer' ? value.expression : null;
|
|
597
|
+
if (/^on[A-Z]/.test(attrName))
|
|
598
|
+
need(n, `passes an event handler (${attrName})`);
|
|
599
|
+
else if (isFunction(expr))
|
|
600
|
+
need(n, `passes a function as a prop (${attrName})`);
|
|
601
|
+
},
|
|
602
|
+
JSXExpressionContainer(node) {
|
|
603
|
+
const n = node;
|
|
604
|
+
if (n.parent?.type === 'JSXAttribute')
|
|
605
|
+
return;
|
|
606
|
+
if (isFunction(n.expression))
|
|
607
|
+
need(n, 'passes a function as a child');
|
|
608
|
+
},
|
|
609
|
+
'Program:exit'() {
|
|
610
|
+
if (reason && !directive)
|
|
611
|
+
context.report({
|
|
612
|
+
node: reason.node,
|
|
613
|
+
messageId: 'missing',
|
|
614
|
+
data: { reason: reason.text },
|
|
615
|
+
});
|
|
616
|
+
else if (directive && !reason)
|
|
617
|
+
context.report({ node: directive, messageId: 'unneeded' });
|
|
618
|
+
},
|
|
619
|
+
};
|
|
620
|
+
},
|
|
621
|
+
};
|
|
622
|
+
export const rules = {
|
|
623
|
+
'no-raw-aria-controls': noRawAriaControls,
|
|
624
|
+
'positioned-scroll-container': positionedScrollContainer,
|
|
625
|
+
'no-static-control-root': noStaticControlRoot,
|
|
626
|
+
'no-target-shrink': noTargetShrink,
|
|
627
|
+
'no-visual-only-target': noVisualOnlyTarget,
|
|
628
|
+
'client-directive': clientDirective,
|
|
629
|
+
};
|
|
630
|
+
const plugin = {
|
|
631
|
+
meta: { name: '@setsquare/ui/lint' },
|
|
632
|
+
rules,
|
|
633
|
+
configs: {},
|
|
634
|
+
};
|
|
635
|
+
/** Flat config: `import setsquare from '@setsquare/ui/lint'; export default [setsquare.configs.recommended]`. */
|
|
636
|
+
plugin.configs.recommended = {
|
|
637
|
+
name: 'setsquare-ui/recommended',
|
|
638
|
+
plugins: { 'setsquare-ui': plugin },
|
|
639
|
+
rules: {
|
|
640
|
+
'setsquare-ui/no-raw-aria-controls': 'error',
|
|
641
|
+
'setsquare-ui/positioned-scroll-container': 'error',
|
|
642
|
+
'setsquare-ui/no-static-control-root': 'error',
|
|
643
|
+
'setsquare-ui/no-target-shrink': 'error',
|
|
644
|
+
'setsquare-ui/no-visual-only-target': 'error',
|
|
645
|
+
},
|
|
646
|
+
};
|
|
647
|
+
export default plugin;
|
|
648
|
+
//# sourceMappingURL=index.js.map
|