nepal-places-react 0.1.1 → 0.2.2

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.
@@ -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.2.2",
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",
@@ -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.2.2"
26
26
  },
27
27
  "keywords": [
28
28
  "nepal",