@wireservers-ui/react-natives 2.0.2-rc.0 → 2.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,38 @@ Versioning: [Semantic Versioning](https://semver.org/)
7
7
 
8
8
  ---
9
9
 
10
+ ## [2.0.4] - 2026-07-02
11
+
12
+ ### Added
13
+ - `Select` now supports multi-select state with `isMulti`, `selectedValues`, `defaultValues`, `onValuesChange`, and configurable close-on-select behavior.
14
+ - `SelectSearchInput` filters dropdown items using built-in text matching or a custom `filterOption` callback.
15
+ - `SelectSelectedBadges` renders selected values as removable badges with class overrides, `maxVisible`, overflow labels, and custom badge rendering.
16
+
17
+ ### Changed
18
+ - `SelectItem` toggles selected values in multi-select mode and preserves single-select behavior by default.
19
+ - `SelectInput` now uses the root `Select` placeholder when no local placeholder override is provided.
20
+
21
+ ---
22
+
23
+ ## [2.0.3] - 2026-04-18
24
+
25
+ ### Fixed
26
+ - Repository URL updated from `wireservers/wireservers-ui` to `wireservers/react-natives` following GitHub repo rename — resolves 404 on npmjs Release Notes link
27
+
28
+ ### Changed
29
+ - CI workflows migrated from `dev`/`prod`/`sot` branch model to `develop`/`main` gitflow
30
+ - `babel-preset-expo` added as explicit dependency to resolve peer dependency resolution failures in certain environments
31
+ - Docs site Dockerfile and nginx config added for self-hosted deployment
32
+
33
+ ---
34
+
35
+ ## [2.0.2] - 2026-03-21
36
+
37
+ ### Changed
38
+ - Promoted `2.0.2-rc.0` to stable `2.0.2` with no additional code changes
39
+
40
+ ---
41
+
10
42
  ## [2.0.2-rc.0] - 2026-03-21
11
43
 
12
44
  ### Added
package/README.md CHANGED
@@ -2,24 +2,25 @@
2
2
 
3
3
  **A comprehensive React Native component library built with NativeWind and Tailwind Variants.**
4
4
 
5
- > **Note:** This project is in active development (v2.0.1). We're building out components, documentation, and tooling. Contributions and feedback are welcome!
5
+ > **Note:** This project is in active development (v2.0.4). We're building out components, documentation, and tooling. Contributions and feedback are welcome!
6
6
 
7
7
  [![MIT License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
8
8
  [![npm version](https://img.shields.io/npm/v/@wireservers-ui/react-natives.svg)](https://www.npmjs.com/package/@wireservers-ui/react-natives)
9
9
 
10
10
  ---
11
11
 
12
- ## Release Notes (v2.0.1)
12
+ ## Release Notes (v2.0.4)
13
13
 
14
14
  ### What was updated
15
15
 
16
- - Published package contents now include `bin/init.js` (packaging parity fix)
17
- - This ensures the installed npm artifact contains the same CLI helper file expected by local package contents
16
+ - `Select` now supports searchable multi-select usage with controlled and uncontrolled selected values
17
+ - `SelectSearchInput` filters dropdown items while keeping existing single-select usage unchanged
18
+ - `SelectSelectedBadges` displays removable selected-value badges with styling and rendering overrides
18
19
 
19
20
  ### Changelog
20
21
 
21
22
  - Full changelog is included in the package at `CHANGELOG.md`
22
- - GitHub: https://github.com/wireservers/wireservers-ui/blob/dev/packages/react-natives/CHANGELOG.md
23
+ - GitHub: https://github.com/wireservers/react-natives/blob/main/packages/react-natives/CHANGELOG.md
23
24
  - npm package page: open the `CHANGELOG.md` file from the package contents/files list
24
25
 
25
26
  ---
@@ -32,17 +33,8 @@ React-Natives is a collection of 70+ production-ready, accessible React Native c
32
33
 
33
34
  ### Key Features
34
35
 
35
- - **70+ Components** — Buttons, forms, modals, tables, calendars, color pickers, and more
36
- - **TypeScript First** — Fully typed with excellent IDE autocomplete
37
- - **NativeWind + Tailwind Variants** — Utility-first styling with type-safe variant APIs
38
- - **Themeable** — CSS variable-based color system with light/dark mode support
39
- - **Accessible** — Built with accessibility in mind
40
- - **Cross Platform** — iOS, Android, and Web via React Native
41
36
 
42
- ### Tech Stack
43
-
44
- | Layer | Technology |
45
- | --- | --- |
37
+ // ...existing code...
46
38
  | Components | React Native |
47
39
  | Styling | NativeWind v4, Tailwind CSS 3, Tailwind Variants |
48
40
  | Types | TypeScript |
@@ -55,16 +47,21 @@ React-Natives is a collection of 70+ production-ready, accessible React Native c
55
47
 
56
48
  Use this for the fastest first-time setup in a new Expo app.
57
49
 
50
+ 1. Create a new Expo app.
51
+ 2. Enter the app folder.
52
+ 3. Install `@wireservers-ui/react-natives`.
53
+ 4. Run `init`.
54
+ 5. Start Expo web.
55
+
58
56
  ```bash
59
- mkdir -p demos/react-natives
60
- npx create-expo-app@latest demos/react-natives/project --template blank-typescript
61
- cd demos/react-natives/project
62
- npm install @wireservers-ui/react-natives@2.0.1
63
- npx @wireservers-ui/react-natives@2.0.1 init
57
+ npx create-expo-app@latest wsui --template blank-typescript
58
+ cd wsui
59
+ npm install @wireservers-ui/react-natives
60
+ npx react-natives init
64
61
  npm exec expo -- start --clear --web
65
62
  ```
66
63
 
67
- This sequence keeps all generated files inside `demos/react-natives/project`.
64
+ This sequence keeps all generated files inside `wsui`.
68
65
  No setup files are written outside the generated project folder when run exactly as above.
69
66
 
70
67
  If `npm exec` prompts to install `expo`, answer `y`.
@@ -86,8 +83,8 @@ npm exec expo -- start --clear --web
86
83
  Always run the init command from inside your generated project folder:
87
84
 
88
85
  ```bash
89
- cd demos/react-natives/project
90
- npx @wireservers-ui/react-natives@2.0.1 init
86
+ cd wsui
87
+ npx react-natives init
91
88
  ```
92
89
 
93
90
  Do not run `init` from the repository root.
@@ -99,6 +96,12 @@ What `init` does for you:
99
96
  - Creates setup files only if missing
100
97
  - Never overwrites existing files in your project
101
98
 
99
+ To overwrite generated setup files intentionally, use:
100
+
101
+ ```bash
102
+ npx react-natives init --force
103
+ ```
104
+
102
105
  ### 2. Manual Setup (Detailed)
103
106
 
104
107
  Use this path if you want full control over every file.
@@ -106,7 +109,7 @@ Use this path if you want full control over every file.
106
109
  #### Step A: Install packages
107
110
 
108
111
  ```bash
109
- npm install @wireservers-ui/react-natives@2.0.1
112
+ npm install @wireservers-ui/react-natives
110
113
  npm install nativewind babel-preset-expo tailwindcss tailwind-variants tailwind-merge react-native-reanimated react-native-worklets react-native-svg react-native-web react-dom
111
114
  ```
112
115
 
@@ -598,7 +601,7 @@ react-natives/
598
601
 
599
602
  ```bash
600
603
  # Clone the repository
601
- git clone https://github.com/wireservers/wireservers-ui.git
604
+ git clone https://github.com/wireservers/react-natives.git
602
605
  cd wireservers-ui/packages/react-natives
603
606
 
604
607
  # Install dependencies
package/bin/cli.js CHANGED
@@ -44,7 +44,7 @@ if (!isExpoProject(cwd)) {
44
44
  " npx create-expo-app@latest demos/react-natives/project --template blank-typescript",
45
45
  );
46
46
  console.error(" cd demos/react-natives/project");
47
- console.error(" npx @wireservers-ui/react-natives@2.0.1 init\n");
47
+ console.error(" npx @wireservers-ui/react-natives@2.0.2 init\n");
48
48
  process.exit(1);
49
49
  }
50
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wireservers-ui/react-natives",
3
- "version": "2.0.2-rc.0",
3
+ "version": "2.0.4",
4
4
  "description": "A comprehensive React Native component library built with NativeWind and Tailwind Variants",
5
5
  "main": "src/index.ts",
6
6
  "types": "src/index.ts",
@@ -11,7 +11,7 @@
11
11
  "CHANGELOG.md"
12
12
  ],
13
13
  "bin": {
14
- "react-natives": "./bin/cli.js"
14
+ "react-natives": "bin/cli.js"
15
15
  },
16
16
  "scripts": {},
17
17
  "keywords": [
@@ -24,7 +24,7 @@
24
24
  ],
25
25
  "repository": {
26
26
  "type": "git",
27
- "url": "https://github.com/wireservers/wireservers-ui.git",
27
+ "url": "git+https://github.com/wireservers/react-natives.git",
28
28
  "directory": "packages/react-natives"
29
29
  },
30
30
  "license": "MIT",
@@ -47,4 +47,4 @@
47
47
  "nativewind": "^4.2.3",
48
48
  "tailwind-variants": "^0.1.20"
49
49
  }
50
- }
50
+ }
package/src/index.ts CHANGED
@@ -255,11 +255,14 @@ export {
255
255
  SelectItemContext,
256
256
  useSelectItemContext,
257
257
  SelectItemText,
258
+ SelectSearchInput,
259
+ SelectSelectedBadges,
258
260
  SelectDragIndicator,
259
261
  SelectScrollView,
260
262
  } from './select';
261
263
  export type {
262
264
  SelectProps,
265
+ SelectOption,
263
266
  SelectTriggerProps,
264
267
  SelectInputProps,
265
268
  SelectIconProps,
@@ -268,6 +271,8 @@ export type {
268
271
  SelectContentProps,
269
272
  SelectItemProps,
270
273
  SelectItemTextProps,
274
+ SelectSearchInputProps,
275
+ SelectSelectedBadgesProps,
271
276
  SelectDragIndicatorProps,
272
277
  SelectScrollViewProps,
273
278
  SelectSize,
@@ -7,10 +7,13 @@ export { SelectBackdrop } from './select-backdrop';
7
7
  export { SelectContent } from './select-content';
8
8
  export { SelectItem, SelectItemContext, useSelectItemContext } from './select-item';
9
9
  export { SelectItemText } from './select-item-text';
10
+ export { SelectSearchInput } from './select-search-input';
11
+ export { SelectSelectedBadges } from './select-selected-badges';
10
12
  export { SelectDragIndicator } from './select-drag-indicator';
11
13
  export { SelectScrollView } from './select-scroll-view';
12
14
  export type {
13
15
  SelectProps,
16
+ SelectOption,
14
17
  SelectTriggerProps,
15
18
  SelectInputProps,
16
19
  SelectIconProps,
@@ -19,6 +22,8 @@ export type {
19
22
  SelectContentProps,
20
23
  SelectItemProps,
21
24
  SelectItemTextProps,
25
+ SelectSearchInputProps,
26
+ SelectSelectedBadgesProps,
22
27
  SelectDragIndicatorProps,
23
28
  SelectScrollViewProps,
24
29
  SelectSize,
@@ -8,10 +8,10 @@ export const SelectInput = React.forwardRef<
8
8
  React.ElementRef<typeof Text>,
9
9
  SelectInputProps
10
10
  >(({ className, placeholder, ...props }, ref) => {
11
- const { selectedLabel, size } = useSelectContext();
11
+ const { selectedLabel, size, placeholder: contextPlaceholder } = useSelectContext();
12
12
 
13
13
  const hasValue = !!selectedLabel;
14
- const displayText = hasValue ? selectedLabel : (placeholder ?? 'Select...');
14
+ const displayText = hasValue ? selectedLabel : (placeholder ?? contextPlaceholder);
15
15
 
16
16
  return (
17
17
  <Text
@@ -25,17 +25,32 @@ export const SelectItem = React.forwardRef<
25
25
  React.ElementRef<typeof Pressable>,
26
26
  SelectItemProps
27
27
  >(({ className, label, value, isDisabled = false, children, ...props }, ref) => {
28
- const { selectedValue, onValueChange, onClose, size } = useSelectContext();
28
+ const {
29
+ isMulti,
30
+ onValueChange,
31
+ size,
32
+ isValueSelected,
33
+ shouldShowItem,
34
+ registerItem,
35
+ } = useSelectContext();
29
36
 
30
- const isSelected = selectedValue === value;
37
+ const isSelected = isValueSelected(value);
38
+ const isVisible = shouldShowItem(value, label);
39
+
40
+ React.useEffect(() => {
41
+ registerItem(value, label);
42
+ }, [label, registerItem, value]);
31
43
 
32
44
  const handlePress = () => {
33
45
  if (!isDisabled) {
34
46
  onValueChange(value, label);
35
- onClose();
36
47
  }
37
48
  };
38
49
 
50
+ if (!isVisible) {
51
+ return null;
52
+ }
53
+
39
54
  return (
40
55
  <SelectItemContext.Provider value={{ label, value, isSelected, isDisabled }}>
41
56
  <Pressable
@@ -55,14 +70,27 @@ export const SelectItem = React.forwardRef<
55
70
  {...props}
56
71
  >
57
72
  {children ?? (
58
- <Text
59
- className={selectItemTextStyle({
60
- size,
61
- isSelected,
62
- })}
63
- >
64
- {label}
65
- </Text>
73
+ <>
74
+ {isMulti ? (
75
+ <Text
76
+ className={selectItemTextStyle({
77
+ size,
78
+ isSelected,
79
+ })}
80
+ style={{ width: 22 }}
81
+ >
82
+ {isSelected ? '\u2713' : ''}
83
+ </Text>
84
+ ) : null}
85
+ <Text
86
+ className={selectItemTextStyle({
87
+ size,
88
+ isSelected,
89
+ })}
90
+ >
91
+ {label}
92
+ </Text>
93
+ </>
66
94
  )}
67
95
  </Pressable>
68
96
  </SelectItemContext.Provider>
@@ -0,0 +1,77 @@
1
+ import React from 'react';
2
+ import { Pressable, Text, TextInput, View } from 'react-native';
3
+ import { useSelectContext } from './select';
4
+ import type { SelectSearchInputProps } from './types';
5
+ import {
6
+ selectSearchClearButtonStyle,
7
+ selectSearchInputFieldStyle,
8
+ selectSearchInputStyle,
9
+ } from './styles';
10
+
11
+ export const SelectSearchInput = React.forwardRef<
12
+ React.ElementRef<typeof TextInput>,
13
+ SelectSearchInputProps
14
+ >(
15
+ (
16
+ {
17
+ className,
18
+ inputClassName,
19
+ clearButtonClassName,
20
+ clearLabel = 'Clear search',
21
+ isClearable = true,
22
+ placeholder = 'Search...',
23
+ value,
24
+ onChangeText,
25
+ style,
26
+ ...props
27
+ },
28
+ ref,
29
+ ) => {
30
+ const { size, searchValue, onSearchChange, isDisabled } = useSelectContext();
31
+ const resolvedValue = value ?? searchValue;
32
+
33
+ const handleChangeText = (nextValue: string) => {
34
+ onSearchChange(nextValue);
35
+ onChangeText?.(nextValue);
36
+ };
37
+
38
+ const handleClear = () => {
39
+ handleChangeText('');
40
+ };
41
+
42
+ return (
43
+ <View className={selectSearchInputStyle({ size, class: className })}>
44
+ <TextInput
45
+ ref={ref}
46
+ value={resolvedValue}
47
+ onChangeText={handleChangeText}
48
+ editable={!isDisabled}
49
+ readOnly={isDisabled}
50
+ placeholder={placeholder}
51
+ placeholderTextColor="rgb(var(--color-typography-400))"
52
+ className={selectSearchInputFieldStyle({
53
+ size,
54
+ class: inputClassName,
55
+ })}
56
+ {...props}
57
+ style={[{ backgroundColor: 'transparent' }, style]}
58
+ />
59
+ {isClearable && resolvedValue ? (
60
+ <Pressable
61
+ onPress={handleClear}
62
+ className={selectSearchClearButtonStyle({
63
+ size,
64
+ class: clearButtonClassName,
65
+ })}
66
+ accessibilityRole="button"
67
+ accessibilityLabel={clearLabel}
68
+ >
69
+ <Text className="text-typography-400">{'\u2715'}</Text>
70
+ </Pressable>
71
+ ) : null}
72
+ </View>
73
+ );
74
+ },
75
+ );
76
+
77
+ SelectSearchInput.displayName = 'SelectSearchInput';
@@ -0,0 +1,119 @@
1
+ import React from 'react';
2
+ import { Pressable, Text, View, type GestureResponderEvent } from 'react-native';
3
+ import { useSelectContext } from './select';
4
+ import type { SelectSelectedBadgesProps } from './types';
5
+ import {
6
+ selectSelectedBadgeCloseButtonStyle,
7
+ selectSelectedBadgeStyle,
8
+ selectSelectedBadgeTextStyle,
9
+ selectSelectedBadgesStyle,
10
+ } from './styles';
11
+
12
+ export const SelectSelectedBadges = React.forwardRef<
13
+ React.ElementRef<typeof View>,
14
+ SelectSelectedBadgesProps
15
+ >(
16
+ (
17
+ {
18
+ className,
19
+ badgeClassName,
20
+ badgeTextClassName,
21
+ badgeCloseButtonClassName,
22
+ maxVisible,
23
+ showRemoveButton = true,
24
+ overflowLabel = (count) => `+${count}`,
25
+ renderBadge,
26
+ ...props
27
+ },
28
+ ref,
29
+ ) => {
30
+ const { selectedItems, onValueRemove, size, isDisabled } = useSelectContext();
31
+ const visibleItems =
32
+ maxVisible === undefined ? selectedItems : selectedItems.slice(0, maxVisible);
33
+ const overflowCount =
34
+ maxVisible === undefined
35
+ ? 0
36
+ : Math.max(selectedItems.length - maxVisible, 0);
37
+
38
+ if (selectedItems.length === 0) {
39
+ return null;
40
+ }
41
+
42
+ return (
43
+ <View
44
+ ref={ref}
45
+ className={selectSelectedBadgesStyle({ class: className })}
46
+ {...props}
47
+ >
48
+ {visibleItems.map((item) => {
49
+ const remove = () => onValueRemove(item.value);
50
+ const handleRemove = (event: GestureResponderEvent) => {
51
+ event.stopPropagation?.();
52
+ remove();
53
+ };
54
+
55
+ if (renderBadge) {
56
+ return (
57
+ <React.Fragment key={item.value}>
58
+ {renderBadge(item, { remove, isDisabled })}
59
+ </React.Fragment>
60
+ );
61
+ }
62
+
63
+ return (
64
+ <View
65
+ key={item.value}
66
+ className={selectSelectedBadgeStyle({
67
+ isDisabled,
68
+ class: badgeClassName,
69
+ })}
70
+ >
71
+ <Text
72
+ numberOfLines={1}
73
+ className={selectSelectedBadgeTextStyle({
74
+ size,
75
+ class: badgeTextClassName,
76
+ })}
77
+ >
78
+ {item.label}
79
+ </Text>
80
+ {showRemoveButton ? (
81
+ <Pressable
82
+ disabled={isDisabled}
83
+ onPress={handleRemove}
84
+ className={selectSelectedBadgeCloseButtonStyle({
85
+ size,
86
+ class: badgeCloseButtonClassName,
87
+ })}
88
+ accessibilityRole="button"
89
+ accessibilityLabel={`Remove ${item.label}`}
90
+ >
91
+ <Text className="text-typography-400">{'\u2715'}</Text>
92
+ </Pressable>
93
+ ) : null}
94
+ </View>
95
+ );
96
+ })}
97
+ {overflowCount > 0 ? (
98
+ <View
99
+ className={selectSelectedBadgeStyle({
100
+ isDisabled,
101
+ class: badgeClassName,
102
+ })}
103
+ >
104
+ <Text
105
+ className={selectSelectedBadgeTextStyle({
106
+ size,
107
+ class: badgeTextClassName,
108
+ })}
109
+ >
110
+ {overflowLabel(overflowCount)}
111
+ </Text>
112
+ </View>
113
+ ) : null}
114
+ </View>
115
+ );
116
+ },
117
+ );
118
+
119
+ SelectSelectedBadges.displayName = 'SelectSelectedBadges';
@@ -1,7 +1,19 @@
1
- import React, { useState, useCallback, createContext, useContext } from 'react';
1
+ import React, {
2
+ useState,
3
+ useCallback,
4
+ createContext,
5
+ useContext,
6
+ useMemo,
7
+ } from 'react';
2
8
  import { View } from 'react-native';
3
9
  import { useFormControlContext } from '../form-control/form-control';
4
- import type { SelectProps, SelectContextValue, SelectSize, TriggerLayout } from './types';
10
+ import type {
11
+ SelectProps,
12
+ SelectContextValue,
13
+ SelectSize,
14
+ TriggerLayout,
15
+ SelectOption,
16
+ } from './types';
5
17
 
6
18
  export const SelectContext = createContext<SelectContextValue | null>(null);
7
19
 
@@ -22,9 +34,20 @@ export const Select = React.forwardRef<
22
34
  (
23
35
  {
24
36
  className,
37
+ isMulti = false,
25
38
  selectedValue: controlledValue,
26
39
  defaultValue,
27
40
  onValueChange: onValueChangeProp,
41
+ selectedValues: controlledValues,
42
+ defaultValues = [],
43
+ onValuesChange,
44
+ valueLabels = {},
45
+ closeOnSelect,
46
+ searchValue: controlledSearchValue,
47
+ defaultSearchValue = '',
48
+ onSearchChange,
49
+ filterOption,
50
+ formatSelectedLabel,
28
51
  size = 'md',
29
52
  variant = 'outline',
30
53
  isDisabled: isDisabledProp,
@@ -38,7 +61,12 @@ export const Select = React.forwardRef<
38
61
  const [internalValue, setInternalValue] = useState<string | null>(
39
62
  defaultValue ?? null,
40
63
  );
64
+ const [internalValues, setInternalValues] = useState<string[]>(defaultValues);
41
65
  const [internalLabel, setInternalLabel] = useState('');
66
+ const [registeredLabels, setRegisteredLabels] =
67
+ useState<Record<string, string>>({});
68
+ const [internalSearchValue, setInternalSearchValue] =
69
+ useState(defaultSearchValue);
42
70
  const [triggerLayout, setTriggerLayout] = useState<TriggerLayout | null>(null);
43
71
 
44
72
  const formControl = useFormControlContext();
@@ -49,8 +77,51 @@ export const Select = React.forwardRef<
49
77
 
50
78
  const isControlled = controlledValue !== undefined;
51
79
  const selectedValue = isControlled ? controlledValue : internalValue;
80
+ const isMultiControlled = controlledValues !== undefined;
81
+ const selectedValues = isMulti
82
+ ? isMultiControlled
83
+ ? controlledValues
84
+ : internalValues
85
+ : selectedValue
86
+ ? [selectedValue]
87
+ : [];
88
+ const isSearchControlled = controlledSearchValue !== undefined;
89
+ const searchValue = isSearchControlled
90
+ ? controlledSearchValue
91
+ : internalSearchValue;
92
+ const shouldCloseOnSelect = closeOnSelect ?? !isMulti;
52
93
 
53
- const selectedLabel = selectedValue ? internalLabel || selectedValue : '';
94
+ const getLabel = useCallback(
95
+ (value: string) => valueLabels[value] ?? registeredLabels[value] ?? value,
96
+ [registeredLabels, valueLabels],
97
+ );
98
+
99
+ const selectedItems = useMemo<SelectOption[]>(
100
+ () => selectedValues.map((value) => ({ value, label: getLabel(value) })),
101
+ [getLabel, selectedValues],
102
+ );
103
+
104
+ const selectedLabel = useMemo(() => {
105
+ if (isMulti) {
106
+ if (formatSelectedLabel) {
107
+ return formatSelectedLabel(selectedItems);
108
+ }
109
+ if (selectedItems.length === 0) return '';
110
+ if (selectedItems.length === 1) return selectedItems[0]?.label ?? '';
111
+ return `${selectedItems.length} items selected`;
112
+ }
113
+
114
+ return selectedValue
115
+ ? internalLabel || getLabel(selectedValue)
116
+ : '';
117
+ }, [
118
+ formatSelectedLabel,
119
+ getLabel,
120
+ internalLabel,
121
+ isMulti,
122
+ selectedItems,
123
+ selectedValue,
124
+ ]);
54
125
 
55
126
  const onOpen = useCallback(() => {
56
127
  if (!isDisabled) {
@@ -64,21 +135,121 @@ export const Select = React.forwardRef<
64
135
 
65
136
  const onValueChange = useCallback(
66
137
  (value: string, label: string) => {
138
+ setRegisteredLabels((current) =>
139
+ current[value] === label ? current : { ...current, [value]: label },
140
+ );
141
+
142
+ if (isMulti) {
143
+ const nextValues = selectedValues.includes(value)
144
+ ? selectedValues.filter((selected) => selected !== value)
145
+ : [...selectedValues, value];
146
+
147
+ if (!isMultiControlled) {
148
+ setInternalValues(nextValues);
149
+ }
150
+ onValuesChange?.(nextValues);
151
+ } else {
152
+ if (!isControlled) {
153
+ setInternalValue(value);
154
+ }
155
+ setInternalLabel(label);
156
+ onValueChangeProp?.(value);
157
+ }
158
+
159
+ if (shouldCloseOnSelect) {
160
+ setIsOpen(false);
161
+ }
162
+ },
163
+ [
164
+ isControlled,
165
+ isMulti,
166
+ isMultiControlled,
167
+ onValueChangeProp,
168
+ onValuesChange,
169
+ selectedValues,
170
+ shouldCloseOnSelect,
171
+ ],
172
+ );
173
+
174
+ const onValueRemove = useCallback(
175
+ (value: string) => {
176
+ if (isMulti) {
177
+ const nextValues = selectedValues.filter((selected) => selected !== value);
178
+ if (!isMultiControlled) {
179
+ setInternalValues(nextValues);
180
+ }
181
+ onValuesChange?.(nextValues);
182
+ return;
183
+ }
184
+
67
185
  if (!isControlled) {
68
- setInternalValue(value);
186
+ setInternalValue(null);
69
187
  }
70
- setInternalLabel(label);
71
- onValueChangeProp?.(value);
188
+ setInternalLabel('');
189
+ onValueChangeProp?.('');
190
+ },
191
+ [
192
+ isControlled,
193
+ isMulti,
194
+ isMultiControlled,
195
+ onValueChangeProp,
196
+ onValuesChange,
197
+ selectedValues,
198
+ ],
199
+ );
200
+
201
+ const handleSearchChange = useCallback(
202
+ (value: string) => {
203
+ if (!isSearchControlled) {
204
+ setInternalSearchValue(value);
205
+ }
206
+ onSearchChange?.(value);
207
+ },
208
+ [isSearchControlled, onSearchChange],
209
+ );
210
+
211
+ const isValueSelected = useCallback(
212
+ (value: string) => selectedValues.includes(value),
213
+ [selectedValues],
214
+ );
215
+
216
+ const shouldShowItem = useCallback(
217
+ (value: string, label: string) => {
218
+ const normalizedSearch = searchValue.trim().toLowerCase();
219
+ if (!normalizedSearch) return true;
220
+
221
+ if (filterOption) {
222
+ return filterOption(searchValue, { value, label });
223
+ }
224
+
225
+ return (
226
+ label.toLowerCase().includes(normalizedSearch) ||
227
+ value.toLowerCase().includes(normalizedSearch)
228
+ );
229
+ },
230
+ [filterOption, searchValue],
231
+ );
232
+
233
+ const registerItem = useCallback(
234
+ (value: string, label: string) => {
235
+ setRegisteredLabels((current) =>
236
+ current[value] === label ? current : { ...current, [value]: label },
237
+ );
72
238
  },
73
- [isControlled, onValueChangeProp],
239
+ [],
74
240
  );
75
241
 
76
242
  return (
77
243
  <SelectContext.Provider
78
244
  value={{
79
245
  isOpen,
246
+ isMulti,
80
247
  selectedValue: selectedValue ?? null,
248
+ selectedValues,
81
249
  selectedLabel,
250
+ selectedItems,
251
+ searchValue,
252
+ placeholder,
82
253
  size: resolvedSize,
83
254
  variant,
84
255
  isDisabled,
@@ -87,6 +258,11 @@ export const Select = React.forwardRef<
87
258
  onOpen,
88
259
  onClose,
89
260
  onValueChange,
261
+ onValueRemove,
262
+ onSearchChange: handleSearchChange,
263
+ isValueSelected,
264
+ shouldShowItem,
265
+ registerItem,
90
266
  setTriggerLayout,
91
267
  }}
92
268
  >
@@ -56,6 +56,94 @@ export const selectInputStyle = tv({
56
56
  },
57
57
  });
58
58
 
59
+ export const selectSelectedBadgesStyle = tv({
60
+ base: 'flex-row flex-wrap items-center gap-1.5 flex-1',
61
+ });
62
+
63
+ export const selectSelectedBadgeStyle = tv({
64
+ base: 'flex-row items-center rounded-full border border-outline-200 bg-background-0 px-2 py-1 shadow-soft-1',
65
+ variants: {
66
+ isDisabled: {
67
+ true: 'opacity-50',
68
+ },
69
+ },
70
+ });
71
+
72
+ export const selectSelectedBadgeTextStyle = tv({
73
+ base: 'font-medium text-typography-900',
74
+ variants: {
75
+ size: {
76
+ sm: 'text-xs',
77
+ md: 'text-sm',
78
+ lg: 'text-base',
79
+ xl: 'text-base',
80
+ },
81
+ },
82
+ defaultVariants: {
83
+ size: 'md',
84
+ },
85
+ });
86
+
87
+ export const selectSelectedBadgeCloseButtonStyle = tv({
88
+ base: 'ml-1.5 rounded-full items-center justify-center',
89
+ variants: {
90
+ size: {
91
+ sm: 'h-4 w-4',
92
+ md: 'h-5 w-5',
93
+ lg: 'h-5 w-5',
94
+ xl: 'h-6 w-6',
95
+ },
96
+ },
97
+ defaultVariants: {
98
+ size: 'md',
99
+ },
100
+ });
101
+
102
+ export const selectSearchInputStyle = tv({
103
+ base: 'mx-2 my-2 flex-row items-center rounded-md border border-outline-200 bg-background-0 px-3',
104
+ variants: {
105
+ size: {
106
+ sm: 'h-8',
107
+ md: 'h-9',
108
+ lg: 'h-10',
109
+ xl: 'h-11',
110
+ },
111
+ },
112
+ defaultVariants: {
113
+ size: 'md',
114
+ },
115
+ });
116
+
117
+ export const selectSearchInputFieldStyle = tv({
118
+ base: 'flex-1 text-typography-900',
119
+ variants: {
120
+ size: {
121
+ sm: 'text-sm',
122
+ md: 'text-base',
123
+ lg: 'text-lg',
124
+ xl: 'text-xl',
125
+ },
126
+ },
127
+ defaultVariants: {
128
+ size: 'md',
129
+ },
130
+ });
131
+
132
+ export const selectSearchClearButtonStyle = tv({
133
+ base: 'ml-2 rounded-full items-center justify-center',
134
+ variants: {
135
+ size: {
136
+ sm: 'h-5 w-5',
137
+ md: 'h-6 w-6',
138
+ lg: 'h-6 w-6',
139
+ xl: 'h-7 w-7',
140
+ },
141
+ },
142
+ defaultVariants: {
143
+ size: 'md',
144
+ },
145
+ });
146
+
59
147
  export const selectIconStyle = tv({
60
148
  base: 'text-typography-400 ml-2',
61
149
  variants: {
@@ -1,8 +1,19 @@
1
- import type { View, Text as RNText, Pressable, ScrollView } from 'react-native';
1
+ import type {
2
+ View,
3
+ Text as RNText,
4
+ TextInput,
5
+ Pressable,
6
+ ScrollView,
7
+ } from 'react-native';
2
8
 
3
9
  export type SelectSize = 'sm' | 'md' | 'lg' | 'xl';
4
10
  export type SelectVariant = 'outline' | 'filled' | 'underlined' | 'rounded';
5
11
 
12
+ export interface SelectOption {
13
+ value: string;
14
+ label: string;
15
+ }
16
+
6
17
  export interface TriggerLayout {
7
18
  pageX: number;
8
19
  pageY: number;
@@ -12,8 +23,13 @@ export interface TriggerLayout {
12
23
 
13
24
  export interface SelectContextValue {
14
25
  isOpen: boolean;
26
+ isMulti: boolean;
15
27
  selectedValue: string | null;
28
+ selectedValues: string[];
16
29
  selectedLabel: string;
30
+ selectedItems: SelectOption[];
31
+ searchValue: string;
32
+ placeholder: string;
17
33
  size: SelectSize;
18
34
  variant: SelectVariant;
19
35
  isDisabled: boolean;
@@ -22,14 +38,30 @@ export interface SelectContextValue {
22
38
  onOpen: () => void;
23
39
  onClose: () => void;
24
40
  onValueChange: (value: string, label: string) => void;
41
+ onValueRemove: (value: string) => void;
42
+ onSearchChange: (value: string) => void;
43
+ isValueSelected: (value: string) => boolean;
44
+ shouldShowItem: (value: string, label: string) => boolean;
45
+ registerItem: (value: string, label: string) => void;
25
46
  setTriggerLayout: (layout: TriggerLayout) => void;
26
47
  }
27
48
 
28
49
  export interface SelectProps {
29
50
  className?: string;
51
+ isMulti?: boolean;
30
52
  selectedValue?: string;
31
53
  defaultValue?: string;
32
54
  onValueChange?: (value: string) => void;
55
+ selectedValues?: string[];
56
+ defaultValues?: string[];
57
+ onValuesChange?: (values: string[]) => void;
58
+ valueLabels?: Record<string, string>;
59
+ closeOnSelect?: boolean;
60
+ searchValue?: string;
61
+ defaultSearchValue?: string;
62
+ onSearchChange?: (value: string) => void;
63
+ filterOption?: (searchValue: string, option: SelectOption) => boolean;
64
+ formatSelectedLabel?: (items: SelectOption[]) => string;
33
65
  size?: SelectSize;
34
66
  variant?: SelectVariant;
35
67
  isDisabled?: boolean;
@@ -81,6 +113,30 @@ export interface SelectItemTextProps
81
113
  className?: string;
82
114
  }
83
115
 
116
+ export interface SelectSearchInputProps
117
+ extends React.ComponentPropsWithoutRef<typeof TextInput> {
118
+ className?: string;
119
+ inputClassName?: string;
120
+ clearButtonClassName?: string;
121
+ clearLabel?: string;
122
+ isClearable?: boolean;
123
+ }
124
+
125
+ export interface SelectSelectedBadgesProps
126
+ extends React.ComponentPropsWithoutRef<typeof View> {
127
+ className?: string;
128
+ badgeClassName?: string;
129
+ badgeTextClassName?: string;
130
+ badgeCloseButtonClassName?: string;
131
+ maxVisible?: number;
132
+ showRemoveButton?: boolean;
133
+ overflowLabel?: (count: number) => string;
134
+ renderBadge?: (
135
+ item: SelectOption,
136
+ helpers: { remove: () => void; isDisabled: boolean },
137
+ ) => React.ReactNode;
138
+ }
139
+
84
140
  export interface SelectDragIndicatorProps
85
141
  extends React.ComponentPropsWithoutRef<typeof View> {
86
142
  className?: string;