@adea-ai/ui 0.83.0 → 0.84.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/dist/components/composites/action-button/action-button.d.ts.map +1 -1
- package/dist/components/composites/action-button/action-button.js +55 -24
- package/dist/components/composites/action-button/action-button.js.map +1 -1
- package/dist/components/layout/sidebar-nav/index.d.ts +1 -0
- package/dist/components/layout/sidebar-nav/index.d.ts.map +1 -1
- package/dist/components/layout/sidebar-nav/index.js +2 -1
- package/dist/components/layout/sidebar-nav/sidebar-nav-resize-handle.d.ts +21 -0
- package/dist/components/layout/sidebar-nav/sidebar-nav-resize-handle.d.ts.map +1 -0
- package/dist/components/layout/sidebar-nav/sidebar-nav-resize-handle.js +84 -0
- package/dist/components/layout/sidebar-nav/sidebar-nav-resize-handle.js.map +1 -0
- package/dist/components/ui/command/command.d.ts.map +1 -1
- package/dist/components/ui/command/command.js +1 -0
- package/dist/components/ui/command/command.js.map +1 -1
- package/dist/index.js +2 -1
- package/dist/lint/index.d.ts +1 -0
- package/dist/lint/index.d.ts.map +1 -1
- package/dist/lint.js +171 -59
- package/dist/lint.js.map +1 -1
- package/dist/r/registry.json +6 -0
- package/dist/r/sidebar-nav.json +6 -0
- package/dist/r/src/components/composites/action-button/action-button.tsx +52 -11
- package/dist/r/src/components/layout/sidebar-nav/index.ts +4 -0
- package/dist/r/src/components/layout/sidebar-nav/sidebar-nav-resize-handle.tsx +59 -0
- package/dist/r/src/components/ui/command/command.tsx +1 -0
- package/package.json +1 -1
- package/registry.json +6 -0
- package/src/components/composites/action-button/action-button.tsx +52 -11
- package/src/components/layout/sidebar-nav/index.ts +4 -0
- package/src/components/layout/sidebar-nav/sidebar-nav-resize-handle.tsx +59 -0
- package/src/components/ui/command/command.tsx +1 -0
- package/src/lint/index.ts +168 -19
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -1558,6 +1558,7 @@
|
|
|
1558
1558
|
"title": "Sidebar Nav",
|
|
1559
1559
|
"description": "",
|
|
1560
1560
|
"dependencies": [
|
|
1561
|
+
"@corvu/resizable@^0.2.5",
|
|
1561
1562
|
"@kobalte/core@^0.13.14",
|
|
1562
1563
|
"lucide-solid@^1.48.0"
|
|
1563
1564
|
],
|
|
@@ -1570,6 +1571,11 @@
|
|
|
1570
1571
|
"type": "registry:ui",
|
|
1571
1572
|
"target": "components/layout/sidebar-nav/index.ts"
|
|
1572
1573
|
},
|
|
1574
|
+
{
|
|
1575
|
+
"path": "src/components/layout/sidebar-nav/sidebar-nav-resize-handle.tsx",
|
|
1576
|
+
"type": "registry:ui",
|
|
1577
|
+
"target": "components/layout/sidebar-nav/sidebar-nav-resize-handle.tsx"
|
|
1578
|
+
},
|
|
1573
1579
|
{
|
|
1574
1580
|
"path": "src/components/layout/sidebar-nav/sidebar-nav.tsx",
|
|
1575
1581
|
"type": "registry:ui",
|
|
@@ -1,4 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Show,
|
|
3
|
+
createSignal,
|
|
4
|
+
splitProps,
|
|
5
|
+
type ComponentProps,
|
|
6
|
+
type JSX,
|
|
7
|
+
type ValidComponent,
|
|
8
|
+
} from 'solid-js'
|
|
2
9
|
import { Button, type ButtonProps } from '../../ui/button'
|
|
3
10
|
import { Spinner } from '../../ui/spinner'
|
|
4
11
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip'
|
|
@@ -27,7 +34,28 @@ export type ActionButtonProps<T extends ValidComponent = 'button'> = ButtonProps
|
|
|
27
34
|
busyLabel?: string
|
|
28
35
|
}
|
|
29
36
|
|
|
30
|
-
function
|
|
37
|
+
function invokeHandler(handler: unknown, event: Event) {
|
|
38
|
+
if (Array.isArray(handler)) {
|
|
39
|
+
const [callback, data] = handler as [(data: unknown, event: Event) => void, unknown]
|
|
40
|
+
callback(data, event)
|
|
41
|
+
} else if (typeof handler === 'function') {
|
|
42
|
+
const callback = handler as (event: Event) => void
|
|
43
|
+
callback(event)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function chainedHandler(handler: unknown, before: (event: Event) => void) {
|
|
48
|
+
return (event: Event) => {
|
|
49
|
+
before(event)
|
|
50
|
+
invokeHandler(handler, event)
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function guardedClick(
|
|
55
|
+
handler: unknown,
|
|
56
|
+
disabled: () => boolean,
|
|
57
|
+
onActivate: (event: MouseEvent) => void = () => {}
|
|
58
|
+
) {
|
|
31
59
|
return (event: MouseEvent) => {
|
|
32
60
|
if (disabled()) {
|
|
33
61
|
event.preventDefault()
|
|
@@ -35,13 +63,8 @@ function guardedClick(handler: unknown, disabled: () => boolean) {
|
|
|
35
63
|
return
|
|
36
64
|
}
|
|
37
65
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
callback(data, event)
|
|
41
|
-
} else if (typeof handler === 'function') {
|
|
42
|
-
const callback = handler as (event: MouseEvent) => void
|
|
43
|
-
callback(event)
|
|
44
|
-
}
|
|
66
|
+
onActivate(event)
|
|
67
|
+
invokeHandler(handler, event)
|
|
45
68
|
}
|
|
46
69
|
}
|
|
47
70
|
|
|
@@ -73,6 +96,8 @@ export function ActionButton<T extends ValidComponent = 'button'>(props: ActionB
|
|
|
73
96
|
const busy = () => local.busy ?? false
|
|
74
97
|
const disabled = () => busy() || !!local.disabled
|
|
75
98
|
const busyStatus = () => local.busyLabel ?? 'Working'
|
|
99
|
+
const [tooltipOpen, setTooltipOpen] = createSignal(false)
|
|
100
|
+
const [tooltipSuppression, setTooltipSuppression] = createSignal<'pointer' | 'focus' | null>(null)
|
|
76
101
|
const onClickCapture = (rest as { onClickCapture?: unknown }).onClickCapture
|
|
77
102
|
const PolymorphicButton = Button as (buttonProps: ButtonProps<T>) => JSX.Element
|
|
78
103
|
const PolymorphicButtonTarget = ButtonTarget as (targetProps: ButtonTargetProps<T>) => JSX.Element
|
|
@@ -90,7 +115,17 @@ export function ActionButton<T extends ValidComponent = 'button'>(props: ActionB
|
|
|
90
115
|
role: local.as === 'a' && disabled() ? 'link' : rest.role,
|
|
91
116
|
href: local.as === 'a' && disabled() ? undefined : (rest as { href?: string }).href,
|
|
92
117
|
'aria-busy': busy() || local['aria-busy'],
|
|
93
|
-
onClick: guardedClick(rest.onClick, disabled)
|
|
118
|
+
onClick: guardedClick(rest.onClick, disabled, (event) => {
|
|
119
|
+
if (!local.tooltip) return
|
|
120
|
+
setTooltipSuppression(event.detail > 0 ? 'pointer' : 'focus')
|
|
121
|
+
setTooltipOpen(false)
|
|
122
|
+
}),
|
|
123
|
+
onPointerLeave: chainedHandler(rest.onPointerLeave, () => {
|
|
124
|
+
if (tooltipSuppression() === 'pointer') setTooltipSuppression(null)
|
|
125
|
+
}),
|
|
126
|
+
onBlur: chainedHandler(rest.onBlur, () => {
|
|
127
|
+
if (tooltipSuppression() === 'focus') setTooltipSuppression(null)
|
|
128
|
+
}),
|
|
94
129
|
onClickCapture: onClickCapture ? guardedClick(onClickCapture, disabled) : undefined,
|
|
95
130
|
'on:click': guardedClick((rest as Record<string, unknown>)['on:click'], disabled),
|
|
96
131
|
'oncapture:click': guardedClick((rest as Record<string, unknown>)['oncapture:click'], disabled),
|
|
@@ -118,7 +153,13 @@ export function ActionButton<T extends ValidComponent = 'button'>(props: ActionB
|
|
|
118
153
|
</PolymorphicButton>
|
|
119
154
|
}
|
|
120
155
|
>
|
|
121
|
-
<Tooltip
|
|
156
|
+
<Tooltip
|
|
157
|
+
open={tooltipOpen()}
|
|
158
|
+
onOpenChange={(open) => {
|
|
159
|
+
if (open && tooltipSuppression()) return
|
|
160
|
+
setTooltipOpen(open)
|
|
161
|
+
}}
|
|
162
|
+
>
|
|
122
163
|
<TooltipTrigger
|
|
123
164
|
as={PolymorphicButtonTarget}
|
|
124
165
|
{...(buttonProps() as unknown as ComponentProps<typeof PolymorphicButtonTarget>)}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import Resizable from '@corvu/resizable'
|
|
2
|
+
import { cn } from '#lib/utils'
|
|
3
|
+
|
|
4
|
+
export type SidebarNavResizeHandleProps = {
|
|
5
|
+
value: number
|
|
6
|
+
minimum: number
|
|
7
|
+
maximum: number
|
|
8
|
+
step?: number
|
|
9
|
+
label?: string
|
|
10
|
+
controls?: string
|
|
11
|
+
class?: string
|
|
12
|
+
onChange(value: number): void
|
|
13
|
+
onCommit?(value: number): void
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Resize a contextual sidebar without making its host recreate a separator
|
|
18
|
+
* widget. Corvu owns pointer capture, cancellation, constraints, keyboard
|
|
19
|
+
* movement and cleanup. Its two invisible panels provide a bounded pixel
|
|
20
|
+
* ruler; the host retains the real pane layout and preference persistence.
|
|
21
|
+
* Place this inside the positioned sidebar. The ruler can extend over the
|
|
22
|
+
* adjacent content, but only its handle participates in pointer hit testing.
|
|
23
|
+
*/
|
|
24
|
+
export function SidebarNavResizeHandle(props: SidebarNavResizeHandleProps) {
|
|
25
|
+
const maximum = () => Math.max(1, props.maximum)
|
|
26
|
+
const minimum = () => Math.min(maximum(), Math.max(0, props.minimum))
|
|
27
|
+
const value = () => Math.min(maximum(), Math.max(minimum(), props.value))
|
|
28
|
+
const commit = () => props.onCommit?.(value())
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<Resizable
|
|
32
|
+
orientation="horizontal"
|
|
33
|
+
sizes={[value() / maximum(), 1 - value() / maximum()]}
|
|
34
|
+
keyboardDelta={`${props.step ?? 16}px`}
|
|
35
|
+
style={{ width: `${maximum()}px` }}
|
|
36
|
+
class={cn('pointer-events-none absolute inset-y-0 start-0 flex', props.class)}
|
|
37
|
+
onSizesChange={(sizes) => {
|
|
38
|
+
const next = Math.round((sizes[0] ?? 0) * maximum())
|
|
39
|
+
if (next !== value()) props.onChange(Math.min(maximum(), Math.max(minimum(), next)))
|
|
40
|
+
}}
|
|
41
|
+
>
|
|
42
|
+
<Resizable.Panel minSize={minimum() / maximum()} maxSize={1} aria-hidden="true" />
|
|
43
|
+
<Resizable.Handle
|
|
44
|
+
aria-label={props.label ?? 'Resize navigation'}
|
|
45
|
+
aria-controls={props.controls}
|
|
46
|
+
aria-orientation="vertical"
|
|
47
|
+
aria-valuemin={minimum()}
|
|
48
|
+
aria-valuemax={maximum()}
|
|
49
|
+
aria-valuenow={value()}
|
|
50
|
+
onHandleDragEnd={commit}
|
|
51
|
+
onKeyUp={(event) => {
|
|
52
|
+
if (['ArrowLeft', 'ArrowRight', 'Home', 'End'].includes(event.key)) commit()
|
|
53
|
+
}}
|
|
54
|
+
class="pointer-events-auto relative z-(--z-docked) w-px -translate-x-full shrink-0 bg-transparent after:absolute after:inset-y-0 after:-inset-x-1 after:w-3 hover:bg-primary focus-visible:bg-primary focus-visible:ring-3 focus-visible:ring-primary-subtle focus-visible:outline-none"
|
|
55
|
+
/>
|
|
56
|
+
<Resizable.Panel minSize={0} aria-hidden="true" />
|
|
57
|
+
</Resizable>
|
|
58
|
+
)
|
|
59
|
+
}
|
|
@@ -110,6 +110,7 @@ export function CommandDialog(props: CommandDialogProps) {
|
|
|
110
110
|
<KobalteDialog.Content
|
|
111
111
|
aria-label={commandProps.label}
|
|
112
112
|
cmdk-dialog=""
|
|
113
|
+
role="dialog"
|
|
113
114
|
class={cn(
|
|
114
115
|
'bg-popover text-popover-foreground fixed top-1/3 left-1/2 z-(--z-dialog) w-full max-w-lg -translate-x-1/2 -translate-y-1/2',
|
|
115
116
|
'overflow-hidden rounded-xl border border-border shadow-xl',
|
package/src/lint/index.ts
CHANGED
|
@@ -79,6 +79,28 @@ const objectHasStaticProperty = (node: any, name: string, seen = new Set<unknown
|
|
|
79
79
|
})
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
/** Returns literal values for a statically named property in an object spread. */
|
|
83
|
+
const staticObjectPropertyValues = (node: any, name: string, seen = new Set<unknown>()): any[] => {
|
|
84
|
+
if (!node || seen.has(node)) return []
|
|
85
|
+
seen.add(node)
|
|
86
|
+
if (
|
|
87
|
+
node.type === 'TSAsExpression' ||
|
|
88
|
+
node.type === 'TSSatisfiesExpression' ||
|
|
89
|
+
node.type === 'TSNonNullExpression'
|
|
90
|
+
)
|
|
91
|
+
return staticObjectPropertyValues(node.expression, name, seen)
|
|
92
|
+
if (node.type !== 'ObjectExpression') return []
|
|
93
|
+
return node.properties.flatMap((property: any) => {
|
|
94
|
+
if (property.type === 'Property')
|
|
95
|
+
return staticPropertyKey(property.key, property.computed === true) === name
|
|
96
|
+
? [property.value]
|
|
97
|
+
: []
|
|
98
|
+
if (property.type === 'SpreadElement')
|
|
99
|
+
return staticObjectPropertyValues(property.argument, name, seen)
|
|
100
|
+
return []
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
82
104
|
const noInlineStyles: RuleModule = {
|
|
83
105
|
meta: {
|
|
84
106
|
type: 'problem',
|
|
@@ -297,6 +319,14 @@ const staticStrings = (node: any): string[] => {
|
|
|
297
319
|
|
|
298
320
|
const roleOf = (node: any): string | null => {
|
|
299
321
|
for (const attribute of node.attributes ?? []) {
|
|
322
|
+
if (attribute.type === 'JSXSpreadAttribute') {
|
|
323
|
+
const role = staticObjectPropertyValues(attribute.argument, 'role')
|
|
324
|
+
.flatMap((value) => staticStrings(value))
|
|
325
|
+
.flatMap((value) => value.split(/\s+/))
|
|
326
|
+
.find((value) => value in ROLE_PRIMITIVES)
|
|
327
|
+
if (role) return role
|
|
328
|
+
continue
|
|
329
|
+
}
|
|
300
330
|
if (attribute.type !== 'JSXAttribute' || attribute.name?.name !== 'role') continue
|
|
301
331
|
return (
|
|
302
332
|
staticStrings(attribute.value)
|
|
@@ -311,31 +341,37 @@ const roleOf = (node: any): string | null => {
|
|
|
311
341
|
const has = (node: any, name: string): boolean =>
|
|
312
342
|
(node.attributes ?? []).some(
|
|
313
343
|
(attribute: any) =>
|
|
314
|
-
attribute.type === 'JSXAttribute' &&
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
344
|
+
(attribute.type === 'JSXAttribute' &&
|
|
345
|
+
(attribute.name?.type === 'JSXNamespacedName'
|
|
346
|
+
? `${attribute.name.namespace.name}:${attribute.name.name.name}`
|
|
347
|
+
: attribute.name?.name) === name) ||
|
|
348
|
+
(attribute.type === 'JSXSpreadAttribute' && objectHasStaticProperty(attribute.argument, name))
|
|
318
349
|
)
|
|
319
350
|
|
|
351
|
+
const tabStopValue = (value: any): boolean => {
|
|
352
|
+
const expression = value?.type === 'JSXExpressionContainer' ? value.expression : value
|
|
353
|
+
if (expression?.type === 'Literal') return !(Number(expression.value) < 0)
|
|
354
|
+
if (
|
|
355
|
+
expression?.type === 'UnaryExpression' &&
|
|
356
|
+
expression.operator === '-' &&
|
|
357
|
+
expression.argument?.type === 'Literal'
|
|
358
|
+
)
|
|
359
|
+
return !(Number(expression.argument.value) > 0)
|
|
360
|
+
return true
|
|
361
|
+
}
|
|
362
|
+
|
|
320
363
|
const hasTabStop = (node: any): boolean =>
|
|
321
364
|
(node.attributes ?? []).some((attribute: any) => {
|
|
322
365
|
if (
|
|
323
|
-
attribute.type
|
|
324
|
-
|
|
325
|
-
)
|
|
326
|
-
return false
|
|
327
|
-
const value =
|
|
328
|
-
attribute.value?.type === 'JSXExpressionContainer'
|
|
329
|
-
? attribute.value.expression
|
|
330
|
-
: attribute.value
|
|
331
|
-
if (value?.type === 'Literal') return !(Number(value.value) < 0)
|
|
332
|
-
if (
|
|
333
|
-
value?.type === 'UnaryExpression' &&
|
|
334
|
-
value.operator === '-' &&
|
|
335
|
-
value.argument?.type === 'Literal'
|
|
366
|
+
attribute.type === 'JSXAttribute' &&
|
|
367
|
+
['tabIndex', 'tabindex'].includes(attribute.name?.name)
|
|
336
368
|
)
|
|
337
|
-
return
|
|
338
|
-
|
|
369
|
+
return tabStopValue(attribute.value)
|
|
370
|
+
if (attribute.type === 'JSXSpreadAttribute')
|
|
371
|
+
return ['tabIndex', 'tabindex'].some((name) =>
|
|
372
|
+
staticObjectPropertyValues(attribute.argument, name).some(tabStopValue)
|
|
373
|
+
)
|
|
374
|
+
return false
|
|
339
375
|
})
|
|
340
376
|
|
|
341
377
|
const noInteractiveWrappers: RuleModule = {
|
|
@@ -415,6 +451,118 @@ const noInteractiveWrappers: RuleModule = {
|
|
|
415
451
|
},
|
|
416
452
|
}
|
|
417
453
|
|
|
454
|
+
const BUTTON_MODULE = '@adea-ai/ui/components/ui/button'
|
|
455
|
+
const ACTION_BUTTON_MODULE = '@adea-ai/ui/components/composites/action-button'
|
|
456
|
+
const ICON_BUTTON_SIZES: ReadonlySet<string> = new Set([
|
|
457
|
+
'icon-2xs',
|
|
458
|
+
'icon-xs',
|
|
459
|
+
'icon-sm',
|
|
460
|
+
'icon-md',
|
|
461
|
+
'icon-lg',
|
|
462
|
+
'icon-xl',
|
|
463
|
+
])
|
|
464
|
+
|
|
465
|
+
const staticJsxValues = (value: any): any[] =>
|
|
466
|
+
value?.type === 'JSXExpressionContainer' ? [value.expression] : value ? [value] : []
|
|
467
|
+
|
|
468
|
+
const attributeValues = (node: any, name: string): any[] => {
|
|
469
|
+
const values: any[] = []
|
|
470
|
+
for (const attribute of node.attributes ?? []) {
|
|
471
|
+
if (attribute.type === 'JSXAttribute' && attribute.name?.name === name)
|
|
472
|
+
values.splice(0, values.length, ...staticJsxValues(attribute.value))
|
|
473
|
+
if (attribute.type === 'JSXSpreadAttribute') {
|
|
474
|
+
const spreadValues = staticObjectPropertyValues(attribute.argument, name)
|
|
475
|
+
if (spreadValues.length > 0) values.splice(0, values.length, spreadValues.at(-1))
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return values
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const staticStringValue = (node: any): string | null => {
|
|
482
|
+
if (node?.type === 'Literal' && typeof node.value === 'string') return node.value
|
|
483
|
+
if (node?.type === 'TemplateLiteral' && node.expressions.length === 0)
|
|
484
|
+
return node.quasis[0].value.cooked ?? node.quasis[0].value.raw
|
|
485
|
+
return null
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const isIconButtonSize = (node: any): boolean =>
|
|
489
|
+
attributeValues(node, 'size').some((value) => {
|
|
490
|
+
const size = staticStringValue(value)
|
|
491
|
+
return size !== null && ICON_BUTTON_SIZES.has(size)
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
const actionTooltipStatus = (node: any): 'valid' | 'blank' | 'unknown' | 'missing' => {
|
|
495
|
+
const values = attributeValues(node, 'tooltip')
|
|
496
|
+
if (values.length === 0) return 'missing'
|
|
497
|
+
const strings = values.map(staticStringValue)
|
|
498
|
+
if (strings.some((value) => value !== null && value.trim().length > 0)) return 'valid'
|
|
499
|
+
if (strings.every((value) => value !== null)) return 'blank'
|
|
500
|
+
return 'unknown'
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
const isImportedComponent = (identifier: any, imports: Set<string>): boolean =>
|
|
504
|
+
identifier?.type === 'Identifier' && imports.has(identifier.name)
|
|
505
|
+
|
|
506
|
+
/** Requires shared icon actions to carry the shared explanatory-tooltip contract. */
|
|
507
|
+
const requireActionButtonTooltip: RuleModule = {
|
|
508
|
+
meta: {
|
|
509
|
+
type: 'problem',
|
|
510
|
+
docs: {
|
|
511
|
+
description:
|
|
512
|
+
'Consumer icon-size shared Button actions use ActionButton with a nonblank tooltip; dynamic values remain a review boundary.',
|
|
513
|
+
},
|
|
514
|
+
schema: [],
|
|
515
|
+
messages: {
|
|
516
|
+
useActionButton:
|
|
517
|
+
'Icon-size Button actions use ActionButton with a nonblank tooltip. Keep the control name in aria-label; the tooltip explains the action.',
|
|
518
|
+
missingTooltip:
|
|
519
|
+
'An icon-size ActionButton needs a supplied nonblank tooltip. Keep its accessible name in aria-label as well.',
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
create(context) {
|
|
523
|
+
const buttons = new Set<string>()
|
|
524
|
+
const actionButtons = new Set<string>()
|
|
525
|
+
const recordImports = (node: any) => {
|
|
526
|
+
const source = node.source?.value
|
|
527
|
+
const target =
|
|
528
|
+
source === BUTTON_MODULE ? buttons : source === ACTION_BUTTON_MODULE ? actionButtons : null
|
|
529
|
+
if (!target) return
|
|
530
|
+
for (const specifier of node.specifiers ?? []) {
|
|
531
|
+
if (specifier.type !== 'ImportSpecifier') continue
|
|
532
|
+
const imported = specifier.imported?.name ?? specifier.imported?.value
|
|
533
|
+
if (
|
|
534
|
+
(source === BUTTON_MODULE && imported === 'Button') ||
|
|
535
|
+
(source === ACTION_BUTTON_MODULE && imported === 'ActionButton')
|
|
536
|
+
)
|
|
537
|
+
target.add(specifier.local?.name)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
ImportDeclaration: recordImports,
|
|
543
|
+
JSXOpeningElement(node: any) {
|
|
544
|
+
if (!isIconButtonSize(node)) return
|
|
545
|
+
const name = node.name?.type === 'JSXIdentifier' ? node.name.name : null
|
|
546
|
+
const isButton = name !== null && buttons.has(name)
|
|
547
|
+
const isActionButton = name !== null && actionButtons.has(name)
|
|
548
|
+
const asValues = attributeValues(node, 'as')
|
|
549
|
+
const targetsButton = asValues.some((value) => isImportedComponent(value, buttons))
|
|
550
|
+
const targetsActionButton = asValues.some((value) =>
|
|
551
|
+
isImportedComponent(value, actionButtons)
|
|
552
|
+
)
|
|
553
|
+
if (isButton || targetsButton) {
|
|
554
|
+
context.report({ node, messageId: 'useActionButton' })
|
|
555
|
+
return
|
|
556
|
+
}
|
|
557
|
+
if (!isActionButton && !targetsActionButton) return
|
|
558
|
+
const status = actionTooltipStatus(node)
|
|
559
|
+
if (status === 'missing' || status === 'blank')
|
|
560
|
+
context.report({ node, messageId: 'missingTooltip' })
|
|
561
|
+
},
|
|
562
|
+
}
|
|
563
|
+
},
|
|
564
|
+
}
|
|
565
|
+
|
|
418
566
|
const noPrimitiveLibraryImports: RuleModule = {
|
|
419
567
|
meta: {
|
|
420
568
|
type: 'problem',
|
|
@@ -484,6 +632,7 @@ const plugin = {
|
|
|
484
632
|
'no-raw-interactive-elements': noRawInteractiveElements,
|
|
485
633
|
'no-primitive-library-imports': noPrimitiveLibraryImports,
|
|
486
634
|
'no-interactive-wrappers': noInteractiveWrappers,
|
|
635
|
+
'require-action-button-tooltip': requireActionButtonTooltip,
|
|
487
636
|
},
|
|
488
637
|
}
|
|
489
638
|
|