nepal-places-react 0.1.1 → 0.3.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.
package/README.md ADDED
@@ -0,0 +1,51 @@
1
+ # nepal-places-react
2
+
3
+ React dropdown component for Nepal's Provinces, Districts, Municipalities, Wards, and Places. Built on top of `nepal-places`.
4
+
5
+ ```
6
+ npm install nepal-places-react
7
+ ```
8
+
9
+ ## Usage
10
+
11
+ ```tsx
12
+ import { NepalPlacesDropdown } from 'nepal-places-react'
13
+
14
+ function AddressForm() {
15
+ return (
16
+ <NepalPlacesDropdown
17
+ depth={3}
18
+ onChange={(selection) => console.log(selection)}
19
+ />
20
+ )
21
+ }
22
+ ```
23
+
24
+ ## Props
25
+
26
+ | Prop | Type | Default | Description |
27
+ |------|------|---------|-------------|
28
+ | `depth` | `1`-`5` | `2` | Number of cascade levels |
29
+ | `mode` | `'cascade'` \| `'flat'` | `'cascade'` | Selection mode |
30
+ | `labels` | `'english'` \| `'nepali'` | `'english'` | Display language |
31
+ | `flat` | `boolean` | `false` | Return flat code string |
32
+ | `filterType` | `LocalLevelType` | — | Filter by local level type |
33
+ | `searchable` | `boolean` | `true` | Enable search |
34
+ | `clearable` | `boolean` | `false` | Show clear button |
35
+ | `disabled` | `boolean` | `false` | Disable the dropdown |
36
+ | `placeholder` | `string` | — | Input placeholder text |
37
+ | `onChange` | `(value) => void` | — | Selection callback |
38
+
39
+ ## Depth levels
40
+
41
+ | Depth | Levels |
42
+ |-------|--------|
43
+ | `1` | Province |
44
+ | `2` | Province → District |
45
+ | `3` | Province → District → Municipality |
46
+ | `4` | Province → District → Municipality → Ward |
47
+ | `5` | Province → District → Municipality → Ward → Place |
48
+
49
+ ## License
50
+
51
+ MIT — [kushal1o1](https://github.com/kushal1o1)
@@ -1,2 +1,2 @@
1
1
  import type { NepalPlacesDropdownProps } from './types.js';
2
- export declare function NepalPlacesDropdown({ depth, labels, flat, filterType, className, onChange, }: NepalPlacesDropdownProps): import("react").JSX.Element;
2
+ export declare function NepalPlacesDropdown({ depth, mode, labels, flat, filterType, className, style: wrapperStyle, theme, searchable, clearable, disabled, selectClassName, inputClassName, dropdownClassName, optionClassName, placeholder, noOptionsMessage, onChange, }: NepalPlacesDropdownProps): import("react").JSX.Element;
@@ -1,11 +1,28 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { getDistricts, getMunicipalities, getProvinces, } from 'nepal-places';
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { getDistricts, getMunicipalities, getPlaces, getProvinces, } from 'nepal-places';
3
3
  import { useEffect, useMemo, useRef, useState } from 'react';
4
- export function NepalPlacesDropdown({ depth = 4, labels = 'english', flat = false, filterType, className, onChange, }) {
4
+ import { SearchableSelect } from './SearchableSelect.js';
5
+ import { SinglePlacePicker } from './SinglePlacePicker.js';
6
+ const defaults = {
7
+ province: 'Province',
8
+ district: 'District',
9
+ municipality: 'Municipality',
10
+ ward: 'Ward',
11
+ place: 'Place',
12
+ };
13
+ const nepalDefaults = {
14
+ province: 'प्रदेश',
15
+ district: 'जिल्ला',
16
+ municipality: 'स्थानीय तह',
17
+ ward: 'वडा',
18
+ place: 'स्थान',
19
+ };
20
+ export function NepalPlacesDropdown({ depth = 4, mode = 'cascade', labels = 'english', flat = false, filterType, className, style: wrapperStyle, theme, searchable = true, clearable = true, disabled = false, selectClassName, inputClassName, dropdownClassName, optionClassName, placeholder, noOptionsMessage, onChange, }) {
5
21
  const [provinceId, setProvinceId] = useState('');
6
22
  const [districtId, setDistrictId] = useState('');
7
23
  const [localLevelId, setLocalLevelId] = useState('');
8
24
  const [wardId, setWardId] = useState('');
25
+ const [placeId, setPlaceId] = useState('');
9
26
  const mounted = useRef(false);
10
27
  const provinces = useMemo(() => getProvinces(), []);
11
28
  const districts = useMemo(() => (provinceId ? getDistricts(Number(provinceId)) : []), [provinceId]);
@@ -13,7 +30,11 @@ export function NepalPlacesDropdown({ depth = 4, labels = 'english', flat = fals
13
30
  if (!districtId)
14
31
  return [];
15
32
  const all = getMunicipalities(Number(districtId));
16
- return filterType ? all.filter((m) => m.type === filterType) : all;
33
+ const filtered = filterType ? all.filter((m) => m.type === filterType) : all;
34
+ return filtered.map((m) => ({
35
+ ...m,
36
+ name: filterType ? m.name : `${m.name} (${m.type.replace('_', ' ')})`,
37
+ }));
17
38
  }, [districtId, filterType]);
18
39
  const selectedLocalLevel = useMemo(() => localLevels.find((m) => m.id === Number(localLevelId)), [localLevels, localLevelId]);
19
40
  const wards = useMemo(() => {
@@ -29,12 +50,20 @@ export function NepalPlacesDropdown({ depth = 4, labels = 'english', flat = fals
29
50
  }
30
51
  return list;
31
52
  }, [selectedLocalLevel]);
32
- const name = (item) => labels === 'nepali' && item.name_np ? item.name_np : item.name;
53
+ const wardOptions = useMemo(() => wards.map((w) => ({
54
+ id: w.id,
55
+ name: labels === 'nepali'
56
+ ? `वडा ${w.ward_number}`
57
+ : `Ward ${w.ward_number}`,
58
+ name_np: `वडा ${w.ward_number}`,
59
+ })), [wards, labels]);
60
+ const places = useMemo(() => (wardId ? getPlaces(Number(wardId)) : []), [wardId]);
33
61
  const output = useMemo(() => {
34
62
  const province = provinces.find((p) => p.id === Number(provinceId));
35
63
  const district = districts.find((d) => d.id === Number(districtId));
36
64
  const localLevel = localLevels.find((m) => m.id === Number(localLevelId));
37
65
  const ward = wards.find((w) => w.id === Number(wardId));
66
+ const place = places.find((p) => p.id === Number(placeId));
38
67
  const sel = {};
39
68
  if (province)
40
69
  sel.province = province;
@@ -44,6 +73,8 @@ export function NepalPlacesDropdown({ depth = 4, labels = 'english', flat = fals
44
73
  sel.localLevel = localLevel;
45
74
  if (ward)
46
75
  sel.ward = ward;
76
+ if (place)
77
+ sel.place = place;
47
78
  if (flat) {
48
79
  const parts = [];
49
80
  if (province)
@@ -54,6 +85,8 @@ export function NepalPlacesDropdown({ depth = 4, labels = 'english', flat = fals
54
85
  parts.push(String(localLevel.id));
55
86
  if (ward)
56
87
  parts.push(String(ward.ward_number));
88
+ if (place)
89
+ parts.push(String(place.id));
57
90
  sel.code = parts.join('/');
58
91
  }
59
92
  return sel;
@@ -62,10 +95,12 @@ export function NepalPlacesDropdown({ depth = 4, labels = 'english', flat = fals
62
95
  districtId,
63
96
  localLevelId,
64
97
  wardId,
98
+ placeId,
65
99
  provinces,
66
100
  districts,
67
101
  localLevels,
68
102
  wards,
103
+ places,
69
104
  flat,
70
105
  ]);
71
106
  useEffect(() => {
@@ -75,13 +110,44 @@ export function NepalPlacesDropdown({ depth = 4, labels = 'english', flat = fals
75
110
  }
76
111
  onChange?.(output);
77
112
  }, [output, onChange]);
78
- const onSelect = (set, ...clear) => (e) => {
79
- const v = e.target.value ? Number(e.target.value) : '';
80
- set(v);
113
+ const handle = (set, ...clear) => (value) => {
114
+ set(value);
81
115
  for (const fn of clear)
82
116
  fn('');
83
117
  };
84
- return (_jsxs("div", { className: `np-places-dropdown ${className ?? ''}`, style: { display: 'flex', gap: '8px', flexWrap: 'wrap' }, children: [depth >= 1 && (_jsxs("select", { className: "np-places-select", value: provinceId, onChange: onSelect(setProvinceId, setDistrictId, setLocalLevelId, setWardId), children: [_jsx("option", { value: "", children: labels === 'nepali' ? 'प्रदेश' : 'Province' }), provinces.map((p) => (_jsx("option", { value: p.id, children: name(p) }, p.id)))] })), depth >= 2 && provinceId !== '' && (_jsxs("select", { className: "np-places-select", value: districtId, onChange: onSelect(setDistrictId, setLocalLevelId, setWardId), children: [_jsx("option", { value: "", children: labels === 'nepali' ? 'जिल्ला' : 'District' }), districts.map((d) => (_jsx("option", { value: d.id, children: name(d) }, d.id)))] })), depth >= 3 && districtId !== '' && (_jsxs("select", { className: "np-places-select", value: localLevelId, onChange: onSelect(setLocalLevelId, setWardId), children: [_jsx("option", { value: "", children: labels === 'nepali' ? 'स्थानीय तह' : 'Municipality' }), localLevels.map((m) => (_jsxs("option", { value: m.id, children: [name(m), filterType ? '' : ` (${m.type.replace('_', ' ')})`] }, m.id)))] })), depth >= 4 && localLevelId !== '' && wards.length > 0 && (_jsxs("select", { className: "np-places-select", value: wardId, onChange: onSelect(setWardId), children: [_jsx("option", { value: "", children: labels === 'nepali' ? 'वडा' : 'Ward' }), wards.map((w) => (_jsx("option", { value: w.id, children: labels === 'nepali'
85
- ? `वडा ${w.ward_number}`
86
- : `Ward ${w.ward_number}` }, w.id)))] }))] }));
118
+ const handleSingle = (p, d, m) => {
119
+ setProvinceId(p);
120
+ setDistrictId(d);
121
+ setLocalLevelId(m);
122
+ setWardId('');
123
+ setPlaceId('');
124
+ };
125
+ const ph = (level) => {
126
+ if (placeholder && typeof placeholder === 'object') {
127
+ return placeholder[level] ?? defaults[level];
128
+ }
129
+ if (placeholder && typeof placeholder === 'string') {
130
+ return placeholder;
131
+ }
132
+ return labels === 'nepali' ? nepalDefaults[level] : defaults[level];
133
+ };
134
+ const shared = {
135
+ labels,
136
+ searchable,
137
+ clearable,
138
+ disabled,
139
+ className: selectClassName,
140
+ inputClassName,
141
+ dropdownClassName,
142
+ optionClassName,
143
+ noOptionsMessage,
144
+ theme,
145
+ };
146
+ const singlePlaceholder = typeof placeholder === 'string' ? placeholder : 'Search places in Nepal...';
147
+ return (_jsx("div", { className: `np-places-dropdown${className ? ` ${className}` : ''}`, style: {
148
+ display: 'flex',
149
+ gap: '8px',
150
+ flexWrap: 'wrap',
151
+ ...wrapperStyle,
152
+ }, children: mode === 'single' ? (_jsxs(_Fragment, { children: [_jsx(SinglePlacePicker, { provinceId: provinceId, districtId: districtId, localLevelId: localLevelId, onChange: handleSingle, labels: labels, disabled: disabled, clearable: clearable, placeholder: singlePlaceholder, className: selectClassName, inputClassName: inputClassName, dropdownClassName: dropdownClassName, optionClassName: optionClassName, noOptionsMessage: noOptionsMessage, theme: theme }), depth >= 4 && localLevelId !== '' && wards.length > 0 && (_jsx(SearchableSelect, { options: wardOptions, value: wardId, onChange: handle(setWardId, setPlaceId), placeholder: ph('ward'), ...shared })), depth >= 5 && wardId !== '' && places.length > 0 && (_jsx(SearchableSelect, { options: places, value: placeId, onChange: handle(setPlaceId), placeholder: ph('place'), ...shared }))] })) : (_jsxs(_Fragment, { children: [depth >= 1 && (_jsx(SearchableSelect, { options: provinces, value: provinceId, onChange: handle(setProvinceId, setDistrictId, setLocalLevelId, setWardId, setPlaceId), placeholder: ph('province'), ...shared })), depth >= 2 && provinceId !== '' && (_jsx(SearchableSelect, { options: districts, value: districtId, onChange: handle(setDistrictId, setLocalLevelId, setWardId, setPlaceId), placeholder: ph('district'), ...shared })), depth >= 3 && districtId !== '' && (_jsx(SearchableSelect, { options: localLevels, value: localLevelId, onChange: handle(setLocalLevelId, setWardId, setPlaceId), placeholder: ph('municipality'), ...shared })), depth >= 4 && localLevelId !== '' && wards.length > 0 && (_jsx(SearchableSelect, { options: wardOptions, value: wardId, onChange: handle(setWardId, setPlaceId), placeholder: ph('ward'), ...shared })), depth >= 5 && wardId !== '' && places.length > 0 && (_jsx(SearchableSelect, { options: places, value: placeId, onChange: handle(setPlaceId), placeholder: ph('place'), ...shared }))] })) }));
87
153
  }
@@ -0,0 +1,30 @@
1
+ export interface SearchableSelectOption {
2
+ id: number;
3
+ name: string;
4
+ name_np?: string;
5
+ }
6
+ export interface SearchableSelectProps {
7
+ options: SearchableSelectOption[];
8
+ value: number | '';
9
+ onChange: (value: number | '') => void;
10
+ placeholder?: string;
11
+ disabled?: boolean;
12
+ labels?: 'english' | 'nepali';
13
+ className?: string;
14
+ inputClassName?: string;
15
+ dropdownClassName?: string;
16
+ optionClassName?: string;
17
+ clearable?: boolean;
18
+ searchable?: boolean;
19
+ noOptionsMessage?: string;
20
+ theme?: {
21
+ borderColor?: string;
22
+ borderColorFocus?: string;
23
+ borderRadius?: number;
24
+ bg?: string;
25
+ textColor?: string;
26
+ shadow?: string;
27
+ accentColor?: string;
28
+ };
29
+ }
30
+ export declare function SearchableSelect({ options, value, onChange, placeholder, disabled, labels, className, inputClassName, dropdownClassName, optionClassName, clearable, searchable, noOptionsMessage, theme: themeProp, }: SearchableSelectProps): import("react").JSX.Element;
@@ -0,0 +1,206 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect, useMemo, useRef, useState } from 'react';
3
+ const s = {
4
+ container: {
5
+ position: 'relative',
6
+ minWidth: 180,
7
+ },
8
+ inputWrap: {
9
+ position: 'relative',
10
+ display: 'flex',
11
+ alignItems: 'center',
12
+ },
13
+ input: {
14
+ width: '100%',
15
+ padding: '8px 28px 8px 12px',
16
+ border: '1px solid var(--np-border, #d2d2d7)',
17
+ borderRadius: 8,
18
+ fontSize: 14,
19
+ outline: 'none',
20
+ cursor: 'pointer',
21
+ boxSizing: 'border-box',
22
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif",
23
+ background: 'var(--np-bg, #fff)',
24
+ color: 'var(--np-text, #1d1d1f)',
25
+ lineHeight: 1.4,
26
+ },
27
+ inputFocus: {
28
+ borderColor: 'var(--np-border-focus, #0071e3)',
29
+ boxShadow: '0 0 0 2px color-mix(in srgb, var(--np-border-focus, #0071e3) 20%, transparent)',
30
+ },
31
+ clear: {
32
+ position: 'absolute',
33
+ right: 6,
34
+ background: 'none',
35
+ border: 'none',
36
+ cursor: 'pointer',
37
+ fontSize: 14,
38
+ color: 'var(--np-text-secondary, #86868b)',
39
+ padding: '2px 4px',
40
+ lineHeight: 1,
41
+ borderRadius: 4,
42
+ },
43
+ dropdown: {
44
+ position: 'absolute',
45
+ top: '100%',
46
+ left: 0,
47
+ right: 0,
48
+ marginTop: 4,
49
+ background: 'var(--np-bg, #fff)',
50
+ border: '1px solid var(--np-border, #d2d2d7)',
51
+ borderRadius: 8,
52
+ boxShadow: 'var(--np-shadow, 0 4px 12px rgba(0,0,0,0.08))',
53
+ maxHeight: 240,
54
+ overflow: 'auto',
55
+ zIndex: 100,
56
+ },
57
+ opt: {
58
+ padding: '8px 12px',
59
+ cursor: 'pointer',
60
+ fontSize: 14,
61
+ transition: 'background 0.1s',
62
+ color: 'var(--np-text, #1d1d1f)',
63
+ },
64
+ optHover: {
65
+ background: 'var(--np-bg-hover, #f5f5f7)',
66
+ },
67
+ empty: {
68
+ padding: 12,
69
+ textAlign: 'center',
70
+ color: 'var(--np-text-secondary, #86868b)',
71
+ fontSize: 13,
72
+ },
73
+ };
74
+ const defTheme = {
75
+ borderColor: 'var(--np-border, #d2d2d7)',
76
+ borderColorFocus: 'var(--np-border-focus, #0071e3)',
77
+ borderRadius: 8,
78
+ bg: 'var(--np-bg, #fff)',
79
+ textColor: 'var(--np-text, #1d1d1f)',
80
+ shadow: 'var(--np-shadow, 0 4px 12px rgba(0,0,0,0.08))',
81
+ accentColor: 'var(--np-border-focus, #0071e3)',
82
+ };
83
+ export function SearchableSelect({ options, value, onChange, placeholder = 'Select', disabled = false, labels = 'english', className, inputClassName, dropdownClassName, optionClassName, clearable = true, searchable = true, noOptionsMessage = 'No options found', theme: themeProp, }) {
84
+ const t = { ...defTheme, ...themeProp };
85
+ const [open, setOpen] = useState(false);
86
+ const [search, setSearch] = useState('');
87
+ const [highlighted, setHighlighted] = useState(-1);
88
+ const [focused, setFocused] = useState(false);
89
+ const containerRef = useRef(null);
90
+ const inputRef = useRef(null);
91
+ const selected = useMemo(() => options.find((o) => o.id === value), [options, value]);
92
+ const filtered = useMemo(() => {
93
+ if (!search || !open)
94
+ return options;
95
+ const q = search.toLowerCase();
96
+ return options.filter((o) => {
97
+ const n = (labels === 'nepali' && o.name_np ? o.name_np : o.name).toLowerCase();
98
+ return n.includes(q);
99
+ });
100
+ }, [options, search, open, labels]);
101
+ const display = (o) => labels === 'nepali' && o.name_np ? o.name_np : o.name;
102
+ useEffect(() => {
103
+ const handler = (e) => {
104
+ if (containerRef.current &&
105
+ !containerRef.current.contains(e.target)) {
106
+ setOpen(false);
107
+ setSearch('');
108
+ setHighlighted(-1);
109
+ }
110
+ };
111
+ document.addEventListener('mousedown', handler);
112
+ return () => document.removeEventListener('mousedown', handler);
113
+ }, []);
114
+ useEffect(() => {
115
+ if (highlighted >= filtered.length) {
116
+ setHighlighted(filtered.length - 1);
117
+ }
118
+ }, [filtered.length, highlighted]);
119
+ const select = (id) => {
120
+ onChange(id);
121
+ setOpen(false);
122
+ setSearch('');
123
+ setHighlighted(-1);
124
+ };
125
+ const onKey = (e) => {
126
+ switch (e.key) {
127
+ case 'ArrowDown':
128
+ e.preventDefault();
129
+ if (!open) {
130
+ setOpen(true);
131
+ return;
132
+ }
133
+ setHighlighted((p) => (p < filtered.length - 1 ? p + 1 : 0));
134
+ break;
135
+ case 'ArrowUp':
136
+ e.preventDefault();
137
+ setHighlighted((p) => (p > 0 ? p - 1 : filtered.length - 1));
138
+ break;
139
+ case 'Enter':
140
+ e.preventDefault();
141
+ if (open && highlighted >= 0 && highlighted < filtered.length) {
142
+ select(filtered[highlighted].id);
143
+ }
144
+ break;
145
+ case 'Escape':
146
+ setOpen(false);
147
+ setSearch('');
148
+ setHighlighted(-1);
149
+ inputRef.current?.blur();
150
+ break;
151
+ }
152
+ };
153
+ const inputStyle = {
154
+ ...s.input,
155
+ borderRadius: t.borderRadius,
156
+ borderColor: t.borderColor,
157
+ background: t.bg,
158
+ color: t.textColor,
159
+ ...(focused
160
+ ? {
161
+ ...s.inputFocus,
162
+ borderColor: t.borderColorFocus,
163
+ boxShadow: `0 0 0 2px ${t.borderColorFocus}33`,
164
+ }
165
+ : {}),
166
+ };
167
+ const dropdownStyle = {
168
+ ...s.dropdown,
169
+ borderRadius: t.borderRadius,
170
+ background: t.bg,
171
+ borderColor: t.borderColor,
172
+ boxShadow: t.shadow,
173
+ };
174
+ const clearStyle = {
175
+ ...s.clear,
176
+ borderRadius: t.borderRadius,
177
+ };
178
+ const inputVal = open ? search : selected ? display(selected) : '';
179
+ return (_jsxs("div", { ref: containerRef, className: `np-searchable-select${className ? ` ${className}` : ''}`, style: s.container, children: [_jsxs("div", { style: s.inputWrap, children: [_jsx("input", { ref: inputRef, type: "text", role: "combobox", "aria-expanded": open, "aria-autocomplete": "list", "aria-label": placeholder, "aria-controls": `np-list-${placeholder}`, "aria-activedescendant": highlighted >= 0 ? `np-opt-${filtered[highlighted]?.id}` : undefined, value: inputVal, onChange: (e) => {
180
+ if (open || searchable) {
181
+ setSearch(e.target.value);
182
+ if (!open)
183
+ setOpen(true);
184
+ setHighlighted(-1);
185
+ }
186
+ }, onFocus: () => {
187
+ setFocused(true);
188
+ if (searchable)
189
+ setOpen(true);
190
+ }, onBlur: () => setFocused(false), onClick: () => {
191
+ if (!searchable)
192
+ setOpen((p) => !p);
193
+ }, onKeyDown: onKey, placeholder: placeholder, disabled: disabled, readOnly: !searchable, className: `np-searchable-input${inputClassName ? ` ${inputClassName}` : ''}`, style: inputStyle }), clearable && value !== '' && !disabled && (_jsx("button", { type: "button", "aria-label": "Clear selection", style: clearStyle, onClick: (e) => {
194
+ e.stopPropagation();
195
+ onChange('');
196
+ setSearch('');
197
+ inputRef.current?.focus();
198
+ }, tabIndex: -1, onMouseDown: (e) => e.preventDefault(), children: "\u2715" }))] }), open && (_jsx("div", { id: `np-list-${placeholder}`, role: "listbox", tabIndex: -1, className: `np-searchable-dropdown${dropdownClassName ? ` ${dropdownClassName}` : ''}`, style: dropdownStyle, children: filtered.length === 0 ? (_jsx("div", { style: s.empty, children: noOptionsMessage })) : (filtered.map((o, i) => (_jsx("div", { id: `np-opt-${o.id}`, role: "option", tabIndex: -1, "aria-selected": o.id === value, className: `np-searchable-option${optionClassName ? ` ${optionClassName}` : ''}`, style: {
199
+ ...s.opt,
200
+ ...(i === highlighted ? s.optHover : {}),
201
+ ...(o.id === value ? { fontWeight: 600 } : {}),
202
+ }, onClick: () => select(o.id), onKeyDown: (e) => {
203
+ if (e.key === 'Enter')
204
+ select(o.id);
205
+ }, onMouseEnter: () => setHighlighted(i), children: display(o) }, o.id)))) }))] }));
206
+ }
@@ -0,0 +1,25 @@
1
+ export interface SinglePlacePickerProps {
2
+ provinceId: number | '';
3
+ districtId: number | '';
4
+ localLevelId: number | '';
5
+ onChange: (p: number | '', d: number | '', m: number | '') => void;
6
+ labels?: 'english' | 'nepali';
7
+ disabled?: boolean;
8
+ className?: string;
9
+ inputClassName?: string;
10
+ dropdownClassName?: string;
11
+ optionClassName?: string;
12
+ clearable?: boolean;
13
+ placeholder?: string;
14
+ noOptionsMessage?: string;
15
+ theme?: {
16
+ borderColor?: string;
17
+ borderColorFocus?: string;
18
+ borderRadius?: number;
19
+ bg?: string;
20
+ textColor?: string;
21
+ shadow?: string;
22
+ accentColor?: string;
23
+ };
24
+ }
25
+ export declare function SinglePlacePicker({ provinceId, districtId, localLevelId, onChange, labels, disabled, className, inputClassName, dropdownClassName, optionClassName, clearable, placeholder, noOptionsMessage, theme: themeProp, }: SinglePlacePickerProps): import("react").JSX.Element;
@@ -0,0 +1,267 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { getDistrict, getDistricts, getMunicipalities, getProvince, getProvinces, } from 'nepal-places';
3
+ import { useEffect, useMemo, useRef, useState } from 'react';
4
+ const defTheme = {
5
+ borderColor: 'var(--np-border, #d2d2d7)',
6
+ borderColorFocus: 'var(--np-border-focus, #0071e3)',
7
+ borderRadius: 8,
8
+ bg: 'var(--np-bg, #fff)',
9
+ textColor: 'var(--np-text, #1d1d1f)',
10
+ shadow: 'var(--np-shadow, 0 4px 12px rgba(0,0,0,0.08))',
11
+ accentColor: 'var(--np-border-focus, #0071e3)',
12
+ };
13
+ export function SinglePlacePicker({ provinceId, districtId, localLevelId, onChange, labels = 'english', disabled = false, className, inputClassName, dropdownClassName, optionClassName, clearable = true, placeholder = 'Search places in Nepal...', noOptionsMessage = 'No places found', theme: themeProp, }) {
14
+ const [open, setOpen] = useState(false);
15
+ const [search, setSearch] = useState('');
16
+ const [highlighted, setHighlighted] = useState(-1);
17
+ const [focused, setFocused] = useState(false);
18
+ const containerRef = useRef(null);
19
+ const inputRef = useRef(null);
20
+ const t = { ...defTheme, ...themeProp };
21
+ const allItems = useMemo(() => {
22
+ const items = [];
23
+ for (const p of getProvinces()) {
24
+ items.push({
25
+ id: `p-${p.id}`,
26
+ level: 'province',
27
+ levelLabel: 'Province',
28
+ name: p.name,
29
+ name_np: p.name_np,
30
+ provinceId: p.id,
31
+ });
32
+ }
33
+ for (const d of getDistricts()) {
34
+ const p = getProvince(d.province_id);
35
+ items.push({
36
+ id: `d-${d.id}`,
37
+ level: 'district',
38
+ levelLabel: 'District',
39
+ name: `${d.name}${p ? ` — ${p.name}` : ''}`,
40
+ name_np: d.name_np,
41
+ provinceId: d.province_id,
42
+ districtId: d.id,
43
+ });
44
+ }
45
+ for (const m of getMunicipalities()) {
46
+ const d = getDistrict(m.district_id);
47
+ const p = d ? getProvince(d.province_id) : undefined;
48
+ items.push({
49
+ id: `m-${m.id}`,
50
+ level: 'municipality',
51
+ levelLabel: m.type === 'metropolitan'
52
+ ? 'Metro'
53
+ : m.type === 'sub_metropolitan'
54
+ ? 'Sub-Metro'
55
+ : m.type === 'municipality'
56
+ ? 'Muni'
57
+ : 'Rural',
58
+ name: `${m.name}${d ? ` — ${d.name}` : ''}`,
59
+ name_np: m.name_np,
60
+ provinceId: p?.id,
61
+ districtId: m.district_id,
62
+ localLevelId: m.id,
63
+ });
64
+ }
65
+ return items;
66
+ }, []);
67
+ const filtered = useMemo(() => {
68
+ if (!search)
69
+ return allItems.slice(0, 50);
70
+ const q = search.toLowerCase();
71
+ return allItems
72
+ .filter((i) => {
73
+ const n = (labels === 'nepali' && i.name_np ? i.name_np : i.name).toLowerCase();
74
+ return n.includes(q);
75
+ })
76
+ .slice(0, 100);
77
+ }, [search, allItems, labels]);
78
+ const selectedProvince = provinceId
79
+ ? getProvince(Number(provinceId))
80
+ : undefined;
81
+ const selectedDistrict = districtId
82
+ ? getDistrict(Number(districtId))
83
+ : undefined;
84
+ const selectedLocalLevel = localLevelId
85
+ ? getMunicipalities().find((m) => m.id === Number(localLevelId))
86
+ : undefined;
87
+ const selectedPath = useMemo(() => {
88
+ if (labels === 'nepali') {
89
+ const parts = [];
90
+ if (selectedLocalLevel?.name_np)
91
+ parts.push(selectedLocalLevel.name_np);
92
+ else if (selectedLocalLevel)
93
+ parts.push(selectedLocalLevel.name);
94
+ if (selectedDistrict?.name_np)
95
+ parts.push(selectedDistrict.name_np);
96
+ else if (selectedDistrict)
97
+ parts.push(selectedDistrict.name);
98
+ if (selectedProvince?.name_np)
99
+ parts.push(selectedProvince.name_np);
100
+ else if (selectedProvince)
101
+ parts.push(selectedProvince.name);
102
+ return parts.join(' > ');
103
+ }
104
+ const parts = [];
105
+ if (selectedLocalLevel)
106
+ parts.push(selectedLocalLevel.name);
107
+ if (selectedDistrict)
108
+ parts.push(selectedDistrict.name);
109
+ if (selectedProvince)
110
+ parts.push(selectedProvince.name);
111
+ return parts.join(' > ');
112
+ }, [selectedProvince, selectedDistrict, selectedLocalLevel, labels]);
113
+ useEffect(() => {
114
+ const handler = (e) => {
115
+ if (containerRef.current &&
116
+ !containerRef.current.contains(e.target)) {
117
+ setOpen(false);
118
+ setSearch('');
119
+ setHighlighted(-1);
120
+ }
121
+ };
122
+ document.addEventListener('mousedown', handler);
123
+ return () => document.removeEventListener('mousedown', handler);
124
+ }, []);
125
+ const selectItem = (item) => {
126
+ if (item.level === 'province') {
127
+ onChange(Number(item.id.slice(2)), '', '');
128
+ }
129
+ else if (item.level === 'district' &&
130
+ item.provinceId &&
131
+ item.districtId) {
132
+ onChange(item.provinceId, item.districtId, '');
133
+ }
134
+ else if (item.level === 'municipality' &&
135
+ item.districtId &&
136
+ item.provinceId &&
137
+ item.localLevelId) {
138
+ onChange(item.provinceId, item.districtId, item.localLevelId);
139
+ }
140
+ setOpen(false);
141
+ setSearch('');
142
+ setHighlighted(-1);
143
+ };
144
+ const handleKey = (e) => {
145
+ switch (e.key) {
146
+ case 'ArrowDown':
147
+ e.preventDefault();
148
+ if (!open) {
149
+ setOpen(true);
150
+ return;
151
+ }
152
+ setHighlighted((p) => (p < filtered.length - 1 ? p + 1 : 0));
153
+ break;
154
+ case 'ArrowUp':
155
+ e.preventDefault();
156
+ setHighlighted((p) => (p > 0 ? p - 1 : filtered.length - 1));
157
+ break;
158
+ case 'Enter':
159
+ e.preventDefault();
160
+ if (open && highlighted >= 0 && highlighted < filtered.length) {
161
+ selectItem(filtered[highlighted]);
162
+ }
163
+ break;
164
+ case 'Escape':
165
+ setOpen(false);
166
+ setSearch('');
167
+ setHighlighted(-1);
168
+ inputRef.current?.blur();
169
+ break;
170
+ }
171
+ };
172
+ return (_jsxs("div", { ref: containerRef, className: `np-single-picker${className ? ` ${className}` : ''}`, style: {
173
+ position: 'relative',
174
+ minWidth: 280,
175
+ fontFamily: "-apple-system, BlinkMacSystemFont, 'SF Pro Display', sans-serif",
176
+ }, children: [_jsxs("div", { style: { position: 'relative', display: 'flex', alignItems: 'center' }, children: [_jsx("input", { ref: inputRef, type: "text", role: "combobox", "aria-expanded": open, "aria-autocomplete": "list", "aria-label": placeholder, "aria-controls": "np-single-list", "aria-activedescendant": highlighted >= 0 ? `np-si-${filtered[highlighted]?.id}` : undefined, value: open ? search : selectedPath, onChange: (e) => {
177
+ setSearch(e.target.value);
178
+ if (!open)
179
+ setOpen(true);
180
+ setHighlighted(-1);
181
+ }, onFocus: () => setFocused(true), onBlur: () => setFocused(false), onClick: () => {
182
+ if (!open) {
183
+ setOpen(true);
184
+ setSearch('');
185
+ }
186
+ }, onKeyDown: handleKey, placeholder: selectedPath ? '' : placeholder, disabled: disabled, className: inputClassName ?? '', style: {
187
+ width: '100%',
188
+ padding: '8px 28px 8px 12px',
189
+ border: `1px solid ${focused ? t.borderColorFocus : t.borderColor}`,
190
+ borderRadius: t.borderRadius,
191
+ fontSize: 14,
192
+ outline: 'none',
193
+ cursor: 'pointer',
194
+ boxSizing: 'border-box',
195
+ background: t.bg,
196
+ color: selectedPath
197
+ ? t.textColor
198
+ : 'var(--np-text-secondary, #86868b)',
199
+ lineHeight: 1.4,
200
+ boxShadow: focused ? `0 0 0 2px ${t.borderColorFocus}33` : 'none',
201
+ } }), clearable &&
202
+ (provinceId || districtId || localLevelId) &&
203
+ !disabled && (_jsx("button", { type: "button", "aria-label": "Clear selection", onClick: (e) => {
204
+ e.stopPropagation();
205
+ onChange('', '', '');
206
+ setSearch('');
207
+ inputRef.current?.focus();
208
+ }, onMouseDown: (e) => e.preventDefault(), style: {
209
+ position: 'absolute',
210
+ right: 6,
211
+ background: 'none',
212
+ border: 'none',
213
+ cursor: 'pointer',
214
+ fontSize: 14,
215
+ color: 'var(--np-text-secondary, #86868b)',
216
+ padding: '2px 4px',
217
+ lineHeight: 1,
218
+ borderRadius: 4,
219
+ }, children: "\u2715" }))] }), open && (_jsx("div", { id: "np-single-list", role: "listbox", tabIndex: -1, className: dropdownClassName ?? '', style: {
220
+ position: 'absolute',
221
+ top: '100%',
222
+ left: 0,
223
+ right: 0,
224
+ marginTop: 4,
225
+ background: t.bg,
226
+ border: `1px solid ${t.borderColor}`,
227
+ borderRadius: t.borderRadius,
228
+ boxShadow: t.shadow,
229
+ maxHeight: 300,
230
+ overflow: 'auto',
231
+ zIndex: 100,
232
+ }, children: filtered.length === 0 ? (_jsx("div", { style: {
233
+ padding: 12,
234
+ textAlign: 'center',
235
+ color: 'var(--np-text-secondary, #86868b)',
236
+ fontSize: 13,
237
+ }, children: noOptionsMessage })) : (filtered.map((item, i) => (_jsxs("div", { id: `np-si-${item.id}`, role: "option", tabIndex: -1, "aria-selected": (item.level === 'province' &&
238
+ item.provinceId === provinceId) ||
239
+ (item.level === 'district' &&
240
+ item.districtId === districtId) ||
241
+ (item.level === 'municipality' &&
242
+ item.localLevelId === localLevelId), className: optionClassName ?? '', onClick: () => selectItem(item), onKeyDown: (e) => {
243
+ if (e.key === 'Enter')
244
+ selectItem(item);
245
+ }, onMouseEnter: () => setHighlighted(i), style: {
246
+ padding: '6px 12px',
247
+ cursor: 'pointer',
248
+ fontSize: 13,
249
+ transition: 'background 0.1s',
250
+ color: t.textColor,
251
+ display: 'flex',
252
+ alignItems: 'center',
253
+ gap: 8,
254
+ ...(i === highlighted
255
+ ? { background: 'var(--np-bg-hover, #f5f5f7)' }
256
+ : {}),
257
+ }, children: [_jsx("span", { style: {
258
+ fontSize: 10,
259
+ fontWeight: 600,
260
+ color: 'var(--np-text-secondary, #86868b)',
261
+ textTransform: 'uppercase',
262
+ minWidth: 44,
263
+ letterSpacing: '0.03em',
264
+ }, children: item.levelLabel }), _jsx("span", { style: { flex: 1 }, children: labels === 'nepali' && item.name_np
265
+ ? item.name_np
266
+ : item.name })] }, item.id)))) }))] }));
267
+ }
package/dist/index.d.ts CHANGED
@@ -1,2 +1,6 @@
1
1
  export { NepalPlacesDropdown } from './NepalPlacesDropdown.js';
2
- export type { NepalPlacesDropdownProps, PlaceSelection, Labels, } from './types.js';
2
+ export { SearchableSelect } from './SearchableSelect.js';
3
+ export { SinglePlacePicker } from './SinglePlacePicker.js';
4
+ export type { NepalPlacesDropdownProps, PlaceSelection, Labels, DropdownMode, Placeholder, ThemeVars, } from './types.js';
5
+ export type { SearchableSelectOption, SearchableSelectProps, } from './SearchableSelect.js';
6
+ export type { SinglePlacePickerProps } from './SinglePlacePicker.js';
package/dist/index.js CHANGED
@@ -1 +1,3 @@
1
1
  export { NepalPlacesDropdown } from './NepalPlacesDropdown.js';
2
+ export { SearchableSelect } from './SearchableSelect.js';
3
+ export { SinglePlacePicker } from './SinglePlacePicker.js';
package/dist/types.d.ts CHANGED
@@ -1,17 +1,52 @@
1
- import type { District, LocalLevel, LocalLevelType, Province, Ward } from 'nepal-places';
1
+ import type { District, LocalLevel, LocalLevelType, Place, Province, Ward } from 'nepal-places';
2
2
  export type Labels = 'english' | 'nepali';
3
+ export type DropdownMode = 'cascade' | 'single';
3
4
  export interface PlaceSelection {
4
5
  province?: Province;
5
6
  district?: District;
6
7
  localLevel?: LocalLevel;
7
8
  ward?: Ward;
9
+ place?: Place;
10
+ province_id?: number;
11
+ district_id?: number;
12
+ local_level_id?: number;
13
+ ward_id?: number;
14
+ place_id?: number;
8
15
  code?: string;
9
16
  }
17
+ export type Placeholder = string | {
18
+ province?: string;
19
+ district?: string;
20
+ municipality?: string;
21
+ ward?: string;
22
+ place?: string;
23
+ };
24
+ export interface ThemeVars {
25
+ borderColor?: string;
26
+ borderColorFocus?: string;
27
+ borderRadius?: number;
28
+ bg?: string;
29
+ textColor?: string;
30
+ shadow?: string;
31
+ accentColor?: string;
32
+ }
10
33
  export interface NepalPlacesDropdownProps {
11
- depth?: 1 | 2 | 3 | 4;
34
+ depth?: 1 | 2 | 3 | 4 | 5;
35
+ mode?: DropdownMode;
12
36
  labels?: Labels;
13
37
  flat?: boolean;
14
38
  filterType?: LocalLevelType;
15
39
  className?: string;
40
+ style?: React.CSSProperties;
41
+ theme?: ThemeVars;
42
+ searchable?: boolean;
43
+ clearable?: boolean;
44
+ disabled?: boolean;
45
+ selectClassName?: string;
46
+ inputClassName?: string;
47
+ dropdownClassName?: string;
48
+ optionClassName?: string;
49
+ placeholder?: Placeholder;
50
+ noOptionsMessage?: string;
16
51
  onChange?: (selection: PlaceSelection) => void;
17
52
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nepal-places-react",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "React dropdown component for Nepal's provinces, districts, municipalities, and wards. Fast, typed, bilingual.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -12,7 +12,7 @@
12
12
  "types": "./dist/index.d.ts"
13
13
  }
14
14
  },
15
- "files": ["dist"],
15
+ "files": ["dist", "README.md"],
16
16
  "scripts": {
17
17
  "build": "tsc",
18
18
  "typecheck": "tsc --noEmit",
@@ -22,7 +22,7 @@
22
22
  "react": "^18.0.0 || ^19.0.0"
23
23
  },
24
24
  "dependencies": {
25
- "nepal-places": "^0.1.0"
25
+ "nepal-places": "^0.3.0"
26
26
  },
27
27
  "keywords": [
28
28
  "nepal",