@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/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.6",
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,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 result for keyboard navigation
101
- setHighlightedIndex(results.length > 0 ? 0 : -1);
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 result for keyboard navigation
116
- setHighlightedIndex(filtered.length > 0 ? 0 : -1);
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(0);
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 > 0 ? prev - 1 : -1));
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
- handleSelect(filteredOptions[highlightedIndex]);
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={() => 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
+ }}
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
- <button
299
- key={option.value}
300
- id={`autocomplete-option-${index}`}
301
- type="button"
302
- onClick={() => handleSelect(option)}
303
- onMouseEnter={() => setHighlightedIndex(index)}
304
- role="option"
305
- aria-selected={highlightedIndex === index}
306
- className={`
307
- w-full text-left px-3 py-2 transition-colors
308
- ${highlightedIndex === index
309
- ? 'bg-accent-50'
310
- : 'hover:bg-paper-50'
311
- }
312
- `}
313
- >
314
- <div className="text-sm font-medium text-ink-900">{option.label}</div>
315
- {option.description && (
316
- <div className="text-xs text-ink-600 mt-0.5">{option.description}</div>
317
- )}
318
- </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
+ )
319
382
  ))}
320
383
  </div>
321
384
  )}