@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.
@@ -8,6 +8,8 @@ export interface AutocompleteOption {
8
8
  label: string;
9
9
  description?: string;
10
10
  metadata?: Record<string, unknown>;
11
+ /** If true, renders as a non-selectable section header */
12
+ isHeader?: boolean;
11
13
  }
12
14
  export interface AutocompleteProps {
13
15
  value: string;
@@ -25,6 +27,8 @@ export interface AutocompleteProps {
25
27
  maxResults?: number;
26
28
  clearable?: boolean;
27
29
  className?: string;
30
+ /** Show static options dropdown on focus when input is empty. Default: true */
31
+ showOptionsOnFocus?: boolean;
28
32
  }
29
33
  declare const Autocomplete: React.ForwardRefExoticComponent<AutocompleteProps & React.RefAttributes<AutocompleteHandle>>;
30
34
  export default Autocomplete;
@@ -1 +1 @@
1
- {"version":3,"file":"Autocomplete.d.ts","sourceRoot":"","sources":["../../src/components/Autocomplete.tsx"],"names":[],"mappings":"AACA,OAAO,KAA8E,MAAM,OAAO,CAAC;AAGnG,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACpC;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/D,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC/B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,QAAA,MAAM,YAAY,8FAqShB,CAAC;AAGH,eAAe,YAAY,CAAC"}
1
+ {"version":3,"file":"Autocomplete.d.ts","sourceRoot":"","sources":["../../src/components/Autocomplete.tsx"],"names":[],"mappings":"AACA,OAAO,KAA8E,MAAM,OAAO,CAAC;AAGnG,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,IAAI,CAAC;IAClB,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IACnC,0DAA0D;IAC1D,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,kBAAkB,KAAK,IAAI,CAAC;IAC/D,OAAO,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAC/B,QAAQ,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,kBAAkB,EAAE,CAAC,CAAC;IAC5D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,+EAA+E;IAC/E,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B;AAED,QAAA,MAAM,YAAY,8FA6WhB,CAAC;AAGH,eAAe,YAAY,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1966,6 +1966,8 @@ interface AutocompleteOption {
1966
1966
  label: string;
1967
1967
  description?: string;
1968
1968
  metadata?: Record<string, unknown>;
1969
+ /** If true, renders as a non-selectable section header */
1970
+ isHeader?: boolean;
1969
1971
  }
1970
1972
  interface AutocompleteProps {
1971
1973
  value: string;
@@ -1983,6 +1985,8 @@ interface AutocompleteProps {
1983
1985
  maxResults?: number;
1984
1986
  clearable?: boolean;
1985
1987
  className?: string;
1988
+ /** Show static options dropdown on focus when input is empty. Default: true */
1989
+ showOptionsOnFocus?: boolean;
1986
1990
  }
1987
1991
  declare const Autocomplete: React__default.ForwardRefExoticComponent<AutocompleteProps & React__default.RefAttributes<AutocompleteHandle>>;
1988
1992
 
package/dist/index.esm.js CHANGED
@@ -5007,7 +5007,7 @@ const MaskedInput = forwardRef(({ value, onChange, maskType = 'phone', customMas
5007
5007
  });
5008
5008
  MaskedInput.displayName = 'MaskedInput';
5009
5009
 
5010
- const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, label, placeholder = 'Search...', required = false, disabled = false, error, helperText, minChars = 1, debounceMs = 300, maxResults = 10, clearable = true, className = '', }, ref) => {
5010
+ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, label, placeholder = 'Search...', required = false, disabled = false, error, helperText, minChars = 1, debounceMs = 300, maxResults = 10, clearable = true, className = '', showOptionsOnFocus = true, }, ref) => {
5011
5011
  const [isOpen, setIsOpen] = useState(false);
5012
5012
  const [filteredOptions, setFilteredOptions] = useState([]);
5013
5013
  const [loading, setLoading] = useState(false);
@@ -5019,6 +5019,30 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
5019
5019
  const labelId = useId();
5020
5020
  const listboxId = useId();
5021
5021
  const errorId = useId();
5022
+ // Helper to find next selectable (non-header) index
5023
+ const findNextSelectableIndex = (currentIndex, optionsList) => {
5024
+ for (let i = currentIndex + 1; i < optionsList.length; i++) {
5025
+ if (!optionsList[i].isHeader)
5026
+ return i;
5027
+ }
5028
+ return currentIndex; // Stay at current if no next selectable
5029
+ };
5030
+ // Helper to find previous selectable (non-header) index
5031
+ const findPrevSelectableIndex = (currentIndex, optionsList) => {
5032
+ for (let i = currentIndex - 1; i >= 0; i--) {
5033
+ if (!optionsList[i].isHeader)
5034
+ return i;
5035
+ }
5036
+ return -1; // Go to -1 if no previous selectable
5037
+ };
5038
+ // Helper to find first selectable (non-header) index
5039
+ const findFirstSelectableIndex = (optionsList) => {
5040
+ for (let i = 0; i < optionsList.length; i++) {
5041
+ if (!optionsList[i].isHeader)
5042
+ return i;
5043
+ }
5044
+ return -1;
5045
+ };
5022
5046
  // Expose methods via ref
5023
5047
  useImperativeHandle(ref, () => ({
5024
5048
  focus: () => inputRef.current?.focus(),
@@ -5048,11 +5072,14 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
5048
5072
  const results = await onSearch(query);
5049
5073
  setFilteredOptions(results.slice(0, maxResults));
5050
5074
  setIsOpen(results.length > 0);
5075
+ // Auto-highlight first selectable (non-header) result
5076
+ setHighlightedIndex(findFirstSelectableIndex(results.slice(0, maxResults)));
5051
5077
  }
5052
5078
  catch (err) {
5053
5079
  console.error('Autocomplete search error:', err);
5054
5080
  setFilteredOptions([]);
5055
5081
  setIsOpen(false);
5082
+ setHighlightedIndex(-1);
5056
5083
  }
5057
5084
  finally {
5058
5085
  setLoading(false);
@@ -5063,6 +5090,16 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
5063
5090
  const filtered = filterOptions(query);
5064
5091
  setFilteredOptions(filtered);
5065
5092
  setIsOpen(filtered.length > 0);
5093
+ // Auto-highlight first selectable (non-header) result
5094
+ setHighlightedIndex(findFirstSelectableIndex(filtered));
5095
+ }
5096
+ };
5097
+ // Show static options (for focus/arrow down when input is empty)
5098
+ const showStaticOptions = () => {
5099
+ if (options.length > 0) {
5100
+ setFilteredOptions(options.slice(0, maxResults));
5101
+ setIsOpen(true);
5102
+ setHighlightedIndex(findFirstSelectableIndex(options.slice(0, maxResults)));
5066
5103
  }
5067
5104
  };
5068
5105
  // Debounced search
@@ -5099,23 +5136,40 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
5099
5136
  const handleKeyDown = (e) => {
5100
5137
  if (!isOpen) {
5101
5138
  if (e.key === 'ArrowDown') {
5102
- handleSearch(value);
5139
+ e.preventDefault();
5140
+ // If we have cached results from a previous search, show them
5141
+ if (filteredOptions.length > 0) {
5142
+ setIsOpen(true);
5143
+ setHighlightedIndex(findFirstSelectableIndex(filteredOptions));
5144
+ }
5145
+ else if (value.length < minChars && options.length > 0) {
5146
+ // Show static options when input is empty/below minChars
5147
+ showStaticOptions();
5148
+ }
5149
+ else if (value.length >= minChars) {
5150
+ // Otherwise trigger a new search
5151
+ handleSearch(value);
5152
+ }
5103
5153
  }
5104
5154
  return;
5105
5155
  }
5106
5156
  switch (e.key) {
5107
5157
  case 'ArrowDown':
5108
5158
  e.preventDefault();
5109
- setHighlightedIndex((prev) => prev < filteredOptions.length - 1 ? prev + 1 : prev);
5159
+ setHighlightedIndex((prev) => findNextSelectableIndex(prev, filteredOptions));
5110
5160
  break;
5111
5161
  case 'ArrowUp':
5112
5162
  e.preventDefault();
5113
- setHighlightedIndex((prev) => (prev > 0 ? prev - 1 : -1));
5163
+ setHighlightedIndex((prev) => findPrevSelectableIndex(prev, filteredOptions));
5114
5164
  break;
5115
5165
  case 'Enter':
5116
5166
  e.preventDefault();
5117
5167
  if (highlightedIndex >= 0 && highlightedIndex < filteredOptions.length) {
5118
- handleSelect(filteredOptions[highlightedIndex]);
5168
+ const option = filteredOptions[highlightedIndex];
5169
+ // Don't select headers
5170
+ if (!option.isHeader) {
5171
+ handleSelect(option);
5172
+ }
5119
5173
  }
5120
5174
  break;
5121
5175
  case 'Escape':
@@ -5145,7 +5199,16 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
5145
5199
  }
5146
5200
  };
5147
5201
  }, []);
