periplo-ui 3.35.2 → 3.37.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.
Files changed (27) hide show
  1. package/dist/components/Combobox/Combobox.d.ts +34 -1
  2. package/dist/components/Combobox/Combobox.js +59 -125
  3. package/dist/components/Combobox/Combobox.js.map +1 -1
  4. package/dist/components/Combobox/ComboboxOptionWithTooltip.d.ts +3 -0
  5. package/dist/components/Combobox/ComboboxOptionWithTooltip.js +23 -0
  6. package/dist/components/Combobox/ComboboxOptionWithTooltip.js.map +1 -0
  7. package/dist/components/Combobox/StaticComboboxList.d.ts +2 -0
  8. package/dist/components/Combobox/StaticComboboxList.js +40 -0
  9. package/dist/components/Combobox/StaticComboboxList.js.map +1 -0
  10. package/dist/components/Combobox/VirtualizedComboboxList.d.ts +2 -0
  11. package/dist/components/Combobox/VirtualizedComboboxList.js +90 -0
  12. package/dist/components/Combobox/VirtualizedComboboxList.js.map +1 -0
  13. package/dist/components/Combobox/index.d.ts +5 -0
  14. package/dist/components/Combobox/index.js +4 -0
  15. package/dist/components/Combobox/index.js.map +1 -1
  16. package/dist/components/Combobox/types.d.ts +25 -0
  17. package/dist/components/Combobox/types.js +2 -0
  18. package/dist/components/Combobox/types.js.map +1 -0
  19. package/dist/components/Combobox/useCombobox.d.ts +21 -0
  20. package/dist/components/Combobox/useCombobox.js +175 -0
  21. package/dist/components/Combobox/useCombobox.js.map +1 -0
  22. package/dist/components/Pagination/Pagination.d.ts +1 -0
  23. package/dist/components/Pagination/Pagination.js +6 -3
  24. package/dist/components/Pagination/Pagination.js.map +1 -1
  25. package/dist/index.js +4 -0
  26. package/dist/index.js.map +1 -1
  27. package/package.json +3 -1
@@ -48,6 +48,18 @@ export type ComboboxSingleProps<T> = ComboboxBaseProps<T> & {
48
48
  onChange: (value: string) => void;
49
49
  /** Custom render function for the selected value display. */
50
50
  renderLabel?: (selectedOption: T) => React.ReactNode;
51
+ /**
52
+ * Async pagination function.
53
+ * Note: Multiple selection is not supported with virtualization.
54
+ */
55
+ fetchPage?: (params: {
56
+ page: number;
57
+ search?: string;
58
+ }) => Promise<{
59
+ items: Array<T>;
60
+ hasNextPage: boolean;
61
+ nextPage: number;
62
+ }>;
51
63
  };
52
64
  export type ComboboxMultipleProps<T> = ComboboxBaseProps<T> & {
53
65
  multiple: true;
@@ -55,12 +67,14 @@ export type ComboboxMultipleProps<T> = ComboboxBaseProps<T> & {
55
67
  onChange: (value: Array<string>) => void;
56
68
  /** Custom render function for the selected value(s) display. */
57
69
  renderLabel?: (selectedOptions: Array<T>, onRemove: (value: string) => void) => React.ReactNode;
70
+ /** fetchPage is not allowed with multiple selection */
71
+ fetchPage?: never;
58
72
  };
59
73
  export type ComboboxProps<T> = ComboboxSingleProps<T> | ComboboxMultipleProps<T>;
60
74
  /**
61
75
  * A searchable combobox component with support for custom rendering, keyboard navigation, and search filtering.
62
76
  *
63
- * @example
77
+ * @example Basic usage
64
78
  * ```tsx
65
79
  * interface User {
66
80
  * id: string
@@ -93,6 +107,25 @@ export type ComboboxProps<T> = ComboboxSingleProps<T> | ComboboxMultipleProps<T>
93
107
  * }
94
108
  * />
95
109
  * ```
110
+ *
111
+ * @example Virtualized infinite scrolling with pagination
112
+ * ```tsx
113
+ * <Combobox<User>
114
+ * options={[]} // Initial options can be empty
115
+ * value={selectedUserId}
116
+ * onChange={setSelectedUserId}
117
+ * getOptionValue={(user) => user.id}
118
+ * getOptionLabel={(user) => user.name}
119
+ * fetchPage={async ({ page, search }) => {
120
+ * const response = await api.getUsers({ page, search })
121
+ * return {
122
+ * items: response.users,
123
+ * hasNextPage: response.page < response.totalPages,
124
+ * nextPage: page + 1
125
+ * }
126
+ * }}
127
+ * />
128
+ * ```
96
129
  */
97
130
  export declare const Combobox: <T extends object>(props: ComboboxProps<T>) => import("react/jsx-runtime").JSX.Element;
98
131
  export {};
@@ -1,35 +1,19 @@
1
- import { jsxs, jsx, Fragment } from 'react/jsx-runtime';
2
- import { CaretDown, X, Check } from '@phosphor-icons/react';
3
- import { useState, useMemo, useRef, useEffect } from 'react';
1
+ import { jsxs, jsx } from 'react/jsx-runtime';
2
+ import { CaretDown, X } from '@phosphor-icons/react';
4
3
  import { cn } from '../../lib/utils.js';
5
4
  import { Button, buttonVariants } from '../Button/Button.js';
6
- import { Command, CommandInput, CommandList, CommandEmpty, CommandGroup, CommandItem } from '../Command/Command.js';
5
+ import { Command, CommandInput } from '../Command/Command.js';
7
6
  import { PopoverRoot, PopoverTrigger, PopoverContent } from '../Popover/Popover.js';
8
- import { TooltipProvider, TooltipRoot, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip.js';
7
+ import { StaticComboboxList } from './StaticComboboxList.js';
8
+ import { useCombobox } from './useCombobox.js';
9
+ import { VirtualizedComboboxList } from './VirtualizedComboboxList.js';
9
10
 
10
- const ComboboxOptionWithTooltip = ({ label }) => {
11
- const textRef = useRef(null);
12
- const [isTruncated, setIsTruncated] = useState(false);
13
- useEffect(() => {
14
- const frame = requestAnimationFrame(() => {
15
- if (textRef.current) {
16
- setIsTruncated(textRef.current.scrollWidth > textRef.current.clientWidth);
17
- }
18
- });
19
- return () => cancelAnimationFrame(frame);
20
- }, [label]);
21
- return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(TooltipRoot, { delayDuration: 300, children: [
22
- /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx("span", { ref: textRef, className: "block truncate", children: label }) }),
23
- isTruncated && /* @__PURE__ */ jsx(TooltipContent, { children: label })
24
- ] }) });
25
- };
26
11
  const Combobox = (props) => {
27
12
  const {
28
13
  id,
29
14
  options,
30
15
  getOptionValue,
31
16
  getOptionLabel,
32
- placeholder = "Select...",
33
17
  searchPlaceholder = "Search...",
34
18
  emptyMessage = "No results found.",
35
19
  className = "w-60",
@@ -39,92 +23,29 @@ const Combobox = (props) => {
39
23
  loading = false,
40
24
  loadingPlaceholder = "Cargando...",
41
25
  error = false,
42
- filterOptions,
43
26
  multiple,
44
- clearable = true,
45
27
  onClear,
46
28
  modal = false,
47
29
  selectedMultiplePlaceholder = "Selected",
48
30
  multipleOptionsPlaceholder = "Options"
49
31
  } = props;
50
- const [open, setOpen] = useState(false);
51
- const [searchTerm, setSearchTerm] = useState("");
52
- const [isHovered, setIsHovered] = useState(false);
53
- const closeOnSelect = props.closeOnSelect ?? !props.multiple;
54
- const filteredOptions = useMemo(() => {
55
- if (!filterOptions) {
56
- return options.filter((option) => getOptionLabel(option).toLowerCase().includes(searchTerm.toLowerCase()));
57
- }
58
- return searchTerm ? filterOptions(options, searchTerm) : options;
59
- }, [filterOptions, options, searchTerm, getOptionLabel]);
60
- const handleSelect = (currentValue) => {
61
- if (multiple) {
62
- const { value = [], onChange } = props;
63
- const isRemoving = value.includes(currentValue);
64
- if (isRemoving && !clearable && value.length === 1) {
65
- return;
66
- }
67
- const newValues = isRemoving ? value.filter((val) => val !== currentValue) : [...value, currentValue];
68
- onChange(newValues);
69
- } else {
70
- const { value, onChange } = props;
71
- const newValue = clearable && currentValue === value ? "" : currentValue;
72
- onChange(newValue);
73
- }
74
- if (closeOnSelect) {
75
- setOpen(false);
76
- }
77
- };
78
- const handleRemove = (valueToRemove) => {
79
- if (props.multiple) {
80
- const { value = [], onChange } = props;
81
- if (!clearable && value.length === 1) {
82
- return;
83
- }
84
- const newValues = value.filter((val) => val !== valueToRemove);
85
- onChange(newValues);
86
- }
87
- };
88
- const handleClear = (event) => {
89
- event.preventDefault();
90
- event.stopPropagation();
91
- if (multiple) {
92
- const { onChange } = props;
93
- if (clearable) {
94
- onChange([]);
95
- }
96
- } else {
97
- const { onChange } = props;
98
- onChange("");
99
- }
100
- if (typeof onClear === "function") {
101
- onClear();
102
- }
103
- };
104
- const displayValue = useMemo(() => {
105
- if (multiple) {
106
- const { value: value2 = [], renderLabel: renderLabel2 } = props;
107
- if (value2.length === 0) return placeholder;
108
- const selectedOptions = options.filter((option) => value2.includes(getOptionValue(option)));
109
- if (renderLabel2) {
110
- return renderLabel2(selectedOptions, handleRemove);
111
- }
112
- return selectedOptions.map(getOptionLabel).join(", ");
113
- }
114
- const { value, renderLabel } = props;
115
- const selectedOption = options.find((option) => getOptionValue(option) === value);
116
- if (renderLabel && selectedOption) {
117
- return renderLabel(selectedOption);
118
- }
119
- return selectedOption ? getOptionLabel(selectedOption) : placeholder;
120
- }, [props, getOptionLabel, getOptionValue, options, placeholder, multiple]);
121
- const hasValue = useMemo(() => {
122
- if (multiple) {
123
- return props.value && props.value.length > 0;
124
- }
125
- return !!props.value;
126
- }, [props.value, multiple]);
127
- const showClearButton = onClear && hasValue && isHovered && clearable;
32
+ const {
33
+ open,
34
+ setOpen,
35
+ searchTerm,
36
+ setSearchTerm,
37
+ setIsHovered,
38
+ hasNextPage,
39
+ loadingMore,
40
+ isVirtualized,
41
+ filteredOptions,
42
+ displayValue,
43
+ hasValue,
44
+ showClearButton,
45
+ handleSelect,
46
+ handleClear,
47
+ loadNextPage
48
+ } = useCombobox(props);
128
49
  return /* @__PURE__ */ jsxs("div", { className: "flex w-full flex-col gap-1", children: [
129
50
  /* @__PURE__ */ jsxs(PopoverRoot, { open, onOpenChange: setOpen, modal, children: [
130
51
  /* @__PURE__ */ jsx(PopoverTrigger, { asChild: true, children: /* @__PURE__ */ jsxs(
@@ -192,29 +113,42 @@ const Combobox = (props) => {
192
113
  onValueChange: setSearchTerm
193
114
  }
194
115
  ),
195
- /* @__PURE__ */ jsx(CommandList, { style: { maxHeight }, className: "overflow-auto", onWheel: (event) => event.stopPropagation(), children: loading ? /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex items-center justify-center py-6 text-sm", children: loadingPlaceholder }) : /* @__PURE__ */ jsxs(Fragment, { children: [
196
- filteredOptions.length === 0 && /* @__PURE__ */ jsx(CommandEmpty, { children: emptyMessage }),
197
- multiple && props.value && props.value.length > 0 && /* @__PURE__ */ jsx(CommandGroup, { heading: selectedMultiplePlaceholder, children: options.filter((option) => props.value?.includes(getOptionValue(option))).map((option) => /* @__PURE__ */ jsxs(
198
- CommandItem,
199
- {
200
- value: getOptionValue(option),
201
- onSelect: handleSelect,
202
- children: [
203
- /* @__PURE__ */ jsx(Check, { className: "mr-2 h-4 w-4 opacity-100" }),
204
- /* @__PURE__ */ jsx(ComboboxOptionWithTooltip, { label: getOptionLabel(option) })
205
- ]
206
- },
207
- getOptionValue(option)
208
- )) }),
209
- /* @__PURE__ */ jsx(CommandGroup, { heading: !multiple ? void 0 : multipleOptionsPlaceholder, children: filteredOptions.filter((option) => !multiple || !props.value?.includes(getOptionValue(option))).map((option) => {
210
- const optionValue = getOptionValue(option);
211
- const isSelected = multiple ? props.value?.includes(optionValue) : props.value === optionValue;
212
- return /* @__PURE__ */ jsx(CommandItem, { value: optionValue, onSelect: handleSelect, children: renderOption ? renderOption(option, isSelected ?? false) : /* @__PURE__ */ jsxs(Fragment, { children: [
213
- /* @__PURE__ */ jsx(Check, { className: `mr-2 h-4 w-4 ${isSelected ? "opacity-100" : "opacity-0"}` }),
214
- /* @__PURE__ */ jsx(ComboboxOptionWithTooltip, { label: getOptionLabel(option) })
215
- ] }) }, optionValue);
216
- }) })
217
- ] }) })
116
+ isVirtualized ? /* @__PURE__ */ jsx(
117
+ VirtualizedComboboxList,
118
+ {
119
+ localOptions: filteredOptions,
120
+ loading,
121
+ loadingPlaceholder,
122
+ emptyMessage,
123
+ value: props.value,
124
+ getOptionValue,
125
+ getOptionLabel,
126
+ handleSelect,
127
+ renderOption,
128
+ maxHeight,
129
+ hasNextPage,
130
+ loadingMore,
131
+ onLoadMore: loadNextPage
132
+ }
133
+ ) : /* @__PURE__ */ jsx(
134
+ StaticComboboxList,
135
+ {
136
+ filteredOptions,
137
+ loading,
138
+ loadingPlaceholder,
139
+ emptyMessage,
140
+ multiple: !!multiple,
141
+ value: props.value,
142
+ getOptionValue,
143
+ getOptionLabel,
144
+ handleSelect,
145
+ renderOption,
146
+ maxHeight,
147
+ selectedMultiplePlaceholder,
148
+ multipleOptionsPlaceholder,
149
+ options
150
+ }
151
+ )
218
152
  ] }) })
219
153
  ] }),
