@papernote/ui 1.7.6 → 1.7.7
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/Autocomplete.d.ts +4 -0
- package/dist/components/Autocomplete.d.ts.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.esm.js +67 -14
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +67 -14
- package/dist/index.js.map +1 -1
- package/dist/styles.css +13 -0
- package/package.json +1 -1
- package/src/components/Autocomplete.tsx +95 -32
package/dist/styles.css
CHANGED
|
@@ -4131,6 +4131,10 @@ input:checked + .slider:before{
|
|
|
4131
4131
|
letter-spacing: -0.025em;
|
|
4132
4132
|
}
|
|
4133
4133
|
|
|
4134
|
+
.tracking-wide{
|
|
4135
|
+
letter-spacing: 0.025em;
|
|
4136
|
+
}
|
|
4137
|
+
|
|
4134
4138
|
.tracking-wider{
|
|
4135
4139
|
letter-spacing: 0.05em;
|
|
4136
4140
|
}
|
|
@@ -4928,6 +4932,15 @@ input:checked + .slider:before{
|
|
|
4928
4932
|
animation: slideInBottom 0.3s ease-out;
|
|
4929
4933
|
}
|
|
4930
4934
|
|
|
4935
|
+
.first\:rounded-t-lg:first-child{
|
|
4936
|
+
border-top-left-radius: 0.5rem;
|
|
4937
|
+
border-top-right-radius: 0.5rem;
|
|
4938
|
+
}
|
|
4939
|
+
|
|
4940
|
+
.first\:border-t-0:first-child{
|
|
4941
|
+
border-top-width: 0px;
|
|
4942
|
+
}
|
|
4943
|
+
|
|
4931
4944
|
.last\:border-b-0:last-child{
|
|
4932
4945
|
border-bottom-width: 0px;
|
|
4933
4946
|
}
|
package/package.json
CHANGED
|
@@ -12,6 +12,8 @@ export interface AutocompleteOption {
|
|
|
12
12
|
label: string;
|
|
13
13
|
description?: string;
|
|
14
14
|
metadata?: Record<string, unknown>;
|
|
15
|
+
/** If true, renders as a non-selectable section header */
|
|
16
|
+
isHeader?: boolean;
|
|
15
17
|
}
|
|
16
18
|
|
|
17
19
|
export interface AutocompleteProps {
|
|
@@ -30,6 +32,8 @@ export interface AutocompleteProps {
|
|
|
30
32
|
maxResults?: number;
|
|
31
33
|
clearable?: boolean;
|
|
32
34
|
className?: string;
|
|
35
|
+
/** Show static options dropdown on focus when input is empty. Default: true */
|
|
36
|
+
showOptionsOnFocus?: boolean;
|
|
33
37
|
}
|
|
34
38
|
|
|
35
39
|
const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
|
|
@@ -48,6 +52,7 @@ const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
|
|
|
48
52
|
maxResults = 10,
|
|
49
53
|
clearable = true,
|
|
50
54
|
className = '',
|
|
55
|
+
showOptionsOnFocus = true,
|
|
51
56
|
}, ref) => {
|
|
52
57
|
const [isOpen, setIsOpen] = useState(false);
|
|
53
58
|
const [filteredOptions, setFilteredOptions] = useState<AutocompleteOption[]>([]);
|
|
@@ -62,6 +67,30 @@ const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
|
|
|
62
67
|
const listboxId = useId();
|
|
63
68
|
const errorId = useId();
|
|
64
69
|
|
|
70
|
+
// Helper to find next selectable (non-header) index
|
|
71
|
+
const findNextSelectableIndex = (currentIndex: number, optionsList: AutocompleteOption[]): number => {
|
|
72
|
+
for (let i = currentIndex + 1; i < optionsList.length; i++) {
|
|
73
|
+
if (!optionsList[i].isHeader) return i;
|
|
74
|
+
}
|
|
75
|
+
return currentIndex; // Stay at current if no next selectable
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
// Helper to find previous selectable (non-header) index
|
|
79
|
+
const findPrevSelectableIndex = (currentIndex: number, optionsList: AutocompleteOption[]): number => {
|
|
80
|
+
for (let i = currentIndex - 1; i >= 0; i--) {
|
|
81
|
+
if (!optionsList[i].isHeader) return i;
|
|
82
|
+
}
|
|
83
|
+
return -1; // Go to -1 if no previous selectable
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
// Helper to find first selectable (non-header) index
|
|
87
|
+
const findFirstSelectableIndex = (optionsList: AutocompleteOption[]): number => {
|
|
88
|
+
for (let i = 0; i < optionsList.length; i++) {
|
|
89
|
+
if (!optionsList[i].isHeader) return i;
|
|
90
|
+
}
|
|
91
|
+
return -1;
|
|
92
|
+
};
|
|
93
|
+
|
|
65
94
|
// Expose methods via ref
|
|
66
95
|
useImperativeHandle(ref, () => ({
|
|
67
96
|
focus: () => inputRef.current?.focus(),
|
|
@@ -97,8 +126,8 @@ const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
|
|
|
97
126
|
const results = await onSearch(query);
|
|
98
127
|
setFilteredOptions(results.slice(0, maxResults));
|
|
99
128
|
setIsOpen(results.length > 0);
|
|
100
|
-
// Auto-highlight first
|
|
101
|
-
setHighlightedIndex(results.
|
|
129
|
+
// Auto-highlight first selectable (non-header) result
|
|
130
|
+
setHighlightedIndex(findFirstSelectableIndex(results.slice(0, maxResults)));
|
|
102
131
|
} catch (err) {
|
|
103
132
|
console.error('Autocomplete search error:', err);
|
|
104
133
|
setFilteredOptions([]);
|
|
@@ -112,8 +141,17 @@ const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
|
|
|
112
141
|
const filtered = filterOptions(query);
|
|
113
142
|
setFilteredOptions(filtered);
|
|
114
143
|
setIsOpen(filtered.length > 0);
|
|
115
|
-
// Auto-highlight first
|
|
116
|
-
setHighlightedIndex(filtered
|
|
144
|
+
// Auto-highlight first selectable (non-header) result
|
|
145
|
+
setHighlightedIndex(findFirstSelectableIndex(filtered));
|
|
146
|
+
}
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Show static options (for focus/arrow down when input is empty)
|
|
150
|
+
const showStaticOptions = () => {
|
|
151
|
+
if (options.length > 0) {
|
|
152
|
+
setFilteredOptions(options.slice(0, maxResults));
|
|
153
|
+
setIsOpen(true);
|
|
154
|
+
setHighlightedIndex(findFirstSelectableIndex(options.slice(0, maxResults)));
|
|
117
155
|
}
|
|
118
156
|
};
|
|
119
157
|
|
|
@@ -160,7 +198,10 @@ const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
|
|
|
160
198
|
// If we have cached results from a previous search, show them
|
|
161
199
|
if (filteredOptions.length > 0) {
|
|
162
200
|
setIsOpen(true);
|
|
163
|
-
setHighlightedIndex(
|
|
201
|
+
setHighlightedIndex(findFirstSelectableIndex(filteredOptions));
|
|
202
|
+
} else if (value.length < minChars && options.length > 0) {
|
|
203
|
+
// Show static options when input is empty/below minChars
|
|
204
|
+
showStaticOptions();
|
|
164
205
|
} else if (value.length >= minChars) {
|
|
165
206
|
// Otherwise trigger a new search
|
|
166
207
|
handleSearch(value);
|
|
@@ -172,18 +213,20 @@ const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
|
|
|
172
213
|
switch (e.key) {
|
|
173
214
|
case 'ArrowDown':
|
|
174
215
|
e.preventDefault();
|
|
175
|
-
setHighlightedIndex((prev) =>
|
|
176
|
-
prev < filteredOptions.length - 1 ? prev + 1 : prev
|
|
177
|
-
);
|
|
216
|
+
setHighlightedIndex((prev) => findNextSelectableIndex(prev, filteredOptions));
|
|
178
217
|
break;
|
|
179
218
|
case 'ArrowUp':
|
|
180
219
|
e.preventDefault();
|
|
181
|
-
setHighlightedIndex((prev) => (prev
|
|
220
|
+
setHighlightedIndex((prev) => findPrevSelectableIndex(prev, filteredOptions));
|
|
182
221
|
break;
|
|
183
222
|
case 'Enter':
|
|
184
223
|
e.preventDefault();
|
|
185
224
|
if (highlightedIndex >= 0 && highlightedIndex < filteredOptions.length) {
|
|
186
|
-
|
|
225
|
+
const option = filteredOptions[highlightedIndex];
|
|
226
|
+
// Don't select headers
|
|
227
|
+
if (!option.isHeader) {
|
|
228
|
+
handleSelect(option);
|
|
229
|
+
}
|
|
187
230
|
}
|
|
188
231
|
break;
|
|
189
232
|
case 'Escape':
|
|
@@ -245,7 +288,15 @@ const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
|
|
|
245
288
|
value={value}
|
|
246
289
|
onChange={handleInputChange}
|
|
247
290
|
onKeyDown={handleKeyDown}
|
|
248
|
-
onFocus={() =>
|
|
291
|
+
onFocus={() => {
|
|
292
|
+
if (showOptionsOnFocus && value.length < minChars && options.length > 0) {
|
|
293
|
+
// Show static options when input is empty/below minChars
|
|
294
|
+
showStaticOptions();
|
|
295
|
+
} else if (value.length >= minChars) {
|
|
296
|
+
// Trigger search if we have enough chars
|
|
297
|
+
handleSearch(value);
|
|
298
|
+
}
|
|
299
|
+
}}
|
|
249
300
|
placeholder={placeholder}
|
|
250
301
|
disabled={disabled}
|
|
251
302
|
className={`
|
|
@@ -295,27 +346,39 @@ const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
|
|
|
295
346
|
aria-label="Search results"
|
|
296
347
|
>
|
|
297
348
|
{filteredOptions.map((option, index) => (
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
349
|
+
option.isHeader ? (
|
|
350
|
+
// Render section header (non-selectable)
|
|
351
|
+
<div
|
|
352
|
+
key={`header-${option.value}`}
|
|
353
|
+
className="px-3 py-2 text-xs font-semibold text-ink-500 uppercase tracking-wide bg-paper-50 border-t border-paper-200 first:border-t-0 first:rounded-t-lg cursor-default"
|
|
354
|
+
role="presentation"
|
|
355
|
+
>
|
|
356
|
+
{option.label}
|
|
357
|
+
</div>
|
|
358
|
+
) : (
|
|
359
|
+
// Render selectable option
|
|
360
|
+
<button
|
|
361
|
+
key={option.value}
|
|
362
|
+
id={`autocomplete-option-${index}`}
|
|
363
|
+
type="button"
|
|
364
|
+
onClick={() => handleSelect(option)}
|
|
365
|
+
onMouseEnter={() => setHighlightedIndex(index)}
|
|
366
|
+
role="option"
|
|
367
|
+
aria-selected={highlightedIndex === index}
|
|
368
|
+
className={`
|
|
369
|
+
w-full text-left px-3 py-2 transition-colors
|
|
370
|
+
${highlightedIndex === index
|
|
371
|
+
? 'bg-accent-50'
|
|
372
|
+
: 'hover:bg-paper-50'
|
|
373
|
+
}
|
|
374
|
+
`}
|
|
375
|
+
>
|
|
376
|
+
<div className="text-sm font-medium text-ink-900">{option.label}</div>
|
|
377
|
+
{option.description && (
|
|
378
|
+
<div className="text-xs text-ink-600 mt-0.5">{option.description}</div>
|
|
379
|
+
)}
|
|
380
|
+
</button>
|
|
381
|
+
)
|
|
319
382
|
))}
|
|
320
383
|
</div>
|
|
321
384
|
)}
|