@a4ui/core 0.26.0 → 0.27.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 +2 -2
- package/dist/elements.css +13 -0
- package/dist/full.css +13 -0
- package/dist/index.d.ts +4 -1
- package/dist/index.js +3393 -3088
- package/dist/layout/Aurora.d.ts +6 -0
- package/dist/lib/createLinkedSelection.d.ts +30 -0
- package/dist/ui/DataView.d.ts +51 -0
- package/dist/ui/SlashMenu.d.ts +43 -0
- package/package.json +1 -1
- package/preset.js +24 -0
- package/src/index.ts +4 -1
- package/src/layout/Aurora.tsx +41 -11
- package/src/lib/createLinkedSelection.ts +60 -0
- package/src/ui/DataView.tsx +198 -0
- package/src/ui/SlashMenu.tsx +141 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
// SlashMenu — filterable "/" insert/command menu (the "type / to insert
|
|
2
|
+
// anything" pattern). Presentational and keyboard-driven; the caller wires
|
|
3
|
+
// the '/' trigger and owns `open` + `query` (usually whatever the user typed
|
|
4
|
+
// after '/').
|
|
5
|
+
import { createEffect, createMemo, createSignal, For, Show, type JSX } from 'solid-js'
|
|
6
|
+
|
|
7
|
+
import { cn } from '../lib/cn'
|
|
8
|
+
|
|
9
|
+
/** A single insertable item in a {@link SlashMenu}. */
|
|
10
|
+
export interface SlashItem {
|
|
11
|
+
/** Value passed to `onSelect` when this item is picked. */
|
|
12
|
+
value: string
|
|
13
|
+
/** Primary text shown on the row. */
|
|
14
|
+
label: string
|
|
15
|
+
/** Optional secondary text shown muted below the label. */
|
|
16
|
+
description?: string
|
|
17
|
+
/** Optional leading icon. */
|
|
18
|
+
icon?: JSX.Element
|
|
19
|
+
/** Extra terms matched against the query, in addition to label + description. */
|
|
20
|
+
keywords?: string[]
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface SlashMenuProps {
|
|
24
|
+
items: SlashItem[]
|
|
25
|
+
/** Filter text — typically whatever the user typed after '/'. */
|
|
26
|
+
query: string
|
|
27
|
+
/** Invoked with the picked item's `value` (click or Enter). */
|
|
28
|
+
onSelect: (value: string) => void
|
|
29
|
+
/** Renders nothing when `false`. @default true */
|
|
30
|
+
open?: boolean
|
|
31
|
+
class?: string
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Floating, keyboard-navigable menu for a "/"-triggered insert/command flow.
|
|
36
|
+
* Filters `items` against `query` (case-insensitive match over label +
|
|
37
|
+
* description + keywords), highlights an active row (↑/↓, wraps), and picks
|
|
38
|
+
* it on Enter or click. Purely presentational: the caller owns the '/'
|
|
39
|
+
* trigger, `open`, and `query` — this component only renders the filtered
|
|
40
|
+
* list and reports the pick.
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* ```tsx
|
|
44
|
+
* const [query, setQuery] = createSignal('')
|
|
45
|
+
* <SlashMenu
|
|
46
|
+
* items={[{ value: 'heading', label: 'Heading', icon: <Heading size={16} /> }]}
|
|
47
|
+
* query={query()}
|
|
48
|
+
* onSelect={(value) => insertBlock(value)}
|
|
49
|
+
* />
|
|
50
|
+
* ```
|
|
51
|
+
*/
|
|
52
|
+
export function SlashMenu(props: SlashMenuProps): JSX.Element {
|
|
53
|
+
const [active, setActive] = createSignal(0)
|
|
54
|
+
let rootRef: HTMLDivElement | undefined
|
|
55
|
+
|
|
56
|
+
const isOpen = () => props.open ?? true
|
|
57
|
+
|
|
58
|
+
const results = createMemo<SlashItem[]>(() => {
|
|
59
|
+
const q = props.query.trim().toLowerCase()
|
|
60
|
+
if (!q) return props.items
|
|
61
|
+
return props.items.filter((item) =>
|
|
62
|
+
`${item.label} ${item.description ?? ''} ${(item.keywords ?? []).join(' ')}`.toLowerCase().includes(q),
|
|
63
|
+
)
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// Keep the highlighted row in range whenever the filtered set changes.
|
|
67
|
+
createEffect(() => {
|
|
68
|
+
results()
|
|
69
|
+
setActive(0)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
// Focus the menu whenever it opens so arrow keys work immediately.
|
|
73
|
+
createEffect(() => {
|
|
74
|
+
if (isOpen()) rootRef?.focus()
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
const pick = (item: SlashItem) => props.onSelect(item.value)
|
|
78
|
+
|
|
79
|
+
const onKeyDown = (e: KeyboardEvent) => {
|
|
80
|
+
const list = results()
|
|
81
|
+
if (list.length === 0) return
|
|
82
|
+
if (e.key === 'ArrowDown') {
|
|
83
|
+
e.preventDefault()
|
|
84
|
+
setActive((i) => (i + 1) % list.length)
|
|
85
|
+
} else if (e.key === 'ArrowUp') {
|
|
86
|
+
e.preventDefault()
|
|
87
|
+
setActive((i) => (i - 1 + list.length) % list.length)
|
|
88
|
+
} else if (e.key === 'Enter') {
|
|
89
|
+
e.preventDefault()
|
|
90
|
+
const item = list[active()]
|
|
91
|
+
if (item) pick(item)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return (
|
|
96
|
+
<Show when={isOpen()}>
|
|
97
|
+
<div
|
|
98
|
+
ref={rootRef}
|
|
99
|
+
role="listbox"
|
|
100
|
+
tabindex={0}
|
|
101
|
+
onKeyDown={onKeyDown}
|
|
102
|
+
class={cn(
|
|
103
|
+
'card max-h-72 overflow-y-auto rounded-lg border border-border bg-card p-1 shadow-lg outline-none',
|
|
104
|
+
props.class,
|
|
105
|
+
)}
|
|
106
|
+
>
|
|
107
|
+
<Show
|
|
108
|
+
when={results().length}
|
|
109
|
+
fallback={<div class="px-3 py-6 text-center text-sm text-muted-foreground">No matches</div>}
|
|
110
|
+
>
|
|
111
|
+
<For each={results()}>
|
|
112
|
+
{(item, i) => (
|
|
113
|
+
<div
|
|
114
|
+
role="option"
|
|
115
|
+
aria-selected={active() === i()}
|
|
116
|
+
onMouseEnter={() => setActive(i())}
|
|
117
|
+
onClick={() => pick(item)}
|
|
118
|
+
class={cn(
|
|
119
|
+
'flex cursor-pointer items-center gap-2 rounded-md px-3 py-2 text-left transition-colors',
|
|
120
|
+
active() === i() ? 'bg-muted' : undefined,
|
|
121
|
+
)}
|
|
122
|
+
>
|
|
123
|
+
<Show when={item.icon}>
|
|
124
|
+
<span class="flex h-4 w-4 shrink-0 items-center justify-center text-muted-foreground">
|
|
125
|
+
{item.icon}
|
|
126
|
+
</span>
|
|
127
|
+
</Show>
|
|
128
|
+
<div class="min-w-0 flex-1">
|
|
129
|
+
<div class="truncate text-sm text-foreground">{item.label}</div>
|
|
130
|
+
<Show when={item.description}>
|
|
131
|
+
<div class="truncate text-xs text-muted-foreground">{item.description}</div>
|
|
132
|
+
</Show>
|
|
133
|
+
</div>
|
|
134
|
+
</div>
|
|
135
|
+
)}
|
|
136
|
+
</For>
|
|
137
|
+
</Show>
|
|
138
|
+
</div>
|
|
139
|
+
</Show>
|
|
140
|
+
)
|
|
141
|
+
}
|