220
154
  typeof error === "string" && /* @__PURE__ */ jsx("span", { className: "text-error-500 text-sm", children: error })
@@ -1 +1 @@
1
- {"version":3,"file":"Combobox.js","sources":["../../../src/components/Combobox/Combobox.tsx"],"sourcesContent":["import { CaretDown, Check, X } from '@phosphor-icons/react'\nimport { useEffect, useMemo, useRef, useState } from 'react'\n\nimport { cn } from '../../lib/utils'\nimport { Button, buttonVariants } from '../Button'\nimport { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList } from '../Command'\nimport { PopoverContent, PopoverRoot, PopoverTrigger } from '../Popover'\nimport { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '../Tooltip'\n\nconst ComboboxOptionWithTooltip = ({ label }: { label: string }) => {\n const textRef = useRef<HTMLSpanElement>(null)\n const [isTruncated, setIsTruncated] = useState(false)\n\n useEffect(() => {\n const frame = requestAnimationFrame(() => {\n if (textRef.current) {\n setIsTruncated(textRef.current.scrollWidth > textRef.current.clientWidth)\n }\n })\n\n return () => cancelAnimationFrame(frame)\n }, [label])\n\n return (\n <TooltipProvider>\n <TooltipRoot delayDuration={300}>\n <TooltipTrigger asChild>\n <span ref={textRef} className=\"block truncate\">\n {label}\n </span>\n </TooltipTrigger>\n {isTruncated && <TooltipContent>{label}</TooltipContent>}\n </TooltipRoot>\n </TooltipProvider>\n )\n}\n\ntype ComboboxBaseProps<T> = {\n /** Unique identifier for the combobox */\n id?: string\n /** Array of options to display in the combobox */\n options: Array<T>\n /** Function to get the unique identifier from an option. */\n getOptionValue: (option: T) => string\n /** Function to get the display text from an option. */\n getOptionLabel: (option: T) => string\n /** Custom render function for options. If not provided, defaults to showing a checkmark and label */\n renderOption?: (option: T, isSelected: boolean) => React.ReactNode\n /** Placeholder text shown when no option is selected */\n placeholder?: string\n /** Placeholder text for the search input field */\n searchPlaceholder?: string\n /** Message shown when no options match the search query */\n emptyMessage?: string\n /** Additional CSS classes to apply to the combobox trigger */\n className?: string\n /** Whether the combobox is disabled */\n disabled?: boolean\n /** Maximum height of the options list. Can be any valid CSS height value */\n maxHeight?: string | number\n /** Whether to close the dropdown when an option is selected. Defaults to `true` for single select and `false` for multi-select. */\n closeOnSelect?: boolean\n /** Whether the combobox is in a loading state */\n loading?: boolean\n /** Message to show when in loading state */\n loadingPlaceholder?: string\n /** Whether the combobox has an error */\n error?: boolean | string\n /** Custom function to filter options based on search term */\n filterOptions?: (options: Array<T>, searchTerm: string) => Array<T>\n /** Callback function executed when the clear button is clicked. When provided, an X button will appear on hover to clear the selection. */\n onClear?: boolean | (() => void)\n /** Whether the selection can be cleared */\n clearable?: boolean\n /** Whether the combobox is inside a modal */\n modal?: boolean\n /** Placeholder text for the selected multiple options */\n selectedMultiplePlaceholder?: string\n /** Placeholder text for the multiple options */\n multipleOptionsPlaceholder?: string\n}\n\nexport type ComboboxSingleProps<T> = ComboboxBaseProps<T> & {\n multiple?: false\n value?: string\n onChange: (value: string) => void\n /** Custom render function for the selected value display. */\n renderLabel?: (selectedOption: T) => React.ReactNode\n}\n\nexport type ComboboxMultipleProps<T> = ComboboxBaseProps<T> & {\n multiple: true\n value?: Array<string>\n onChange: (value: Array<string>) => void\n /** Custom render function for the selected value(s) display. */\n renderLabel?: (selectedOptions: Array<T>, onRemove: (value: string) => void) => React.ReactNode\n}\n\nexport type ComboboxProps<T> = ComboboxSingleProps<T> | ComboboxMultipleProps<T>\n\n/**\n * A searchable combobox component with support for custom rendering, keyboard navigation, and search filtering.\n *\n * @example\n * ```tsx\n * interface User {\n * id: string\n * name: string\n * email: string\n * }\n *\n * <Combobox<User>\n * options={users}\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * />\n * ```\n *\n * @example Custom filtering\n * ```tsx\n * <Combobox<User>\n * options={users}\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * filterOptions={(options, searchTerm) =>\n * options.filter(user =>\n * user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||\n * user.email.toLowerCase().includes(searchTerm.toLowerCase())\n * )\n * }\n * />\n * ```\n */\nexport const Combobox = <T extends object>(props: ComboboxProps<T>) => {\n const {\n id,\n options,\n getOptionValue,\n getOptionLabel,\n placeholder = 'Select...',\n searchPlaceholder = 'Search...',\n emptyMessage = 'No results found.',\n className = 'w-60',\n disabled = false,\n maxHeight = '300px',\n renderOption,\n loading = false,\n loadingPlaceholder = 'Cargando...',\n error = false,\n filterOptions,\n multiple,\n clearable = true,\n onClear,\n modal = false,\n selectedMultiplePlaceholder = 'Selected',\n multipleOptionsPlaceholder = 'Options',\n } = props\n\n const [open, setOpen] = useState(false)\n const [searchTerm, setSearchTerm] = useState('')\n const [isHovered, setIsHovered] = useState(false)\n\n const closeOnSelect = props.closeOnSelect ?? !props.multiple\n\n const filteredOptions = useMemo(() => {\n if (!filterOptions) {\n return options.filter((option) => getOptionLabel(option).toLowerCase().includes(searchTerm.toLowerCase()))\n }\n\n return searchTerm ? filterOptions(options, searchTerm) : options\n }, [filterOptions, options, searchTerm, getOptionLabel])\n\n const handleSelect = (currentValue: string) => {\n if (multiple) {\n const { value = [], onChange } = props\n const isRemoving = value.includes(currentValue)\n\n // If trying to remove the last item and clearable is false, don't allow it\n if (isRemoving && !clearable && value.length === 1) {\n return\n }\n\n const newValues = isRemoving ? value.filter((val) => val !== currentValue) : [...value, currentValue]\n onChange(newValues)\n } else {\n const { value, onChange } = props\n const newValue = clearable && currentValue === value ? '' : currentValue\n onChange(newValue)\n }\n\n if (closeOnSelect) {\n setOpen(false)\n }\n }\n\n const handleRemove = (valueToRemove: string) => {\n if (props.multiple) {\n const { value = [], onChange } = props\n\n // If trying to remove the last item and clearable is false, don't allow it\n if (!clearable && value.length === 1) {\n return\n }\n\n const newValues = value.filter((val) => val !== valueToRemove)\n onChange(newValues)\n }\n }\n\n const handleClear = (event: React.MouseEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (multiple) {\n const { onChange } = props\n\n // Only clear if clearable is true\n if (clearable) {\n onChange([])\n }\n } else {\n const { onChange } = props\n onChange('')\n }\n\n if (typeof onClear === 'function') {\n onClear()\n }\n }\n\n const displayValue = useMemo(() => {\n if (multiple) {\n const { value = [], renderLabel } = props\n if (value.length === 0) return placeholder\n\n const selectedOptions = options.filter((option) => value.includes(getOptionValue(option)))\n\n if (renderLabel) {\n return renderLabel(selectedOptions, handleRemove)\n }\n\n return selectedOptions.map(getOptionLabel).join(', ')\n }\n\n const { value, renderLabel } = props\n const selectedOption = options.find((option) => getOptionValue(option) === value)\n if (renderLabel && selectedOption) {\n return renderLabel(selectedOption)\n }\n return selectedOption ? getOptionLabel(selectedOption) : placeholder\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props, getOptionLabel, getOptionValue, options, placeholder, multiple])\n\n const hasValue = useMemo(() => {\n if (multiple) {\n return props.value && props.value.length > 0\n }\n return !!props.value\n }, [props.value, multiple])\n\n const showClearButton = onClear && hasValue && isHovered && clearable\n\n return (\n <div className=\"flex w-full flex-col gap-1\">\n <PopoverRoot open={open} onOpenChange={setOpen} modal={modal}>\n <PopoverTrigger asChild>\n <Button\n id={id}\n type=\"button\"\n disabled={disabled}\n variant=\"text\"\n className={cn(\n buttonVariants({ variant: 'input', size: 'lg' }),\n 'relative flex justify-between rounded-lg',\n multiple && 'renderLabel' in props && props.renderLabel ? 'h-auto min-h-12' : 'h-12',\n open && 'border-neutral-950',\n disabled && 'cursor-not-allowed',\n error && 'border-error-400 focus-visible:border-error-700',\n className,\n )}\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n <span\n className={cn(\n 'block',\n !hasValue && 'text-neutral-300',\n !(multiple && 'renderLabel' in props && props.renderLabel) && 'truncate',\n )}\n >\n {displayValue}\n </span>\n <CaretDown\n className={cn(\n 'h-4 w-4 shrink-0 opacity-50 transition-opacity duration-150',\n showClearButton ? 'opacity-0' : 'opacity-50',\n )}\n />\n {onClear && hasValue && (\n <X\n data-testid=\"clear-button\"\n className={cn(\n 'absolute right-4 z-10 h-4 w-4 shrink-0 cursor-pointer transition-opacity duration-150',\n showClearButton ? 'opacity-100 hover:opacity-70' : 'opacity-0',\n )}\n onClick={handleClear}\n />\n )}\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"p-0\">\n <Command shouldFilter={false}>\n <CommandInput\n placeholder={searchPlaceholder}\n disabled={loading}\n value={searchTerm}\n onValueChange={setSearchTerm}\n />\n <CommandList style={{ maxHeight }} className=\"overflow-auto\" onWheel={(event) => event.stopPropagation()}>\n {loading ? (\n <div className=\"text-muted-foreground flex items-center justify-center py-6 text-sm\">\n {loadingPlaceholder}\n </div>\n ) : (\n <>\n {filteredOptions.length === 0 && <CommandEmpty>{emptyMessage}</CommandEmpty>}\n {multiple && props.value && props.value.length > 0 && (\n <CommandGroup heading={selectedMultiplePlaceholder}>\n {options\n .filter((option) => props.value?.includes(getOptionValue(option)))\n .map((option) => (\n <CommandItem\n key={getOptionValue(option)}\n value={getOptionValue(option)}\n onSelect={handleSelect}\n >\n <Check className=\"mr-2 h-4 w-4 opacity-100\" />\n <ComboboxOptionWithTooltip label={getOptionLabel(option)} />\n </CommandItem>\n ))}\n </CommandGroup>\n )}\n\n <CommandGroup heading={!multiple ? undefined : multipleOptionsPlaceholder}>\n {filteredOptions\n .filter((option) => !multiple || !props.value?.includes(getOptionValue(option)))\n .map((option) => {\n const optionValue = getOptionValue(option)\n const isSelected = multiple ? props.value?.includes(optionValue) : props.value === optionValue\n\n return (\n <CommandItem key={optionValue} value={optionValue} onSelect={handleSelect}>\n {renderOption ? (\n renderOption(option, isSelected ?? false)\n ) : (\n <>\n <Check className={`mr-2 h-4 w-4 ${isSelected ? 'opacity-100' : 'opacity-0'}`} />\n <ComboboxOptionWithTooltip label={getOptionLabel(option)} />\n </>\n )}\n </CommandItem>\n )\n })}\n </CommandGroup>\n </>\n )}\n </CommandList>\n </Command>\n </PopoverContent>\n </PopoverRoot>\n {typeof error === 'string' && <span className=\"text-error-500 text-sm\">{error}</span>}\n </div>\n )\n}\n"],"names":["value","renderLabel"],"mappings":"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"}
1
+ {"version":3,"file":"Combobox.js","sources":["../../../src/components/Combobox/Combobox.tsx"],"sourcesContent":["import { CaretDown, X } from '@phosphor-icons/react'\n\nimport { cn } from '../../lib/utils'\nimport { Button, buttonVariants } from '../Button'\nimport { Command, CommandInput } from '../Command'\nimport { PopoverContent, PopoverRoot, PopoverTrigger } from '../Popover'\n\nimport { StaticComboboxList } from './StaticComboboxList'\nimport { useCombobox } from './useCombobox'\nimport { VirtualizedComboboxList } from './VirtualizedComboboxList'\n\ntype ComboboxBaseProps<T> = {\n /** Unique identifier for the combobox */\n id?: string\n /** Array of options to display in the combobox */\n options: Array<T>\n /** Function to get the unique identifier from an option. */\n getOptionValue: (option: T) => string\n /** Function to get the display text from an option. */\n getOptionLabel: (option: T) => string\n /** Custom render function for options. If not provided, defaults to showing a checkmark and label */\n renderOption?: (option: T, isSelected: boolean) => React.ReactNode\n /** Placeholder text shown when no option is selected */\n placeholder?: string\n /** Placeholder text for the search input field */\n searchPlaceholder?: string\n /** Message shown when no options match the search query */\n emptyMessage?: string\n /** Additional CSS classes to apply to the combobox trigger */\n className?: string\n /** Whether the combobox is disabled */\n disabled?: boolean\n /** Maximum height of the options list. Can be any valid CSS height value */\n maxHeight?: string | number\n /** Whether to close the dropdown when an option is selected. Defaults to `true` for single select and `false` for multi-select. */\n closeOnSelect?: boolean\n /** Whether the combobox is in a loading state */\n loading?: boolean\n /** Message to show when in loading state */\n loadingPlaceholder?: string\n /** Whether the combobox has an error */\n error?: boolean | string\n /** Custom function to filter options based on search term */\n filterOptions?: (options: Array<T>, searchTerm: string) => Array<T>\n /** Callback function executed when the clear button is clicked. When provided, an X button will appear on hover to clear the selection. */\n onClear?: boolean | (() => void)\n /** Whether the selection can be cleared */\n clearable?: boolean\n /** Whether the combobox is inside a modal */\n modal?: boolean\n /** Placeholder text for the selected multiple options */\n selectedMultiplePlaceholder?: string\n /** Placeholder text for the multiple options */\n multipleOptionsPlaceholder?: string\n}\n\nexport type ComboboxSingleProps<T> = ComboboxBaseProps<T> & {\n multiple?: false\n value?: string\n onChange: (value: string) => void\n /** Custom render function for the selected value display. */\n renderLabel?: (selectedOption: T) => React.ReactNode\n /**\n * Async pagination function.\n * Note: Multiple selection is not supported with virtualization.\n */\n fetchPage?: (params: { page: number; search?: string }) => Promise<{\n items: Array<T>\n hasNextPage: boolean\n nextPage: number\n }>\n}\n\nexport type ComboboxMultipleProps<T> = ComboboxBaseProps<T> & {\n multiple: true\n value?: Array<string>\n onChange: (value: Array<string>) => void\n /** Custom render function for the selected value(s) display. */\n renderLabel?: (selectedOptions: Array<T>, onRemove: (value: string) => void) => React.ReactNode\n /** fetchPage is not allowed with multiple selection */\n fetchPage?: never\n}\n\nexport type ComboboxProps<T> = ComboboxSingleProps<T> | ComboboxMultipleProps<T>\n\n/**\n * A searchable combobox component with support for custom rendering, keyboard navigation, and search filtering.\n *\n * @example Basic usage\n * ```tsx\n * interface User {\n * id: string\n * name: string\n * email: string\n * }\n *\n * <Combobox<User>\n * options={users}\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * />\n * ```\n *\n * @example Custom filtering\n * ```tsx\n * <Combobox<User>\n * options={users}\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * filterOptions={(options, searchTerm) =>\n * options.filter(user =>\n * user.name.toLowerCase().includes(searchTerm.toLowerCase()) ||\n * user.email.toLowerCase().includes(searchTerm.toLowerCase())\n * )\n * }\n * />\n * ```\n *\n * @example Virtualized infinite scrolling with pagination\n * ```tsx\n * <Combobox<User>\n * options={[]} // Initial options can be empty\n * value={selectedUserId}\n * onChange={setSelectedUserId}\n * getOptionValue={(user) => user.id}\n * getOptionLabel={(user) => user.name}\n * fetchPage={async ({ page, search }) => {\n * const response = await api.getUsers({ page, search })\n * return {\n * items: response.users,\n * hasNextPage: response.page < response.totalPages,\n * nextPage: page + 1\n * }\n * }}\n * />\n * ```\n */\nexport const Combobox = <T extends object>(props: ComboboxProps<T>) => {\n const {\n id,\n options,\n getOptionValue,\n getOptionLabel,\n searchPlaceholder = 'Search...',\n emptyMessage = 'No results found.',\n className = 'w-60',\n disabled = false,\n maxHeight = '300px',\n renderOption,\n loading = false,\n loadingPlaceholder = 'Cargando...',\n error = false,\n multiple,\n onClear,\n modal = false,\n selectedMultiplePlaceholder = 'Selected',\n multipleOptionsPlaceholder = 'Options',\n } = props\n\n const {\n open,\n setOpen,\n searchTerm,\n setSearchTerm,\n setIsHovered,\n hasNextPage,\n loadingMore,\n isVirtualized,\n filteredOptions,\n displayValue,\n hasValue,\n showClearButton,\n handleSelect,\n handleClear,\n loadNextPage,\n } = useCombobox(props)\n\n return (\n <div className=\"flex w-full flex-col gap-1\">\n <PopoverRoot open={open} onOpenChange={setOpen} modal={modal}>\n <PopoverTrigger asChild>\n <Button\n id={id}\n type=\"button\"\n disabled={disabled}\n variant=\"text\"\n className={cn(\n buttonVariants({ variant: 'input', size: 'lg' }),\n 'relative flex justify-between rounded-lg',\n multiple && 'renderLabel' in props && props.renderLabel ? 'h-auto min-h-12' : 'h-12',\n open && 'border-neutral-950',\n disabled && 'cursor-not-allowed',\n error && 'border-error-400 focus-visible:border-error-700',\n className,\n )}\n aria-expanded={open}\n aria-haspopup=\"listbox\"\n onMouseEnter={() => setIsHovered(true)}\n onMouseLeave={() => setIsHovered(false)}\n >\n <span\n className={cn(\n 'block',\n !hasValue && 'text-neutral-300',\n !(multiple && 'renderLabel' in props && props.renderLabel) && 'truncate',\n )}\n >\n {displayValue}\n </span>\n <CaretDown\n className={cn(\n 'h-4 w-4 shrink-0 opacity-50 transition-opacity duration-150',\n showClearButton ? 'opacity-0' : 'opacity-50',\n )}\n />\n {onClear && hasValue && (\n <X\n data-testid=\"clear-button\"\n className={cn(\n 'absolute right-4 z-10 h-4 w-4 shrink-0 cursor-pointer transition-opacity duration-150',\n showClearButton ? 'opacity-100 hover:opacity-70' : 'opacity-0',\n )}\n onClick={handleClear}\n />\n )}\n </Button>\n </PopoverTrigger>\n <PopoverContent className=\"p-0\">\n <Command shouldFilter={false}>\n <CommandInput\n placeholder={searchPlaceholder}\n disabled={loading}\n value={searchTerm}\n onValueChange={setSearchTerm}\n />\n {isVirtualized ? (\n <VirtualizedComboboxList\n localOptions={filteredOptions}\n loading={loading}\n loadingPlaceholder={loadingPlaceholder}\n emptyMessage={emptyMessage}\n value={props.value as string}\n getOptionValue={getOptionValue}\n getOptionLabel={getOptionLabel}\n handleSelect={handleSelect}\n renderOption={renderOption}\n maxHeight={maxHeight}\n hasNextPage={hasNextPage}\n loadingMore={loadingMore}\n onLoadMore={loadNextPage}\n />\n ) : (\n <StaticComboboxList\n filteredOptions={filteredOptions}\n loading={loading}\n loadingPlaceholder={loadingPlaceholder}\n emptyMessage={emptyMessage}\n multiple={!!multiple}\n value={props.value}\n getOptionValue={getOptionValue}\n getOptionLabel={getOptionLabel}\n handleSelect={handleSelect}\n renderOption={renderOption}\n maxHeight={maxHeight}\n selectedMultiplePlaceholder={selectedMultiplePlaceholder}\n multipleOptionsPlaceholder={multipleOptionsPlaceholder}\n options={options}\n />\n )}\n </Command>\n </PopoverContent>\n </PopoverRoot>\n {typeof error === 'string' && <span className=\"text-error-500 text-sm\">{error}</span>}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export declare const ComboboxOptionWithTooltip: ({ label }: {
2
+ label: string;
3
+ }) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,23 @@
1
+ import { jsx, jsxs } from 'react/jsx-runtime';
2
+ import { useRef, useState, useEffect } from 'react';
3
+ import { TooltipProvider, TooltipRoot, TooltipTrigger, TooltipContent } from '../Tooltip/Tooltip.js';
4
+
5
+ const ComboboxOptionWithTooltip = ({ label }) => {
6
+ const textRef = useRef(null);
7
+ const [isTruncated, setIsTruncated] = useState(false);
8
+ useEffect(() => {
9
+ const frame = requestAnimationFrame(() => {
10
+ if (textRef.current) {
11
+ setIsTruncated(textRef.current.scrollWidth > textRef.current.clientWidth);
12
+ }
13
+ });
14
+ return () => cancelAnimationFrame(frame);
15
+ }, [label]);
16
+ return /* @__PURE__ */ jsx(TooltipProvider, { children: /* @__PURE__ */ jsxs(TooltipRoot, { delayDuration: 300, children: [
17
+ /* @__PURE__ */ jsx(TooltipTrigger, { asChild: true, children: /* @__PURE__ */ jsx("span", { ref: textRef, className: "block truncate", children: label }) }),
18
+ isTruncated && /* @__PURE__ */ jsx(TooltipContent, { children: label })
19
+ ] }) });
20
+ };
21
+
22
+ export { ComboboxOptionWithTooltip };
23
+ //# sourceMappingURL=ComboboxOptionWithTooltip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ComboboxOptionWithTooltip.js","sources":["../../../src/components/Combobox/ComboboxOptionWithTooltip.tsx"],"sourcesContent":["import { useRef, useState, useEffect } from 'react'\n\nimport { TooltipContent, TooltipProvider, TooltipRoot, TooltipTrigger } from '../Tooltip'\n\nexport const ComboboxOptionWithTooltip = ({ label }: { label: string }) => {\n const textRef = useRef<HTMLSpanElement>(null)\n const [isTruncated, setIsTruncated] = useState(false)\n\n useEffect(() => {\n const frame = requestAnimationFrame(() => {\n if (textRef.current) {\n setIsTruncated(textRef.current.scrollWidth > textRef.current.clientWidth)\n }\n })\n\n return () => cancelAnimationFrame(frame)\n }, [label])\n\n return (\n <TooltipProvider>\n <TooltipRoot delayDuration={300}>\n <TooltipTrigger asChild>\n <span ref={textRef} className=\"block truncate\">\n {label}\n </span>\n </TooltipTrigger>\n {isTruncated && <TooltipContent>{label}</TooltipContent>}\n </TooltipRoot>\n </TooltipProvider>\n )\n}\n"],"names":[],"mappings":";;;;AAIO,MAAM,yBAAA,GAA4B,CAAC,EAAE,KAAA,EAAM,KAAyB;AACzE,EAAA,MAAM,OAAA,GAAU,OAAwB,IAAI,CAAA;AAC5C,EAAA,MAAM,CAAC,WAAA,EAAa,cAAc,CAAA,GAAI,SAAS,KAAK,CAAA;AAEpD,EAAA,SAAA,CAAU,MAAM;AACd,IAAA,MAAM,KAAA,GAAQ,sBAAsB,MAAM;AACxC,MAAA,IAAI,QAAQ,OAAA,EAAS;AACnB,QAAA,cAAA,CAAe,OAAA,CAAQ,OAAA,CAAQ,WAAA,GAAc,OAAA,CAAQ,QAAQ,WAAW,CAAA;AAAA;AAC1E,KACD,CAAA;AAED,IAAA,OAAO,MAAM,qBAAqB,KAAK,CAAA;AAAA,GACzC,EAAG,CAAC,KAAK,CAAC,CAAA;AAEV,EAAA,uBACE,GAAA,CAAC,eAAA,EAAA,EACC,QAAA,kBAAA,IAAA,CAAC,WAAA,EAAA,EAAY,eAAe,GAAA,EAC1B,QAAA,EAAA;AAAA,oBAAA,GAAA,CAAC,cAAA,EAAA,EAAe,OAAA,EAAO,IAAA,EACrB,QAAA,kBAAA,GAAA,CAAC,MAAA,EAAA,EAAK,KAAK,OAAA,EAAS,SAAA,EAAU,gBAAA,EAC3B,QAAA,EAAA,KAAA,EACH,CAAA,EACF,CAAA;AAAA,IACC,WAAA,oBAAe,GAAA,CAAC,cAAA,EAAA,EAAgB,QAAA,EAAA,KAAA,EAAM;AAAA,GAAA,EACzC,CAAA,EACF,CAAA;AAEJ;;;;"}
@@ -0,0 +1,2 @@
1
+ import { StaticComboboxListProps } from './types';
2
+ export declare const StaticComboboxList: <T extends object>({ filteredOptions, loading, loadingPlaceholder, emptyMessage, multiple, value, getOptionValue, getOptionLabel, handleSelect, renderOption, maxHeight, selectedMultiplePlaceholder, multipleOptionsPlaceholder, options, }: StaticComboboxListProps<T>) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,40 @@
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { Check } from '@phosphor-icons/react';
3
+ import { CommandList, CommandEmpty, CommandGroup, CommandItem } from '../Command/Command.js';
4
+ import { ComboboxOptionWithTooltip } from './ComboboxOptionWithTooltip.js';
5
+
6
+ const StaticComboboxList = ({
7
+ filteredOptions,
8
+ loading,
9
+ loadingPlaceholder,
10
+ emptyMessage,
11
+ multiple,
12
+ value,
13
+ getOptionValue,
14
+ getOptionLabel,
15
+ handleSelect,
16
+ renderOption,
17
+ maxHeight,
18
+ selectedMultiplePlaceholder,
19
+ multipleOptionsPlaceholder,
20
+ options
21
+ }) => {
22
+ return /* @__PURE__ */ jsx(CommandList, { style: { maxHeight }, className: "overflow-auto", onWheel: (event) => event.stopPropagation(), children: loading ? /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex items-center justify-center py-6 text-sm", children: loadingPlaceholder }) : /* @__PURE__ */ jsxs(Fragment, { children: [
23
+ filteredOptions.length === 0 && /* @__PURE__ */ jsx(CommandEmpty, { children: emptyMessage }),
24
+ multiple && Array.isArray(value) && value.length > 0 && /* @__PURE__ */ jsx(CommandGroup, { heading: selectedMultiplePlaceholder, children: options.filter((option) => value.includes(getOptionValue(option))).map((option) => /* @__PURE__ */ jsxs(CommandItem, { value: getOptionValue(option), onSelect: handleSelect, children: [
25
+ /* @__PURE__ */ jsx(Check, { className: "mr-2 h-4 w-4 opacity-100" }),
26
+ /* @__PURE__ */ jsx(ComboboxOptionWithTooltip, { label: getOptionLabel(option) })
27
+ ] }, getOptionValue(option))) }),
28
+ /* @__PURE__ */ jsx(CommandGroup, { heading: multiple ? multipleOptionsPlaceholder : void 0, children: filteredOptions.filter((option) => !multiple || !Array.isArray(value) || !value.includes(getOptionValue(option))).map((option) => {
29
+ const optionValue = getOptionValue(option);
30
+ const isSelected = multiple ? Array.isArray(value) && value.includes(optionValue) : value === optionValue;
31
+ return /* @__PURE__ */ jsx(CommandItem, { value: optionValue, onSelect: handleSelect, children: renderOption ? renderOption(option, isSelected) : /* @__PURE__ */ jsxs(Fragment, { children: [
32
+ /* @__PURE__ */ jsx(Check, { className: `mr-2 h-4 w-4 ${isSelected ? "opacity-100" : "opacity-0"}` }),
33
+ /* @__PURE__ */ jsx(ComboboxOptionWithTooltip, { label: getOptionLabel(option) })
34
+ ] }) }, optionValue);
35
+ }) })
36
+ ] }) });
37
+ };
38
+
39
+ export { StaticComboboxList };
40
+ //# sourceMappingURL=StaticComboboxList.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"StaticComboboxList.js","sources":["../../../src/components/Combobox/StaticComboboxList.tsx"],"sourcesContent":["import { Check } from '@phosphor-icons/react'\n\nimport { CommandEmpty, CommandGroup, CommandItem, CommandList } from '../Command'\n\nimport { ComboboxOptionWithTooltip } from './ComboboxOptionWithTooltip'\nimport type { StaticComboboxListProps } from './types'\n\nexport const StaticComboboxList = <T extends object>({\n filteredOptions,\n loading,\n loadingPlaceholder,\n emptyMessage,\n multiple,\n value,\n getOptionValue,\n getOptionLabel,\n handleSelect,\n renderOption,\n maxHeight,\n selectedMultiplePlaceholder,\n multipleOptionsPlaceholder,\n options,\n}: StaticComboboxListProps<T>) => {\n return (\n <CommandList style={{ maxHeight }} className=\"overflow-auto\" onWheel={(event) => event.stopPropagation()}>\n {loading ? (\n <div className=\"text-muted-foreground flex items-center justify-center py-6 text-sm\">{loadingPlaceholder}</div>\n ) : (\n <>\n {filteredOptions.length === 0 && <CommandEmpty>{emptyMessage}</CommandEmpty>}\n {multiple && Array.isArray(value) && value.length > 0 && (\n <CommandGroup heading={selectedMultiplePlaceholder}>\n {options\n .filter((option) => value.includes(getOptionValue(option)))\n .map((option) => (\n <CommandItem key={getOptionValue(option)} value={getOptionValue(option)} onSelect={handleSelect}>\n <Check className=\"mr-2 h-4 w-4 opacity-100\" />\n <ComboboxOptionWithTooltip label={getOptionLabel(option)} />\n </CommandItem>\n ))}\n </CommandGroup>\n )}\n\n <CommandGroup heading={multiple ? multipleOptionsPlaceholder : undefined}>\n {filteredOptions\n .filter((option) => !multiple || !Array.isArray(value) || !value.includes(getOptionValue(option)))\n .map((option) => {\n const optionValue = getOptionValue(option)\n const isSelected = multiple\n ? Array.isArray(value) && value.includes(optionValue)\n : value === optionValue\n\n return (\n <CommandItem key={optionValue} value={optionValue} onSelect={handleSelect}>\n {renderOption ? (\n renderOption(option, isSelected)\n ) : (\n <>\n <Check className={`mr-2 h-4 w-4 ${isSelected ? 'opacity-100' : 'opacity-0'}`} />\n <ComboboxOptionWithTooltip label={getOptionLabel(option)} />\n </>\n )}\n </CommandItem>\n )\n })}\n </CommandGroup>\n </>\n )}\n </CommandList>\n )\n}\n"],"names":[],"mappings":";;;;;AAOO,MAAM,qBAAqB,CAAmB;AAAA,EACnD,eAAA;AAAA,EACA,OAAA;AAAA,EACA,kBAAA;AAAA,EACA,YAAA;AAAA,EACA,QAAA;AAAA,EACA,KAAA;AAAA,EACA,cAAA;AAAA,EACA,cAAA;AAAA,EACA,YAAA;AAAA,EACA,YAAA;AAAA,EACA,SAAA;AAAA,EACA,2BAAA;AAAA,EACA,0BAAA;AAAA,EACA;AACF,CAAA,KAAkC;AAChC,EAAA,uBACE,GAAA,CAAC,eAAY,KAAA,EAAO,EAAE,WAAU,EAAG,SAAA,EAAU,iBAAgB,OAAA,EAAS,CAAC,UAAU,KAAA,CAAM,eAAA,IACpF,QAAA,EAAA,OAAA,mBACC,GAAA,CAAC,SAAI,SAAA,EAAU,qEAAA,EAAuE,QAAA,EAAA,kBAAA,EAAmB,CAAA,mBAEzG,IAAA,CAAA,QAAA,EAAA,EACG,QAAA,EAAA;AAAA,IAAA,eAAA,CAAgB,MAAA,KAAW,CAAA,oBAAK,GAAA,CAAC,YAAA,EAAA,EAAc,QAAA,EAAA,YAAA,EAAa,CAAA;AAAA,IAC5D,QAAA,IAAY,KAAA,CAAM,OAAA,CAAQ,KAAK,KAAK,KAAA,CAAM,MAAA,GAAS,CAAA,oBAClD,GAAA,CAAC,YAAA,EAAA,EAAa,OAAA,EAAS,2BAAA,EACpB,QAAA,EAAA,OAAA,CACE,OAAO,CAAC,MAAA,KAAW,KAAA,CAAM,QAAA,CAAS,cAAA,CAAe,MAAM,CAAC,CAAC,EACzD,GAAA,CAAI,CAAC,MAAA,qBACJ,IAAA,CAAC,eAAyC,KAAA,EAAO,cAAA,CAAe,MAAM,CAAA,EAAG,UAAU,YAAA,EACjF,QAAA,EAAA;AAAA,sBAAA,GAAA,CAAC,KAAA,EAAA,EAAM,WAAU,0BAAA,EAA2B,CAAA;AAAA,sBAC5C,GAAA,CAAC,yBAAA,EAAA,EAA0B,KAAA,EAAO,cAAA,CAAe,MAAM,CAAA,EAAG;AAAA,KAAA,EAAA,EAF1C,cAAA,CAAe,MAAM,CAGvC,CACD,CAAA,EACL,CAAA;AAAA,oBAGF,GAAA,CAAC,YAAA,EAAA,EAAa,OAAA,EAAS,QAAA,GAAW,0BAAA,GAA6B,MAAA,EAC5D,QAAA,EAAA,eAAA,CACE,MAAA,CAAO,CAAC,MAAA,KAAW,CAAC,QAAA,IAAY,CAAC,KAAA,CAAM,OAAA,CAAQ,KAAK,CAAA,IAAK,CAAC,KAAA,CAAM,QAAA,CAAS,cAAA,CAAe,MAAM,CAAC,CAAC,CAAA,CAChG,GAAA,CAAI,CAAC,MAAA,KAAW;AACf,MAAA,MAAM,WAAA,GAAc,eAAe,MAAM,CAAA;AACzC,MAAA,MAAM,UAAA,GAAa,QAAA,GACf,KAAA,CAAM,OAAA,CAAQ,KAAK,KAAK,KAAA,CAAM,QAAA,CAAS,WAAW,CAAA,GAClD,KAAA,KAAU,WAAA;AAEd,MAAA,uBACE,GAAA,CAAC,WAAA,EAAA,EAA8B,KAAA,EAAO,WAAA,EAAa,QAAA,EAAU,YAAA,EAC1D,QAAA,EAAA,YAAA,GACC,YAAA,CAAa,MAAA,EAAQ,UAAU,CAAA,mBAE/B,IAAA,CAAA,QAAA,EAAA,EACE,QAAA,EAAA;AAAA,wBAAA,GAAA,CAAC,SAAM,SAAA,EAAW,CAAA,aAAA,EAAgB,UAAA,GAAa,aAAA,GAAgB,WAAW,CAAA,CAAA,EAAI,CAAA;AAAA,wBAC9E,GAAA,CAAC,yBAAA,EAAA,EAA0B,KAAA,EAAO,cAAA,CAAe,MAAM,CAAA,EAAG;AAAA,OAAA,EAC5D,KAPc,WASlB,CAAA;AAAA,KAEH,CAAA,EACL;AAAA,GAAA,EACF,CAAA,EAEJ,CAAA;AAEJ;;;;"}
@@ -0,0 +1,2 @@
1
+ import { VirtualizedComboboxListProps } from './types';
2
+ export declare const VirtualizedComboboxList: <T extends object>({ localOptions, loading, loadingPlaceholder, emptyMessage, value, getOptionValue, getOptionLabel, handleSelect, renderOption, maxHeight, hasNextPage, loadingMore, onLoadMore, }: VirtualizedComboboxListProps<T>) => import("react/jsx-runtime").JSX.Element;
@@ -0,0 +1,90 @@
1
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
2
+ import { Check } from '@phosphor-icons/react';
3
+ import { useVirtualizer } from '@tanstack/react-virtual';
4
+ import { useRef, useEffect } from 'react';
5
+ import { CommandEmpty, CommandItem } from '../Command/Command.js';
6
+ import { ComboboxOptionWithTooltip } from './ComboboxOptionWithTooltip.js';
7
+
8
+ const VirtualizedComboboxList = ({
9
+ localOptions,
10
+ loading,
11
+ loadingPlaceholder,
12
+ emptyMessage,
13
+ value,
14
+ getOptionValue,
15
+ getOptionLabel,
16
+ handleSelect,
17
+ renderOption,
18
+ maxHeight,
19
+ hasNextPage,
20
+ loadingMore,
21
+ onLoadMore
22
+ }) => {
23
+ const parentRef = useRef(null);
24
+ const hasTriggeredRef = useRef(false);
25
+ const totalCount = localOptions.length;
26
+ const PREFETCH_THRESHOLD = 25;
27
+ const rowVirtualizer = useVirtualizer({
28
+ count: totalCount,
29
+ getScrollElement: () => parentRef.current,
30
+ estimateSize: () => 40,
31
+ overscan: 10
32
+ });
33
+ useEffect(() => {
34
+ hasTriggeredRef.current = false;
35
+ }, [totalCount]);
36
+ const virtualItems = rowVirtualizer.getVirtualItems();
37
+ useEffect(() => {
38
+ if (virtualItems.length === 0 || !hasNextPage || loadingMore || loading || hasTriggeredRef.current) {
39
+ return;
40
+ }
41
+ const lastVisibleItem = virtualItems.at(-1);
42
+ const itemsFromEnd = totalCount - (lastVisibleItem?.index ?? 0);
43
+ if (itemsFromEnd <= PREFETCH_THRESHOLD) {
44
+ hasTriggeredRef.current = true;
45
+ onLoadMore();
46
+ }
47
+ }, [virtualItems, totalCount, hasNextPage, loadingMore, loading, onLoadMore]);
48
+ if (loading && localOptions.length === 0) {
49
+ return /* @__PURE__ */ jsx("div", { className: "text-muted-foreground flex items-center justify-center py-6 text-sm", children: loadingPlaceholder });
50
+ }
51
+ if (localOptions.length === 0 && !loading && !loadingMore) {
52
+ return /* @__PURE__ */ jsx(CommandEmpty, { children: emptyMessage });
53
+ }
54
+ const getItemAtIndex = (index) => {
55
+ const option = localOptions[index];
56
+ const isSelected = value === getOptionValue(option);
57
+ return { option, isSelected };
58
+ };
59
+ return /* @__PURE__ */ jsxs("div", { ref: parentRef, className: "overflow-auto", style: { maxHeight }, onWheel: (event) => event.stopPropagation(), children: [
60
+ /* @__PURE__ */ jsx("div", { style: { height: `${rowVirtualizer.getTotalSize()}px`, position: "relative" }, children: rowVirtualizer.getVirtualItems().map((virtualRow) => {
61
+ const { option, isSelected } = getItemAtIndex(virtualRow.index);
62
+ const optionValue = getOptionValue(option);
63
+ return /* @__PURE__ */ jsx(
64
+ "div",
65
+ {
66
+ "data-index": virtualRow.index,
67
+ ref: (el) => {
68
+ if (el) rowVirtualizer.measureElement(el);
69
+ },
70
+ style: {
71
+ position: "absolute",
72
+ top: 0,
73
+ left: 0,
74
+ width: "100%",
75
+ transform: `translateY(${virtualRow.start}px)`
76
+ },
77
+ children: /* @__PURE__ */ jsx(CommandItem, { value: optionValue, onSelect: handleSelect, children: renderOption ? renderOption(option, isSelected) : /* @__PURE__ */ jsxs(Fragment, { children: [
78
+ /* @__PURE__ */ jsx(Check, { className: `mr-2 h-4 w-4 ${isSelected ? "opacity-100" : "opacity-0"}` }),
79
+ /* @__PURE__ */ jsx(ComboboxOptionWithTooltip, { label: getOptionLabel(option) })
80
+ ] }) })
81
+ },
82
+ optionValue
83
+ );
84
+ }) }),
85
+ loadingMore && /* @__PURE__ */ jsx("div", { className: "flex items-center justify-center py-2 text-sm text-neutral-500", children: loadingPlaceholder })
86
+ ] });
87
+ };
88
+
89
+ export { VirtualizedComboboxList };
90
+ //# sourceMappingURL=VirtualizedComboboxList.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"VirtualizedComboboxList.js","sources":["../../../src/components/Combobox/VirtualizedComboboxList.tsx"],"sourcesContent":["import { Check } from '@phosphor-icons/react'\nimport { useVirtualizer } from '@tanstack/react-virtual'\nimport { useRef, useEffect } from 'react'\n\nimport { CommandEmpty, CommandItem } from '../Command'\n\nimport { ComboboxOptionWithTooltip } from './ComboboxOptionWithTooltip'\nimport type { VirtualizedComboboxListProps } from './types'\n\nexport const VirtualizedComboboxList = <T extends object>({\n localOptions,\n loading,\n loadingPlaceholder,\n emptyMessage,\n value,\n getOptionValue,\n getOptionLabel,\n handleSelect,\n renderOption,\n maxHeight,\n hasNextPage,\n loadingMore,\n onLoadMore,\n}: VirtualizedComboboxListProps<T>) => {\n const parentRef = useRef<HTMLDivElement>(null)\n const hasTriggeredRef = useRef(false)\n\n const totalCount = localOptions.length\n const PREFETCH_THRESHOLD = 25\n\n const rowVirtualizer = useVirtualizer({\n count: totalCount,\n getScrollElement: () => parentRef.current,\n estimateSize: () => 40,\n overscan: 10,\n })\n\n useEffect(() => {\n hasTriggeredRef.current = false\n }, [totalCount])\n\n const virtualItems = rowVirtualizer.getVirtualItems()\n\n useEffect(() => {\n if (virtualItems.length === 0 || !hasNextPage || loadingMore || loading || hasTriggeredRef.current) {\n return\n }\n\n const lastVisibleItem = virtualItems.at(-1)\n const itemsFromEnd = totalCount - (lastVisibleItem?.index ?? 0)\n\n if (itemsFromEnd <= PREFETCH_THRESHOLD) {\n hasTriggeredRef.current = true\n onLoadMore()\n }\n }, [virtualItems, totalCount, hasNextPage, loadingMore, loading, onLoadMore])\n\n if (loading && localOptions.length === 0) {\n return (\n <div className=\"text-muted-foreground flex items-center justify-center py-6 text-sm\">{loadingPlaceholder}</div>\n )\n }\n\n if (localOptions.length === 0 && !loading && !loadingMore) {\n return <CommandEmpty>{emptyMessage}</CommandEmpty>\n }\n\n const getItemAtIndex = (index: number) => {\n const option = localOptions[index]\n const isSelected = value === getOptionValue(option)\n return { option, isSelected }\n }\n\n return (\n <div ref={parentRef} className=\"overflow-auto\" style={{ maxHeight }} onWheel={(event) => event.stopPropagation()}>\n <div style={{ height: `${rowVirtualizer.getTotalSize()}px`, position: 'relative' }}>\n {rowVirtualizer.getVirtualItems().map((virtualRow) => {\n const { option, isSelected } = getItemAtIndex(virtualRow.index)\n const optionValue = getOptionValue(option)\n\n return (\n <div\n key={optionValue}\n data-index={virtualRow.index}\n ref={(el) => {\n if (el) rowVirtualizer.measureElement(el)\n }}\n style={{\n position: 'absolute',\n top: 0,\n left: 0,\n width: '100%',\n transform: `translateY(${virtualRow.start}px)`,\n }}\n >\n <CommandItem value={optionValue} onSelect={handleSelect}>\n {renderOption ? (\n renderOption(option, isSelected)\n ) : (\n <>\n <Check className={`mr-2 h-4 w-4 ${isSelected ? 'opacity-100' : 'opacity-0'}`} />\n <ComboboxOptionWithTooltip label={getOptionLabel(option)} />\n </>\n )}\n </CommandItem>\n </div>\n )\n })}\n </div>\n {loadingMore && (\n <div className=\"flex items-center justify-center py-2 text-sm text-neutral-500\">{loadingPlaceholder}</div>\n )}\n </div>\n )\n}\n"],"names":[],"mappings":"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"}
@@ -1 +1,6 @@
1
1
  export * from './Combobox';
