@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.
Files changed (31) hide show
  1. package/dist/components/composites/action-button/action-button.d.ts.map +1 -1
  2. package/dist/components/composites/action-button/action-button.js +55 -24
  3. package/dist/components/composites/action-button/action-button.js.map +1 -1
  4. package/dist/components/layout/sidebar-nav/index.d.ts +1 -0
  5. package/dist/components/layout/sidebar-nav/index.d.ts.map +1 -1
  6. package/dist/components/layout/sidebar-nav/index.js +2 -1
  7. package/dist/components/layout/sidebar-nav/sidebar-nav-resize-handle.d.ts +21 -0
  8. package/dist/components/layout/sidebar-nav/sidebar-nav-resize-handle.d.ts.map +1 -0
  9. package/dist/components/layout/sidebar-nav/sidebar-nav-resize-handle.js +84 -0
  10. package/dist/components/layout/sidebar-nav/sidebar-nav-resize-handle.js.map +1 -0
  11. package/dist/components/ui/command/command.d.ts.map +1 -1
  12. package/dist/components/ui/command/command.js +1 -0
  13. package/dist/components/ui/command/command.js.map +1 -1
  14. package/dist/index.js +2 -1
  15. package/dist/lint/index.d.ts +1 -0
  16. package/dist/lint/index.d.ts.map +1 -1
  17. package/dist/lint.js +171 -59
  18. package/dist/lint.js.map +1 -1
  19. package/dist/r/registry.json +6 -0
  20. package/dist/r/sidebar-nav.json +6 -0
  21. package/dist/r/src/components/composites/action-button/action-button.tsx +52 -11
  22. package/dist/r/src/components/layout/sidebar-nav/index.ts +4 -0
  23. package/dist/r/src/components/layout/sidebar-nav/sidebar-nav-resize-handle.tsx +59 -0
  24. package/dist/r/src/components/ui/command/command.tsx +1 -0
  25. package/package.json +1 -1
  26. package/registry.json +6 -0
  27. package/src/components/composites/action-button/action-button.tsx +52 -11
  28. package/src/components/layout/sidebar-nav/index.ts +4 -0
  29. package/src/components/layout/sidebar-nav/sidebar-nav-resize-handle.tsx +59 -0
  30. package/src/components/ui/command/command.tsx +1 -0
  31. package/src/lint/index.ts +168 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adea-ai/ui",
3
- "version": "0.83.0",
3
+ "version": "0.84.0",
4
4
  "description": "The Adea design system: Kobalte-backed Solid components, semantic OKLCH tokens, and the app-shell layout primitives shared by Adea and Cortana.",
5
5
  "keywords": [
6
6
  "adea",
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 { Show, splitProps, type ComponentProps, type JSX, type ValidComponent } from 'solid-js'
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 guardedClick(handler: unknown, disabled: () => boolean) {
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
- if (Array.isArray(handler)) {
39
- const [callback, data] = handler as [(data: unknown, event: MouseEvent) => void, unknown]
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>)}
@@ -13,3 +13,7 @@ export {
13
13
  type SidebarNavProps,
14
14
  type SidebarNavTitleProps,
15
15
  } from './sidebar-nav'
16
+ export {
17
+ SidebarNavResizeHandle,
18
+ type SidebarNavResizeHandleProps,
19
+ } from './sidebar-nav-resize-handle'
@@ -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
- (attribute.name?.type === 'JSXNamespacedName'
316
- ? `${attribute.name.namespace.name}:${attribute.name.name.name}`
317
- : attribute.name?.name) === name
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 !== 'JSXAttribute' ||
324
- !['tabIndex', 'tabindex'].includes(attribute.name?.name)
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 !(Number(value.argument.value) > 0)
338
- return true
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