@octane-xplat/date-picker 0.0.1 → 0.9.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.
@@ -0,0 +1,335 @@
1
+ // Adapted from @expo/ui (MIT) — Copyright 2025-present 650 Industries.
2
+ // Upstream: expo/packages/expo-ui/android/.../ui/DatePickerView.kt (sdk-57).
3
+ // The ExpoModules glue (@OptimizedRecord/@OptimizedComposeProps records,
4
+ // ModifierRegistry, AsyncFunctionHandle) is replaced by the
5
+ // @nativescript/jetpack-compose provider contract: updateData(Map) + onEvent.
6
+ // Colors arrive as CSS hex strings instead of expo ColorValue ints. The
7
+ // dialog wrappers (DatePickerDialog/TimePickerDialog) are not ported; the
8
+ // inline DatePicker/TimePicker composables cover this leaf's contract.
9
+
10
+ package com.octanexplat.datepicker
11
+
12
+ import androidx.compose.material3.DatePicker
13
+ import androidx.compose.material3.DatePickerDefaults
14
+ import androidx.compose.material3.DatePickerState
15
+ import androidx.compose.material3.DisplayMode
16
+ import androidx.compose.material3.ExperimentalMaterial3Api
17
+ import androidx.compose.material3.LocalContentColor
18
+ import androidx.compose.material3.MaterialTheme
19
+ import androidx.compose.material3.SelectableDates
20
+ import androidx.compose.material3.TimePicker
21
+ import androidx.compose.material3.TimePickerDefaults
22
+ import androidx.compose.material3.TimePickerLayoutType
23
+ import androidx.compose.material3.TimePickerState
24
+ import androidx.compose.runtime.Composable
25
+ import androidx.compose.runtime.CompositionLocalProvider
26
+ import androidx.compose.runtime.LaunchedEffect
27
+ import androidx.compose.runtime.getValue
28
+ import androidx.compose.runtime.mutableStateOf
29
+ import androidx.compose.runtime.remember
30
+ import androidx.compose.runtime.setValue
31
+ import androidx.compose.ui.Modifier
32
+ import androidx.compose.ui.graphics.Color
33
+ import androidx.compose.ui.platform.ComposeView
34
+ import androidx.compose.ui.platform.LocalConfiguration
35
+ import org.json.JSONObject
36
+ import java.util.Calendar
37
+ import java.util.Date
38
+
39
+ private fun Any?.asBoolean(): Boolean = when (this) {
40
+ is Boolean -> this
41
+ is Number -> toInt() != 0
42
+ else -> false
43
+ }
44
+
45
+ private fun Any?.asLong(): Long? = when (this) {
46
+ is Number -> toLong()
47
+ else -> null
48
+ }
49
+
50
+ private fun Any?.asColor(): Color? = when (this) {
51
+ is String -> runCatching { Color(android.graphics.Color.parseColor(this)) }.getOrNull()
52
+ else -> null
53
+ }
54
+
55
+ private data class SelectableRange(
56
+ val start: Long? = null,
57
+ val end: Long? = null,
58
+ )
59
+
60
+ private data class DatePickerUiState(
61
+ val initialDate: Long? = null,
62
+ val variant: String = "picker",
63
+ val displayedComponents: String = "date",
64
+ val showVariantToggle: Boolean = true,
65
+ val is24Hour: Boolean = true,
66
+ val color: Color? = null,
67
+ val elementColors: Map<*, *> = emptyMap<Any?, Any?>(),
68
+ val selectableDates: SelectableRange = SelectableRange(),
69
+ )
70
+
71
+ private fun toUtcDayMillis(localMillis: Long): Long {
72
+ val cal = Calendar.getInstance()
73
+ cal.timeInMillis = localMillis
74
+ val utcCal = Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"))
75
+ utcCal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.get(Calendar.DAY_OF_MONTH), 0, 0, 0)
76
+ utcCal.set(Calendar.MILLISECOND, 0)
77
+ return utcCal.timeInMillis
78
+ }
79
+
80
+ // Material3 stores selectedDateMillis as UTC-midnight epoch; emitting it raw
81
+ // makes `new Date(ms)` resolve to the previous day in negative-UTC zones.
82
+ // Convert back to local-midnight millis so the JS-side Date lands on the
83
+ // calendar day the user picked.
84
+ private fun toLocalDayMillis(utcMillis: Long): Long {
85
+ val utcCal = Calendar.getInstance(java.util.TimeZone.getTimeZone("UTC"))
86
+ utcCal.timeInMillis = utcMillis
87
+ val localCal = Calendar.getInstance()
88
+ localCal.clear()
89
+ localCal.set(utcCal.get(Calendar.YEAR), utcCal.get(Calendar.MONTH), utcCal.get(Calendar.DAY_OF_MONTH))
90
+ return localCal.timeInMillis
91
+ }
92
+
93
+ @OptIn(ExperimentalMaterial3Api::class)
94
+ @Composable
95
+ private fun rememberSelectableDates(range: SelectableRange): SelectableDates {
96
+ val start = range.start
97
+ val end = range.end
98
+ return remember(start, end) {
99
+ if (start != null || end != null) {
100
+ val startUtcDayMillis = start?.let { toUtcDayMillis(it) }
101
+ val endUtcDayMillis = end?.let { toUtcDayMillis(it) }
102
+ val startYear = start?.let {
103
+ val cal = Calendar.getInstance()
104
+ cal.timeInMillis = it
105
+ cal.get(Calendar.YEAR)
106
+ }
107
+ val endYear = end?.let {
108
+ val cal = Calendar.getInstance()
109
+ cal.timeInMillis = it
110
+ cal.get(Calendar.YEAR)
111
+ }
112
+ object : SelectableDates {
113
+ override fun isSelectableDate(utcTimeMillis: Long): Boolean {
114
+ if (startUtcDayMillis != null && utcTimeMillis < startUtcDayMillis) return false
115
+ if (endUtcDayMillis != null && utcTimeMillis > endUtcDayMillis) return false
116
+ return true
117
+ }
118
+
119
+ override fun isSelectableYear(year: Int): Boolean {
120
+ if (startYear != null && year < startYear) return false
121
+ if (endYear != null && year > endYear) return false
122
+ return true
123
+ }
124
+ }
125
+ } else {
126
+ DatePickerDefaults.AllDates
127
+ }
128
+ }
129
+ }
130
+
131
+ // `selectableDates` only greys out unselectable cells; the calendar's actual
132
+ // extent (months list, year grid, range content description) comes from
133
+ // `yearRange`. Derive it from min/max so the picker reflects the constraints
134
+ // instead of always spanning the 1900..2100 default.
135
+ // https://github.com/expo/expo/issues/47206
136
+ @OptIn(ExperimentalMaterial3Api::class)
137
+ @Composable
138
+ private fun rememberDatePickerYearRange(range: SelectableRange, initialDateMillis: Long): IntRange {
139
+ val start = range.start
140
+ val end = range.end
141
+ return remember(start, end, initialDateMillis) {
142
+ val defaults = DatePickerDefaults.YearRange
143
+ val yearOf = { millis: Long ->
144
+ Calendar.getInstance().apply { timeInMillis = millis }.get(Calendar.YEAR)
145
+ }
146
+ val startYear = start?.let(yearOf) ?: defaults.first
147
+ val endYear = end?.let(yearOf) ?: defaults.last
148
+ val initialYear = yearOf(initialDateMillis)
149
+ minOf(startYear, initialYear)..maxOf(endYear, initialYear)
150
+ }
151
+ }
152
+
153
+ private fun Map<*, *>.color(name: String): Color? = this[name].asColor()
154
+
155
+ @OptIn(ExperimentalMaterial3Api::class)
156
+ @Composable
157
+ private fun buildDatePickerColors(
158
+ elementColors: Map<*, *>,
159
+ colorProp: Color?,
160
+ ): androidx.compose.material3.DatePickerColors {
161
+ val defaults = DatePickerDefaults.colors()
162
+ return defaults.copy(
163
+ containerColor = elementColors.color("containerColor") ?: defaults.containerColor,
164
+ titleContentColor = elementColors.color("titleContentColor") ?: colorProp ?: defaults.titleContentColor,
165
+ headlineContentColor = elementColors.color("headlineContentColor") ?: colorProp ?: defaults.headlineContentColor,
166
+ weekdayContentColor = elementColors.color("weekdayContentColor") ?: defaults.weekdayContentColor,
167
+ subheadContentColor = elementColors.color("subheadContentColor") ?: defaults.subheadContentColor,
168
+ navigationContentColor = elementColors.color("navigationContentColor") ?: defaults.navigationContentColor,
169
+ yearContentColor = elementColors.color("yearContentColor") ?: defaults.yearContentColor,
170
+ disabledYearContentColor = elementColors.color("disabledYearContentColor") ?: defaults.disabledYearContentColor,
171
+ currentYearContentColor = elementColors.color("currentYearContentColor") ?: defaults.currentYearContentColor,
172
+ selectedYearContentColor = elementColors.color("selectedYearContentColor") ?: defaults.selectedYearContentColor,
173
+ disabledSelectedYearContentColor = elementColors.color("disabledSelectedYearContentColor") ?: defaults.disabledSelectedYearContentColor,
174
+ selectedYearContainerColor = elementColors.color("selectedYearContainerColor") ?: defaults.selectedYearContainerColor,
175
+ disabledSelectedYearContainerColor = elementColors.color("disabledSelectedYearContainerColor") ?: defaults.disabledSelectedYearContainerColor,
176
+ dayContentColor = elementColors.color("dayContentColor") ?: defaults.dayContentColor,
177
+ disabledDayContentColor = elementColors.color("disabledDayContentColor") ?: defaults.disabledDayContentColor,
178
+ selectedDayContentColor = elementColors.color("selectedDayContentColor") ?: defaults.selectedDayContentColor,
179
+ disabledSelectedDayContentColor = elementColors.color("disabledSelectedDayContentColor") ?: defaults.disabledSelectedDayContentColor,
180
+ selectedDayContainerColor = elementColors.color("selectedDayContainerColor") ?: colorProp ?: defaults.selectedDayContainerColor,
181
+ disabledSelectedDayContainerColor = elementColors.color("disabledSelectedDayContainerColor") ?: defaults.disabledSelectedDayContainerColor,
182
+ todayContentColor = elementColors.color("todayContentColor") ?: defaults.todayContentColor,
183
+ todayDateBorderColor = elementColors.color("todayDateBorderColor") ?: colorProp ?: defaults.todayDateBorderColor,
184
+ dayInSelectionRangeContentColor = elementColors.color("dayInSelectionRangeContentColor") ?: defaults.dayInSelectionRangeContentColor,
185
+ dayInSelectionRangeContainerColor = elementColors.color("dayInSelectionRangeContainerColor") ?: defaults.dayInSelectionRangeContainerColor,
186
+ dividerColor = elementColors.color("dividerColor") ?: defaults.dividerColor,
187
+ )
188
+ }
189
+
190
+ @OptIn(ExperimentalMaterial3Api::class)
191
+ @Composable
192
+ private fun buildTimePickerColors(
193
+ elementColors: Map<*, *>,
194
+ colorProp: Color?,
195
+ ): androidx.compose.material3.TimePickerColors {
196
+ val defaults = TimePickerDefaults.colors()
197
+ return defaults.copy(
198
+ containerColor = elementColors.color("containerColor") ?: defaults.containerColor,
199
+ clockDialColor = elementColors.color("clockDialColor") ?: colorProp?.copy(alpha = 0.3f) ?: defaults.clockDialColor,
200
+ clockDialSelectedContentColor = elementColors.color("clockDialSelectedContentColor") ?: defaults.clockDialSelectedContentColor,
201
+ clockDialUnselectedContentColor = elementColors.color("clockDialUnselectedContentColor") ?: defaults.clockDialUnselectedContentColor,
202
+ selectorColor = elementColors.color("selectorColor") ?: colorProp ?: defaults.selectorColor,
203
+ periodSelectorBorderColor = elementColors.color("periodSelectorBorderColor") ?: defaults.periodSelectorBorderColor,
204
+ periodSelectorSelectedContainerColor = elementColors.color("periodSelectorSelectedContainerColor") ?: defaults.periodSelectorSelectedContainerColor,
205
+ periodSelectorUnselectedContainerColor = elementColors.color("periodSelectorUnselectedContainerColor") ?: defaults.periodSelectorUnselectedContainerColor,
206
+ periodSelectorSelectedContentColor = elementColors.color("periodSelectorSelectedContentColor") ?: defaults.periodSelectorSelectedContentColor,
207
+ periodSelectorUnselectedContentColor = elementColors.color("periodSelectorUnselectedContentColor") ?: defaults.periodSelectorUnselectedContentColor,
208
+ timeSelectorSelectedContainerColor = elementColors.color("timeSelectorSelectedContainerColor") ?: colorProp ?: defaults.timeSelectorSelectedContainerColor,
209
+ timeSelectorUnselectedContainerColor = elementColors.color("timeSelectorUnselectedContainerColor") ?: defaults.timeSelectorUnselectedContainerColor,
210
+ timeSelectorSelectedContentColor = elementColors.color("timeSelectorSelectedContentColor") ?: defaults.timeSelectorSelectedContentColor,
211
+ timeSelectorUnselectedContentColor = elementColors.color("timeSelectorUnselectedContentColor") ?: defaults.timeSelectorUnselectedContentColor,
212
+ )
213
+ }
214
+
215
+ @OptIn(ExperimentalMaterial3Api::class)
216
+ @Composable
217
+ private fun ExpoDatePicker(modifier: Modifier = Modifier, state: DatePickerUiState, onDateSelected: (Long?) -> Unit) {
218
+ val locale = LocalConfiguration.current.locales[0]
219
+ val displayMode = if (state.variant == "input") DisplayMode.Input else DisplayMode.Picker
220
+ val fallbackDate = remember { Date().time }
221
+ val initialDate = state.initialDate ?: fallbackDate
222
+ val selectableDates = rememberSelectableDates(state.selectableDates)
223
+ val yearRange = rememberDatePickerYearRange(state.selectableDates, initialDate)
224
+
225
+ val pickerState = remember(displayMode, initialDate, selectableDates, yearRange) {
226
+ DatePickerState(
227
+ initialDisplayMode = displayMode,
228
+ locale = locale,
229
+ initialSelectedDateMillis = state.initialDate,
230
+ initialDisplayedMonthMillis = initialDate,
231
+ yearRange = yearRange,
232
+ selectableDates = selectableDates,
233
+ )
234
+ }
235
+
236
+ LaunchedEffect(pickerState.selectedDateMillis) {
237
+ onDateSelected(pickerState.selectedDateMillis)
238
+ }
239
+
240
+ val colors = buildDatePickerColors(state.elementColors, state.color)
241
+
242
+ // Material3's year-selector chevron tints from the ambient
243
+ // LocalContentColor (which defaults to black), not
244
+ // `navigationContentColor`; bind the local so the chevron honors the
245
+ // navigation color.
246
+ CompositionLocalProvider(LocalContentColor provides colors.navigationContentColor) {
247
+ DatePicker(
248
+ modifier = modifier,
249
+ state = pickerState,
250
+ showModeToggle = state.showVariantToggle,
251
+ colors = colors,
252
+ )
253
+ }
254
+ }
255
+
256
+ @OptIn(ExperimentalMaterial3Api::class)
257
+ @Composable
258
+ private fun ExpoTimePicker(modifier: Modifier = Modifier, state: DatePickerUiState, onDateSelected: (Long?) -> Unit) {
259
+ val initialDate = state.initialDate
260
+
261
+ val pickerState = remember(initialDate, state.is24Hour) {
262
+ val cal = Calendar.getInstance()
263
+ cal.isLenient = false
264
+ if (initialDate != null) {
265
+ cal.timeInMillis = initialDate
266
+ }
267
+ TimePickerState(
268
+ initialHour = cal.get(Calendar.HOUR_OF_DAY),
269
+ initialMinute = cal.get(Calendar.MINUTE),
270
+ is24Hour = state.is24Hour,
271
+ )
272
+ }
273
+
274
+ LaunchedEffect(pickerState.hour, pickerState.minute) {
275
+ val cal = Calendar.getInstance()
276
+ cal.isLenient = false
277
+ if (initialDate != null) {
278
+ cal.timeInMillis = initialDate
279
+ }
280
+ cal.set(Calendar.HOUR_OF_DAY, pickerState.hour)
281
+ cal.set(Calendar.MINUTE, pickerState.minute)
282
+ onDateSelected(cal.time.time)
283
+ }
284
+
285
+ TimePicker(
286
+ modifier = modifier,
287
+ state = pickerState,
288
+ layoutType = TimePickerLayoutType.Vertical,
289
+ colors = buildTimePickerColors(state.elementColors, state.color),
290
+ )
291
+ }
292
+
293
+ class XplatDatePickerProvider {
294
+ private var state by mutableStateOf(DatePickerUiState())
295
+ var onEvent: ((Any) -> Unit)? = null
296
+
297
+ fun generateComposeView(view: ComposeView) {
298
+ view.setContent {
299
+ MaterialTheme {
300
+ if (state.displayedComponents == "hourAndMinute") {
301
+ ExpoTimePicker(state = state) { millis ->
302
+ onEvent?.invoke(JSONObject().put("date", millis ?: JSONObject.NULL))
303
+ }
304
+ } else {
305
+ ExpoDatePicker(state = state) { millis ->
306
+ onEvent?.invoke(
307
+ JSONObject().put(
308
+ "date",
309
+ millis?.let { toLocalDayMillis(it) } ?: JSONObject.NULL,
310
+ ),
311
+ )
312
+ }
313
+ }
314
+ }
315
+ }
316
+ }
317
+
318
+ fun updateData(input: Map<Any, Any>) {
319
+ val data = input["data"] as? Map<*, *> ?: input
320
+ val range = data["selectableDates"] as? Map<*, *>
321
+ state = DatePickerUiState(
322
+ initialDate = data["initialDate"].asLong(),
323
+ variant = data["variant"] as? String ?: "picker",
324
+ displayedComponents = data["displayedComponents"] as? String ?: "date",
325
+ showVariantToggle = data["showVariantToggle"]?.asBoolean() ?: true,
326
+ is24Hour = data["is24Hour"]?.asBoolean() ?: true,
327
+ color = data["color"].asColor(),
328
+ elementColors = data["elementColors"] as? Map<*, *> ?: emptyMap<Any?, Any?>(),
329
+ selectableDates = SelectableRange(
330
+ start = range?.get("start").asLong(),
331
+ end = range?.get("end").asLong(),
332
+ ),
333
+ )
334
+ }
335
+ }
@@ -0,0 +1,144 @@
1
+ // Adapted from @expo/ui (MIT) — Copyright 2025-present 650 Industries.
2
+ // Upstream: expo/packages/expo-ui/ios/DatePickerView.swift (sdk-57).
3
+ // The ExpoModules glue (@Field props record, EventDispatcher, Children label
4
+ // slot) is replaced by the @nativescript/swift-ui provider contract:
5
+ // updateData(NSDictionary) + onEvent. Children-as-label is not ported; the
6
+ // title string prop covers the label slot.
7
+
8
+ import SwiftUI
9
+
10
+ private final class DatePickerModel: ObservableObject {
11
+ @Published var title: String? = nil
12
+ @Published var selection: Date? = nil
13
+ @Published var rangeStart: Date? = nil
14
+ @Published var rangeEnd: Date? = nil
15
+ @Published var displayedComponents: DatePicker.Components = [.date]
16
+ @Published var pickerStyle: String = "automatic"
17
+ @Published var disabled = false
18
+ var onDateChange: ((Date) -> Void)?
19
+ }
20
+
21
+ private struct DatePickerContent: View {
22
+ @ObservedObject var model: DatePickerModel
23
+ @State private var date = Date()
24
+
25
+ // `.graphical` has an AutoLayout bug (it uses UICalendarView under the
26
+ // hood): it shrinks height when the user taps a date.
27
+ // https://github.com/expo/expo/issues/47062
28
+ // Fixed min width of 320 while the style is graphical.
29
+ private var isGraphicalStyle: Bool {
30
+ model.pickerStyle == "graphical"
31
+ }
32
+
33
+ var body: some View {
34
+ styledPicker()
35
+ .disabled(model.disabled)
36
+ .frame(minWidth: isGraphicalStyle ? 320 : nil)
37
+ .onChange(of: date) { newDate in
38
+ if model.selection == newDate { return }
39
+ model.onDateChange?(newDate)
40
+ }
41
+ .onChange(of: model.selection) { newValue in
42
+ date = newValue ?? Date()
43
+ }
44
+ .onAppear {
45
+ date = model.selection ?? Date()
46
+ }
47
+ }
48
+
49
+ @ViewBuilder
50
+ private func styledPicker() -> some View {
51
+ switch model.pickerStyle {
52
+ case "compact":
53
+ createDatePicker().datePickerStyle(.compact)
54
+ case "graphical":
55
+ createDatePicker().datePickerStyle(.graphical)
56
+ case "wheel":
57
+ createDatePicker().datePickerStyle(.wheel)
58
+ default:
59
+ createDatePicker().datePickerStyle(.automatic)
60
+ }
61
+ }
62
+
63
+ @ViewBuilder
64
+ private func createDatePicker() -> some View {
65
+ let components = model.displayedComponents
66
+ let start = model.rangeStart
67
+ let end = model.rangeEnd
68
+ let title = model.title ?? ""
69
+
70
+ let picker = Group {
71
+ if let start, let end {
72
+ DatePicker(title, selection: $date, in: start...end, displayedComponents: components)
73
+ } else if let start {
74
+ DatePicker(title, selection: $date, in: start..., displayedComponents: components)
75
+ } else if let end {
76
+ DatePicker(title, selection: $date, in: ...end, displayedComponents: components)
77
+ } else {
78
+ DatePicker(title, selection: $date, displayedComponents: components)
79
+ }
80
+ }
81
+
82
+ if model.title == nil {
83
+ picker.labelsHidden()
84
+ } else {
85
+ picker
86
+ }
87
+ }
88
+ }
89
+
90
+ @objc(XplatDatePickerProvider)
91
+ public final class XplatDatePickerProvider: UIViewController, SwiftUIProvider {
92
+ private let model = DatePickerModel()
93
+
94
+ public required init() {
95
+ super.init(nibName: nil, bundle: nil)
96
+ model.onDateChange = { [weak self] date in
97
+ self?.onEvent?(["date": date.timeIntervalSince1970 * 1000])
98
+ }
99
+ }
100
+
101
+ public required init?(coder: NSCoder) {
102
+ super.init(coder: coder)
103
+ model.onDateChange = { [weak self] date in
104
+ self?.onEvent?(["date": date.timeIntervalSince1970 * 1000])
105
+ }
106
+ }
107
+
108
+ public override func viewDidLoad() {
109
+ super.viewDidLoad()
110
+ setupSwiftUIView(content: DatePickerContent(model: model))
111
+ }
112
+
113
+ private static func millisToDate(_ value: Any?) -> Date? {
114
+ guard let millis = value as? NSNumber else { return nil }
115
+ return Date(timeIntervalSince1970: millis.doubleValue / 1000)
116
+ }
117
+
118
+ public func updateData(data: NSDictionary) {
119
+ if let title = data["title"] as? String {
120
+ model.title = title.isEmpty ? nil : title
121
+ }
122
+ if let selection = data["selection"] as? NSNumber {
123
+ model.selection = Date(timeIntervalSince1970: selection.doubleValue / 1000)
124
+ }
125
+ if let range = data["range"] as? [String: Any] {
126
+ model.rangeStart = Self.millisToDate(range["start"])
127
+ model.rangeEnd = Self.millisToDate(range["end"])
128
+ }
129
+ if let components = data["displayedComponents"] as? [String] {
130
+ var mapped: DatePicker.Components = []
131
+ if components.contains("date") { mapped.insert(.date) }
132
+ if components.contains("hourAndMinute") { mapped.insert(.hourAndMinute) }
133
+ if !mapped.isEmpty { model.displayedComponents = mapped }
134
+ }
135
+ if let pickerStyle = data["pickerStyle"] as? String {
136
+ model.pickerStyle = pickerStyle
137
+ }
138
+ if let disabled = data["disabled"] as? NSNumber {
139
+ model.disabled = disabled.boolValue
140
+ }
141
+ }
142
+
143
+ public var onEvent: ((NSDictionary) -> Void)?
144
+ }
@@ -0,0 +1,56 @@
1
+ /** @jsxImportSource @xplat/macos/renderer */
2
+ import { useEffect, useRef } from 'octane'
3
+ import type { AppKitDatePickerProps } from './macos/types'
4
+
5
+ const host = (): any => (globalThis as any).__xplatAppKit ?? {}
6
+
7
+ /** Embeds a real NSDatePicker in the leaf's backing view via the
8
+ * `__xplatAppKit` bridge. 'date' uses a text field (or the inline
9
+ * graphical calendar when `pickerStyle` is 'graphical'); 'time' uses the
10
+ * clock-and-calendar field; 'dateAndTime' shows both element groups.
11
+ * The host element carries an explicit size — the picker is pinned to
12
+ * its edges. */
13
+ export function AppKitDatePicker(props: AppKitDatePickerProps) @{
14
+ const anchor = useRef<any>(null)
15
+ const handle = useRef<any>(null)
16
+
17
+ const options = () => ({
18
+ mode: props.components === 'time' ? 'time' : 'date',
19
+ style: props.pickerStyle,
20
+ selectionMillis: (props.selection ?? props.defaultSelection)?.getTime?.(),
21
+ minimumMillis: props.minimumDate?.getTime?.(),
22
+ maximumMillis: props.maximumDate?.getTime?.(),
23
+ enabled: !props.disabled,
24
+ })
25
+
26
+ useEffect(() => {
27
+ const view = anchor.current
28
+ const attach = host().attachDatePicker
29
+ if (!view || typeof attach !== 'function') return
30
+ handle.current = attach(view, options(), (millis: number) => {
31
+ props.onSelectionChange?.(new Date(millis))
32
+ })
33
+
34
+ return () => {
35
+ handle.current?.detach()
36
+ handle.current = null
37
+ }
38
+ }, [props.components, props.pickerStyle])
39
+
40
+ // Push controlled selection/range updates into the mounted picker; the
41
+ // bridge update is echo-suppressed by comparing against dateValue.
42
+ useEffect(() => {
43
+ handle.current?.update(options())
44
+ })
45
+
46
+ <flexboxlayout
47
+ ref={(v: any) => { anchor.current = v }}
48
+ id={props.id}
49
+ className={props.className}
50
+ style={{
51
+ minWidth: 240,
52
+ minHeight: props.pickerStyle === 'graphical' ? 300 : 26,
53
+ ...props.style,
54
+ }}
55
+ />;
56
+ }
@@ -0,0 +1,30 @@
1
+ /** @jsxImportSource octane */
2
+ import { useState } from 'octane'
3
+ import type { DateInputProps } from './web/types'
4
+
5
+ /** Renders a labeled browser date/time input. */
6
+ export function DateInput(props: DateInputProps) @{
7
+ const controlled = props.value !== undefined
8
+ const [internalValue, setInternalValue] = useState(props.defaultValue ?? '')
9
+ const value = controlled ? props.value! : internalValue
10
+ const labelId = props.id ? `${props.id}-label` : undefined
11
+
12
+ <label className={props.className} style={props.style}>
13
+ <span id={labelId}>{props.label}</span>
14
+ <input
15
+ id={props.id}
16
+ type={props.type ?? 'date'}
17
+ aria-label={props.accessibilityLabel ?? props.label}
18
+ aria-labelledby={labelId}
19
+ value={value}
20
+ min={props.min}
21
+ max={props.max}
22
+ disabled={props.disabled}
23
+ onChange={(event: any) => {
24
+ const nextValue = event.target.value as string
25
+ if (!controlled) setInternalValue(nextValue)
26
+ props.onChange?.(nextValue)
27
+ }}
28
+ />
29
+ </label>
30
+ }
@@ -0,0 +1,49 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import { registerElement } from '@nativescript-community/octane'
3
+ import type { Attributes } from '@nativescript-community/octane/intrinsics'
4
+ import { ComposeDataDriver, JetpackCompose, registerJetpackCompose } from '@nativescript/jetpack-compose'
5
+ import type { MaterialDatePickerProps } from './android/types'
6
+
7
+ declare const com: any
8
+
9
+ declare module '@nativescript-community/octane/intrinsics' {
10
+ interface NativeScriptElements {
11
+ xplatandroiddatepicker: Attributes<typeof JetpackCompose>
12
+ }
13
+ }
14
+
15
+ registerElement('xplatandroiddatepicker', JetpackCompose)
16
+ registerJetpackCompose('xplat-material-datepicker', (owner: any) =>
17
+ new ComposeDataDriver(new com.octanexplat.datepicker.XplatDatePickerProvider(), owner),
18
+ )
19
+
20
+ /** Renders a Material 3 DatePicker/TimePicker through the NativeScript Jetpack Compose bridge. */
21
+ export function MaterialDatePicker(props: MaterialDatePickerProps) @{
22
+ const data = {
23
+ initialDate: props.initialDate?.getTime() ?? null,
24
+ variant: props.variant ?? 'picker',
25
+ displayedComponents: props.displayedComponents ?? 'date',
26
+ // The bridge's dataSerialize throws on booleans; send numbers.
27
+ showVariantToggle: (props.showVariantToggle ?? true) ? 1 : 0,
28
+ is24Hour: (props.is24Hour ?? true) ? 1 : 0,
29
+ color: props.color,
30
+ elementColors: props.elementColors,
31
+ selectableDates: {
32
+ start: props.selectableDates?.start?.getTime() ?? null,
33
+ end: props.selectableDates?.end?.getTime() ?? null,
34
+ },
35
+ }
36
+
37
+ <xplatandroiddatepicker
38
+ id={props.id}
39
+ className={props.className}
40
+ style={props.style}
41
+ accessibilityLabel={props.accessibilityLabel}
42
+ composeId="xplat-material-datepicker"
43
+ data={data}
44
+ onComposeEvent={(event: any) => {
45
+ const millis = event.data?.date
46
+ props.onDateSelected?.(typeof millis === 'number' ? new Date(millis) : null)
47
+ }}
48
+ />;
49
+ }
@@ -0,0 +1,51 @@
1
+ /** @jsxImportSource @nativescript-community/octane */
2
+ import { registerElement } from '@nativescript-community/octane'
3
+ import type { Attributes } from '@nativescript-community/octane/intrinsics'
4
+ import { SwiftUI, UIDataDriver, registerSwiftUI } from '@nativescript/swift-ui'
5
+ import { useState } from 'octane'
6
+ import type { SwiftUIDatePickerProps } from './ios/types'
7
+
8
+ declare const XplatDatePickerProvider: any
9
+
10
+ declare module '@nativescript-community/octane/intrinsics' {
11
+ interface NativeScriptElements {
12
+ xplatiosdatepicker: Attributes<typeof SwiftUI>
13
+ }
14
+ }
15
+
16
+ registerElement('xplatiosdatepicker', SwiftUI)
17
+ registerSwiftUI('xplat-swiftui-datepicker', (owner: any) =>
18
+ new UIDataDriver(new XplatDatePickerProvider(), owner),
19
+ )
20
+
21
+ /** Renders a SwiftUI DatePicker through NativeScript's SwiftUI bridge. */
22
+ export function SwiftUIDatePicker(props: SwiftUIDatePickerProps) @{
23
+ const controlled = props.selection !== undefined
24
+ const [internalSelection, setInternalSelection] = useState<Date | undefined>(props.defaultSelection)
25
+ const selection = controlled ? props.selection! : internalSelection
26
+
27
+ <xplatiosdatepicker
28
+ id={props.id}
29
+ className={props.className}
30
+ style={props.style}
31
+ swiftId="xplat-swiftui-datepicker"
32
+ data={{
33
+ title: props.title,
34
+ selection: selection?.getTime(),
35
+ range: {
36
+ start: props.minimumDate?.getTime(),
37
+ end: props.maximumDate?.getTime(),
38
+ },
39
+ displayedComponents: props.displayedComponents ?? ['date'],
40
+ pickerStyle: props.pickerStyle ?? 'automatic',
41
+ disabled: props.disabled ?? false,
42
+ }}
43
+ onSwiftUIEvent={(event: any) => {
44
+ const millis = event.data?.date
45
+ if (typeof millis !== 'number') return
46
+ const next = new Date(millis)
47
+ if (!controlled) setInternalSelection(next)
48
+ props.onSelectionChange?.(next)
49
+ }}
50
+ />;
51
+ }
@@ -0,0 +1,7 @@
1
+ export { MaterialDatePicker } from '../MaterialDatePicker.android.tsrx'
2
+ export type {
3
+ MaterialDatePickerComponents,
4
+ MaterialDatePickerElementColors,
5
+ MaterialDatePickerProps,
6
+ MaterialDatePickerVariant,
7
+ } from './types'