2
+ export { ComboboxOptionWithTooltip } from './ComboboxOptionWithTooltip';
3
+ export { StaticComboboxList } from './StaticComboboxList';
4
+ export { useCombobox } from './useCombobox';
5
+ export { VirtualizedComboboxList } from './VirtualizedComboboxList';
6
+ export type { ComboboxListBaseProps, StaticComboboxListProps, VirtualizedComboboxListProps } from './types';
@@ -1,2 +1,6 @@
1
1
  export { Combobox } from './Combobox.js';
2
+ export { ComboboxOptionWithTooltip } from './ComboboxOptionWithTooltip.js';
3
+ export { StaticComboboxList } from './StaticComboboxList.js';
4
+ export { useCombobox } from './useCombobox.js';
5
+ export { VirtualizedComboboxList } from './VirtualizedComboboxList.js';
2
6
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;"}
@@ -0,0 +1,25 @@
1
+ export type ComboboxListBaseProps<T> = {
2
+ loading: boolean;
3
+ loadingPlaceholder: string;
4
+ emptyMessage: string;
5
+ getOptionValue: (option: T) => string;
6
+ getOptionLabel: (option: T) => string;
7
+ handleSelect: (value: string) => void;
8
+ renderOption?: (option: T, isSelected: boolean) => React.ReactNode;
9
+ maxHeight: string | number;
10
+ };
11
+ export type VirtualizedComboboxListProps<T> = ComboboxListBaseProps<T> & {
12
+ localOptions: Array<T>;
13
+ value?: string;
14
+ hasNextPage: boolean;
15
+ loadingMore: boolean;
16
+ onLoadMore: () => void;
17
+ };
18
+ export type StaticComboboxListProps<T> = ComboboxListBaseProps<T> & {
19
+ filteredOptions: Array<T>;
20
+ multiple: boolean;
21
+ value?: string | Array<string>;
22
+ selectedMultiplePlaceholder?: string;
23
+ multipleOptionsPlaceholder?: string;
24
+ options: Array<T>;
25
+ };
@@ -0,0 +1,2 @@
1
+
2
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sources":[],"sourcesContent":[],"names":[],"mappings":""}
@@ -0,0 +1,21 @@
1
+ import { ComboboxProps } from './Combobox';
2
+ export declare const useCombobox: <T extends object>(props: ComboboxProps<T>) => {
3
+ open: boolean;
4
+ setOpen: import('react').Dispatch<import('react').SetStateAction<boolean>>;
5
+ searchTerm: string;
6
+ setSearchTerm: import('react').Dispatch<import('react').SetStateAction<string>>;
7
+ isHovered: boolean;
8
+ setIsHovered: import('react').Dispatch<import('react').SetStateAction<boolean>>;
9
+ localOptions: T[];
10
+ hasNextPage: boolean;
11
+ loadingMore: boolean;
12
+ isVirtualized: boolean;
13
+ filteredOptions: T[];
14
+ displayValue: import('react').ReactNode;
15
+ hasValue: boolean | undefined;
16
+ showClearButton: boolean | undefined;
17
+ handleSelect: (currentValue: string) => void;
18
+ handleRemove: (valueToRemove: string) => void;
19
+ handleClear: (event: React.MouseEvent) => void;
20
+ loadNextPage: () => Promise<void>;
21
+ };
@@ -0,0 +1,175 @@
1
+ import { useState, useRef, useEffect, useCallback, useMemo } from 'react';
2
+
3
+ const useCombobox = (props) => {
4
+ const {
5
+ options,
6
+ getOptionValue,
7
+ getOptionLabel,
8
+ placeholder = "Select...",
9
+ loading = false,
10
+ filterOptions,
11
+ multiple,
12
+ clearable = true,
13
+ onClear,
14
+ fetchPage
15
+ } = props;
16
+ const [open, setOpen] = useState(false);
17
+ const [searchTerm, setSearchTerm] = useState("");
18
+ const [debouncedSearchTerm, setDebouncedSearchTerm] = useState("");
19
+ const [isHovered, setIsHovered] = useState(false);
20
+ const [page, setPage] = useState(1);
21
+ const [localOptions, setLocalOptions] = useState(options);
22
+ const [hasNextPage, setHasNextPage] = useState(true);
23
+ const [loadingMore, setLoadingMore] = useState(false);
24
+ const debounceTimerRef = useRef(void 0);
25
+ const closeOnSelect = props.closeOnSelect ?? !props.multiple;
26
+ const isVirtualized = !!fetchPage;
27
+ useEffect(() => {
28
+ if (!isVirtualized) return;
29
+ clearTimeout(debounceTimerRef.current);
30
+ debounceTimerRef.current = setTimeout(() => {
31
+ setDebouncedSearchTerm(searchTerm);
32
+ }, 300);
33
+ return () => {
34
+ clearTimeout(debounceTimerRef.current);
35
+ };
36
+ }, [searchTerm, isVirtualized]);
37
+ useEffect(() => {
38
+ if (isVirtualized) {
39
+ setPage(1);
40
+ setLocalOptions([]);
41
+ setHasNextPage(true);
42
+ setLoadingMore(false);
43
+ }
44
+ }, [debouncedSearchTerm, isVirtualized]);
45
+ const loadNextPage = useCallback(async () => {
46
+ if (!fetchPage || loadingMore || !hasNextPage || loading) return;
47
+ setLoadingMore(true);
48
+ try {
49
+ const result = await fetchPage({ page, search: debouncedSearchTerm || void 0 });
50
+ setLocalOptions((prev) => [...prev, ...result.items]);
51
+ setPage(result.nextPage);
52
+ setHasNextPage(result.hasNextPage);
53
+ } finally {
54
+ setLoadingMore(false);
55
+ }
56
+ }, [fetchPage, loadingMore, hasNextPage, loading, page, debouncedSearchTerm]);
57
+ useEffect(() => {
58
+ if (isVirtualized && open && localOptions.length === 0 && !loading && !loadingMore) {
59
+ loadNextPage();
60
+ }
61
+ }, [isVirtualized, open, localOptions.length, loading, loadingMore, loadNextPage]);
62
+ const filteredOptions = useMemo(() => {
63
+ if (isVirtualized) {
64
+ return localOptions;
65
+ }
66
+ if (!filterOptions) {
67
+ return options.filter((option) => getOptionLabel(option).toLowerCase().includes(searchTerm.toLowerCase()));
68
+ }
69
+ return searchTerm ? filterOptions(options, searchTerm) : options;
70
+ }, [isVirtualized, localOptions, filterOptions, options, searchTerm, getOptionLabel]);
71
+ const handleSelect = useCallback(
72
+ (currentValue) => {
73
+ if (multiple) {
74
+ const { value = [], onChange } = props;
75
+ const isRemoving = value.includes(currentValue);
76
+ if (isRemoving && !clearable && value.length === 1) {
77
+ return;
78
+ }
79
+ const newValues = isRemoving ? value.filter((val) => val !== currentValue) : [...value, currentValue];
80
+ onChange(newValues);
81
+ } else {
82
+ const { value, onChange } = props;
83
+ const newValue = clearable && currentValue === value ? "" : currentValue;
84
+ onChange(newValue);
85
+ }
86
+ if (closeOnSelect) {
87
+ setOpen(false);
88
+ }
89
+ },
90
+ [multiple, clearable, closeOnSelect, props]
91
+ );
92
+ const handleRemove = useCallback(
93
+ (valueToRemove) => {
94
+ if (props.multiple) {
95
+ const { value = [], onChange } = props;
96
+ if (!clearable && value.length === 1) {
97
+ return;
98
+ }
99
+ const newValues = value.filter((val) => val !== valueToRemove);
100
+ onChange(newValues);
101
+ }
102
+ },
103
+ [props, clearable]
104
+ );
105
+ const handleClear = useCallback(
106
+ (event) => {
107
+ event.preventDefault();
108
+ event.stopPropagation();
109
+ if (multiple) {
110
+ const { onChange } = props;
111
+ if (clearable) {
112
+ onChange([]);
113
+ }
114
+ } else {
115
+ const { onChange } = props;
116
+ onChange("");
117
+ }
118
+ if (typeof onClear === "function") {
119
+ onClear();
120
+ }
121
+ },
122
+ [multiple, clearable, onClear, props]
123
+ );
124
+ const displayValue = useMemo(() => {
125
+ const availableOptions = isVirtualized ? localOptions : options;
126
+ if (multiple) {
127
+ const { value: value2 = [], renderLabel: renderLabel2 } = props;
128
+ if (value2.length === 0) return placeholder;
129
+ const selectedOptions = availableOptions.filter((option) => value2.includes(getOptionValue(option)));
130
+ if (renderLabel2) {
131
+ return renderLabel2(selectedOptions, handleRemove);
132
+ }
133
+ return selectedOptions.map(getOptionLabel).join(", ");
134
+ }
135
+ const { value, renderLabel } = props;
136
+ const selectedOption = availableOptions.find((option) => getOptionValue(option) === value);
137
+ if (renderLabel && selectedOption) {
138
+ return renderLabel(selectedOption);
139
+ }
140
+ return selectedOption ? getOptionLabel(selectedOption) : placeholder;
141
+ }, [props, getOptionLabel, getOptionValue, options, localOptions, isVirtualized, placeholder, multiple]);
142
+ const hasValue = useMemo(() => {
143
+ if (multiple) {
144
+ return props.value && props.value.length > 0;
145
+ }
146
+ return !!props.value;
147
+ }, [props.value, multiple]);
148
+ const showClearButton = onClear && hasValue && isHovered && clearable;
149
+ return {
150
+ // State
151
+ open,
152
+ setOpen,
153
+ searchTerm,
154
+ setSearchTerm,
155
+ isHovered,
156
+ setIsHovered,
157
+ localOptions,
158
+ hasNextPage,
159
+ loadingMore,
160
+ // Computed values
161
+ isVirtualized,
162
+ filteredOptions,
163
+ displayValue,
164
+ hasValue,
165
+ showClearButton,
166
+ // Handlers
167
+ handleSelect,
168
+ handleRemove,
169
+ handleClear,
170
+ loadNextPage
171
+ };
172
+ };
173
+
174
+ export { useCombobox };
175
+ //# sourceMappingURL=useCombobox.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useCombobox.js","sources":["../../../src/components/Combobox/useCombobox.ts"],"sourcesContent":["import { useCallback, useEffect, useMemo, useRef, useState } from 'react'\n\nimport type { ComboboxProps } from './Combobox'\n\nexport const useCombobox = <T extends object>(props: ComboboxProps<T>) => {\n const {\n options,\n getOptionValue,\n getOptionLabel,\n placeholder = 'Select...',\n loading = false,\n filterOptions,\n multiple,\n clearable = true,\n onClear,\n fetchPage,\n } = props\n\n const [open, setOpen] = useState(false)\n const [searchTerm, setSearchTerm] = useState('')\n const [debouncedSearchTerm, setDebouncedSearchTerm] = useState('')\n const [isHovered, setIsHovered] = useState(false)\n\n const [page, setPage] = useState(1)\n const [localOptions, setLocalOptions] = useState<Array<T>>(options)\n const [hasNextPage, setHasNextPage] = useState(true)\n const [loadingMore, setLoadingMore] = useState(false)\n\n const debounceTimerRef = useRef<NodeJS.Timeout | undefined>(undefined)\n\n const closeOnSelect = props.closeOnSelect ?? !props.multiple\n const isVirtualized = !!fetchPage\n\n useEffect(() => {\n if (!isVirtualized) return\n\n clearTimeout(debounceTimerRef.current)\n\n debounceTimerRef.current = setTimeout(() => {\n setDebouncedSearchTerm(searchTerm)\n }, 300)\n\n return () => {\n clearTimeout(debounceTimerRef.current)\n }\n }, [searchTerm, isVirtualized])\n\n useEffect(() => {\n if (isVirtualized) {\n setPage(1)\n setLocalOptions([])\n setHasNextPage(true)\n setLoadingMore(false)\n }\n }, [debouncedSearchTerm, isVirtualized])\n\n const loadNextPage = useCallback(async () => {\n if (!fetchPage || loadingMore || !hasNextPage || loading) return\n\n setLoadingMore(true)\n try {\n const result = await fetchPage({ page, search: debouncedSearchTerm || undefined })\n setLocalOptions((prev) => [...prev, ...result.items])\n setPage(result.nextPage)\n setHasNextPage(result.hasNextPage)\n } finally {\n setLoadingMore(false)\n }\n }, [fetchPage, loadingMore, hasNextPage, loading, page, debouncedSearchTerm])\n\n // Load initial page when combobox opens\n useEffect(() => {\n if (isVirtualized && open && localOptions.length === 0 && !loading && !loadingMore) {\n loadNextPage()\n }\n }, [isVirtualized, open, localOptions.length, loading, loadingMore, loadNextPage])\n\n const filteredOptions = useMemo(() => {\n if (isVirtualized) {\n return localOptions\n }\n\n if (!filterOptions) {\n return options.filter((option) => getOptionLabel(option).toLowerCase().includes(searchTerm.toLowerCase()))\n }\n\n return searchTerm ? filterOptions(options, searchTerm) : options\n }, [isVirtualized, localOptions, filterOptions, options, searchTerm, getOptionLabel])\n\n const handleSelect = useCallback(\n (currentValue: string) => {\n if (multiple) {\n const { value = [], onChange } = props\n const isRemoving = value.includes(currentValue)\n\n if (isRemoving && !clearable && value.length === 1) {\n return\n }\n\n const newValues = isRemoving ? value.filter((val) => val !== currentValue) : [...value, currentValue]\n onChange(newValues)\n } else {\n const { value, onChange } = props\n const newValue = clearable && currentValue === value ? '' : currentValue\n onChange(newValue)\n }\n\n if (closeOnSelect) {\n setOpen(false)\n }\n },\n [multiple, clearable, closeOnSelect, props],\n )\n\n const handleRemove = useCallback(\n (valueToRemove: string) => {\n if (props.multiple) {\n const { value = [], onChange } = props\n\n if (!clearable && value.length === 1) {\n return\n }\n\n const newValues = value.filter((val) => val !== valueToRemove)\n onChange(newValues)\n }\n },\n [props, clearable],\n )\n\n const handleClear = useCallback(\n (event: React.MouseEvent) => {\n event.preventDefault()\n event.stopPropagation()\n\n if (multiple) {\n const { onChange } = props\n\n if (clearable) {\n onChange([])\n }\n } else {\n const { onChange } = props\n onChange('')\n }\n\n if (typeof onClear === 'function') {\n onClear()\n }\n },\n [multiple, clearable, onClear, props],\n )\n\n const displayValue = useMemo(() => {\n const availableOptions = isVirtualized ? localOptions : options\n\n if (multiple) {\n const { value = [], renderLabel } = props\n if (value.length === 0) return placeholder\n\n const selectedOptions = availableOptions.filter((option) => value.includes(getOptionValue(option)))\n\n if (renderLabel) {\n return renderLabel(selectedOptions, handleRemove)\n }\n\n return selectedOptions.map(getOptionLabel).join(', ')\n }\n\n const { value, renderLabel } = props\n const selectedOption = availableOptions.find((option) => getOptionValue(option) === value)\n if (renderLabel && selectedOption) {\n return renderLabel(selectedOption)\n }\n return selectedOption ? getOptionLabel(selectedOption) : placeholder\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [props, getOptionLabel, getOptionValue, options, localOptions, isVirtualized, placeholder, multiple])\n\n const hasValue = useMemo(() => {\n if (multiple) {\n return props.value && props.value.length > 0\n }\n return !!props.value\n }, [props.value, multiple])\n\n const showClearButton = onClear && hasValue && isHovered && clearable\n\n return {\n // State\n open,\n setOpen,\n searchTerm,\n setSearchTerm,\n isHovered,\n setIsHovered,\n localOptions,\n hasNextPage,\n loadingMore,\n\n // Computed values\n isVirtualized,\n filteredOptions,\n displayValue,\n hasValue,\n showClearButton,\n\n // Handlers\n handleSelect,\n handleRemove,\n handleClear,\n loadNextPage,\n }\n}\n"],"names":["value","renderLabel"],"mappings":"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"}
@@ -12,6 +12,7 @@ type AnchorComponentProps = {
12
12
  'aria-current'?: 'page';
13
13
  children: React.ReactNode;
14
14
  onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
15
+ rel?: string;
15
16
  };
