@seed-ship/mcp-ui-solid 2.0.1 → 2.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/dist/components/AutocompleteDropdown.cjs +201 -0
- package/dist/components/AutocompleteDropdown.cjs.map +1 -0
- package/dist/components/AutocompleteDropdown.d.ts +71 -0
- package/dist/components/AutocompleteDropdown.d.ts.map +1 -0
- package/dist/components/AutocompleteDropdown.js +201 -0
- package/dist/components/AutocompleteDropdown.js.map +1 -0
- package/dist/components/AutocompleteFormField.cjs +289 -0
- package/dist/components/AutocompleteFormField.cjs.map +1 -0
- package/dist/components/AutocompleteFormField.d.ts +52 -0
- package/dist/components/AutocompleteFormField.d.ts.map +1 -0
- package/dist/components/AutocompleteFormField.js +289 -0
- package/dist/components/AutocompleteFormField.js.map +1 -0
- package/dist/components/DraggableGridItem.cjs +133 -0
- package/dist/components/DraggableGridItem.cjs.map +1 -0
- package/dist/components/DraggableGridItem.d.ts +95 -0
- package/dist/components/DraggableGridItem.d.ts.map +1 -0
- package/dist/components/DraggableGridItem.js +133 -0
- package/dist/components/DraggableGridItem.js.map +1 -0
- package/dist/components/EditableUIResourceRenderer.cjs +203 -0
- package/dist/components/EditableUIResourceRenderer.cjs.map +1 -0
- package/dist/components/EditableUIResourceRenderer.d.ts +43 -0
- package/dist/components/EditableUIResourceRenderer.d.ts.map +1 -0
- package/dist/components/EditableUIResourceRenderer.js +203 -0
- package/dist/components/EditableUIResourceRenderer.js.map +1 -0
- package/dist/components/GhostText.cjs +105 -0
- package/dist/components/GhostText.cjs.map +1 -0
- package/dist/components/GhostText.d.ts +113 -0
- package/dist/components/GhostText.d.ts.map +1 -0
- package/dist/components/GhostText.js +105 -0
- package/dist/components/GhostText.js.map +1 -0
- package/dist/components/ResizeHandle.cjs +173 -0
- package/dist/components/ResizeHandle.cjs.map +1 -0
- package/dist/components/ResizeHandle.d.ts +50 -0
- package/dist/components/ResizeHandle.d.ts.map +1 -0
- package/dist/components/ResizeHandle.js +173 -0
- package/dist/components/ResizeHandle.js.map +1 -0
- package/dist/context/AutocompleteContext.cjs +158 -0
- package/dist/context/AutocompleteContext.cjs.map +1 -0
- package/dist/context/AutocompleteContext.d.ts +77 -0
- package/dist/context/AutocompleteContext.d.ts.map +1 -0
- package/dist/context/AutocompleteContext.js +158 -0
- package/dist/context/AutocompleteContext.js.map +1 -0
- package/dist/hooks/index.d.ts +6 -0
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/useAutocomplete.cjs +234 -0
- package/dist/hooks/useAutocomplete.cjs.map +1 -0
- package/dist/hooks/useAutocomplete.d.ts +119 -0
- package/dist/hooks/useAutocomplete.d.ts.map +1 -0
- package/dist/hooks/useAutocomplete.js +234 -0
- package/dist/hooks/useAutocomplete.js.map +1 -0
- package/dist/hooks/useDragDrop.cjs +170 -0
- package/dist/hooks/useDragDrop.cjs.map +1 -0
- package/dist/hooks/useDragDrop.d.ts +100 -0
- package/dist/hooks/useDragDrop.d.ts.map +1 -0
- package/dist/hooks/useDragDrop.js +170 -0
- package/dist/hooks/useDragDrop.js.map +1 -0
- package/dist/hooks/useResize.cjs +209 -0
- package/dist/hooks/useResize.cjs.map +1 -0
- package/dist/hooks/useResize.d.ts +87 -0
- package/dist/hooks/useResize.d.ts.map +1 -0
- package/dist/hooks/useResize.js +209 -0
- package/dist/hooks/useResize.js.map +1 -0
- package/dist/hooks.cjs +6 -0
- package/dist/hooks.cjs.map +1 -1
- package/dist/hooks.d.cts +6 -0
- package/dist/hooks.d.ts +6 -0
- package/dist/hooks.js +6 -0
- package/dist/hooks.js.map +1 -1
- package/dist/index.cjs +29 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -3
- package/dist/index.d.ts +18 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +29 -0
- package/dist/index.js.map +1 -1
- package/dist/plugins/duckdb.cjs +192 -0
- package/dist/plugins/duckdb.cjs.map +1 -0
- package/dist/plugins/duckdb.d.ts +20 -0
- package/dist/plugins/duckdb.d.ts.map +1 -0
- package/dist/plugins/duckdb.js +170 -0
- package/dist/plugins/duckdb.js.map +1 -0
- package/dist/plugins/groq.cjs +97 -0
- package/dist/plugins/groq.cjs.map +1 -0
- package/dist/plugins/groq.d.ts +13 -0
- package/dist/plugins/groq.d.ts.map +1 -0
- package/dist/plugins/groq.js +97 -0
- package/dist/plugins/groq.js.map +1 -0
- package/dist/plugins/index.d.ts +10 -0
- package/dist/plugins/index.d.ts.map +1 -0
- package/dist/plugins/rest.cjs +92 -0
- package/dist/plugins/rest.cjs.map +1 -0
- package/dist/plugins/rest.d.ts +13 -0
- package/dist/plugins/rest.d.ts.map +1 -0
- package/dist/plugins/rest.js +92 -0
- package/dist/plugins/rest.js.map +1 -0
- package/dist/plugins/supabase.cjs +79 -0
- package/dist/plugins/supabase.cjs.map +1 -0
- package/dist/plugins/supabase.d.ts +13 -0
- package/dist/plugins/supabase.d.ts.map +1 -0
- package/dist/plugins/supabase.js +79 -0
- package/dist/plugins/supabase.js.map +1 -0
- package/dist/types/index.d.ts +430 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types.d.cts +430 -0
- package/dist/types.d.ts +430 -0
- package/package.json +16 -1
- package/src/components/AutocompleteDropdown.tsx +329 -0
- package/src/components/AutocompleteFormField.tsx +288 -0
- package/src/components/DraggableGridItem.tsx +274 -0
- package/src/components/EditableUIResourceRenderer.tsx +273 -0
- package/src/components/GhostText.tsx +262 -0
- package/src/components/ResizeHandle.tsx +262 -0
- package/src/context/AutocompleteContext.tsx +317 -0
- package/src/hooks/index.ts +23 -0
- package/src/hooks/useAutocomplete.test.ts +334 -0
- package/src/hooks/useAutocomplete.ts +466 -0
- package/src/hooks/useDragDrop.test.ts +355 -0
- package/src/hooks/useDragDrop.ts +379 -0
- package/src/hooks/useResize.test.ts +313 -0
- package/src/hooks/useResize.ts +372 -0
- package/src/index.ts +71 -0
- package/src/plugins/duckdb.ts +269 -0
- package/src/plugins/groq.ts +137 -0
- package/src/plugins/index.ts +14 -0
- package/src/plugins/rest.ts +147 -0
- package/src/plugins/supabase.ts +120 -0
- package/src/styles/autocomplete.css +356 -0
- package/src/styles/drag-drop.css +297 -0
- package/src/styles/index.css +7 -0
- package/src/types/index.ts +529 -0
- package/src/vite-env.d.ts +18 -0
- package/tsconfig.tsbuildinfo +1 -1
- package/vite.config.ts +2 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AutocompleteContext - Context provider for autocomplete functionality
|
|
3
|
+
* Manages plugins and provides unified autocomplete API
|
|
4
|
+
*
|
|
5
|
+
* Sprint Autocomplete Feature
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
createContext,
|
|
10
|
+
useContext,
|
|
11
|
+
ParentComponent,
|
|
12
|
+
Accessor,
|
|
13
|
+
createSignal,
|
|
14
|
+
createMemo,
|
|
15
|
+
onCleanup
|
|
16
|
+
} from 'solid-js'
|
|
17
|
+
import type {
|
|
18
|
+
AutocompletePlugin,
|
|
19
|
+
AutocompleteResult,
|
|
20
|
+
AutocompleteContext as AutocompleteContextData,
|
|
21
|
+
AutocompleteProviderConfig
|
|
22
|
+
} from '../types'
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Cache entry for autocomplete results
|
|
26
|
+
*/
|
|
27
|
+
interface CacheEntry {
|
|
28
|
+
result: AutocompleteResult
|
|
29
|
+
timestamp: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Context value interface
|
|
34
|
+
*/
|
|
35
|
+
export interface AutocompleteContextValue {
|
|
36
|
+
/**
|
|
37
|
+
* Get suggestions from a plugin
|
|
38
|
+
*/
|
|
39
|
+
getSuggestions: (
|
|
40
|
+
input: string,
|
|
41
|
+
pluginId?: string,
|
|
42
|
+
context?: AutocompleteContextData
|
|
43
|
+
) => Promise<AutocompleteResult>
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get registered plugins
|
|
47
|
+
*/
|
|
48
|
+
plugins: Accessor<AutocompletePlugin[]>
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Get default plugin ID
|
|
52
|
+
*/
|
|
53
|
+
defaultPluginId: Accessor<string | undefined>
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Check if a plugin is ready
|
|
57
|
+
*/
|
|
58
|
+
isPluginReady: (pluginId: string) => boolean
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get plugin by ID
|
|
62
|
+
*/
|
|
63
|
+
getPlugin: (pluginId: string) => AutocompletePlugin | undefined
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register a new plugin
|
|
67
|
+
*/
|
|
68
|
+
registerPlugin: (plugin: AutocompletePlugin) => void
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Unregister a plugin
|
|
72
|
+
*/
|
|
73
|
+
unregisterPlugin: (pluginId: string) => void
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Clear cache
|
|
77
|
+
*/
|
|
78
|
+
clearCache: () => void
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Global config
|
|
82
|
+
*/
|
|
83
|
+
config: {
|
|
84
|
+
debounceMs: number
|
|
85
|
+
minChars: number
|
|
86
|
+
cacheTtl: number
|
|
87
|
+
cacheEnabled: boolean
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Create context with undefined default
|
|
92
|
+
const AutocompleteCtx = createContext<AutocompleteContextValue>()
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Props for AutocompleteProvider
|
|
96
|
+
*/
|
|
97
|
+
export interface AutocompleteProviderProps extends AutocompleteProviderConfig {
|
|
98
|
+
children: any
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate cache key
|
|
103
|
+
*/
|
|
104
|
+
function getCacheKey(input: string, pluginId: string, context?: AutocompleteContextData): string {
|
|
105
|
+
const contextKey = context ? JSON.stringify(context) : ''
|
|
106
|
+
return `${pluginId}:${input}:${contextKey}`
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* AutocompleteProvider Component
|
|
111
|
+
* Provides autocomplete context to child components
|
|
112
|
+
*/
|
|
113
|
+
export const AutocompleteProvider: ParentComponent<AutocompleteProviderProps> = (props) => {
|
|
114
|
+
// Plugin registry
|
|
115
|
+
const [plugins, setPlugins] = createSignal<AutocompletePlugin[]>(props.plugins || [])
|
|
116
|
+
|
|
117
|
+
// Result cache
|
|
118
|
+
const [cache, setCache] = createSignal<Map<string, CacheEntry>>(new Map())
|
|
119
|
+
|
|
120
|
+
// Config with defaults
|
|
121
|
+
const config = createMemo(() => ({
|
|
122
|
+
debounceMs: props.debounceMs ?? 150,
|
|
123
|
+
minChars: props.minChars ?? 1,
|
|
124
|
+
cacheTtl: props.cacheTtl ?? 60000,
|
|
125
|
+
cacheEnabled: props.cacheEnabled ?? true
|
|
126
|
+
}))
|
|
127
|
+
|
|
128
|
+
// Default plugin ID
|
|
129
|
+
const defaultPluginId = createMemo(() => {
|
|
130
|
+
if (props.defaultPlugin) return props.defaultPlugin
|
|
131
|
+
const firstPlugin = plugins()[0]
|
|
132
|
+
return firstPlugin?.id
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get plugin by ID
|
|
137
|
+
*/
|
|
138
|
+
const getPlugin = (pluginId: string): AutocompletePlugin | undefined => {
|
|
139
|
+
return plugins().find(p => p.id === pluginId)
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Check if plugin is ready
|
|
144
|
+
*/
|
|
145
|
+
const isPluginReady = (pluginId: string): boolean => {
|
|
146
|
+
const plugin = getPlugin(pluginId)
|
|
147
|
+
if (!plugin) return false
|
|
148
|
+
if (plugin.isReady) return plugin.isReady()
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Check cache for result
|
|
154
|
+
*/
|
|
155
|
+
const getFromCache = (key: string): AutocompleteResult | null => {
|
|
156
|
+
if (!config().cacheEnabled) return null
|
|
157
|
+
|
|
158
|
+
const entry = cache().get(key)
|
|
159
|
+
if (!entry) return null
|
|
160
|
+
|
|
161
|
+
const age = Date.now() - entry.timestamp
|
|
162
|
+
if (age > config().cacheTtl) {
|
|
163
|
+
// Expired, remove from cache
|
|
164
|
+
setCache(prev => {
|
|
165
|
+
const next = new Map(prev)
|
|
166
|
+
next.delete(key)
|
|
167
|
+
return next
|
|
168
|
+
})
|
|
169
|
+
return null
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return { ...entry.result, cached: true }
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Add result to cache
|
|
177
|
+
*/
|
|
178
|
+
const addToCache = (key: string, result: AutocompleteResult): void => {
|
|
179
|
+
if (!config().cacheEnabled) return
|
|
180
|
+
|
|
181
|
+
setCache(prev => {
|
|
182
|
+
const next = new Map(prev)
|
|
183
|
+
next.set(key, { result, timestamp: Date.now() })
|
|
184
|
+
return next
|
|
185
|
+
})
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Get suggestions from plugin
|
|
190
|
+
*/
|
|
191
|
+
const getSuggestions = async (
|
|
192
|
+
input: string,
|
|
193
|
+
pluginId?: string,
|
|
194
|
+
context?: AutocompleteContextData
|
|
195
|
+
): Promise<AutocompleteResult> => {
|
|
196
|
+
const targetPluginId = pluginId || defaultPluginId()
|
|
197
|
+
|
|
198
|
+
if (!targetPluginId) {
|
|
199
|
+
return { type: 'options', options: [], pluginId: undefined }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const plugin = getPlugin(targetPluginId)
|
|
203
|
+
if (!plugin) {
|
|
204
|
+
console.warn(`[Autocomplete] Plugin not found: ${targetPluginId}`)
|
|
205
|
+
return { type: 'options', options: [], pluginId: targetPluginId }
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check cache
|
|
209
|
+
const cacheKey = getCacheKey(input, targetPluginId, context)
|
|
210
|
+
const cached = getFromCache(cacheKey)
|
|
211
|
+
if (cached) {
|
|
212
|
+
return cached
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const result = await plugin.getSuggestions(input, context)
|
|
217
|
+
result.pluginId = targetPluginId
|
|
218
|
+
|
|
219
|
+
// Cache result
|
|
220
|
+
addToCache(cacheKey, result)
|
|
221
|
+
|
|
222
|
+
return result
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(`[Autocomplete] Plugin error (${targetPluginId}):`, error)
|
|
225
|
+
return {
|
|
226
|
+
type: 'options',
|
|
227
|
+
options: [],
|
|
228
|
+
pluginId: targetPluginId
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/**
|
|
234
|
+
* Register a new plugin
|
|
235
|
+
*/
|
|
236
|
+
const registerPlugin = (plugin: AutocompletePlugin): void => {
|
|
237
|
+
setPlugins(prev => {
|
|
238
|
+
// Remove existing plugin with same ID
|
|
239
|
+
const filtered = prev.filter(p => p.id !== plugin.id)
|
|
240
|
+
return [...filtered, plugin]
|
|
241
|
+
})
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/**
|
|
245
|
+
* Unregister a plugin
|
|
246
|
+
*/
|
|
247
|
+
const unregisterPlugin = (pluginId: string): void => {
|
|
248
|
+
const plugin = getPlugin(pluginId)
|
|
249
|
+
if (plugin?.dispose) {
|
|
250
|
+
plugin.dispose()
|
|
251
|
+
}
|
|
252
|
+
setPlugins(prev => prev.filter(p => p.id !== pluginId))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Clear cache
|
|
257
|
+
*/
|
|
258
|
+
const clearCache = (): void => {
|
|
259
|
+
setCache(new Map())
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Cleanup on unmount
|
|
263
|
+
onCleanup(() => {
|
|
264
|
+
// Dispose all plugins
|
|
265
|
+
plugins().forEach(plugin => {
|
|
266
|
+
if (plugin.dispose) {
|
|
267
|
+
try {
|
|
268
|
+
plugin.dispose()
|
|
269
|
+
} catch (e) {
|
|
270
|
+
console.error(`[Autocomplete] Error disposing plugin ${plugin.id}:`, e)
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
})
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
// Context value
|
|
277
|
+
const contextValue: AutocompleteContextValue = {
|
|
278
|
+
getSuggestions,
|
|
279
|
+
plugins,
|
|
280
|
+
defaultPluginId,
|
|
281
|
+
isPluginReady,
|
|
282
|
+
getPlugin,
|
|
283
|
+
registerPlugin,
|
|
284
|
+
unregisterPlugin,
|
|
285
|
+
clearCache,
|
|
286
|
+
config: config()
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return (
|
|
290
|
+
<AutocompleteCtx.Provider value={contextValue}>
|
|
291
|
+
{props.children}
|
|
292
|
+
</AutocompleteCtx.Provider>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Hook to use autocomplete context
|
|
298
|
+
* @throws Error if used outside provider
|
|
299
|
+
*/
|
|
300
|
+
export function useAutocompleteContext(): AutocompleteContextValue {
|
|
301
|
+
const context = useContext(AutocompleteCtx)
|
|
302
|
+
if (!context) {
|
|
303
|
+
throw new Error(
|
|
304
|
+
'useAutocompleteContext must be used within an AutocompleteProvider'
|
|
305
|
+
)
|
|
306
|
+
}
|
|
307
|
+
return context
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Safe hook that returns undefined if outside provider
|
|
312
|
+
*/
|
|
313
|
+
export function useAutocompleteContextSafe(): AutocompleteContextValue | undefined {
|
|
314
|
+
return useContext(AutocompleteCtx)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export { AutocompleteCtx }
|
package/src/hooks/index.ts
CHANGED
|
@@ -34,3 +34,26 @@ export type { UseModalReturn, UseConfirmModalReturn } from './useModal'
|
|
|
34
34
|
// Form persistence hooks (Sprint 4)
|
|
35
35
|
export { useFormPersistence } from './useFormPersistence'
|
|
36
36
|
export type { UseFormPersistenceOptions, UseFormPersistenceReturn } from './useFormPersistence'
|
|
37
|
+
|
|
38
|
+
// Drag-Drop hooks (Sprint Drag-Drop)
|
|
39
|
+
export { useDragDrop } from './useDragDrop'
|
|
40
|
+
export type {
|
|
41
|
+
UseDragDropOptions,
|
|
42
|
+
UseDragDropReturn,
|
|
43
|
+
DragProps,
|
|
44
|
+
} from './useDragDrop'
|
|
45
|
+
|
|
46
|
+
export { useResize } from './useResize'
|
|
47
|
+
export type {
|
|
48
|
+
UseResizeOptions,
|
|
49
|
+
UseResizeReturn,
|
|
50
|
+
ResizeEdge,
|
|
51
|
+
ResizeHandleProps,
|
|
52
|
+
} from './useResize'
|
|
53
|
+
|
|
54
|
+
// Autocomplete hooks (Sprint Autocomplete)
|
|
55
|
+
export { useAutocomplete } from './useAutocomplete'
|
|
56
|
+
export type {
|
|
57
|
+
UseAutocompleteOptions,
|
|
58
|
+
UseAutocompleteReturn,
|
|
59
|
+
} from './useAutocomplete'
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useAutocomplete Tests
|
|
3
|
+
* Sprint Autocomplete Feature
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
|
|
7
|
+
import { createSignal, createRoot } from 'solid-js'
|
|
8
|
+
import { useAutocomplete } from './useAutocomplete'
|
|
9
|
+
// Note: Full integration tests require AutocompleteProvider
|
|
10
|
+
// These tests focus on the hook's internal logic
|
|
11
|
+
|
|
12
|
+
describe('useAutocomplete', () => {
|
|
13
|
+
beforeEach(() => {
|
|
14
|
+
vi.useFakeTimers()
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
vi.useRealTimers()
|
|
19
|
+
vi.clearAllMocks()
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
describe('initialization', () => {
|
|
23
|
+
it('initializes with correct default state', () => {
|
|
24
|
+
createRoot((dispose) => {
|
|
25
|
+
const [inputValue] = createSignal('')
|
|
26
|
+
|
|
27
|
+
const autocomplete = useAutocomplete({
|
|
28
|
+
inputValue,
|
|
29
|
+
enabled: true
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
expect(autocomplete.completion()).toBeNull()
|
|
33
|
+
expect(autocomplete.ghostText()).toBe('')
|
|
34
|
+
expect(autocomplete.options()).toEqual([])
|
|
35
|
+
expect(autocomplete.selectedIndex()).toBe(-1)
|
|
36
|
+
expect(autocomplete.isLoading()).toBe(false)
|
|
37
|
+
expect(autocomplete.error()).toBeNull()
|
|
38
|
+
expect(autocomplete.isOpen()).toBe(false)
|
|
39
|
+
expect(autocomplete.resultType()).toBeNull()
|
|
40
|
+
|
|
41
|
+
dispose()
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('does not fetch when disabled', () => {
|
|
46
|
+
createRoot((dispose) => {
|
|
47
|
+
const [inputValue, setInputValue] = createSignal('')
|
|
48
|
+
|
|
49
|
+
const autocomplete = useAutocomplete({
|
|
50
|
+
inputValue,
|
|
51
|
+
enabled: false
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
setInputValue('test')
|
|
55
|
+
vi.advanceTimersByTime(200)
|
|
56
|
+
|
|
57
|
+
expect(autocomplete.isLoading()).toBe(false)
|
|
58
|
+
expect(autocomplete.isOpen()).toBe(false)
|
|
59
|
+
|
|
60
|
+
dispose()
|
|
61
|
+
})
|
|
62
|
+
})
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
describe('ghost text computation', () => {
|
|
66
|
+
it('computes ghost text correctly', () => {
|
|
67
|
+
createRoot((dispose) => {
|
|
68
|
+
const [inputValue] = createSignal('hel')
|
|
69
|
+
const onInputChange = vi.fn()
|
|
70
|
+
|
|
71
|
+
const autocomplete = useAutocomplete({
|
|
72
|
+
inputValue,
|
|
73
|
+
enabled: true,
|
|
74
|
+
onInputChange
|
|
75
|
+
})
|
|
76
|
+
|
|
77
|
+
// Manually set completion for testing
|
|
78
|
+
// In real usage, this would come from the context
|
|
79
|
+
expect(autocomplete.ghostText()).toBe('')
|
|
80
|
+
|
|
81
|
+
dispose()
|
|
82
|
+
})
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('option navigation', () => {
|
|
87
|
+
it('navigates options with nextOption', () => {
|
|
88
|
+
createRoot((dispose) => {
|
|
89
|
+
const [inputValue] = createSignal('test')
|
|
90
|
+
|
|
91
|
+
const autocomplete = useAutocomplete({
|
|
92
|
+
inputValue,
|
|
93
|
+
enabled: true
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Initially no selection
|
|
97
|
+
expect(autocomplete.selectedIndex()).toBe(-1)
|
|
98
|
+
|
|
99
|
+
dispose()
|
|
100
|
+
})
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
it('navigates options with prevOption', () => {
|
|
104
|
+
createRoot((dispose) => {
|
|
105
|
+
const [inputValue] = createSignal('test')
|
|
106
|
+
|
|
107
|
+
const autocomplete = useAutocomplete({
|
|
108
|
+
inputValue,
|
|
109
|
+
enabled: true
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
// Initially no selection
|
|
113
|
+
expect(autocomplete.selectedIndex()).toBe(-1)
|
|
114
|
+
|
|
115
|
+
dispose()
|
|
116
|
+
})
|
|
117
|
+
})
|
|
118
|
+
})
|
|
119
|
+
|
|
120
|
+
describe('keyboard handling', () => {
|
|
121
|
+
it('handles Tab key for completion', () => {
|
|
122
|
+
createRoot((dispose) => {
|
|
123
|
+
const [inputValue] = createSignal('test')
|
|
124
|
+
const onInputChange = vi.fn()
|
|
125
|
+
|
|
126
|
+
const autocomplete = useAutocomplete({
|
|
127
|
+
inputValue,
|
|
128
|
+
enabled: true,
|
|
129
|
+
onInputChange
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
const event = {
|
|
133
|
+
key: 'Tab',
|
|
134
|
+
preventDefault: vi.fn()
|
|
135
|
+
} as unknown as KeyboardEvent
|
|
136
|
+
|
|
137
|
+
// Should return false when not open
|
|
138
|
+
const handled = autocomplete.handleKeyDown(event)
|
|
139
|
+
expect(handled).toBe(false)
|
|
140
|
+
|
|
141
|
+
dispose()
|
|
142
|
+
})
|
|
143
|
+
})
|
|
144
|
+
|
|
145
|
+
it('handles Escape key to dismiss', () => {
|
|
146
|
+
createRoot((dispose) => {
|
|
147
|
+
const [inputValue] = createSignal('test')
|
|
148
|
+
|
|
149
|
+
const autocomplete = useAutocomplete({
|
|
150
|
+
inputValue,
|
|
151
|
+
enabled: true
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const event = {
|
|
155
|
+
key: 'Escape',
|
|
156
|
+
preventDefault: vi.fn()
|
|
157
|
+
} as unknown as KeyboardEvent
|
|
158
|
+
|
|
159
|
+
// Should return false when not open
|
|
160
|
+
const handled = autocomplete.handleKeyDown(event)
|
|
161
|
+
expect(handled).toBe(false)
|
|
162
|
+
|
|
163
|
+
dispose()
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('handles ArrowDown for dropdown navigation', () => {
|
|
168
|
+
createRoot((dispose) => {
|
|
169
|
+
const [inputValue] = createSignal('test')
|
|
170
|
+
|
|
171
|
+
const autocomplete = useAutocomplete({
|
|
172
|
+
inputValue,
|
|
173
|
+
enabled: true
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const event = {
|
|
177
|
+
key: 'ArrowDown',
|
|
178
|
+
preventDefault: vi.fn()
|
|
179
|
+
} as unknown as KeyboardEvent
|
|
180
|
+
|
|
181
|
+
// Should return false when not open
|
|
182
|
+
const handled = autocomplete.handleKeyDown(event)
|
|
183
|
+
expect(handled).toBe(false)
|
|
184
|
+
|
|
185
|
+
dispose()
|
|
186
|
+
})
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
it('handles ArrowUp for dropdown navigation', () => {
|
|
190
|
+
createRoot((dispose) => {
|
|
191
|
+
const [inputValue] = createSignal('test')
|
|
192
|
+
|
|
193
|
+
const autocomplete = useAutocomplete({
|
|
194
|
+
inputValue,
|
|
195
|
+
enabled: true
|
|
196
|
+
})
|
|
197
|
+
|
|
198
|
+
const event = {
|
|
199
|
+
key: 'ArrowUp',
|
|
200
|
+
preventDefault: vi.fn()
|
|
201
|
+
} as unknown as KeyboardEvent
|
|
202
|
+
|
|
203
|
+
// Should return false when not open
|
|
204
|
+
const handled = autocomplete.handleKeyDown(event)
|
|
205
|
+
expect(handled).toBe(false)
|
|
206
|
+
|
|
207
|
+
dispose()
|
|
208
|
+
})
|
|
209
|
+
})
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
describe('dismiss', () => {
|
|
213
|
+
it('clears all state on dismiss', () => {
|
|
214
|
+
createRoot((dispose) => {
|
|
215
|
+
const [inputValue] = createSignal('test')
|
|
216
|
+
|
|
217
|
+
const autocomplete = useAutocomplete({
|
|
218
|
+
inputValue,
|
|
219
|
+
enabled: true
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
autocomplete.dismiss()
|
|
223
|
+
|
|
224
|
+
expect(autocomplete.completion()).toBeNull()
|
|
225
|
+
expect(autocomplete.options()).toEqual([])
|
|
226
|
+
expect(autocomplete.selectedIndex()).toBe(-1)
|
|
227
|
+
expect(autocomplete.isOpen()).toBe(false)
|
|
228
|
+
expect(autocomplete.error()).toBeNull()
|
|
229
|
+
|
|
230
|
+
dispose()
|
|
231
|
+
})
|
|
232
|
+
})
|
|
233
|
+
})
|
|
234
|
+
|
|
235
|
+
describe('min chars', () => {
|
|
236
|
+
it('respects minChars option', () => {
|
|
237
|
+
createRoot((dispose) => {
|
|
238
|
+
const [inputValue, setInputValue] = createSignal('a')
|
|
239
|
+
|
|
240
|
+
const autocomplete = useAutocomplete({
|
|
241
|
+
inputValue,
|
|
242
|
+
enabled: true,
|
|
243
|
+
minChars: 3
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
// Should not trigger with only 1 char
|
|
247
|
+
setInputValue('ab')
|
|
248
|
+
vi.advanceTimersByTime(200)
|
|
249
|
+
|
|
250
|
+
expect(autocomplete.isLoading()).toBe(false)
|
|
251
|
+
expect(autocomplete.isOpen()).toBe(false)
|
|
252
|
+
|
|
253
|
+
dispose()
|
|
254
|
+
})
|
|
255
|
+
})
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
describe('debounce', () => {
|
|
259
|
+
it('debounces input changes', () => {
|
|
260
|
+
createRoot((dispose) => {
|
|
261
|
+
const [inputValue, setInputValue] = createSignal('')
|
|
262
|
+
|
|
263
|
+
const autocomplete = useAutocomplete({
|
|
264
|
+
inputValue,
|
|
265
|
+
enabled: true,
|
|
266
|
+
debounceMs: 300
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
// Rapid changes should be debounced
|
|
270
|
+
setInputValue('a')
|
|
271
|
+
vi.advanceTimersByTime(100)
|
|
272
|
+
setInputValue('ab')
|
|
273
|
+
vi.advanceTimersByTime(100)
|
|
274
|
+
setInputValue('abc')
|
|
275
|
+
vi.advanceTimersByTime(100)
|
|
276
|
+
|
|
277
|
+
// Not enough time has passed
|
|
278
|
+
expect(autocomplete.isLoading()).toBe(false)
|
|
279
|
+
|
|
280
|
+
// After full debounce time
|
|
281
|
+
vi.advanceTimersByTime(300)
|
|
282
|
+
|
|
283
|
+
// Would be loading if context was available
|
|
284
|
+
expect(autocomplete.isOpen()).toBe(false) // No context
|
|
285
|
+
|
|
286
|
+
dispose()
|
|
287
|
+
})
|
|
288
|
+
})
|
|
289
|
+
})
|
|
290
|
+
|
|
291
|
+
describe('option selection', () => {
|
|
292
|
+
it('calls onInputChange when selecting option', () => {
|
|
293
|
+
createRoot((dispose) => {
|
|
294
|
+
const [inputValue] = createSignal('test')
|
|
295
|
+
const onInputChange = vi.fn()
|
|
296
|
+
|
|
297
|
+
const autocomplete = useAutocomplete({
|
|
298
|
+
inputValue,
|
|
299
|
+
enabled: true,
|
|
300
|
+
onInputChange
|
|
301
|
+
})
|
|
302
|
+
|
|
303
|
+
autocomplete.selectOption({ value: 'selected', label: 'Selected Option' })
|
|
304
|
+
|
|
305
|
+
expect(onInputChange).toHaveBeenCalledWith('selected')
|
|
306
|
+
|
|
307
|
+
dispose()
|
|
308
|
+
})
|
|
309
|
+
})
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
describe('accept completion', () => {
|
|
313
|
+
it('calls onInputChange when accepting completion', () => {
|
|
314
|
+
createRoot((dispose) => {
|
|
315
|
+
const [inputValue] = createSignal('test')
|
|
316
|
+
const onInputChange = vi.fn()
|
|
317
|
+
|
|
318
|
+
const autocomplete = useAutocomplete({
|
|
319
|
+
inputValue,
|
|
320
|
+
enabled: true,
|
|
321
|
+
onInputChange
|
|
322
|
+
})
|
|
323
|
+
|
|
324
|
+
// Without actual completion, this should not call onChange
|
|
325
|
+
autocomplete.acceptCompletion()
|
|
326
|
+
|
|
327
|
+
// Since completion is null, onInputChange should not be called
|
|
328
|
+
expect(onInputChange).not.toHaveBeenCalled()
|
|
329
|
+
|
|
330
|
+
dispose()
|
|
331
|
+
})
|
|
332
|
+
})
|
|
333
|
+
})
|
|
334
|
+
})
|