@adea-ai/ui 0.81.0 → 0.82.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/README.md +41 -8
- package/dist/components/composites/account-menu/account-menu.d.ts +5 -0
- package/dist/components/composites/account-menu/account-menu.d.ts.map +1 -1
- package/dist/components/composites/account-menu/account-menu.js +122 -70
- package/dist/components/composites/account-menu/account-menu.js.map +1 -1
- package/dist/components/composites/action-button/action-button.d.ts +27 -0
- package/dist/components/composites/action-button/action-button.d.ts.map +1 -0
- package/dist/components/composites/action-button/action-button.js +121 -0
- package/dist/components/composites/action-button/action-button.js.map +1 -0
- package/dist/components/composites/action-button/index.d.ts +3 -0
- package/dist/components/composites/action-button/index.d.ts.map +1 -0
- package/dist/components/composites/action-button/index.js +2 -0
- package/dist/components/conversation/thread-panel.d.ts +2 -0
- package/dist/components/conversation/thread-panel.d.ts.map +1 -1
- package/dist/components/conversation/thread-panel.js +36 -31
- package/dist/components/conversation/thread-panel.js.map +1 -1
- package/dist/components/layout/sidebar-nav/index.d.ts +1 -1
- package/dist/components/layout/sidebar-nav/index.d.ts.map +1 -1
- package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts +6 -1
- package/dist/components/layout/sidebar-nav/sidebar-nav.d.ts.map +1 -1
- package/dist/components/layout/sidebar-nav/sidebar-nav.js +66 -62
- package/dist/components/layout/sidebar-nav/sidebar-nav.js.map +1 -1
- package/dist/components/ui/board/board.d.ts.map +1 -1
- package/dist/components/ui/board/board.js +54 -7
- package/dist/components/ui/board/board.js.map +1 -1
- package/dist/components/ui/command/command.d.ts +5 -1
- package/dist/components/ui/command/command.d.ts.map +1 -1
- package/dist/components/ui/command/command.js +8 -3
- package/dist/components/ui/command/command.js.map +1 -1
- package/dist/components/ui/input/index.d.ts +1 -1
- package/dist/components/ui/input/index.d.ts.map +1 -1
- package/dist/components/ui/input/input.d.ts +5 -1
- package/dist/components/ui/input/input.d.ts.map +1 -1
- package/dist/components/ui/input/input.js +36 -17
- package/dist/components/ui/input/input.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/lint/index.d.ts.map +1 -1
- package/dist/lint.js +47 -7
- package/dist/lint.js.map +1 -1
- package/dist/r/account-menu.json +1 -0
- package/dist/r/action-button.json +24 -0
- package/dist/r/conversation.json +1 -0
- package/dist/r/registry.json +26 -0
- package/dist/r/src/components/composites/account-menu/account-menu.tsx +100 -44
- package/dist/r/src/components/composites/action-button/action-button.tsx +139 -0
- package/dist/r/src/components/composites/action-button/index.ts +2 -0
- package/dist/r/src/components/conversation/thread-panel.tsx +22 -12
- package/dist/r/src/components/layout/sidebar-nav/index.ts +1 -0
- package/dist/r/src/components/layout/sidebar-nav/sidebar-nav.tsx +29 -21
- package/dist/r/src/components/ui/board/board.tsx +74 -6
- package/dist/r/src/components/ui/command/command.tsx +15 -4
- package/dist/r/src/components/ui/input/index.ts +1 -1
- package/dist/r/src/components/ui/input/input.tsx +38 -21
- package/package.json +1 -1
- package/registry.json +26 -0
- package/src/components/composites/account-menu/account-menu.tsx +100 -44
- package/src/components/composites/action-button/action-button.tsx +139 -0
- package/src/components/composites/action-button/index.ts +2 -0
- package/src/components/conversation/thread-panel.tsx +22 -12
- package/src/components/layout/sidebar-nav/index.ts +1 -0
- package/src/components/layout/sidebar-nav/sidebar-nav.tsx +29 -21
- package/src/components/ui/board/board.tsx +74 -6
- package/src/components/ui/command/command.tsx +15 -4
- package/src/components/ui/input/index.ts +1 -1
- package/src/components/ui/input/input.tsx +38 -21
- package/src/index.ts +1 -0
- package/src/lint/index.ts +71 -7
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { Show, splitProps, type ComponentProps, type JSX, type ValidComponent } from 'solid-js'
|
|
2
|
+
import { Button, type ButtonProps } from '../../ui/button'
|
|
3
|
+
import { Spinner } from '../../ui/spinner'
|
|
4
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../../ui/tooltip'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ActionButton.
|
|
8
|
+
*
|
|
9
|
+
* A Button with optional explanatory tooltip and busy feedback. Use it for a
|
|
10
|
+
* simple action that needs these states; actions with product-specific rules
|
|
11
|
+
* should compose Button directly.
|
|
12
|
+
*
|
|
13
|
+
* The tooltip trigger renders the polymorphic Button itself. A disabled native
|
|
14
|
+
* control cannot receive pointer or keyboard events. When a tooltip is set,
|
|
15
|
+
* disabled actions therefore use focusable `aria-disabled` semantics and guard
|
|
16
|
+
* activation; without a tooltip, native buttons retain native disabling.
|
|
17
|
+
* Custom polymorphic components must forward aria, event, and href props for
|
|
18
|
+
* these guarantees to apply.
|
|
19
|
+
*/
|
|
20
|
+
export type ActionButtonProps<T extends ValidComponent = 'button'> = ButtonProps<T> & {
|
|
21
|
+
/** A short explanation, also shown on keyboard focus. The Button still needs its own name. */
|
|
22
|
+
tooltip?: string
|
|
23
|
+
tooltipSide?: 'top' | 'right' | 'bottom' | 'left'
|
|
24
|
+
/** Disables the action and announces the busy label through a polite status region. */
|
|
25
|
+
busy?: boolean
|
|
26
|
+
/** The activity announced while busy. The Button's accessible name stays unchanged. */
|
|
27
|
+
busyLabel?: string
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function guardedClick(handler: unknown, disabled: () => boolean) {
|
|
31
|
+
return (event: MouseEvent) => {
|
|
32
|
+
if (disabled()) {
|
|
33
|
+
event.preventDefault()
|
|
34
|
+
event.stopImmediatePropagation()
|
|
35
|
+
return
|
|
36
|
+
}
|
|
37
|
+
|
|
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
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
type ButtonTargetProps<T extends ValidComponent = 'button'> = Omit<ButtonProps<T>, 'as'> & {
|
|
49
|
+
actionAs?: T
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Adapter keeps TooltipTrigger's `as` separate from Button's polymorphic `as`. */
|
|
53
|
+
function ButtonTarget<T extends ValidComponent = 'button'>(props: ButtonTargetProps<T>) {
|
|
54
|
+
const [local, rest] = splitProps(props as ButtonTargetProps, ['actionAs'])
|
|
55
|
+
const PolymorphicButton = Button as (targetProps: ButtonProps<T>) => JSX.Element
|
|
56
|
+
return <PolymorphicButton as={(local.actionAs ?? 'button') as T} {...(rest as ButtonProps<T>)} />
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function ActionButton<T extends ValidComponent = 'button'>(props: ActionButtonProps<T>) {
|
|
60
|
+
const [local, rest] = splitProps(props as ActionButtonProps, [
|
|
61
|
+
'as',
|
|
62
|
+
'variant',
|
|
63
|
+
'size',
|
|
64
|
+
'class',
|
|
65
|
+
'children',
|
|
66
|
+
'disabled',
|
|
67
|
+
'aria-busy',
|
|
68
|
+
'tooltip',
|
|
69
|
+
'tooltipSide',
|
|
70
|
+
'busy',
|
|
71
|
+
'busyLabel',
|
|
72
|
+
])
|
|
73
|
+
const busy = () => local.busy ?? false
|
|
74
|
+
const disabled = () => busy() || !!local.disabled
|
|
75
|
+
const busyStatus = () => local.busyLabel ?? 'Working'
|
|
76
|
+
const onClickCapture = (rest as { onClickCapture?: unknown }).onClickCapture
|
|
77
|
+
const PolymorphicButton = Button as (buttonProps: ButtonProps<T>) => JSX.Element
|
|
78
|
+
const PolymorphicButtonTarget = ButtonTarget as (targetProps: ButtonTargetProps<T>) => JSX.Element
|
|
79
|
+
|
|
80
|
+
const buttonProps = () => ({
|
|
81
|
+
...rest,
|
|
82
|
+
variant: local.variant,
|
|
83
|
+
size: local.size,
|
|
84
|
+
class: [local.class, local.tooltip && disabled() && 'aria-disabled:pointer-events-auto']
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
.join(' '),
|
|
87
|
+
disabled: disabled() && !local.tooltip,
|
|
88
|
+
'aria-disabled': disabled() ? true : rest['aria-disabled'],
|
|
89
|
+
tabIndex: local.as === 'a' && disabled() ? (local.tooltip ? 0 : -1) : rest.tabIndex,
|
|
90
|
+
role: local.as === 'a' && disabled() ? 'link' : rest.role,
|
|
91
|
+
href: local.as === 'a' && disabled() ? undefined : (rest as { href?: string }).href,
|
|
92
|
+
'aria-busy': busy() || local['aria-busy'],
|
|
93
|
+
onClick: guardedClick(rest.onClick, disabled),
|
|
94
|
+
onClickCapture: onClickCapture ? guardedClick(onClickCapture, disabled) : undefined,
|
|
95
|
+
'on:click': guardedClick((rest as Record<string, unknown>)['on:click'], disabled),
|
|
96
|
+
'oncapture:click': guardedClick((rest as Record<string, unknown>)['oncapture:click'], disabled),
|
|
97
|
+
})
|
|
98
|
+
|
|
99
|
+
const contents = () => (
|
|
100
|
+
<>
|
|
101
|
+
<Show when={busy()}>
|
|
102
|
+
<Spinner size="sm" label={false} aria-hidden="true" data-icon="inline-start" />
|
|
103
|
+
</Show>
|
|
104
|
+
{local.children}
|
|
105
|
+
</>
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
return (
|
|
109
|
+
<>
|
|
110
|
+
<Show
|
|
111
|
+
when={local.tooltip}
|
|
112
|
+
fallback={
|
|
113
|
+
<PolymorphicButton
|
|
114
|
+
as={(local.as ?? 'button') as T}
|
|
115
|
+
{...(buttonProps() as unknown as ButtonProps<T>)}
|
|
116
|
+
>
|
|
117
|
+
{contents()}
|
|
118
|
+
</PolymorphicButton>
|
|
119
|
+
}
|
|
120
|
+
>
|
|
121
|
+
<Tooltip>
|
|
122
|
+
<TooltipTrigger
|
|
123
|
+
as={PolymorphicButtonTarget}
|
|
124
|
+
{...(buttonProps() as unknown as ComponentProps<typeof PolymorphicButtonTarget>)}
|
|
125
|
+
actionAs={(local.as ?? 'button') as T}
|
|
126
|
+
>
|
|
127
|
+
{contents()}
|
|
128
|
+
</TooltipTrigger>
|
|
129
|
+
<TooltipContent side={local.tooltipSide}>{local.tooltip}</TooltipContent>
|
|
130
|
+
</Tooltip>
|
|
131
|
+
</Show>
|
|
132
|
+
<Show when={busy()}>
|
|
133
|
+
<span role="status" aria-live="polite" class="visually-hidden">
|
|
134
|
+
{busyStatus()}
|
|
135
|
+
</span>
|
|
136
|
+
</Show>
|
|
137
|
+
</>
|
|
138
|
+
)
|
|
139
|
+
}
|
|
@@ -3,6 +3,7 @@ import type { ComponentProps, JSX } from 'solid-js'
|
|
|
3
3
|
import { Show, splitProps } from 'solid-js'
|
|
4
4
|
import { cn } from '#lib/utils'
|
|
5
5
|
import { Button } from '../ui/button/button'
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip/tooltip'
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* ThreadPanel.
|
|
@@ -26,6 +27,8 @@ export type ThreadPanelProps = ComponentProps<'aside'> & {
|
|
|
26
27
|
root?: JSX.Element
|
|
27
28
|
/** The composer for a reply. */
|
|
28
29
|
composer?: JSX.Element
|
|
30
|
+
/** Host actions, such as marking the thread unread, before the close control. */
|
|
31
|
+
headerActions?: JSX.Element
|
|
29
32
|
onClose?: () => void
|
|
30
33
|
}
|
|
31
34
|
|
|
@@ -36,6 +39,7 @@ export function ThreadPanel(props: ThreadPanelProps) {
|
|
|
36
39
|
'count',
|
|
37
40
|
'root',
|
|
38
41
|
'composer',
|
|
42
|
+
'headerActions',
|
|
39
43
|
'onClose',
|
|
40
44
|
'children',
|
|
41
45
|
])
|
|
@@ -45,7 +49,7 @@ export function ThreadPanel(props: ThreadPanelProps) {
|
|
|
45
49
|
data-slot="thread-panel"
|
|
46
50
|
aria-label={`Thread: ${local.label}`}
|
|
47
51
|
class={cn(
|
|
48
|
-
'bg-surface flex h-full w-96 shrink-0 flex-col border-s border-border',
|
|
52
|
+
'bg-surface flex h-full w-96 max-w-full shrink-0 flex-col border-s border-border',
|
|
49
53
|
local.class
|
|
50
54
|
)}
|
|
51
55
|
{...rest}
|
|
@@ -59,17 +63,23 @@ export function ThreadPanel(props: ThreadPanelProps) {
|
|
|
59
63
|
</span>
|
|
60
64
|
</Show>
|
|
61
65
|
</div>
|
|
62
|
-
<
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
66
|
+
<div class="ms-auto flex shrink-0 items-center gap-1">
|
|
67
|
+
{local.headerActions}
|
|
68
|
+
<Show when={local.onClose}>
|
|
69
|
+
<Tooltip>
|
|
70
|
+
<TooltipTrigger
|
|
71
|
+
as={Button}
|
|
72
|
+
variant="ghost"
|
|
73
|
+
size="icon-sm"
|
|
74
|
+
aria-label="Close thread"
|
|
75
|
+
onClick={() => local.onClose?.()}
|
|
76
|
+
>
|
|
77
|
+
<X />
|
|
78
|
+
</TooltipTrigger>
|
|
79
|
+
<TooltipContent>Close thread</TooltipContent>
|
|
80
|
+
</Tooltip>
|
|
81
|
+
</Show>
|
|
82
|
+
</div>
|
|
73
83
|
</header>
|
|
74
84
|
|
|
75
85
|
<Show when={local.root}>
|
|
@@ -56,13 +56,19 @@ export function SidebarNavHeader(props: ComponentProps<'div'>) {
|
|
|
56
56
|
)
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
export
|
|
60
|
-
|
|
59
|
+
export type SidebarNavTitleProps<T extends ValidComponent = 'div'> = PolymorphicProps<
|
|
60
|
+
T,
|
|
61
|
+
{ class?: string }
|
|
62
|
+
>
|
|
63
|
+
|
|
64
|
+
export function SidebarNavTitle<T extends ValidComponent = 'div'>(props: SidebarNavTitleProps<T>) {
|
|
65
|
+
const [local, rest] = splitProps(props as SidebarNavTitleProps, ['class'])
|
|
61
66
|
return (
|
|
62
|
-
<
|
|
67
|
+
<Polymorphic
|
|
68
|
+
as="div"
|
|
63
69
|
data-slot="sidebar-nav-title"
|
|
64
70
|
class={cn('truncate text-sm font-semibold tracking-tight', local.class)}
|
|
65
|
-
{...rest}
|
|
71
|
+
{...(rest as ComponentProps<'div'>)}
|
|
66
72
|
/>
|
|
67
73
|
)
|
|
68
74
|
}
|
|
@@ -105,6 +111,8 @@ export function SidebarNavSection(
|
|
|
105
111
|
count?: number
|
|
106
112
|
/** A control at the trailing edge of the heading row. */
|
|
107
113
|
action?: JSX.Element
|
|
114
|
+
/** Semantic heading element for a host's section hierarchy. */
|
|
115
|
+
headingAs?: 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
|
108
116
|
}
|
|
109
117
|
) {
|
|
110
118
|
const [local, rest] = splitProps(props, [
|
|
@@ -114,6 +122,7 @@ export function SidebarNavSection(
|
|
|
114
122
|
'defaultOpen',
|
|
115
123
|
'count',
|
|
116
124
|
'action',
|
|
125
|
+
'headingAs',
|
|
117
126
|
'children',
|
|
118
127
|
])
|
|
119
128
|
const [open, setOpen] = createSignal(local.defaultOpen ?? true)
|
|
@@ -138,25 +147,24 @@ export function SidebarNavSection(
|
|
|
138
147
|
return (
|
|
139
148
|
<div data-slot="sidebar-nav-section" class={cn('flex flex-col gap-0.5', local.class)} {...rest}>
|
|
140
149
|
<div class="group/section-header flex items-center gap-1 rounded-md px-2 py-1.5 hover:bg-sidebar-accent/60">
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
{heading}
|
|
146
|
-
</div>
|
|
147
|
-
}
|
|
150
|
+
<Polymorphic
|
|
151
|
+
as={local.headingAs ?? 'div'}
|
|
152
|
+
aria-label={local.headingAs ? local.label : undefined}
|
|
153
|
+
class="flex min-w-0 flex-1 items-center gap-1.5 text-2xs font-medium tracking-wide text-sidebar-muted-foreground uppercase"
|
|
148
154
|
>
|
|
149
|
-
<
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
155
|
+
<Show when={local.collapsible} fallback={heading}>
|
|
156
|
+
<button
|
|
157
|
+
type="button"
|
|
158
|
+
aria-expanded={open()}
|
|
159
|
+
onClick={() => setOpen((value) => !value)}
|
|
160
|
+
class="flex min-w-0 flex-1 items-center gap-1.5 rounded-sm text-2xs font-medium tracking-wide text-sidebar-muted-foreground uppercase outline-none transition-colors ease-out hover:text-sidebar-foreground focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle"
|
|
161
|
+
>
|
|
162
|
+
{heading}
|
|
163
|
+
</button>
|
|
164
|
+
</Show>
|
|
165
|
+
</Polymorphic>
|
|
158
166
|
<Show when={local.action}>
|
|
159
|
-
<span class="shrink-0 opacity-0 transition-opacity ease-out group-hover/section-header:opacity-100 focus-within:opacity-100">
|
|
167
|
+
<span class="shrink-0 opacity-0 transition-opacity ease-out group-hover/section-header:opacity-100 focus-within:opacity-100 [@media(hover:none)]:opacity-100">
|
|
160
168
|
{local.action}
|
|
161
169
|
</span>
|
|
162
170
|
</Show>
|
|
@@ -1,4 +1,13 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
createEffect,
|
|
3
|
+
createMemo,
|
|
4
|
+
createSignal,
|
|
5
|
+
For,
|
|
6
|
+
onCleanup,
|
|
7
|
+
Show,
|
|
8
|
+
splitProps,
|
|
9
|
+
type JSX,
|
|
10
|
+
} from 'solid-js'
|
|
2
11
|
import { cva, type VariantProps } from '#lib/variants'
|
|
3
12
|
import { cn } from '#lib/utils'
|
|
4
13
|
|
|
@@ -88,6 +97,50 @@ export function Board<T>(props: BoardProps<T>) {
|
|
|
88
97
|
const [dragging, setDragging] = createSignal<T | null>(null)
|
|
89
98
|
const [overColumn, setOverColumn] = createSignal<string | null>(null)
|
|
90
99
|
const [announcement, setAnnouncement] = createSignal('')
|
|
100
|
+
const cardElements = new Map<string, HTMLElement>()
|
|
101
|
+
const [pendingFocus, setPendingFocus] = createSignal<{
|
|
102
|
+
itemId: string
|
|
103
|
+
columnId: string
|
|
104
|
+
source: HTMLElement
|
|
105
|
+
} | null>(null)
|
|
106
|
+
const cancelOnPointer = () => setPendingFocus(null)
|
|
107
|
+
|
|
108
|
+
// A controlled move can settle after a server response. Restore only the
|
|
109
|
+
// focus that belonged to the moved card; another focus action cancels it.
|
|
110
|
+
createEffect(() => {
|
|
111
|
+
const pending = pendingFocus()
|
|
112
|
+
if (!pending) return
|
|
113
|
+
const document = pending.source.ownerDocument
|
|
114
|
+
const cancelOnFocus = (event: FocusEvent) => {
|
|
115
|
+
if (!pending.source.contains(event.target as Node)) setPendingFocus(null)
|
|
116
|
+
}
|
|
117
|
+
document.addEventListener('focusin', cancelOnFocus)
|
|
118
|
+
document.addEventListener('pointerdown', cancelOnPointer)
|
|
119
|
+
onCleanup(() => {
|
|
120
|
+
document.removeEventListener('focusin', cancelOnFocus)
|
|
121
|
+
document.removeEventListener('pointerdown', cancelOnPointer)
|
|
122
|
+
})
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
createEffect(() => {
|
|
126
|
+
const pending = pendingFocus()
|
|
127
|
+
if (!pending) return
|
|
128
|
+
const moved = local.items.find((item) => local.itemId(item) === pending.itemId)
|
|
129
|
+
if (!moved || local.itemColumn(moved) !== pending.columnId) return
|
|
130
|
+
queueMicrotask(() => {
|
|
131
|
+
if (pendingFocus() !== pending) return
|
|
132
|
+
const target = cardElements.get(pending.itemId)
|
|
133
|
+
setPendingFocus(null)
|
|
134
|
+
if (target?.isConnected) target.focus()
|
|
135
|
+
})
|
|
136
|
+
})
|
|
137
|
+
|
|
138
|
+
const prepareFocus = (item: T, columnId: string, source?: HTMLElement) => {
|
|
139
|
+
const card = source ?? cardElements.get(local.itemId(item))
|
|
140
|
+
if (card?.contains(card.ownerDocument.activeElement)) {
|
|
141
|
+
setPendingFocus({ itemId: local.itemId(item), columnId, source: card })
|
|
142
|
+
}
|
|
143
|
+
}
|
|
91
144
|
|
|
92
145
|
const byColumn = createMemo(() => {
|
|
93
146
|
const grouped = new Map<string, T[]>()
|
|
@@ -106,9 +159,11 @@ export function Board<T>(props: BoardProps<T>) {
|
|
|
106
159
|
const drop = (column: BoardColumn) => {
|
|
107
160
|
const item = dragging()
|
|
108
161
|
const from = item ? local.itemColumn(item) : null
|
|
162
|
+
const accepted = accepts(column)
|
|
109
163
|
setDragging(null)
|
|
110
164
|
setOverColumn(null)
|
|
111
|
-
if (!item || !from || from === column.id || !
|
|
165
|
+
if (!item || !from || from === column.id || !accepted) return
|
|
166
|
+
prepareFocus(item, column.id)
|
|
112
167
|
local.onMove({ itemId: local.itemId(item), from, to: column.id })
|
|
113
168
|
}
|
|
114
169
|
|
|
@@ -117,11 +172,12 @@ export function Board<T>(props: BoardProps<T>) {
|
|
|
117
172
|
* thing within its container" that does not collide with scrolling or with the
|
|
118
173
|
* text caret, and it is the same chord a browser tab or a spreadsheet uses.
|
|
119
174
|
*/
|
|
120
|
-
const moveByKeyboard = (item: T, direction: -1 | 1) => {
|
|
175
|
+
const moveByKeyboard = (item: T, direction: -1 | 1, source: HTMLElement) => {
|
|
121
176
|
const from = local.itemColumn(item)
|
|
122
177
|
const index = local.columns.findIndex((column) => column.id === from)
|
|
123
|
-
|
|
124
|
-
const ordered =
|
|
178
|
+
if (index < 0) return
|
|
179
|
+
const ordered =
|
|
180
|
+
direction === 1 ? local.columns.slice(index + 1) : local.columns.slice(0, index).toReversed()
|
|
125
181
|
const target = ordered.find(
|
|
126
182
|
(column) => !column.disabled && local.canDrop(item, from, column.id)
|
|
127
183
|
)
|
|
@@ -129,6 +185,7 @@ export function Board<T>(props: BoardProps<T>) {
|
|
|
129
185
|
setAnnouncement(`No column after ${from} accepts this item`)
|
|
130
186
|
return
|
|
131
187
|
}
|
|
188
|
+
prepareFocus(item, target.id, source)
|
|
132
189
|
local.onMove({ itemId: local.itemId(item), from, to: target.id })
|
|
133
190
|
setAnnouncement(`Moved to ${target.label}`)
|
|
134
191
|
}
|
|
@@ -185,6 +242,13 @@ export function Board<T>(props: BoardProps<T>) {
|
|
|
185
242
|
}
|
|
186
243
|
return (
|
|
187
244
|
<article
|
|
245
|
+
ref={(element) => {
|
|
246
|
+
const id = local.itemId(item)
|
|
247
|
+
cardElements.set(id, element)
|
|
248
|
+
onCleanup(() => {
|
|
249
|
+
if (cardElements.get(id) === element) cardElements.delete(id)
|
|
250
|
+
})
|
|
251
|
+
}}
|
|
188
252
|
class={cn(
|
|
189
253
|
'rounded-md border border-border bg-card text-sm shadow-xs',
|
|
190
254
|
cardDragState[isDragging() ? 'dragging' : 'idle']
|
|
@@ -203,7 +267,11 @@ export function Board<T>(props: BoardProps<T>) {
|
|
|
203
267
|
if (!(event.ctrlKey || event.metaKey)) return
|
|
204
268
|
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight') return
|
|
205
269
|
event.preventDefault()
|
|
206
|
-
moveByKeyboard(
|
|
270
|
+
moveByKeyboard(
|
|
271
|
+
item,
|
|
272
|
+
event.key === 'ArrowRight' ? 1 : -1,
|
|
273
|
+
event.currentTarget
|
|
274
|
+
)
|
|
207
275
|
}}
|
|
208
276
|
>
|
|
209
277
|
{local.children(item, { dragging: isDragging() })}
|
|
@@ -16,6 +16,12 @@ import { createEffect, createSignal, splitProps } from 'solid-js'
|
|
|
16
16
|
import { menuItem } from '#lib/overlay'
|
|
17
17
|
import { cn } from '#lib/utils'
|
|
18
18
|
|
|
19
|
+
// cmdk emits data-disabled="false" on enabled items; Kobalte omits it.
|
|
20
|
+
// Presence selectors would dim and disable every command in an app palette.
|
|
21
|
+
const commandItem = menuItem
|
|
22
|
+
.replace('data-[disabled]:pointer-events-none', '')
|
|
23
|
+
.replace('data-[disabled]:opacity-50', '')
|
|
24
|
+
|
|
19
25
|
/**
|
|
20
26
|
* Command.
|
|
21
27
|
*
|
|
@@ -181,8 +187,9 @@ export function CommandItem(props: ComponentProps<typeof CmdkItem>) {
|
|
|
181
187
|
return (
|
|
182
188
|
<CmdkItem
|
|
183
189
|
class={cn(
|
|
184
|
-
|
|
185
|
-
'data-[
|
|
190
|
+
commandItem,
|
|
191
|
+
'data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50',
|
|
192
|
+
'text-popover-foreground data-[selected=true]:bg-surface-hover data-[selected=true]:text-popover-foreground',
|
|
186
193
|
local.class
|
|
187
194
|
)}
|
|
188
195
|
{...rest}
|
|
@@ -205,14 +212,18 @@ export function CommandLoading(props: ComponentProps<typeof CmdkLoading>) {
|
|
|
205
212
|
)
|
|
206
213
|
}
|
|
207
214
|
|
|
208
|
-
/**
|
|
215
|
+
/**
|
|
216
|
+
* The keyboard hint a palette item usually carries.
|
|
217
|
+
* Muted canvas text can fail contrast on a raised or selected command row;
|
|
218
|
+
* hierarchy comes from its size and monospace face, with the row's readable ink.
|
|
219
|
+
*/
|
|
209
220
|
export function CommandShortcut(props: {
|
|
210
221
|
class?: string
|
|
211
222
|
children?: ComponentProps<'span'>['children']
|
|
212
223
|
}) {
|
|
213
224
|
return (
|
|
214
225
|
<span
|
|
215
|
-
class={cn('text-
|
|
226
|
+
class={cn('text-popover-foreground ms-auto font-mono text-2xs tracking-widest', props.class)}
|
|
216
227
|
>
|
|
217
228
|
{props.children}
|
|
218
229
|
</span>
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export { Input } from './input'
|
|
1
|
+
export { Input, type InputProps } from './input'
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ComponentProps } from 'solid-js'
|
|
2
|
-
import { splitProps } from 'solid-js'
|
|
2
|
+
import { createUniqueId, For, Show, splitProps } from 'solid-js'
|
|
3
3
|
import { cn } from '#lib/utils'
|
|
4
4
|
|
|
5
5
|
/**
|
|
@@ -14,28 +14,45 @@ import { cn } from '#lib/utils'
|
|
|
14
14
|
* Height comes from `h-control-md`; an input cannot be given a new height
|
|
15
15
|
* without a new token, which is what keeps it aligned with Button and Select.
|
|
16
16
|
*/
|
|
17
|
-
export
|
|
18
|
-
|
|
17
|
+
export type InputProps = ComponentProps<'input'> & {
|
|
18
|
+
/** Native text suggestions; custom values remain valid and submit unchanged. */
|
|
19
|
+
suggestions?: readonly string[]
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function Input(props: InputProps) {
|
|
23
|
+
const [local, rest] = splitProps(props, ['class', 'type', 'list', 'suggestions'])
|
|
24
|
+
const suggestionsId = createUniqueId()
|
|
25
|
+
const listId = () => local.list ?? `input-suggestions-${suggestionsId}`
|
|
19
26
|
|
|
20
27
|
return (
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
<>
|
|
29
|
+
<input
|
|
30
|
+
data-slot="input"
|
|
31
|
+
type={local.type ?? 'text'}
|
|
32
|
+
list={local.suggestions ? listId() : local.list}
|
|
33
|
+
class={cn(
|
|
34
|
+
'h-control-md w-full min-w-0 rounded-md border border-input bg-transparent px-control-md py-1 text-sm',
|
|
35
|
+
'transition-[color,box-shadow,border-color] ease-out outline-none',
|
|
36
|
+
'placeholder:text-muted-foreground',
|
|
37
|
+
'selection:bg-primary selection:text-primary-foreground',
|
|
38
|
+
'file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground',
|
|
39
|
+
/* A pointer click on a text field is a typing affordance, so the ring
|
|
31
40
|
appears where the user agent already matches :focus-visible. */
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
41
|
+
'focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-primary-subtle',
|
|
42
|
+
'aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive-subtle',
|
|
43
|
+
'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
|
|
44
|
+
'read-only:bg-surface-hover',
|
|
45
|
+
local.class
|
|
46
|
+
)}
|
|
47
|
+
{...rest}
|
|
48
|
+
/>
|
|
49
|
+
<Show when={local.suggestions}>
|
|
50
|
+
{(suggestions) => (
|
|
51
|
+
<datalist id={listId()}>
|
|
52
|
+
<For each={suggestions()}>{(value) => <option value={value} />}</For>
|
|
53
|
+
</datalist>
|
|
54
|
+
)}
|
|
55
|
+
</Show>
|
|
56
|
+
</>
|
|
40
57
|
)
|
|
41
58
|
}
|
package/src/index.ts
CHANGED
|
@@ -95,6 +95,7 @@ export * from './components/theme'
|
|
|
95
95
|
|
|
96
96
|
/* --- Composites ---------------------------------------------------------- */
|
|
97
97
|
export * from './components/composites/account-menu'
|
|
98
|
+
export * from './components/composites/action-button'
|
|
98
99
|
export * from './components/composites/list-row'
|
|
99
100
|
export * from './components/composites/settings'
|
|
100
101
|
export * from './components/composites/stat'
|