@papernote/ui 1.7.5 → 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/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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@papernote/ui",
3
- "version": "1.7.5",
3
+ "version": "1.7.7",
4
4
  "type": "module",
5
5
  "description": "A modern React component library with a paper notebook aesthetic - minimal, professional, and expressive",
6
6
  "main": "dist/index.js",
@@ -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,10 +126,13 @@ 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);
129
+ // Auto-highlight first selectable (non-header) result
130
+ setHighlightedIndex(findFirstSelectableIndex(results.slice(0, maxResults)));
100
131
  } catch (err) {
101
132
  console.error('Autocomplete search error:', err);
102
133
  setFilteredOptions([]);
103
134
  setIsOpen(false);
135
+ setHighlightedIndex(-1);
104
136
  } finally {
105
137
  setLoading(false);
106
138
  }
@@ -109,6 +141,17 @@ const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
109
141
  const filtered = filterOptions(query);
110
142
  setFilteredOptions(filtered);
111
143
  setIsOpen(filtered.length > 0);
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)));
112
155
  }
113
156
  };
114
157
 
@@ -151,7 +194,18 @@ const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
151
194
  const handleKeyDown = (e: React.KeyboardEvent) => {
152
195
  if (!isOpen) {
153
196
  if (e.key === 'ArrowDown') {
154
- handleSearch(value);
197
+ e.preventDefault();
198
+ // If we have cached results from a previous search, show them
199
+ if (filteredOptions.length > 0) {
200
+ setIsOpen(true);
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();
205
+ } else if (value.length >= minChars) {
206
+ // Otherwise trigger a new search
207
+ handleSearch(value);
208
+ }
155
209
  }
156
210
  return;
157
211
  }
@@ -159,18 +213,20 @@ const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
159
213
  switch (e.key) {
160
214
  case 'ArrowDown':
161
215
  e.preventDefault();
162
- setHighlightedIndex((prev) =>
163
- prev < filteredOptions.length - 1 ? prev + 1 : prev
164
- );
216
+ setHighlightedIndex((prev) => findNextSelectableIndex(prev, filteredOptions));
165
217
  break;
166
218
  case 'ArrowUp':
167
219
  e.preventDefault();
168
- setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : -1));
220
+ setHighlightedIndex((prev) => findPrevSelectableIndex(prev, filteredOptions));
169
221
  break;
170
222
  case 'Enter':
171
223
  e.preventDefault();
172
224
  if (highlightedIndex >= 0 && highlightedIndex < filteredOptions.length) {
173
- handleSelect(filteredOptions[highlightedIndex]);
225
+ const option = filteredOptions[highlightedIndex];
226
+ // Don't select headers
227
+ if (!option.isHeader) {
228
+ handleSelect(option);
229
+ }
174
230
  }
175
231
  break;
176
232
  case 'Escape':
@@ -232,7 +288,15 @@ const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
232
288
  value={value}
233
289
  onChange={handleInputChange}
234
290
  onKeyDown={handleKeyDown}
235
- onFocus={() => value.length >= minChars && handleSearch(value)}
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
+ }}
236
300
  placeholder={placeholder}
237
301
  disabled={disabled}
238
302
  className={`
@@ -282,27 +346,39 @@ const Autocomplete = forwardRef<AutocompleteHandle, AutocompleteProps>(({
282
346
  aria-label="Search results"
283
347
  >
284
348
  {filteredOptions.map((option, index) => (
285
- <button
286
- key={option.value}
287
- id={`autocomplete-option-${index}`}
288
- type="button"
289
- onClick={() => handleSelect(option)}
290
- onMouseEnter={() => setHighlightedIndex(index)}
291
- role="option"
292
- aria-selected={highlightedIndex === index}
293
- className={`
294
- w-full text-left px-3 py-2 transition-colors
295
- ${highlightedIndex === index
296
- ? 'bg-accent-50'
297
- : 'hover:bg-paper-50'
298
- }
299
- `}
300
- >
301
- <div className="text-sm font-medium text-ink-900">{option.label}</div>
302
- {option.description && (
303
- <div className="text-xs text-ink-600 mt-0.5">{option.description}</div>
304
- )}
305
- </button>
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
+ )
306
382
  ))}
307
383
  </div>
308
384
  )}