@elik56a/react-native-pickers 0.0.0-stage → 0.1.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/LICENSE +21 -0
- package/README.md +276 -2
- package/lib/date/Calendar.d.ts +89 -0
- package/lib/date/Calendar.d.ts.map +1 -0
- package/lib/date/Calendar.js +249 -0
- package/lib/date/Calendar.js.map +1 -0
- package/lib/date/DatePicker.d.ts +14 -0
- package/lib/date/DatePicker.d.ts.map +1 -0
- package/lib/date/DatePicker.js +17 -0
- package/lib/date/DatePicker.js.map +1 -0
- package/lib/date/DateWheelPicker.d.ts +29 -0
- package/lib/date/DateWheelPicker.d.ts.map +1 -0
- package/lib/date/DateWheelPicker.js +83 -0
- package/lib/date/DateWheelPicker.js.map +1 -0
- package/lib/emoji/EmojiPicker.d.ts +47 -0
- package/lib/emoji/EmojiPicker.d.ts.map +1 -0
- package/lib/emoji/EmojiPicker.js +161 -0
- package/lib/emoji/EmojiPicker.js.map +1 -0
- package/lib/emoji/data.d.ts +2 -0
- package/lib/emoji/data.d.ts.map +1 -0
- package/lib/emoji/data.js +4 -0
- package/lib/emoji/data.js.map +1 -0
- package/lib/emoji/emojis.d.ts +23 -0
- package/lib/emoji/emojis.d.ts.map +1 -0
- package/lib/emoji/emojis.js +100 -0
- package/lib/emoji/emojis.js.map +1 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +17 -0
- package/lib/index.js.map +1 -0
- package/lib/select/SelectList.d.ts +58 -0
- package/lib/select/SelectList.d.ts.map +1 -0
- package/lib/select/SelectList.js +91 -0
- package/lib/select/SelectList.js.map +1 -0
- package/lib/select/SelectPicker.d.ts +35 -0
- package/lib/select/SelectPicker.d.ts.map +1 -0
- package/lib/select/SelectPicker.js +64 -0
- package/lib/select/SelectPicker.js.map +1 -0
- package/lib/select/filter.d.ts +3 -0
- package/lib/select/filter.d.ts.map +1 -0
- package/lib/select/filter.js +13 -0
- package/lib/select/filter.js.map +1 -0
- package/lib/sheet/BottomSheet.d.ts +29 -0
- package/lib/sheet/BottomSheet.d.ts.map +1 -0
- package/lib/sheet/BottomSheet.js +89 -0
- package/lib/sheet/BottomSheet.js.map +1 -0
- package/lib/sheet/PickerSheet.d.ts +26 -0
- package/lib/sheet/PickerSheet.d.ts.map +1 -0
- package/lib/sheet/PickerSheet.js +27 -0
- package/lib/sheet/PickerSheet.js.map +1 -0
- package/lib/sheet/sheets.d.ts +48 -0
- package/lib/sheet/sheets.d.ts.map +1 -0
- package/lib/sheet/sheets.js +58 -0
- package/lib/sheet/sheets.js.map +1 -0
- package/lib/theme.d.ts +81 -0
- package/lib/theme.d.ts.map +1 -0
- package/lib/theme.js +96 -0
- package/lib/theme.js.map +1 -0
- package/lib/time/TimePicker.d.ts +41 -0
- package/lib/time/TimePicker.d.ts.map +1 -0
- package/lib/time/TimePicker.js +105 -0
- package/lib/time/TimePicker.js.map +1 -0
- package/lib/types.d.ts +9 -0
- package/lib/types.d.ts.map +1 -0
- package/lib/types.js +2 -0
- package/lib/types.js.map +1 -0
- package/lib/utils/OwnScroll.d.ts +10 -0
- package/lib/utils/OwnScroll.d.ts.map +1 -0
- package/lib/utils/OwnScroll.js +13 -0
- package/lib/utils/OwnScroll.js.map +1 -0
- package/lib/utils/date.d.ts +30 -0
- package/lib/utils/date.d.ts.map +1 -0
- package/lib/utils/date.js +115 -0
- package/lib/utils/date.js.map +1 -0
- package/lib/wheel/MultiWheelPicker.d.ts +26 -0
- package/lib/wheel/MultiWheelPicker.d.ts.map +1 -0
- package/lib/wheel/MultiWheelPicker.js +28 -0
- package/lib/wheel/MultiWheelPicker.js.map +1 -0
- package/lib/wheel/WheelPicker.d.ts +59 -0
- package/lib/wheel/WheelPicker.d.ts.map +1 -0
- package/lib/wheel/WheelPicker.js +257 -0
- package/lib/wheel/WheelPicker.js.map +1 -0
- package/lib/wheel/typeToSelect.d.ts +8 -0
- package/lib/wheel/typeToSelect.d.ts.map +1 -0
- package/lib/wheel/typeToSelect.js +10 -0
- package/lib/wheel/typeToSelect.js.map +1 -0
- package/package.json +68 -3
- package/src/date/Calendar.tsx +586 -0
- package/src/date/DatePicker.tsx +22 -0
- package/src/date/DateWheelPicker.tsx +149 -0
- package/src/emoji/EmojiPicker.tsx +410 -0
- package/src/emoji/data.ts +3 -0
- package/src/emoji/emojis.ts +127 -0
- package/src/index.ts +16 -0
- package/src/select/SelectList.tsx +263 -0
- package/src/select/SelectPicker.tsx +203 -0
- package/src/select/filter.ts +17 -0
- package/src/sheet/BottomSheet.tsx +199 -0
- package/src/sheet/PickerSheet.tsx +86 -0
- package/src/sheet/sheets.tsx +218 -0
- package/src/theme.tsx +160 -0
- package/src/time/TimePicker.tsx +199 -0
- package/src/types.ts +10 -0
- package/src/utils/OwnScroll.tsx +14 -0
- package/src/utils/date.ts +131 -0
- package/src/wheel/MultiWheelPicker.tsx +96 -0
- package/src/wheel/WheelPicker.tsx +534 -0
- package/src/wheel/typeToSelect.ts +12 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eliyahu Kriel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,277 @@
|
|
|
1
|
-
#
|
|
1
|
+
# react-native-pickers
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Beautiful, fully customizable pickers for React Native — **100% JavaScript, zero native code.**
|
|
4
|
+
Works with Expo Go, bare React Native, and React Native Web out of the box.
|
|
5
|
+
|
|
6
|
+
| | |
|
|
7
|
+
|---|---|
|
|
8
|
+
| 🎡 **WheelPicker** | iOS-style 3D wheel, infinite loop, disabled items, multi-column |
|
|
9
|
+
| ⏰ **TimePicker** | 12h / 24h (auto from locale), minute & second intervals, min/max time |
|
|
10
|
+
| 📅 **DatePicker** | Calendar (single · range · multiple) or wheel, min/max, disabled days, marked dates, i18n |
|
|
11
|
+
| 📋 **SelectPicker** | Field + bottom sheet, search, groups, multi-select with chips, icons |
|
|
12
|
+
| 😀 **EmojiPicker** | 1,900+ emojis, categories, search, skin tones, recents, version filtering |
|
|
13
|
+
|
|
14
|
+
Every picker also ships as a **bottom sheet** (`DatePickerSheet`, `TimePickerSheet`, `WheelPickerSheet`, `EmojiPickerSheet`), with drag-to-dismiss and Cancel / Done.
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
npm install @elik56a/react-native-pickers
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
No other dependencies. Requires `react >= 18` and `react-native >= 0.72`.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Quick start
|
|
25
|
+
|
|
26
|
+
```tsx
|
|
27
|
+
import { DatePicker, TimePicker, SelectPicker, EmojiPicker, WheelPicker } from '@elik56a/react-native-pickers';
|
|
28
|
+
|
|
29
|
+
<WheelPicker items={['S', 'M', 'L', 'XL']} value={size} onChange={setSize} />
|
|
30
|
+
|
|
31
|
+
<TimePicker value={time} onChange={setTime} minuteInterval={5} />
|
|
32
|
+
|
|
33
|
+
<DatePicker value={date} onChange={setDate} minDate={new Date()} />
|
|
34
|
+
<DatePicker mode="range" value={range} onChange={setRange} />
|
|
35
|
+
<DatePicker display="wheel" value={birthday} onChange={setBirthday} />
|
|
36
|
+
|
|
37
|
+
<SelectPicker label="Country" options={countries} value={country} onChange={setCountry} />
|
|
38
|
+
|
|
39
|
+
<EmojiPicker onSelect={(emoji) => setText((t) => t + emoji)} />
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Bottom sheets keep a draft and only commit on **Done**:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
<DatePickerSheet
|
|
46
|
+
visible={open}
|
|
47
|
+
onClose={() => setOpen(false)}
|
|
48
|
+
title="Check-in / Check-out"
|
|
49
|
+
mode="range"
|
|
50
|
+
value={range}
|
|
51
|
+
onConfirm={setRange}
|
|
52
|
+
/>
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Every picker works **controlled** (`value` + `onChange`) or **uncontrolled** (`defaultValue`).
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## Theming
|
|
60
|
+
|
|
61
|
+
Wrap your app (optional — sensible light & dark defaults apply without it):
|
|
62
|
+
|
|
63
|
+
```tsx
|
|
64
|
+
import { PickersProvider } from '@elik56a/react-native-pickers';
|
|
65
|
+
|
|
66
|
+
<PickersProvider
|
|
67
|
+
colorScheme="auto" // 'auto' | 'light' | 'dark'
|
|
68
|
+
theme={{ colors: { primary: '#FF5A5F' }, radius: { md: 8 }, typography: { fontFamily: 'Inter' } }}
|
|
69
|
+
darkTheme={{ colors: { primary: '#FF8A8E', background: '#000' } }}
|
|
70
|
+
>
|
|
71
|
+
<App />
|
|
72
|
+
</PickersProvider>
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
Any component also accepts `theme` for a local override. Available tokens:
|
|
76
|
+
|
|
77
|
+
- `colors`: background, surface, sheet, text, textSecondary, textDisabled, primary, onPrimary, primaryMuted, border, backdrop, indicator, handle
|
|
78
|
+
- `radius`: sm, md, lg, full, sheet
|
|
79
|
+
- `spacing`: xs, sm, md, lg, xl
|
|
80
|
+
- `typography`: fontFamily, fontFamilyMedium, sizes, weights
|
|
81
|
+
- `animation`: duration
|
|
82
|
+
|
|
83
|
+
### Slot styles
|
|
84
|
+
|
|
85
|
+
Every visual part is reachable through `styles`, so you never have to fork:
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
<DatePicker
|
|
89
|
+
styles={{
|
|
90
|
+
selectedDay: { borderRadius: 8 },
|
|
91
|
+
dayText: { fontFamily: 'Inter' },
|
|
92
|
+
rangeBand: { backgroundColor: '#FFE4E6' },
|
|
93
|
+
headerTitle: { fontSize: 20 },
|
|
94
|
+
}}
|
|
95
|
+
/>
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### Render props
|
|
99
|
+
|
|
100
|
+
For full control: `renderItem` / `renderIndicator` (wheel), `renderDay` / `renderArrow` (calendar), `renderOption` / `renderTrigger` / `renderGroupHeader` (select), `renderEmoji` (emoji).
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## API
|
|
105
|
+
|
|
106
|
+
### WheelPicker
|
|
107
|
+
|
|
108
|
+
| Prop | Type | Default | |
|
|
109
|
+
|---|---|---|---|
|
|
110
|
+
| `items` | `(T \| { label, value, disabled? })[]` | — | Primitives or objects |
|
|
111
|
+
| `value` / `defaultValue` | `T` | — | |
|
|
112
|
+
| `onChange` | `(value, index) => void` | — | Fires when the wheel settles |
|
|
113
|
+
| `onValueChanging` | `(value, index) => void` | — | Fires on every tick — hook haptics here |
|
|
114
|
+
| `itemHeight` | `number` | `40` | |
|
|
115
|
+
| `visibleItems` | `number` (odd) | `5` | |
|
|
116
|
+
| `loop` | `boolean` | `false` | Infinite wheel |
|
|
117
|
+
| `keyboardInput` | `boolean` | `true` | Tap the selected row to type a value; the wheel jumps to the first matching label |
|
|
118
|
+
| `variant` | `'3d' \| 'flat'` | `'3d'` | |
|
|
119
|
+
| `rotation` | `number` | `20` | Degrees between rows (3d) |
|
|
120
|
+
| `fadeTo` | `number` | `0.3` | Opacity of the farthest rows |
|
|
121
|
+
| `isEqual` | `(a, b) => boolean` | `Object.is` | For object values |
|
|
122
|
+
| `styles` | slots: `container item itemText selectedItemText disabledItemText indicator` | | |
|
|
123
|
+
|
|
124
|
+
`MultiWheelPicker` renders several wheels sharing one selection band: `columns={[{ key, items, value, onChange, flex, loop }]}` and an optional `separator`.
|
|
125
|
+
|
|
126
|
+
Haptics example (no dependency imposed — bring your own):
|
|
127
|
+
|
|
128
|
+
```tsx
|
|
129
|
+
import * as Haptics from 'expo-haptics';
|
|
130
|
+
<WheelPicker onValueChanging={() => Haptics.selectionAsync()} ... />
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### TimePicker
|
|
134
|
+
|
|
135
|
+
| Prop | Type | Default |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `value` / `defaultValue` | `Date` | now |
|
|
138
|
+
| `onChange` / `onValueChanging` | `(date) => void` | |
|
|
139
|
+
| `format` | `'12h' \| '24h' \| 'auto'` | `'auto'` (from `locale`) |
|
|
140
|
+
| `locale` | `string` | device |
|
|
141
|
+
| `minuteInterval` | `number` | `1` |
|
|
142
|
+
| `showSeconds` / `secondInterval` | `boolean` / `number` | `false` / `1` |
|
|
143
|
+
| `minTime` / `maxTime` | `Date` (time of day) | |
|
|
144
|
+
| `loop` | `boolean` | `true` |
|
|
145
|
+
| `dayPeriodLabels` | `[am, pm]` | from locale |
|
|
146
|
+
| `dayPeriodFirst` | `boolean` | `false` |
|
|
147
|
+
|
|
148
|
+
### DatePicker
|
|
149
|
+
|
|
150
|
+
`display="calendar"` (default) or `display="wheel"`.
|
|
151
|
+
|
|
152
|
+
Selection modes:
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
mode?: 'single' value: Date | null onChange(date)
|
|
156
|
+
mode: 'range' value: { start, end } onChange(range) minRangeDays, maxRangeDays, allowSingleDayRange
|
|
157
|
+
mode: 'multiple' value: Date[] onChange(dates) maxSelections
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
| Prop | Type | Default | |
|
|
161
|
+
|---|---|---|---|
|
|
162
|
+
| `minDate` / `maxDate` | `Date` | | |
|
|
163
|
+
| `isDateDisabled` | `(date) => boolean` | | e.g. weekends |
|
|
164
|
+
| `markedDates` | `{ 'YYYY-MM-DD': { dots?, label?, disabled?, textColor? } }` | | Dots, prices, events |
|
|
165
|
+
| `locale` | `string` | device | Month & weekday names via `Intl` |
|
|
166
|
+
| `firstDayOfWeek` | `0–6` | from locale | |
|
|
167
|
+
| `showOutsideDays` / `fixedWeeks` | `boolean` | `true` | |
|
|
168
|
+
| `enableSwipe` | `boolean` | `true` | Swipe between months |
|
|
169
|
+
| `enableQuickJump` | `boolean` | `true` | Tap title → month grid → year grid |
|
|
170
|
+
| `weekdayFormat` | `'short' \| 'narrow'` | `'short'` | |
|
|
171
|
+
| `dayHeight` | `number` | `44` | |
|
|
172
|
+
| `onMonthChange` | `(month) => void` | | |
|
|
173
|
+
|
|
174
|
+
Ranges never cross disabled days — tapping past one starts a new range.
|
|
175
|
+
|
|
176
|
+
Wheel mode: `order` (`['day','month','year']`, defaults from locale), `monthFormat` (`'long' | 'short' | 'numeric'`), `yearRange`.
|
|
177
|
+
|
|
178
|
+
### SelectPicker / SelectList
|
|
179
|
+
|
|
180
|
+
`SelectPicker` is a field that opens a sheet; `SelectList` is the inline list.
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
options: { label, value, description?, icon?, group?, disabled?, keywords? }[]
|
|
184
|
+
multiple?: false value: T | null onChange(value, option)
|
|
185
|
+
multiple: true value: T[] onChange(values, options) maxSelected
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| Prop | Default | |
|
|
189
|
+
|---|---|---|
|
|
190
|
+
| `searchable` | `> 8 options` | Accent- and niqqud-insensitive |
|
|
191
|
+
| `filterOption` | | Custom search |
|
|
192
|
+
| `placeholder` / `label` / `sheetTitle` | | |
|
|
193
|
+
| `clearable` | `false` | |
|
|
194
|
+
| `chips` | `true` | Removable chips in multi mode |
|
|
195
|
+
| `closeOnSelect` | `true` | Single mode |
|
|
196
|
+
| `disabled` | `false` | |
|
|
197
|
+
|
|
198
|
+
### EmojiPicker
|
|
199
|
+
|
|
200
|
+
| Prop | Default | |
|
|
201
|
+
|---|---|---|
|
|
202
|
+
| `onSelect` | — | `(emoji: string, data: Emoji) => void` |
|
|
203
|
+
| `columns` / `emojiSize` / `height` | `8` / auto / `380` | |
|
|
204
|
+
| `categories` | all | Filter & reorder |
|
|
205
|
+
| `exclude` | | Characters to hide |
|
|
206
|
+
| `maxEmojiVersion` | | Hide emojis older devices can't render |
|
|
207
|
+
| `skinTone` / `defaultSkinTone` / `onSkinToneChange` | `0` | 0 = default, 1–5 light → dark |
|
|
208
|
+
| `recent` / `defaultRecent` / `onRecentChange` / `maxRecent` | `[]` / `24` | Persist with your own storage |
|
|
209
|
+
| `categoryBarPosition` | `'top'` | or `'bottom'` |
|
|
210
|
+
| `categoryLabels` / `categoryIcons` | | i18n and custom icons |
|
|
211
|
+
| `searchPlaceholder` / `emptyText` | | |
|
|
212
|
+
|
|
213
|
+
Persisting recents with AsyncStorage:
|
|
214
|
+
|
|
215
|
+
```tsx
|
|
216
|
+
const [recent, setRecent] = useState<string[]>([]);
|
|
217
|
+
useEffect(() => { AsyncStorage.getItem('recent').then((v) => v && setRecent(JSON.parse(v))); }, []);
|
|
218
|
+
|
|
219
|
+
<EmojiPicker
|
|
220
|
+
recent={recent}
|
|
221
|
+
onRecentChange={(r) => { setRecent(r); AsyncStorage.setItem('recent', JSON.stringify(r)); }}
|
|
222
|
+
onSelect={...}
|
|
223
|
+
/>
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
Utilities: `getAllEmojis()`, `searchEmojis(query)`, `findEmoji(char)`, `withSkinTone(emoji, tone)`.
|
|
227
|
+
|
|
228
|
+
### BottomSheet
|
|
229
|
+
|
|
230
|
+
Use the same sheet for your own content:
|
|
231
|
+
|
|
232
|
+
```tsx
|
|
233
|
+
<BottomSheet visible={open} onClose={close} title="Filters" footer={<ApplyButton />}>
|
|
234
|
+
...
|
|
235
|
+
</BottomSheet>
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Props: `title`, `headerLeft`, `headerRight`, `footer`, `enableSwipeToClose`, `closeOnBackdropPress`, `height`, `maxHeightRatio`, `bottomInset`, `styles`.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
## RTL & i18n
|
|
243
|
+
|
|
244
|
+
Layouts use logical `start` / `end`, so they flip automatically with `I18nManager.isRTL`; calendar arrows and swipe direction follow. All month, weekday, and AM/PM names come from `Intl` (Hermes supports it), with English fallbacks. Every user-facing string (Done, Cancel, Search, empty states, category labels) is a prop.
|
|
245
|
+
|
|
246
|
+
## Accessibility
|
|
247
|
+
|
|
248
|
+
Wheels are `adjustable` with increment / decrement actions for VoiceOver and TalkBack. Days announce their full date, and selected / disabled states are exposed everywhere.
|
|
249
|
+
|
|
250
|
+
## Performance
|
|
251
|
+
|
|
252
|
+
Animations run on the native driver. Wheels and the emoji grid are virtualized `FlatList`s with fixed row layouts, and rows are memoized so scrolling re-renders only the two rows that change.
|
|
253
|
+
|
|
254
|
+
## Development
|
|
255
|
+
|
|
256
|
+
```sh
|
|
257
|
+
npm test # unit tests
|
|
258
|
+
npm run typecheck
|
|
259
|
+
npm run build # lib/ (ESM + .d.ts)
|
|
260
|
+
npm run example:web # bundles example/App.tsx for React Native Web → example/web/dist
|
|
261
|
+
npm run gen:emoji # regenerate emoji data from emojibase-data
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
### Running the example on iOS / Android
|
|
265
|
+
|
|
266
|
+
[`example/native`](example/native) is an Expo app that renders [`example/App.tsx`](example/App.tsx) against the library source, so edits in `src/` hot-reload.
|
|
267
|
+
|
|
268
|
+
```sh
|
|
269
|
+
pnpm install # from the repo root (pnpm workspace)
|
|
270
|
+
pnpm example # press i for the iOS Simulator, a for the Android emulator
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
Emoji data comes from [emojibase](https://emojibase.dev) (MIT).
|
|
274
|
+
|
|
275
|
+
## License
|
|
276
|
+
|
|
277
|
+
MIT
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { StyleProp, ViewStyle } from 'react-native';
|
|
3
|
+
import type { PickerTheme } from '../theme';
|
|
4
|
+
import type { SlotStyles, ThemableProps } from '../types';
|
|
5
|
+
export interface DateRange {
|
|
6
|
+
start: Date | null;
|
|
7
|
+
end: Date | null;
|
|
8
|
+
}
|
|
9
|
+
export interface MarkedDate {
|
|
10
|
+
/** Dot colors under the day number */
|
|
11
|
+
dots?: string[];
|
|
12
|
+
disabled?: boolean;
|
|
13
|
+
/** Small text under the day (e.g. a price) */
|
|
14
|
+
label?: string;
|
|
15
|
+
textColor?: string;
|
|
16
|
+
}
|
|
17
|
+
export type CalendarSlot = 'container' | 'header' | 'headerTitle' | 'arrow' | 'arrowText' | 'weekdays' | 'weekdayText' | 'week' | 'day' | 'dayInner' | 'dayText' | 'selectedDay' | 'selectedDayText' | 'todayDay' | 'todayDayText' | 'disabledDayText' | 'outsideDayText' | 'rangeBand' | 'dot' | 'dayLabel' | 'gridCell' | 'gridCellText';
|
|
18
|
+
export interface DayState {
|
|
19
|
+
date: Date;
|
|
20
|
+
selected: boolean;
|
|
21
|
+
rangeStart: boolean;
|
|
22
|
+
rangeEnd: boolean;
|
|
23
|
+
inRange: boolean;
|
|
24
|
+
today: boolean;
|
|
25
|
+
disabled: boolean;
|
|
26
|
+
outside: boolean;
|
|
27
|
+
marking?: MarkedDate;
|
|
28
|
+
}
|
|
29
|
+
export interface CalendarBaseProps extends ThemableProps {
|
|
30
|
+
locale?: string;
|
|
31
|
+
minDate?: Date;
|
|
32
|
+
maxDate?: Date;
|
|
33
|
+
/** Return true to disable a day */
|
|
34
|
+
isDateDisabled?: (date: Date) => boolean;
|
|
35
|
+
/** 0 = Sunday ... 6 = Saturday. Default: from locale, else 0 */
|
|
36
|
+
firstDayOfWeek?: number;
|
|
37
|
+
/** Keyed by `YYYY-MM-DD` */
|
|
38
|
+
markedDates?: Record<string, MarkedDate>;
|
|
39
|
+
/** Month shown initially. Default: the selected date or today */
|
|
40
|
+
initialMonth?: Date;
|
|
41
|
+
onMonthChange?: (month: Date) => void;
|
|
42
|
+
showOutsideDays?: boolean;
|
|
43
|
+
/** Always render 6 weeks to keep the height stable. Default: true */
|
|
44
|
+
fixedWeeks?: boolean;
|
|
45
|
+
/** Swipe left/right to change months. Default: true */
|
|
46
|
+
enableSwipe?: boolean;
|
|
47
|
+
/** Tap the title to jump by month / year. Default: true */
|
|
48
|
+
enableQuickJump?: boolean;
|
|
49
|
+
weekdayFormat?: 'short' | 'narrow';
|
|
50
|
+
showWeekdays?: boolean;
|
|
51
|
+
/** Override the title text */
|
|
52
|
+
formatTitle?: (month: Date, locale?: string) => string;
|
|
53
|
+
/** Fully custom day cell */
|
|
54
|
+
renderDay?: (state: DayState, theme: PickerTheme) => React.ReactNode;
|
|
55
|
+
/** Custom arrows */
|
|
56
|
+
renderArrow?: (direction: 'prev' | 'next', disabled: boolean) => React.ReactNode;
|
|
57
|
+
/** Range of years in the year grid. Default: minDate/maxDate or ±100 years */
|
|
58
|
+
yearRange?: [number, number];
|
|
59
|
+
/** Day cell height. Default: 44 */
|
|
60
|
+
dayHeight?: number;
|
|
61
|
+
style?: StyleProp<ViewStyle>;
|
|
62
|
+
styles?: SlotStyles<CalendarSlot>;
|
|
63
|
+
testID?: string;
|
|
64
|
+
}
|
|
65
|
+
export type CalendarSelectionProps = {
|
|
66
|
+
mode?: 'single';
|
|
67
|
+
value?: Date | null;
|
|
68
|
+
defaultValue?: Date | null;
|
|
69
|
+
onChange?: (date: Date) => void;
|
|
70
|
+
} | {
|
|
71
|
+
mode: 'range';
|
|
72
|
+
value?: DateRange;
|
|
73
|
+
defaultValue?: DateRange;
|
|
74
|
+
onChange?: (range: DateRange) => void;
|
|
75
|
+
/** Minimum nights in a range (end - start, in days) */
|
|
76
|
+
minRangeDays?: number;
|
|
77
|
+
maxRangeDays?: number;
|
|
78
|
+
/** Allow start === end. Default: true */
|
|
79
|
+
allowSingleDayRange?: boolean;
|
|
80
|
+
} | {
|
|
81
|
+
mode: 'multiple';
|
|
82
|
+
value?: Date[];
|
|
83
|
+
defaultValue?: Date[];
|
|
84
|
+
onChange?: (dates: Date[]) => void;
|
|
85
|
+
maxSelections?: number;
|
|
86
|
+
};
|
|
87
|
+
export type CalendarProps = CalendarBaseProps & CalendarSelectionProps;
|
|
88
|
+
export declare function Calendar(props: CalendarProps): React.JSX.Element;
|
|
89
|
+
//# sourceMappingURL=Calendar.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Calendar.d.ts","sourceRoot":"","sources":["../../src/date/Calendar.tsx"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
3
|
+
import { Animated, I18nManager, PanResponder, Platform, Pressable, StyleSheet, Text, View } from 'react-native';
|
|
4
|
+
import { textStyle, usePickerTheme } from '../theme';
|
|
5
|
+
import { addMonths, buildMonthGrid, compareDay, formatDate, formatMonthYear, getFirstDayOfWeek, getMonthNames, getWeekdayNames, isBetweenDays, isOutOfRange, isSameDay, startOfMonth, toDateKey, } from '../utils/date';
|
|
6
|
+
const DAY_MS = 86400000;
|
|
7
|
+
export function Calendar(props) {
|
|
8
|
+
const { locale, minDate, maxDate, isDateDisabled, markedDates, initialMonth, onMonthChange, showOutsideDays = true, fixedWeeks = true, enableSwipe = true, enableQuickJump = true, weekdayFormat = 'short', showWeekdays = true, formatTitle, renderDay, renderArrow, yearRange, dayHeight = 44, style, styles: slots, theme: themeOverride, testID, } = props;
|
|
9
|
+
const theme = usePickerTheme(themeOverride);
|
|
10
|
+
const mode = props.mode ?? 'single';
|
|
11
|
+
const firstDay = props.firstDayOfWeek ?? getFirstDayOfWeek(locale);
|
|
12
|
+
const rtl = !!I18nManager.isRTL;
|
|
13
|
+
// ----- selection state (controlled or uncontrolled) -----
|
|
14
|
+
const [innerSingle, setInnerSingle] = useState(mode === 'single' ? (props.defaultValue ?? null) : null);
|
|
15
|
+
const [innerRange, setInnerRange] = useState(mode === 'range' ? (props.defaultValue ?? { start: null, end: null }) : { start: null, end: null });
|
|
16
|
+
const [innerMulti, setInnerMulti] = useState(mode === 'multiple' ? (props.defaultValue ?? []) : []);
|
|
17
|
+
const single = mode === 'single' ? (props.value !== undefined ? props.value : innerSingle) : null;
|
|
18
|
+
const range = mode === 'range' ? (props.value ?? innerRange) : { start: null, end: null };
|
|
19
|
+
const multi = mode === 'multiple' ? (props.value ?? innerMulti) : [];
|
|
20
|
+
const anchor = initialMonth ?? single ?? range.start ?? multi[0] ?? new Date();
|
|
21
|
+
const [month, setMonth] = useState(() => startOfMonth(anchor));
|
|
22
|
+
const [view, setView] = useState('days');
|
|
23
|
+
const today = useMemo(() => new Date(), []);
|
|
24
|
+
// Follow external value changes in single mode
|
|
25
|
+
const singleKey = single ? toDateKey(single) : '';
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (single && view === 'days' && (single.getMonth() !== month.getMonth() || single.getFullYear() !== month.getFullYear())) {
|
|
28
|
+
setMonth(startOfMonth(single));
|
|
29
|
+
}
|
|
30
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
31
|
+
}, [singleKey]);
|
|
32
|
+
const isDisabled = useCallback((d) => isOutOfRange(d, minDate, maxDate) || !!isDateDisabled?.(d) || !!markedDates?.[toDateKey(d)]?.disabled, [isDateDisabled, markedDates, maxDate, minDate]);
|
|
33
|
+
// ----- month navigation with slide animation -----
|
|
34
|
+
const slide = useRef(new Animated.Value(0)).current;
|
|
35
|
+
const native = Platform.OS !== 'web';
|
|
36
|
+
const canPrev = !minDate || compareDay(addMonths(month, -1), startOfMonth(minDate)) >= 0;
|
|
37
|
+
const canNext = !maxDate || compareDay(addMonths(month, 1), startOfMonth(maxDate)) <= 0;
|
|
38
|
+
const goMonth = useCallback((delta) => {
|
|
39
|
+
if ((delta < 0 && !canPrev) || (delta > 0 && !canNext))
|
|
40
|
+
return;
|
|
41
|
+
const next = addMonths(month, delta);
|
|
42
|
+
setMonth(next);
|
|
43
|
+
onMonthChange?.(next);
|
|
44
|
+
slide.setValue(delta * (rtl ? -1 : 1));
|
|
45
|
+
Animated.timing(slide, { toValue: 0, duration: theme.animation.duration, useNativeDriver: native }).start();
|
|
46
|
+
}, [canNext, canPrev, month, native, onMonthChange, rtl, slide, theme.animation.duration]);
|
|
47
|
+
const goRef = useRef(goMonth);
|
|
48
|
+
goRef.current = goMonth;
|
|
49
|
+
const pan = useRef(PanResponder.create({
|
|
50
|
+
onMoveShouldSetPanResponder: (_, g) => Math.abs(g.dx) > 14 && Math.abs(g.dx) > Math.abs(g.dy) * 1.5,
|
|
51
|
+
onPanResponderRelease: (_, g) => {
|
|
52
|
+
if (Math.abs(g.dx) > 50 || Math.abs(g.vx) > 0.5) {
|
|
53
|
+
const dir = g.dx < 0 ? 1 : -1;
|
|
54
|
+
goRef.current(I18nManager.isRTL === true ? -dir : dir);
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
})).current;
|
|
58
|
+
// ----- selection logic -----
|
|
59
|
+
const select = useCallback((d) => {
|
|
60
|
+
if (mode === 'single') {
|
|
61
|
+
if (props.value === undefined)
|
|
62
|
+
setInnerSingle(d);
|
|
63
|
+
props.onChange?.(d);
|
|
64
|
+
}
|
|
65
|
+
else if (mode === 'range') {
|
|
66
|
+
const p = props;
|
|
67
|
+
let next;
|
|
68
|
+
if (!range.start || range.end || compareDay(d, range.start) < 0) {
|
|
69
|
+
next = { start: d, end: null };
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
const days = Math.round((d.getTime() - range.start.getTime()) / DAY_MS);
|
|
73
|
+
if (days === 0 && p.allowSingleDayRange === false)
|
|
74
|
+
return;
|
|
75
|
+
if (p.minRangeDays !== undefined && days < p.minRangeDays)
|
|
76
|
+
return;
|
|
77
|
+
if (p.maxRangeDays !== undefined && days > p.maxRangeDays)
|
|
78
|
+
next = { start: d, end: null };
|
|
79
|
+
else {
|
|
80
|
+
// Reject ranges that cross disabled days
|
|
81
|
+
let crosses = false;
|
|
82
|
+
for (let t = new Date(range.start); compareDay(t, d) < 0; t = new Date(t.getFullYear(), t.getMonth(), t.getDate() + 1)) {
|
|
83
|
+
if (isDisabled(t)) {
|
|
84
|
+
crosses = true;
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
next = crosses ? { start: d, end: null } : { start: range.start, end: d };
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (p.value === undefined)
|
|
92
|
+
setInnerRange(next);
|
|
93
|
+
p.onChange?.(next);
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
const p = props;
|
|
97
|
+
const exists = multi.some((x) => isSameDay(x, d));
|
|
98
|
+
if (!exists && p.maxSelections !== undefined && multi.length >= p.maxSelections)
|
|
99
|
+
return;
|
|
100
|
+
const next = exists ? multi.filter((x) => !isSameDay(x, d)) : [...multi, d].sort((a, b) => a.getTime() - b.getTime());
|
|
101
|
+
if (p.value === undefined)
|
|
102
|
+
setInnerMulti(next);
|
|
103
|
+
p.onChange?.(next);
|
|
104
|
+
}
|
|
105
|
+
}, [isDisabled, mode, multi, props, range.end, range.start]);
|
|
106
|
+
const grid = useMemo(() => buildMonthGrid(month.getFullYear(), month.getMonth(), firstDay, fixedWeeks), [firstDay, fixedWeeks, month]);
|
|
107
|
+
const weekdayNames = useMemo(() => {
|
|
108
|
+
const names = getWeekdayNames(locale, weekdayFormat);
|
|
109
|
+
return Array.from({ length: 7 }, (_, i) => names[(firstDay + i) % 7]);
|
|
110
|
+
}, [firstDay, locale, weekdayFormat]);
|
|
111
|
+
const title = formatTitle ? formatTitle(month, locale) : formatMonthYear(month, locale);
|
|
112
|
+
const accent = theme.colors.primary;
|
|
113
|
+
const header = (_jsxs(View, { style: [s.header, { paddingHorizontal: theme.spacing.sm, marginBottom: theme.spacing.sm }, slots?.header], children: [_jsxs(Pressable, { onPress: enableQuickJump ? () => setView(view === 'days' ? 'months' : 'days') : undefined, style: ({ pressed }) => [s.titleBtn, { borderRadius: theme.radius.sm, opacity: pressed ? 0.6 : 1 }], accessibilityRole: enableQuickJump ? 'button' : 'header', accessibilityLabel: title, children: [_jsx(Text, { style: [textStyle(theme, 'md', 'bold'), slots?.headerTitle], children: view === 'years' ? yearPageLabel(month.getFullYear()) : view === 'months' ? String(month.getFullYear()) : title }), enableQuickJump && (_jsx(Text, { style: [textStyle(theme, 'sm', 'bold'), { color: accent, marginStart: 6 }], children: view === 'days' ? '▾' : '▴' }))] }), _jsx(View, { style: s.arrows, children: ['prev', 'next'].map((dir) => {
|
|
114
|
+
const disabled = view === 'days' ? (dir === 'prev' ? !canPrev : !canNext) : false;
|
|
115
|
+
const onPress = () => {
|
|
116
|
+
const delta = dir === 'prev' ? -1 : 1;
|
|
117
|
+
if (view === 'days')
|
|
118
|
+
goMonth(delta);
|
|
119
|
+
else if (view === 'months')
|
|
120
|
+
setMonth(new Date(month.getFullYear() + delta, month.getMonth(), 1));
|
|
121
|
+
else
|
|
122
|
+
setMonth(new Date(month.getFullYear() + delta * 12, month.getMonth(), 1));
|
|
123
|
+
};
|
|
124
|
+
return (_jsx(Pressable, { onPress: disabled ? undefined : onPress, hitSlop: 8, style: ({ pressed }) => [
|
|
125
|
+
s.arrow,
|
|
126
|
+
{ borderRadius: theme.radius.full, backgroundColor: pressed ? theme.colors.surface : 'transparent' },
|
|
127
|
+
slots?.arrow,
|
|
128
|
+
], accessibilityRole: "button", accessibilityLabel: dir === 'prev' ? 'Previous' : 'Next', accessibilityState: { disabled }, children: renderArrow ? (renderArrow(dir, disabled)) : (_jsx(Text, { style: [
|
|
129
|
+
textStyle(theme, 'lg', 'bold'),
|
|
130
|
+
{ color: disabled ? theme.colors.textDisabled : accent, lineHeight: 22 },
|
|
131
|
+
slots?.arrowText,
|
|
132
|
+
], children: (dir === 'prev') !== rtl ? '‹' : '›' })) }, dir));
|
|
133
|
+
}) })] }));
|
|
134
|
+
const translateX = slide.interpolate({ inputRange: [-1, 0, 1], outputRange: [-40, 0, 40] });
|
|
135
|
+
const opacity = slide.interpolate({ inputRange: [-1, 0, 1], outputRange: [0, 1, 0] });
|
|
136
|
+
let body;
|
|
137
|
+
if (view === 'months') {
|
|
138
|
+
const names = getMonthNames(locale, 'short');
|
|
139
|
+
body = (_jsx(QuickGrid, { theme: theme, slots: slots, cells: names.map((label, m) => {
|
|
140
|
+
const d = new Date(month.getFullYear(), m, 1);
|
|
141
|
+
const disabled = (!!minDate && compareDay(new Date(month.getFullYear(), m + 1, 0), minDate) < 0) || (!!maxDate && compareDay(d, maxDate) > 0);
|
|
142
|
+
return { key: String(m), label, selected: m === month.getMonth(), disabled, onPress: () => {
|
|
143
|
+
setMonth(d);
|
|
144
|
+
onMonthChange?.(d);
|
|
145
|
+
setView('days');
|
|
146
|
+
} };
|
|
147
|
+
}), onTitle: () => setView('years'), height: dayHeight * (fixedWeeks ? 6 : 5) + (showWeekdays ? 28 : 0) }));
|
|
148
|
+
}
|
|
149
|
+
else if (view === 'years') {
|
|
150
|
+
const y0 = Math.floor(month.getFullYear() / 12) * 12;
|
|
151
|
+
const [minY, maxY] = yearRange ?? [minDate?.getFullYear() ?? today.getFullYear() - 100, maxDate?.getFullYear() ?? today.getFullYear() + 100];
|
|
152
|
+
body = (_jsx(QuickGrid, { theme: theme, slots: slots, cells: Array.from({ length: 12 }, (_, i) => {
|
|
153
|
+
const y = y0 + i;
|
|
154
|
+
return { key: String(y), label: String(y), selected: y === month.getFullYear(), disabled: y < minY || y > maxY, onPress: () => {
|
|
155
|
+
setMonth(new Date(y, month.getMonth(), 1));
|
|
156
|
+
setView('months');
|
|
157
|
+
} };
|
|
158
|
+
}), height: dayHeight * (fixedWeeks ? 6 : 5) + (showWeekdays ? 28 : 0) }));
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
body = (_jsxs(Animated.View, { style: { transform: [{ translateX }], opacity }, ...(enableSwipe ? pan.panHandlers : {}), children: [showWeekdays && (_jsx(View, { style: [s.week, { marginBottom: 4 }, slots?.weekdays], children: weekdayNames.map((w, i) => (_jsx(View, { style: s.cell, children: _jsx(Text, { style: [textStyle(theme, 'xs', 'medium'), { color: theme.colors.textSecondary, textAlign: 'center' }, slots?.weekdayText], children: w }) }, i))) })), grid.map((week, wi) => (_jsx(View, { style: [s.week, slots?.week], children: week.map((d) => {
|
|
162
|
+
const outside = d.getMonth() !== month.getMonth();
|
|
163
|
+
const rangeStart = mode === 'range' && isSameDay(d, range.start);
|
|
164
|
+
const rangeEnd = mode === 'range' && isSameDay(d, range.end);
|
|
165
|
+
const state = {
|
|
166
|
+
date: d,
|
|
167
|
+
outside,
|
|
168
|
+
today: isSameDay(d, today),
|
|
169
|
+
disabled: isDisabled(d),
|
|
170
|
+
rangeStart,
|
|
171
|
+
rangeEnd,
|
|
172
|
+
inRange: mode === 'range' && isBetweenDays(d, range.start, range.end),
|
|
173
|
+
selected: mode === 'single' ? isSameDay(d, single) : mode === 'range' ? rangeStart || rangeEnd : multi.some((x) => isSameDay(x, d)),
|
|
174
|
+
marking: markedDates?.[toDateKey(d)],
|
|
175
|
+
};
|
|
176
|
+
return (_jsx(DayCell, { state: state, hidden: outside && !showOutsideDays, hasRangeEnd: !!range.end, theme: theme, slots: slots, height: dayHeight, locale: locale, renderDay: renderDay, onPress: select }, d.getTime()));
|
|
177
|
+
}) }, wi)))] }));
|
|
178
|
+
}
|
|
179
|
+
return (_jsxs(View, { style: [{ backgroundColor: theme.colors.background, paddingVertical: theme.spacing.sm }, style, slots?.container], testID: testID, children: [header, body] }));
|
|
180
|
+
}
|
|
181
|
+
function yearPageLabel(year) {
|
|
182
|
+
const y0 = Math.floor(year / 12) * 12;
|
|
183
|
+
return `${y0} – ${y0 + 11}`;
|
|
184
|
+
}
|
|
185
|
+
const DayCell = memo(function DayCell({ state, hidden, hasRangeEnd, theme, slots, height, locale, renderDay, onPress }) {
|
|
186
|
+
const { date, selected, disabled, outside, today, inRange, rangeStart, rangeEnd, marking } = state;
|
|
187
|
+
if (hidden)
|
|
188
|
+
return _jsx(View, { style: [s.cell, { height }] });
|
|
189
|
+
const c = theme.colors;
|
|
190
|
+
const size = Math.min(height - 4, 40);
|
|
191
|
+
const band = inRange || (hasRangeEnd && (rangeStart || rangeEnd) && !(rangeStart && rangeEnd));
|
|
192
|
+
const textColor = selected
|
|
193
|
+
? c.onPrimary
|
|
194
|
+
: disabled
|
|
195
|
+
? c.textDisabled
|
|
196
|
+
: marking?.textColor ?? (outside ? c.textDisabled : today ? c.primary : c.text);
|
|
197
|
+
return (_jsx(Pressable, { onPress: disabled ? undefined : () => onPress(date), style: [s.cell, { height }, slots?.day], accessibilityRole: "button", accessibilityLabel: formatDate(date, locale, { weekday: 'long', day: 'numeric', month: 'long', year: 'numeric' }), accessibilityState: { selected, disabled }, children: ({ pressed }) => (_jsxs(_Fragment, { children: [band && (_jsx(View, { style: [
|
|
198
|
+
s.band,
|
|
199
|
+
{ top: (height - size) / 2, height: size, backgroundColor: c.primaryMuted },
|
|
200
|
+
rangeStart ? { start: '50%' } : rangeEnd ? { end: '50%' } : null,
|
|
201
|
+
slots?.rangeBand,
|
|
202
|
+
] })), renderDay ? (renderDay(state, theme)) : (_jsxs(View, { style: [
|
|
203
|
+
s.dayInner,
|
|
204
|
+
{ width: size, height: size, borderRadius: size / 2 },
|
|
205
|
+
selected && { backgroundColor: c.primary },
|
|
206
|
+
!selected && pressed && !disabled && { backgroundColor: c.surface },
|
|
207
|
+
!selected && today && { borderWidth: 1.5, borderColor: c.primary },
|
|
208
|
+
slots?.dayInner,
|
|
209
|
+
today && slots?.todayDay,
|
|
210
|
+
selected && slots?.selectedDay,
|
|
211
|
+
], children: [_jsx(Text, { style: [
|
|
212
|
+
textStyle(theme, 'md', selected || today ? 'bold' : 'regular'),
|
|
213
|
+
{ color: textColor, textDecorationLine: disabled && !outside ? 'line-through' : 'none' },
|
|
214
|
+
slots?.dayText,
|
|
215
|
+
outside && slots?.outsideDayText,
|
|
216
|
+
disabled && slots?.disabledDayText,
|
|
217
|
+
today && slots?.todayDayText,
|
|
218
|
+
selected && slots?.selectedDayText,
|
|
219
|
+
], children: date.getDate() }), marking?.label ? (_jsx(Text, { style: [{ fontSize: 9, color: selected ? c.onPrimary : c.textSecondary, marginTop: -2 }, slots?.dayLabel], numberOfLines: 1, children: marking.label })) : null, marking?.dots?.length ? (_jsx(View, { style: s.dots, children: marking.dots.slice(0, 3).map((color, i) => (_jsx(View, { style: [s.dot, { backgroundColor: selected ? c.onPrimary : color }, slots?.dot] }, i))) })) : null] }))] })) }));
|
|
220
|
+
});
|
|
221
|
+
function QuickGrid({ cells, theme, slots, height }) {
|
|
222
|
+
const rows = [cells.slice(0, 3), cells.slice(3, 6), cells.slice(6, 9), cells.slice(9, 12)];
|
|
223
|
+
return (_jsx(View, { style: { height, justifyContent: 'space-evenly', paddingHorizontal: theme.spacing.sm }, children: rows.map((row, i) => (_jsx(View, { style: s.week, children: row.map((c) => (_jsx(Pressable, { onPress: c.disabled ? undefined : c.onPress, style: ({ pressed }) => [
|
|
224
|
+
s.quick,
|
|
225
|
+
{
|
|
226
|
+
borderRadius: theme.radius.full,
|
|
227
|
+
backgroundColor: c.selected ? theme.colors.primary : pressed ? theme.colors.surface : 'transparent',
|
|
228
|
+
},
|
|
229
|
+
slots?.gridCell,
|
|
230
|
+
], accessibilityRole: "button", accessibilityState: { selected: c.selected, disabled: c.disabled }, children: _jsx(Text, { style: [
|
|
231
|
+
textStyle(theme, 'md', c.selected ? 'bold' : 'regular'),
|
|
232
|
+
{ color: c.selected ? theme.colors.onPrimary : c.disabled ? theme.colors.textDisabled : theme.colors.text },
|
|
233
|
+
slots?.gridCellText,
|
|
234
|
+
], children: c.label }) }, c.key))) }, i))) }));
|
|
235
|
+
}
|
|
236
|
+
const s = StyleSheet.create({
|
|
237
|
+
header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
238
|
+
titleBtn: { flexDirection: 'row', alignItems: 'center', paddingVertical: 6, paddingHorizontal: 8 },
|
|
239
|
+
arrows: { flexDirection: 'row', gap: 4 },
|
|
240
|
+
arrow: { width: 36, height: 36, alignItems: 'center', justifyContent: 'center' },
|
|
241
|
+
week: { flexDirection: 'row' },
|
|
242
|
+
cell: { flex: 1, alignItems: 'center', justifyContent: 'center' },
|
|
243
|
+
dayInner: { alignItems: 'center', justifyContent: 'center' },
|
|
244
|
+
band: { position: 'absolute', start: 0, end: 0 },
|
|
245
|
+
dots: { flexDirection: 'row', gap: 2, position: 'absolute', bottom: 4 },
|
|
246
|
+
dot: { width: 4, height: 4, borderRadius: 2 },
|
|
247
|
+
quick: { flex: 1, marginHorizontal: 6, height: 44, alignItems: 'center', justifyContent: 'center' },
|
|
248
|
+
});
|
|
249
|
+
//# sourceMappingURL=Calendar.js.map
|