16
17
  type PaginationLinkProps = {
17
18
  isActive?: boolean;
@@ -20,7 +20,7 @@ PaginationContent.displayName = "PaginationContent";
20
20
  const PaginationItem = React.forwardRef(({ className, ...props }, ref) => /* @__PURE__ */ jsx("li", { ref, className: cn(baseStyles.item, className), ...props }));
21
21
  PaginationItem.displayName = "PaginationItem";
22
22
  const PaginationLink = React.forwardRef(
23
- ({ className, isActive, size = "sm", disabled = false, href, ...props }, ref) => {
23
+ ({ className, isActive, size = "sm", disabled = false, href, rel, ...props }, ref) => {
24
24
  const buttonClassName = cn(
25
25
  buttonVariants({
26
26
  variant: isActive ? "primary" : "text",
@@ -39,6 +39,7 @@ const PaginationLink = React.forwardRef(
39
39
  {
40
40
  ref,
41
41
  href: href ?? "#",
42
+ rel,
42
43
  "aria-current": isActive ? "page" : void 0,
43
44
  className: cn(baseStyles.link, buttonClassName),
44
45
  children: props.children
@@ -52,10 +53,11 @@ const PaginationPrevious = React.forwardRef(
52
53
  PaginationLink,
53
54
  {
54
55
  ref,
55
- "aria-label": previousLabel !== "" ? previousLabel : "Go to previous page",
56
+ "aria-label": previousLabel || "Go to previous page",
56
57
  size: "sm",
57
58
  disabled,
58
59
  href,
60
+ rel: "prev",
59
61
  className: cn("flex items-center gap-2", baseStyles.button, className),
60
62
  ...props,
61
63
  children: [
@@ -71,10 +73,11 @@ const PaginationNext = React.forwardRef(
71
73
  PaginationLink,
72
74
  {
73
75
  ref,
74
- "aria-label": nextLabel === "" ? "Go to next page" : nextLabel,
76
+ "aria-label": nextLabel || "Go to next page",
75
77
  size: "sm",
76
78
  disabled,
77
79
  href,
80
+ rel: "next",
78
81
  className: cn("flex items-center gap-2", baseStyles.button, className),
79
82
  ...props,
80
83
  children: [
@@ -1 +1 @@
1
- {"version":3,"file":"Pagination.js","sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["'use client'\n\nimport { CaretLeft, CaretRight } from '@phosphor-icons/react/dist/ssr'\nimport * as React from 'react'\n\nimport { Button, ButtonProps, buttonVariants } from '../Button'\n\nimport { cn } from '@/lib/utils'\n\nconst baseStyles = {\n nav: 'flex justify-center',\n content: 'flex flex-row items-center gap-1',\n item: 'list-none',\n link: 'outline-none',\n button: 'min-w-8 px-2 transition-none',\n ellipsis: 'flex h-8 w-8 items-center justify-center',\n}\n\ntype BasePaginationProps = {\n className?: string\n}\n\ntype PaginationRootProps = BasePaginationProps & React.ComponentProps<'nav'>\ntype PaginationContentProps = BasePaginationProps & React.ComponentProps<'ul'>\ntype PaginationItemProps = BasePaginationProps & React.ComponentProps<'li'>\n\ntype AnchorComponentProps = {\n href: string\n className?: string\n 'aria-current'?: 'page'\n children: React.ReactNode\n onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n}\n\ntype PaginationLinkProps = {\n isActive?: boolean\n disabled?: boolean\n href?: string\n anchorComponent?: React.ComponentType<AnchorComponentProps>\n previousLabel?: string\n nextLabel?: string\n} & Pick<ButtonProps, 'size'> &\n React.ComponentProps<'a'>\n\n/** Base props shared between both link and button modes */\ntype BasePaginationComponentProps = {\n /** The current active page number (1-based indexing) */\n currentPage: number\n /** The total number of available pages */\n totalPages: number\n /** Optional CSS class name for styling the pagination container */\n className?: string\n labels?: {\n /** Text to show in the previous button (default: '') */\n previous?: string\n /** Text to show in the next button (default: '') */\n next?: string\n /** Aria label to show in the page number (default: 'Page {number}') */\n page?: string\n }\n}\n\n/** Props specific to link mode */\ntype LinkModePaginationProps = {\n mode: 'link'\n /**\n * Custom component to use for links (e.g., Next.js Link)\n * Must accept standard anchor props (href, className, etc.)\n */\n anchorComponent: React.ComponentType<AnchorComponentProps>\n /**\n * Function to generate the href for each page link\n */\n generateHref: (page: number) => string\n /** Optional callback when page changes - not required in link mode since links handle navigation */\n onPageChange?: (page: number) => void\n} & BasePaginationComponentProps\n\n/** Props specific to button mode */\ntype ButtonModePaginationProps = {\n mode: 'button'\n /** Callback function called when a page is selected - required in button mode */\n onPageChange: (page: number) => void\n /** These props are not used in button mode */\n anchorComponent?: never\n generateHref?: never\n} & BasePaginationComponentProps\n\n/** Union type for all possible pagination props */\nexport type PaginationProps =\n | LinkModePaginationProps\n | ButtonModePaginationProps\n | (Omit<BasePaginationComponentProps, 'mode'> & {\n mode: undefined\n onPageChange: (page: number) => void\n anchorComponent?: React.ComponentType<AnchorComponentProps>\n generateHref?: (page: number) => string\n })\n\nconst PaginationRoot = React.forwardRef<HTMLElement, PaginationRootProps>(({ className, ...props }, ref) => (\n <nav ref={ref} role=\"navigation\" aria-label=\"pagination\" className={cn(baseStyles.nav, className)} {...props} />\n))\nPaginationRoot.displayName = 'PaginationRoot'\n\nconst PaginationContent = React.forwardRef<HTMLUListElement, PaginationContentProps>(({ className, ...props }, ref) => (\n <ul ref={ref} className={cn(baseStyles.content, className)} {...props} />\n))\nPaginationContent.displayName = 'PaginationContent'\n\nconst PaginationItem = React.forwardRef<HTMLLIElement, PaginationItemProps>(({ className, ...props }, ref) => (\n <li ref={ref} className={cn(baseStyles.item, className)} {...props} />\n))\nPaginationItem.displayName = 'PaginationItem'\n\nconst PaginationLink = React.forwardRef<HTMLAnchorElement, PaginationLinkProps>(\n ({ className, isActive, size = 'sm', disabled = false, href, ...props }, ref) => {\n const buttonClassName = cn(\n buttonVariants({\n variant: isActive ? 'primary' : 'text',\n size,\n }),\n disabled && 'opacity-50',\n baseStyles.button,\n className,\n )\n\n if (disabled) {\n return (\n <Button variant=\"text\" size=\"sm\" disabled className={buttonClassName}>\n {props.children}\n </Button>\n )\n }\n\n const AnchorComponent = props.anchorComponent ?? 'a'\n\n return (\n <AnchorComponent\n ref={ref}\n href={href ?? '#'}\n aria-current={isActive ? 'page' : undefined}\n className={cn(baseStyles.link, buttonClassName)}\n >\n {props.children}\n </AnchorComponent>\n )\n },\n)\nPaginationLink.displayName = 'PaginationLink'\n\nconst PaginationPrevious = React.forwardRef<HTMLAnchorElement, PaginationLinkProps>(\n ({ className, disabled, href, previousLabel = '', ...props }, ref) => (\n <PaginationLink\n ref={ref}\n aria-label={previousLabel !== '' ? previousLabel : 'Go to previous page'}\n size=\"sm\"\n disabled={disabled}\n href={href}\n className={cn('flex items-center gap-2', baseStyles.button, className)}\n {...props}\n >\n <CaretLeft className=\"h-4 w-4 shrink-0\" />\n {previousLabel && <span>{previousLabel}</span>}\n </PaginationLink>\n ),\n)\nPaginationPrevious.displayName = 'PaginationPrevious'\n\nconst PaginationNext = React.forwardRef<HTMLAnchorElement, PaginationLinkProps>(\n ({ className, disabled, href, nextLabel = '', ...props }, ref) => (\n <PaginationLink\n ref={ref}\n aria-label={nextLabel === '' ? 'Go to next page' : nextLabel}\n size=\"sm\"\n disabled={disabled}\n href={href}\n className={cn('flex items-center gap-2', baseStyles.button, className)}\n {...props}\n >\n {nextLabel && <span>{nextLabel}</span>}\n <CaretRight className=\"h-4 w-4 shrink-0\" />\n </PaginationLink>\n ),\n)\nPaginationNext.displayName = 'PaginationNext'\n\nconst PaginationEllipsis = ({ className }: BasePaginationProps) => (\n <span className={cn(baseStyles.ellipsis, className)}>...</span>\n)\nPaginationEllipsis.displayName = 'PaginationEllipsis'\n\n/**\n * A pagination component that displays page numbers and navigation controls.\n *\n * @example\n * ```tsx\n * // Basic usage with anchor tags\n * <Pagination\n * currentPage={1}\n * totalPages={10}\n * />\n *\n * // With Next.js Link component\n * <Pagination\n * currentPage={1}\n * totalPages={10}\n * onPageChange={setPage}\n * anchorComponent={Link}\n * generateHref={(page) => `/posts?page=${page}`}\n * />\n *\n * // Client-side table navigation\n * <Pagination\n * currentPage={1}\n * totalPages={10}\n * onPageChange={setPage}\n * mode=\"button\"\n * />\n * ```\n */\nconst Pagination: React.FC<PaginationProps> = ({\n currentPage,\n totalPages,\n onPageChange,\n className,\n mode = 'button',\n anchorComponent,\n generateHref = () => '#',\n labels = {\n previous: '',\n next: '',\n page: 'Page {number}',\n },\n}) => {\n const handlePageChange = (pageNum: number) => {\n if (onPageChange) {\n onPageChange(pageNum)\n }\n }\n\n const renderPageNumbers = () => {\n const items: Array<React.ReactNode> = []\n\n const addPageNumber = (pageNum: number) => {\n const commonProps = {\n isActive: currentPage === pageNum,\n href: mode === 'link' ? generateHref(pageNum) : undefined,\n anchorComponent,\n pageLabel: labels.page,\n }\n\n items.push(\n <PaginationItem\n key={pageNum}\n aria-label={labels.page ? labels.page.replace('{number}', pageNum.toString()) : undefined}\n >\n {mode === 'button' ? (\n <Button\n variant={commonProps.isActive ? 'primary' : 'text'}\n size=\"sm\"\n className={baseStyles.button}\n onClick={(event: React.MouseEvent) => {\n event.preventDefault()\n handlePageChange(pageNum)\n }}\n aria-label={\n labels.page ? labels.page.replace('{number}', pageNum.toString()) : `Go to page ${pageNum.toString()}`\n }\n aria-current={commonProps.isActive ? 'page' : undefined}\n >\n {pageNum}\n </Button>\n ) : (\n <PaginationLink {...commonProps}>{pageNum}</PaginationLink>\n )}\n </PaginationItem>,\n )\n }\n\n addPageNumber(1)\n\n if (currentPage > 3) {\n items.push(<PaginationEllipsis key=\"ellipsis-1\" />)\n }\n\n for (let index = Math.max(2, currentPage - 1); index <= Math.min(totalPages - 1, currentPage + 1); index++) {\n addPageNumber(index)\n }\n\n if (currentPage < totalPages - 2) {\n items.push(<PaginationEllipsis key=\"ellipsis-2\" />)\n }\n\n if (totalPages > 1) {\n addPageNumber(totalPages)\n }\n\n return items\n }\n\n const commonNavProps = (page: number) => ({\n onClick: (event: React.MouseEvent) => {\n event.preventDefault()\n handlePageChange(page)\n },\n href: generateHref(page),\n anchorComponent,\n })\n\n return (\n <PaginationRoot className={className}>\n <PaginationContent>\n <PaginationItem>\n {mode === 'button' ? (\n <Button\n variant=\"text\"\n size=\"sm\"\n className={cn('flex items-center gap-2', baseStyles.button, currentPage === 1 && 'opacity-50')}\n onClick={currentPage > 1 ? commonNavProps(currentPage - 1).onClick : undefined}\n disabled={currentPage === 1}\n aria-label={labels.previous === '' ? 'Go to previous page' : labels.previous}\n >\n <CaretLeft className=\"h-4 w-4 shrink-0\" />\n {labels.previous && <span>{labels.previous}</span>}\n </Button>\n ) : (\n <PaginationPrevious\n {...commonNavProps(currentPage - 1)}\n disabled={currentPage === 1}\n previousLabel={labels.previous}\n />\n )}\n </PaginationItem>\n {renderPageNumbers()}\n <PaginationItem>\n {mode === 'button' ? (\n <Button\n variant=\"text\"\n size=\"sm\"\n className={cn('flex items-center gap-2', baseStyles.button, currentPage === totalPages && 'opacity-50')}\n onClick={currentPage < totalPages ? commonNavProps(currentPage + 1).onClick : undefined}\n disabled={currentPage === totalPages || totalPages === 0}\n aria-label={labels.next === '' ? 'Go to next page' : labels.next}\n >\n {labels.next && <span>{labels.next}</span>}\n <CaretRight className=\"h-4 w-4 shrink-0\" />\n </Button>\n ) : (\n <PaginationNext\n {...commonNavProps(currentPage + 1)}\n disabled={currentPage === totalPages || totalPages === 0}\n nextLabel={labels.next}\n />\n )}\n </PaginationItem>\n </PaginationContent>\n </PaginationRoot>\n )\n}\n\nexport {\n PaginationRoot,\n PaginationContent,\n PaginationItem,\n PaginationLink,\n PaginationNext,\n PaginationPrevious,\n PaginationEllipsis,\n Pagination,\n}\n"],"names":[],"mappings":";;;;;;;AASA;AAAmB;AACZ;AACI;AACH;AACA;AACE;AAEV;AAmFA;AAGA;AAEA;AAGA;AAEA;AAGA;AAEA;AAA6B;AAEzB;AAAwB;AACP;AACmB;AAChC;AACD;AACW;AACD;AACX;AAGF;AACE;AAGE;AAIJ;AAEA;AACE;AAAC;AAAA;AACC;AACc;AACoB;AACY;AAEvC;AAAA;AACT;AAGN;AACA;AAEA;AAAiC;AAE7B;AAAC;AAAA;AACC;AACmD;AAC9C;AACL;AACA;AACqE;AACjE;AAEJ;AAAwC;AACD;AAAA;AAAA;AAG7C;AACA;AAEA;AAA6B;AAEzB;AAAC;AAAA;AACC;AACmD;AAC9C;AACL;AACA;AACqE;AACjE;AAEH;AAA8B;AACU;AAAA;AAAA;AAG/C;AACA;AAEA;AAGA;AA+BA;AAA+C;AAC7C;AACA;AACA;AACA;AACO;AACP;AACqB;AACZ;AACG;AACJ;AACA;AAEV;AACE;AACE;AACE;AAAoB;AACtB;AAGF;AACE;AAEA;AACE;AAAoB;AACQ;AACsB;AAChD;AACkB;AAGpB;AAAM;AACJ;AAAC;AAAA;AAEiF;AAG9E;AAAC;AAAA;AAC6C;AACvC;AACiB;AAEpB;AACA;AAAwB;AAC1B;AAEsG;AAExD;AAE7C;AAAA;AAGuC;AAAA;AApBvC;AAsBP;AACF;AAGF;AAEA;AACE;AAAkD;AAGpD;AACE;AAAmB;AAGrB;AACE;AAAkD;AAGpD;AACE;AAAwB;AAG1B;AAAO;AAGT;AAA0C;AAEtC;AACA;AAAqB;AACvB;AACuB;AACvB;AAGF;AAGM;AAEI;AAAC;AAAA;AACS;AACH;AACwF;AACxB;AAC3C;AAC0C;AAEpE;AAAwC;AACG;AAAA;AAAA;AAG7C;AAAC;AAAA;AACmC;AACR;AACJ;AAAA;AAG5B;AACmB;AAGf;AAAC;AAAA;AACS;AACH;AACiG;AACxB;AACvB;AACK;AAE3D;AAAkC;AACM;AAAA;AAAA;AAG3C;AAAC;AAAA;AACmC;AACqB;AACrC;AAAA;AAGxB;AAIR;;"}
1
+ {"version":3,"file":"Pagination.js","sources":["../../../src/components/Pagination/Pagination.tsx"],"sourcesContent":["'use client'\n\nimport { CaretLeft, CaretRight } from '@phosphor-icons/react/dist/ssr'\nimport * as React from 'react'\n\nimport { Button, ButtonProps, buttonVariants } from '../Button'\n\nimport { cn } from '@/lib/utils'\n\nconst baseStyles = {\n nav: 'flex justify-center',\n content: 'flex flex-row items-center gap-1',\n item: 'list-none',\n link: 'outline-none',\n button: 'min-w-8 px-2 transition-none',\n ellipsis: 'flex h-8 w-8 items-center justify-center',\n}\n\ntype BasePaginationProps = {\n className?: string\n}\n\ntype PaginationRootProps = BasePaginationProps & React.ComponentProps<'nav'>\ntype PaginationContentProps = BasePaginationProps & React.ComponentProps<'ul'>\ntype PaginationItemProps = BasePaginationProps & React.ComponentProps<'li'>\n\ntype AnchorComponentProps = {\n href: string\n className?: string\n 'aria-current'?: 'page'\n children: React.ReactNode\n onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void\n rel?: string\n}\n\ntype PaginationLinkProps = {\n isActive?: boolean\n disabled?: boolean\n href?: string\n anchorComponent?: React.ComponentType<AnchorComponentProps>\n previousLabel?: string\n nextLabel?: string\n} & Pick<ButtonProps, 'size'> &\n React.ComponentProps<'a'>\n\n/** Base props shared between both link and button modes */\ntype BasePaginationComponentProps = {\n /** The current active page number (1-based indexing) */\n currentPage: number\n /** The total number of available pages */\n totalPages: number\n /** Optional CSS class name for styling the pagination container */\n className?: string\n labels?: {\n /** Text to show in the previous button (default: '') */\n previous?: string\n /** Text to show in the next button (default: '') */\n next?: string\n /** Aria label to show in the page number (default: 'Page {number}') */\n page?: string\n }\n}\n\n/** Props specific to link mode */\ntype LinkModePaginationProps = {\n mode: 'link'\n /**\n * Custom component to use for links (e.g., Next.js Link)\n * Must accept standard anchor props (href, className, etc.)\n */\n anchorComponent: React.ComponentType<AnchorComponentProps>\n /**\n * Function to generate the href for each page link\n */\n generateHref: (page: number) => string\n /** Optional callback when page changes - not required in link mode since links handle navigation */\n onPageChange?: (page: number) => void\n} & BasePaginationComponentProps\n\n/** Props specific to button mode */\ntype ButtonModePaginationProps = {\n mode: 'button'\n /** Callback function called when a page is selected - required in button mode */\n onPageChange: (page: number) => void\n /** These props are not used in button mode */\n anchorComponent?: never\n generateHref?: never\n} & BasePaginationComponentProps\n\n/** Union type for all possible pagination props */\nexport type PaginationProps =\n | LinkModePaginationProps\n | ButtonModePaginationProps\n | (Omit<BasePaginationComponentProps, 'mode'> & {\n mode: undefined\n onPageChange: (page: number) => void\n anchorComponent?: React.ComponentType<AnchorComponentProps>\n generateHref?: (page: number) => string\n })\n\nconst PaginationRoot = React.forwardRef<HTMLElement, PaginationRootProps>(({ className, ...props }, ref) => (\n <nav ref={ref} role=\"navigation\" aria-label=\"pagination\" className={cn(baseStyles.nav, className)} {...props} />\n))\nPaginationRoot.displayName = 'PaginationRoot'\n\nconst PaginationContent = React.forwardRef<HTMLUListElement, PaginationContentProps>(({ className, ...props }, ref) => (\n <ul ref={ref} className={cn(baseStyles.content, className)} {...props} />\n))\nPaginationContent.displayName = 'PaginationContent'\n\nconst PaginationItem = React.forwardRef<HTMLLIElement, PaginationItemProps>(({ className, ...props }, ref) => (\n <li ref={ref} className={cn(baseStyles.item, className)} {...props} />\n))\nPaginationItem.displayName = 'PaginationItem'\n\nconst PaginationLink = React.forwardRef<HTMLAnchorElement, PaginationLinkProps>(\n ({ className, isActive, size = 'sm', disabled = false, href, rel, ...props }, ref) => {\n const buttonClassName = cn(\n buttonVariants({\n variant: isActive ? 'primary' : 'text',\n size,\n }),\n disabled && 'opacity-50',\n baseStyles.button,\n className,\n )\n\n if (disabled) {\n return (\n <Button variant=\"text\" size=\"sm\" disabled className={buttonClassName}>\n {props.children}\n </Button>\n )\n }\n\n const AnchorComponent = props.anchorComponent ?? 'a'\n\n return (\n <AnchorComponent\n ref={ref}\n href={href ?? '#'}\n rel={rel}\n aria-current={isActive ? 'page' : undefined}\n className={cn(baseStyles.link, buttonClassName)}\n >\n {props.children}\n </AnchorComponent>\n )\n },\n)\nPaginationLink.displayName = 'PaginationLink'\n\nconst PaginationPrevious = React.forwardRef<HTMLAnchorElement, PaginationLinkProps>(\n ({ className, disabled, href, previousLabel = '', ...props }, ref) => (\n <PaginationLink\n ref={ref}\n aria-label={previousLabel || 'Go to previous page'}\n size=\"sm\"\n disabled={disabled}\n href={href}\n rel=\"prev\"\n className={cn('flex items-center gap-2', baseStyles.button, className)}\n {...props}\n >\n <CaretLeft className=\"h-4 w-4 shrink-0\" />\n {previousLabel && <span>{previousLabel}</span>}\n </PaginationLink>\n ),\n)\nPaginationPrevious.displayName = 'PaginationPrevious'\n\nconst PaginationNext = React.forwardRef<HTMLAnchorElement, PaginationLinkProps>(\n ({ className, disabled, href, nextLabel = '', ...props }, ref) => (\n <PaginationLink\n ref={ref}\n aria-label={nextLabel || 'Go to next page'}\n size=\"sm\"\n disabled={disabled}\n href={href}\n rel=\"next\"\n className={cn('flex items-center gap-2', baseStyles.button, className)}\n {...props}\n >\n {nextLabel && <span>{nextLabel}</span>}\n <CaretRight className=\"h-4 w-4 shrink-0\" />\n </PaginationLink>\n ),\n)\nPaginationNext.displayName = 'PaginationNext'\n\nconst PaginationEllipsis = ({ className }: BasePaginationProps) => (\n <span className={cn(baseStyles.ellipsis, className)}>...</span>\n)\nPaginationEllipsis.displayName = 'PaginationEllipsis'\n\n/**\n * A pagination component that displays page numbers and navigation controls.\n *\n * @example\n * ```tsx\n * // Basic usage with anchor tags\n * <Pagination\n * currentPage={1}\n * totalPages={10}\n * />\n *\n * // With Next.js Link component\n * <Pagination\n * currentPage={1}\n * totalPages={10}\n * onPageChange={setPage}\n * anchorComponent={Link}\n * generateHref={(page) => `/posts?page=${page}`}\n * />\n *\n * // Client-side table navigation\n * <Pagination\n * currentPage={1}\n * totalPages={10}\n * onPageChange={setPage}\n * mode=\"button\"\n * />\n * ```\n */\nconst Pagination: React.FC<PaginationProps> = ({\n currentPage,\n totalPages,\n onPageChange,\n className,\n mode = 'button',\n anchorComponent,\n generateHref = () => '#',\n labels = {\n previous: '',\n next: '',\n page: 'Page {number}',\n },\n}) => {\n const handlePageChange = (pageNum: number) => {\n if (onPageChange) {\n onPageChange(pageNum)\n }\n }\n\n const renderPageNumbers = () => {\n const items: Array<React.ReactNode> = []\n\n const addPageNumber = (pageNum: number) => {\n const commonProps = {\n isActive: currentPage === pageNum,\n href: mode === 'link' ? generateHref(pageNum) : undefined,\n anchorComponent,\n pageLabel: labels.page,\n }\n\n items.push(\n <PaginationItem\n key={pageNum}\n aria-label={labels.page ? labels.page.replace('{number}', pageNum.toString()) : undefined}\n >\n {mode === 'button' ? (\n <Button\n variant={commonProps.isActive ? 'primary' : 'text'}\n size=\"sm\"\n className={baseStyles.button}\n onClick={(event: React.MouseEvent) => {\n event.preventDefault()\n handlePageChange(pageNum)\n }}\n aria-label={\n labels.page ? labels.page.replace('{number}', pageNum.toString()) : `Go to page ${pageNum.toString()}`\n }\n aria-current={commonProps.isActive ? 'page' : undefined}\n >\n {pageNum}\n </Button>\n ) : (\n <PaginationLink {...commonProps}>{pageNum}</PaginationLink>\n )}\n </PaginationItem>,\n )\n }\n\n addPageNumber(1)\n\n if (currentPage > 3) {\n items.push(<PaginationEllipsis key=\"ellipsis-1\" />)\n }\n\n for (let index = Math.max(2, currentPage - 1); index <= Math.min(totalPages - 1, currentPage + 1); index++) {\n addPageNumber(index)\n }\n\n if (currentPage < totalPages - 2) {\n items.push(<PaginationEllipsis key=\"ellipsis-2\" />)\n }\n\n if (totalPages > 1) {\n addPageNumber(totalPages)\n }\n\n return items\n }\n\n const commonNavProps = (page: number) => ({\n onClick: (event: React.MouseEvent) => {\n event.preventDefault()\n handlePageChange(page)\n },\n href: generateHref(page),\n anchorComponent,\n })\n\n return (\n <PaginationRoot className={className}>\n <PaginationContent>\n <PaginationItem>\n {mode === 'button' ? (\n <Button\n variant=\"text\"\n size=\"sm\"\n className={cn('flex items-center gap-2', baseStyles.button, currentPage === 1 && 'opacity-50')}\n onClick={currentPage > 1 ? commonNavProps(currentPage - 1).onClick : undefined}\n disabled={currentPage === 1}\n aria-label={labels.previous === '' ? 'Go to previous page' : labels.previous}\n >\n <CaretLeft className=\"h-4 w-4 shrink-0\" />\n {labels.previous && <span>{labels.previous}</span>}\n </Button>\n ) : (\n <PaginationPrevious\n {...commonNavProps(currentPage - 1)}\n disabled={currentPage === 1}\n previousLabel={labels.previous}\n />\n )}\n </PaginationItem>\n {renderPageNumbers()}\n <PaginationItem>\n {mode === 'button' ? (\n <Button\n variant=\"text\"\n size=\"sm\"\n className={cn('flex items-center gap-2', baseStyles.button, currentPage === totalPages && 'opacity-50')}\n onClick={currentPage < totalPages ? commonNavProps(currentPage + 1).onClick : undefined}\n disabled={currentPage === totalPages || totalPages === 0}\n aria-label={labels.next === '' ? 'Go to next page' : labels.next}\n >\n {labels.next && <span>{labels.next}</span>}\n <CaretRight className=\"h-4 w-4 shrink-0\" />\n </Button>\n ) : (\n <PaginationNext\n {...commonNavProps(currentPage + 1)}\n disabled={currentPage === totalPages || totalPages === 0}\n nextLabel={labels.next}\n />\n )}\n </PaginationItem>\n </PaginationContent>\n </PaginationRoot>\n )\n}\n\nexport {\n PaginationRoot,\n PaginationContent,\n PaginationItem,\n PaginationLink,\n PaginationNext,\n PaginationPrevious,\n PaginationEllipsis,\n Pagination,\n}\n"],"names":[],"mappings":";;;;;;;AASA;AAAmB;AACZ;AACI;AACH;AACA;AACE;AAEV;AAoFA;AAGA;AAEA;AAGA;AAEA;AAGA;AAEA;AAA6B;AAEzB;AAAwB;AACP;AACmB;AAChC;AACD;AACW;AACD;AACX;AAGF;AACE;AAGE;AAIJ;AAEA;AACE;AAAC;AAAA;AACC;AACc;AACd;AACkC;AACY;AAEvC;AAAA;AACT;AAGN;AACA;AAEA;AAAiC;AAE7B;AAAC;AAAA;AACC;AAC6B;AACxB;AACL;AACA;AACI;AACiE;AACjE;AAEJ;AAAwC;AACD;AAAA;AAAA;AAG7C;AACA;AAEA;AAA6B;AAEzB;AAAC;AAAA;AACC;AACyB;AACpB;AACL;AACA;AACI;AACiE;AACjE;AAEH;AAA8B;AACU;AAAA;AAAA;AAG/C;AACA;AAEA;AAGA;AA+BA;AAA+C;AAC7C;AACA;AACA;AACA;AACO;AACP;AACqB;AACZ;AACG;AACJ;AACA;AAEV;AACE;AACE;AACE;AAAoB;AACtB;AAGF;AACE;AAEA;AACE;AAAoB;AACQ;AACsB;AAChD;AACkB;AAGpB;AAAM;AACJ;AAAC;AAAA;AAEiF;AAG9E;AAAC;AAAA;AAC6C;AACvC;AACiB;AAEpB;AACA;AAAwB;AAC1B;AAEsG;AAExD;AAE7C;AAAA;AAGuC;AAAA;AApBvC;AAsBP;AACF;AAGF;AAEA;AACE;AAAkD;AAGpD;AACE;AAAmB;AAGrB;AACE;AAAkD;AAGpD;AACE;AAAwB;AAG1B;AAAO;AAGT;AAA0C;AAEtC;AACA;AAAqB;AACvB;AACuB;AACvB;AAGF;AAGM;AAEI;AAAC;AAAA;AACS;AACH;AACwF;AACxB;AAC3C;AAC0C;AAEpE;AAAwC;AACG;AAAA;AAAA;AAG7C;AAAC;AAAA;AACmC;AACR;AACJ;AAAA;AAG5B;AACmB;AAGf;AAAC;AAAA;AACS;AACH;AACiG;AACxB;AACvB;AACK;AAE3D;AAAkC;AACM;AAAA;AAAA;AAG3C;AAAC;AAAA;AACmC;AACqB;AACrC;AAAA;AAGxB;AAIR;;"}
package/dist/index.js CHANGED
@@ -35,6 +35,10 @@ export { Marquee } from './components/Marquee/Marquee.js';
35
35
  export { DatePicker } from './components/DatePicker/DatePicker.js';
36
36
  export { SidebarContent, SidebarFooter, SidebarGroup, SidebarGroupAction, SidebarGroupContent, SidebarGroupLabel, SidebarHeader, SidebarInput, SidebarInset, SidebarMenu, SidebarMenuAction, SidebarMenuBadge, SidebarMenuButton, SidebarMenuItem, SidebarMenuSkeleton, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, SidebarProvider, SidebarRail, SidebarRoot, SidebarSeparator, SidebarTrigger, useSidebar } from './components/Sidebar/Sidebar.js';
37
37
  export { Combobox } from './components/Combobox/Combobox.js';
38
+ export { ComboboxOptionWithTooltip } from './components/Combobox/ComboboxOptionWithTooltip.js';
39
+ export { StaticComboboxList } from './components/Combobox/StaticComboboxList.js';
40
+ export { useCombobox } from './components/Combobox/useCombobox.js';
41
+ export { VirtualizedComboboxList } from './components/Combobox/VirtualizedComboboxList.js';
38
42
  export { DataTable } from './components/DataTable/DataTable.js';
39
43
  export { CollapsibleContent, CollapsibleRoot, CollapsibleTrigger } from './components/Collapsible/Collapsible.js';
40
44
  export { InlineMultiSelect } from './components/InlineMultiSelect/InlineMultiSelect.js';
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
1
+ {"version":3,"file":"index.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;"}
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "periplo-ui",
3
3
  "description": "IATI UI library",
4
4
  "private": false,
5
- "version": "3.35.2",
5
+ "version": "3.37.0",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
8
8
  "types": "dist/index.d.ts",
@@ -120,6 +120,7 @@
120
120
  "@radix-ui/react-tooltip": "1.2.7",
121
121
  "@tailwindcss/vite": "^4.1.11",
122
122
  "@tanstack/react-table": "8.21.2",
123
+ "@tanstack/react-virtual": "^3.13.12",
123
124
  "class-variance-authority": "0.7.1",
124
125
  "clsx": "2.1.1",
125
126
  "cmdk": "1.1.1",
@@ -128,6 +129,7 @@
128
129
  "glob": "11.0.3",
129
130
  "react-day-picker": "9.7.0",
130
131
  "react-hook-form": "7.59.0",
132
+ "react-intersection-observer": "^9.16.0",
131
133
  "sonner": "2.0.5",
132
134
  "tailwind-merge": "3.3.1",
133
135
  "tw-animate-css": "^1.3.5",