5148
- return (jsxs("div", { className: `relative ${className}`, children: [label && (jsxs("label", { id: labelId, className: "block text-sm font-medium text-ink-900 mb-1.5", children: [label, required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxs("div", { className: "relative", children: [jsx("div", { className: "absolute left-3 top-1/2 -translate-y-1/2", children: loading ? (jsx(Loader2, { className: "h-4 w-4 text-ink-400 animate-spin" })) : (jsx(Search, { className: "h-4 w-4 text-ink-400" })) }), jsx("input", { ref: inputRef, type: "text", value: value, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: () => value.length >= minChars && handleSearch(value), placeholder: placeholder, disabled: disabled, className: `
5202
+ return (jsxs("div", { className: `relative ${className}`, children: [label && (jsxs("label", { id: labelId, className: "block text-sm font-medium text-ink-900 mb-1.5", children: [label, required && jsx("span", { className: "text-error-500 ml-1", children: "*" })] })), jsxs("div", { className: "relative", children: [jsx("div", { className: "absolute left-3 top-1/2 -translate-y-1/2", children: loading ? (jsx(Loader2, { className: "h-4 w-4 text-ink-400 animate-spin" })) : (jsx(Search, { className: "h-4 w-4 text-ink-400" })) }), jsx("input", { ref: inputRef, type: "text", value: value, onChange: handleInputChange, onKeyDown: handleKeyDown, onFocus: () => {
5203
+ if (showOptionsOnFocus && value.length < minChars && options.length > 0) {
5204
+ // Show static options when input is empty/below minChars
5205
+ showStaticOptions();
5206
+ }
5207
+ else if (value.length >= minChars) {
5208
+ // Trigger search if we have enough chars
5209
+ handleSearch(value);
5210
+ }
5211
+ }, placeholder: placeholder, disabled: disabled, className: `
5149
5212
  w-full pl-9 pr-9 py-2
5150
5213
  text-sm text-ink-900 placeholder-ink-400
5151
5214
  bg-white border rounded-lg
@@ -5155,12 +5218,16 @@ const Autocomplete = forwardRef(({ value, onChange, options = [], onSearch, labe
5155
5218
  ${error
5156
5219
  ? 'border-error-500 focus:ring-error-400 focus:border-error-400'
5157
5220
  : 'border-paper-300'}
5158
- `, role: "combobox", "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Search' : undefined, "aria-autocomplete": "list", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-activedescendant": highlightedIndex >= 0 ? `autocomplete-option-${highlightedIndex}` : undefined, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : undefined, "aria-busy": loading }), clearable && value && !disabled && (jsx("button", { type: "button", onClick: handleClear, className: "absolute right-3 top-1/2 -translate-y-1/2 text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Clear", children: jsx(X, { className: "h-4 w-4" }) }))] }), isOpen && filteredOptions.length > 0 && (jsx("div", { ref: dropdownRef, id: listboxId, className: "absolute z-50 w-full mt-1 bg-white border border-paper-200 rounded-lg shadow-lg max-h-60 overflow-y-auto", role: "listbox", "aria-label": "Search results", children: filteredOptions.map((option, index) => (jsxs("button", { id: `autocomplete-option-${index}`, type: "button", onClick: () => handleSelect(option), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": highlightedIndex === index, className: `
5159
- w-full text-left px-3 py-2 transition-colors
5160
- ${highlightedIndex === index
5221
+ `, role: "combobox", "aria-labelledby": label ? labelId : undefined, "aria-label": !label ? 'Search' : undefined, "aria-autocomplete": "list", "aria-expanded": isOpen, "aria-controls": listboxId, "aria-activedescendant": highlightedIndex >= 0 ? `autocomplete-option-${highlightedIndex}` : undefined, "aria-invalid": error ? 'true' : undefined, "aria-describedby": error ? errorId : undefined, "aria-busy": loading }), clearable && value && !disabled && (jsx("button", { type: "button", onClick: handleClear, className: "absolute right-3 top-1/2 -translate-y-1/2 text-ink-400 hover:text-ink-600 transition-colors", "aria-label": "Clear", children: jsx(X, { className: "h-4 w-4" }) }))] }), isOpen && filteredOptions.length > 0 && (jsx("div", { ref: dropdownRef, id: listboxId, className: "absolute z-50 w-full mt-1 bg-white border border-paper-200 rounded-lg shadow-lg max-h-60 overflow-y-auto", role: "listbox", "aria-label": "Search results", children: filteredOptions.map((option, index) => (option.isHeader ? (
5222
+ // Render section header (non-selectable)
5223
+ jsx("div", { 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", role: "presentation", children: option.label }, `header-${option.value}`)) : (
5224
+ // Render selectable option
5225
+ jsxs("button", { id: `autocomplete-option-${index}`, type: "button", onClick: () => handleSelect(option), onMouseEnter: () => setHighlightedIndex(index), role: "option", "aria-selected": highlightedIndex === index, className: `
5226
+ w-full text-left px-3 py-2 transition-colors
5227
+ ${highlightedIndex === index
5161
5228
  ? 'bg-accent-50'
5162
5229
  : 'hover:bg-paper-50'}
5163
- `, children: [jsx("div", { className: "text-sm font-medium text-ink-900", children: option.label }), option.description && (jsx("div", { className: "text-xs text-ink-600 mt-0.5", children: option.description }))] }, option.value))) })), isOpen && !loading && filteredOptions.length === 0 && value.length >= minChars && (jsx("div", { className: "absolute z-50 w-full mt-1 bg-white border border-paper-200 rounded-lg shadow-lg p-3", role: "status", "aria-live": "polite", children: jsx("p", { className: "text-sm text-ink-500 text-center", children: "No results found" }) })), error && (jsx("p", { id: errorId, className: "mt-1.5 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsx("p", { className: "mt-1.5 text-xs text-ink-600", children: helperText }))] }));
5230
+ `, children: [jsx("div", { className: "text-sm font-medium text-ink-900", children: option.label }), option.description && (jsx("div", { className: "text-xs text-ink-600 mt-0.5", children: option.description }))] }, option.value)))) })), isOpen && !loading && filteredOptions.length === 0 && value.length >= minChars && (jsx("div", { className: "absolute z-50 w-full mt-1 bg-white border border-paper-200 rounded-lg shadow-lg p-3", role: "status", "aria-live": "polite", children: jsx("p", { className: "text-sm text-ink-500 text-center", children: "No results found" }) })), error && (jsx("p", { id: errorId, className: "mt-1.5 text-xs text-error-600", role: "alert", "aria-live": "assertive", children: error })), helperText && !error && (jsx("p", { className: "mt-1.5 text-xs text-ink-600", children: helperText }))] }));
5164
5231
  });
5165
5232
  Autocomplete.displayName = 'Autocomplete';
5166
5233