@colixsystems/widget-sdk 0.234.0 → 0.235.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,221 @@
1
+ // REQ-WSDK-PLATFORM §6 — the ONE `<DateTimePicker>` field both hosts render.
2
+ // The field (icon, value in the viewer's language, clear button) is shared;
3
+ // what opens when it is pressed is the host's `Surface`: a themed calendar
4
+ // sheet on the web, the operating system's own picker on a device.
5
+
6
+ import React from "react";
7
+ import { useHostLocale, useHostTheme } from "./hooks.js";
8
+ import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
9
+ import { Icon } from "./icon.js";
10
+ import { FIELD_OUTLINE_OFF, fieldStateStyle, resolveFormControlTokens } from "./form-control-tokens.js";
11
+ import {
12
+ formatDisplayLabel,
13
+ normaliseMode,
14
+ partsToIso,
15
+ valueToParts,
16
+ } from "./datetimepicker-format.js";
17
+ import { resolveBounds, resolvePickerLocale } from "./datetimepicker-calendar.js";
18
+
19
+ const h = React.createElement;
20
+ const CLEAR_ICON_SIZE = 18;
21
+
22
+ function isSet(value) {
23
+ return value != null && value !== "";
24
+ }
25
+
26
+ /**
27
+ * Bind the shared field to one platform's primitives and picking surface.
28
+ *
29
+ * @param {{View: any, Text: any, Pressable: any, Platform: any, Surface: any}} rn
30
+ */
31
+ export function makeDateTimePicker(rn) {
32
+ const { View, Text, Pressable, Platform, Surface } = rn;
33
+
34
+ function focusNode(node) {
35
+ if (Platform.OS === "web" && node && typeof node.focus === "function") node.focus();
36
+ }
37
+
38
+ function ClearButton({ t, label, onPress, testID }) {
39
+ return h(
40
+ Pressable,
41
+ {
42
+ onPress,
43
+ accessibilityRole: "button",
44
+ accessibilityLabel: label,
45
+ hitSlop: 8,
46
+ testID: testID ? `${testID}-clear` : undefined,
47
+ style: ({ pressed }) => [
48
+ {
49
+ alignItems: "center",
50
+ justifyContent: "center",
51
+ alignSelf: "stretch",
52
+ paddingHorizontal: t.paddingHorizontal,
53
+ },
54
+ FIELD_OUTLINE_OFF,
55
+ pressed ? { opacity: 0.6 } : null,
56
+ ],
57
+ },
58
+ h(Icon, { name: "x", size: CLEAR_ICON_SIZE, color: t.placeholderColor }),
59
+ );
60
+ }
61
+
62
+ function Trigger({ t, triggerRef, open, canEdit, hasValue, shown, mode, spoken, onOpen, onFocusChange, testID }) {
63
+ const ink = hasValue ? t.textColor : t.placeholderColor;
64
+ return h(
65
+ Pressable,
66
+ {
67
+ ref: triggerRef,
68
+ onPress: () => {
69
+ if (canEdit) onOpen();
70
+ },
71
+ onKeyDown: (event) => {
72
+ if (canEdit && event.key === "ArrowDown") {
73
+ event.preventDefault();
74
+ onOpen();
75
+ }
76
+ },
77
+ onFocus: () => onFocusChange(true),
78
+ onBlur: () => onFocusChange(false),
79
+ disabled: !canEdit,
80
+ accessibilityRole: "button",
81
+ "aria-haspopup": "dialog",
82
+ "aria-expanded": open,
83
+ "aria-disabled": !canEdit,
84
+ accessibilityLabel: spoken,
85
+ testID,
86
+ style: [
87
+ {
88
+ flex: 1,
89
+ flexDirection: "row",
90
+ alignItems: "center",
91
+ gap: t.rowGap,
92
+ minHeight: t.height,
93
+ paddingLeft: t.paddingHorizontal,
94
+ paddingRight: hasValue && canEdit ? 0 : t.paddingHorizontal,
95
+ paddingVertical: t.paddingVertical,
96
+ },
97
+ FIELD_OUTLINE_OFF,
98
+ ],
99
+ },
100
+ h(Icon, { name: mode === "time" ? "clock" : "calendar", size: CLEAR_ICON_SIZE, color: ink }),
101
+ h(
102
+ Text,
103
+ { numberOfLines: 1, style: { flexShrink: 1, color: ink, fontFamily: t.fontFamily, fontSize: t.fontSize } },
104
+ shown,
105
+ ),
106
+ );
107
+ }
108
+
109
+ function DateTimePicker({
110
+ value,
111
+ onChange,
112
+ mode,
113
+ minimumDate,
114
+ maximumDate,
115
+ disabled = false,
116
+ accessibilityLabel,
117
+ placeholder,
118
+ clearLabel = "Clear",
119
+ doneLabel = "Done",
120
+ closeAccessibilityLabel,
121
+ testID,
122
+ }) {
123
+ const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
124
+ const t = resolveFormControlTokens(theme);
125
+ const appLocale = useHostLocale();
126
+ const locale = React.useMemo(() => resolvePickerLocale(appLocale), [appLocale]);
127
+ const effectiveMode = normaliseMode(mode);
128
+ const [open, setOpen] = React.useState(false);
129
+ const [focused, setFocused] = React.useState(false);
130
+ const triggerRef = React.useRef(null);
131
+ const parts = React.useMemo(() => valueToParts(value, effectiveMode), [value, effectiveMode]);
132
+ const bounds = React.useMemo(
133
+ () => resolveBounds(minimumDate, maximumDate, effectiveMode),
134
+ [minimumDate, maximumDate, effectiveMode],
135
+ );
136
+ const hasValue = isSet(value);
137
+ const shown =
138
+ !hasValue && isSet(placeholder) ? placeholder : formatDisplayLabel(value, effectiveMode, locale);
139
+ const canEdit = !disabled && typeof onChange === "function";
140
+
141
+ const emit = (next) => {
142
+ if (typeof onChange === "function") onChange(partsToIso(next, effectiveMode));
143
+ };
144
+ // Back to the field once the sheet is gone, or its focus trap pulls it in.
145
+ const wasOpen = React.useRef(false);
146
+ React.useEffect(() => {
147
+ if (wasOpen.current && !open) focusNode(triggerRef.current);
148
+ wasOpen.current = open;
149
+ }, [open]);
150
+
151
+ const trigger = h(Trigger, {
152
+ t,
153
+ triggerRef,
154
+ open,
155
+ canEdit,
156
+ hasValue,
157
+ shown,
158
+ mode: effectiveMode,
159
+ spoken: [accessibilityLabel, shown].filter(Boolean).join(", "),
160
+ onOpen: () => setOpen(true),
161
+ onFocusChange: setFocused,
162
+ testID,
163
+ });
164
+
165
+ const field = h(
166
+ View,
167
+ {
168
+ style: [
169
+ {
170
+ flexDirection: "row",
171
+ alignItems: "center",
172
+ borderWidth: 1,
173
+ borderColor: t.borderColor,
174
+ borderRadius: t.radius,
175
+ backgroundColor: t.background,
176
+ },
177
+ fieldStateStyle(t, { focused, invalid: false }),
178
+ disabled ? { opacity: t.disabledOpacity } : null,
179
+ ],
180
+ },
181
+ trigger,
182
+ hasValue && canEdit
183
+ ? h(ClearButton, {
184
+ t,
185
+ label: clearLabel,
186
+ testID,
187
+ onPress: () => {
188
+ emit(null);
189
+ // The button unmounts once cleared; focus stays on the field.
190
+ focusNode(triggerRef.current);
191
+ },
192
+ })
193
+ : null,
194
+ );
195
+
196
+ // A sibling of the field: iOS draws its picker inline, below it.
197
+ return h(
198
+ React.Fragment,
199
+ null,
200
+ field,
201
+ h(Surface, {
202
+ open,
203
+ onClose: () => setOpen(false),
204
+ mode: effectiveMode,
205
+ parts,
206
+ bounds,
207
+ locale,
208
+ onPick: emit,
209
+ title: accessibilityLabel || shown,
210
+ heading: accessibilityLabel,
211
+ doneLabel,
212
+ closeAccessibilityLabel,
213
+ testID,
214
+ t,
215
+ }),
216
+ );
217
+ }
218
+
219
+ DateTimePicker.displayName = "DateTimePicker";
220
+ return DateTimePicker;
221
+ }
@@ -1,166 +1,15 @@
1
- // REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` SDK primitive (web implementation).
2
- //
3
- // `@react-native-community/datetimepicker` ships iOS / Android only — it has
4
- // no react-native-web mapping, so importing it in the browser yields no
5
- // rendered control. The web build therefore uses the browser's native
6
- // `<input type="date" | "time" | "datetime-local">` directly and keeps the
7
- // public contract identical to the native build: the value prop and the
8
- // onChange callback both speak ISO 8601 strings (the datastore wire format),
9
- // so a widget's JSX is byte-for-byte the same on both platforms.
10
- //
11
- // Props (mirrors datetimepicker.native.js):
12
- // value: string | null — ISO 8601 (`2026-05-28` for date mode,
13
- // `14:30` for time mode,
14
- // `2026-05-28T14:30:00.000Z` for datetime mode).
15
- // `null` / "" leaves the input blank.
16
- // onChange: (iso: string) => void
17
- // mode: "date" | "time" | "datetime" — default "date"
18
- // minimumDate / maximumDate: string | null — ISO bounds
19
- // disabled: boolean
20
- // accessibilityLabel: string | undefined — written to the DOM input as
21
- // `aria-label`. The shared form-field renderer
22
- // in `frontend/src/components/widgets/_shared/
23
- // formFields.jsx` passes the column's label
24
- // through this prop so Playwright / screen
25
- // readers can locate the input by its label.
1
+ // `<DateTimePicker>` (web). Binds ./datetimepicker-view.js to react-native-web
2
+ // and the themed calendar sheet; see ./primitives.js for why not the bare
3
+ // specifier.
26
4
 
27
- import React from "react";
28
- import { useHostTheme } from "./hooks.js";
29
- import {
30
- isDarkColor,
31
- isoToDateTimeLocalInput,
32
- dateTimeLocalInputToIso,
33
- } from "./datetimepicker-format.js";
34
- import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
5
+ import { View, Text, Pressable, ScrollView, Platform } from "react-native-web";
6
+ import { makeDateTimePicker } from "./datetimepicker-view.js";
7
+ import { makeDateTimeSheet } from "./datetimepicker-sheet.js";
8
+ import { Overlay } from "./overlay.js";
35
9
 
36
- const MODE_TO_INPUT_TYPE = {
37
- date: "date",
38
- time: "time",
39
- datetime: "datetime-local",
40
- };
10
+ const primitives = { View, Text, Pressable, ScrollView, Platform };
41
11
 
42
- // Render the saved ISO value in the format each `<input>` accepts.
43
- //
44
- // `date` and `time` are already local / timezone-free in the SDK contract,
45
- // so they pass through verbatim. `datetime-local` needs `YYYY-MM-DDTHH:mm`
46
- // in LOCAL time — we round-trip through Date to convert a stored UTC ISO
47
- // (e.g. `2026-05-28T13:30:00.000Z`) to the user's local clock.
48
- function _isoToInputValue(value, mode) {
49
- if (value == null || value === "") return "";
50
- if (mode === "date") {
51
- // Strip any time portion if a datetime ISO accidentally lands in a
52
- // date-mode picker — keep the YYYY-MM-DD head.
53
- return String(value).slice(0, 10);
54
- }
55
- if (mode === "time") {
56
- return String(value).slice(0, 5);
57
- }
58
- return isoToDateTimeLocalInput(value);
59
- }
60
-
61
- // Translate the input's typed string back to the ISO format the wire wants.
62
- // `date` and `time` are already in the right shape; `datetime-local` is
63
- // parsed as local time and serialized as the full UTC ISO so the value
64
- // round-trips into a DATE column unchanged.
65
- function _inputValueToIso(raw, mode) {
66
- if (raw == null || raw === "") return "";
67
- if (mode === "date" || mode === "time") return raw;
68
- return dateTimeLocalInputToIso(raw);
69
- }
70
-
71
- export function DateTimePicker({
72
- value,
73
- onChange,
74
- mode,
75
- minimumDate,
76
- maximumDate,
77
- disabled,
78
- accessibilityLabel,
79
- }) {
80
- const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
81
- const effectiveMode = mode === "time" || mode === "datetime" ? mode : "date";
82
- const inputType = MODE_TO_INPUT_TYPE[effectiveMode];
83
- const inputValue = _isoToInputValue(value, effectiveMode);
84
-
85
- const handleChange = (event) => {
86
- if (typeof onChange !== "function") return;
87
- const iso = _inputValueToIso(event.target.value, effectiveMode);
88
- onChange(iso);
89
- };
90
-
91
- // The min/max props accept ISO strings too — pass them through in the
92
- // input-friendly representation. Undefined when not provided so the
93
- // browser doesn't show an empty constraint chip.
94
- const min =
95
- minimumDate != null && minimumDate !== ""
96
- ? _isoToInputValue(minimumDate, effectiveMode)
97
- : undefined;
98
- const max =
99
- maximumDate != null && maximumDate !== ""
100
- ? _isoToInputValue(maximumDate, effectiveMode)
101
- : undefined;
102
-
103
- // sc-1878 — clicking the formatted date text (or focusing it and pressing
104
- // Enter / Space) opens the native calendar, not just the small built-in
105
- // calendar icon. `showPicker()` requires a user gesture; click/keydown both
106
- // qualify. Guarded: not every browser/build exposes it, and calling it while
107
- // the picker is already open throws — neither should break the field.
108
- const openPicker = (target) => {
109
- if (disabled || !target || typeof target.showPicker !== "function") return;
110
- try {
111
- target.showPicker();
112
- } catch {
113
- // Already open, or blocked outside a user gesture — leave the input as-is.
114
- }
115
- };
116
-
117
- const handleClick = (event) => openPicker(event.currentTarget);
118
-
119
- const handleKeyDown = (event) => {
120
- if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
121
- event.preventDefault();
122
- openPicker(event.currentTarget);
123
- }
124
- };
125
-
126
- // Inline styles match the SDK's other web-only primitives — the host's
127
- // form widgets wrap this in their own labelled field, so the input just
128
- // needs to look like a normal text input. `cursor: pointer` signals the
129
- // whole field is the click affordance (sc-1878). Colours come from the
130
- // theme so the field is legible on any surface (sc-3370); `colorScheme`
131
- // steers the browser's built-in date UI (edit segments + calendar icon).
132
- // The scheme keys off the theme's TEXT colour (onSurface, which the host
133
- // already picked to contrast the surface) rather than re-deriving dark-vs-
134
- // light from the surface with a second luminance formula that could disagree
135
- // with the host: light text ⇒ dark surface ⇒ dark scheme.
136
- const style = {
137
- boxSizing: "border-box",
138
- width: "100%",
139
- minHeight: 44,
140
- padding: "8px 12px",
141
- fontSize: 16,
142
- fontFamily: "inherit",
143
- color: theme.colors.onSurface,
144
- backgroundColor: "transparent",
145
- colorScheme: isDarkColor(theme.colors.onSurface) ? "light" : "dark",
146
- borderWidth: 1,
147
- borderStyle: "solid",
148
- borderColor: theme.colors.border,
149
- borderRadius: 6,
150
- outline: "none",
151
- cursor: disabled ? "default" : "pointer",
152
- };
153
-
154
- return React.createElement("input", {
155
- type: inputType,
156
- value: inputValue,
157
- onChange: handleChange,
158
- onClick: handleClick,
159
- onKeyDown: handleKeyDown,
160
- min,
161
- max,
162
- disabled: !!disabled,
163
- style,
164
- "aria-label": accessibilityLabel || undefined,
165
- });
166
- }
12
+ export const DateTimePicker = makeDateTimePicker({
13
+ ...primitives,
14
+ Surface: makeDateTimeSheet({ ...primitives, Overlay }),
15
+ });
@@ -1,131 +1,43 @@
1
- // REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` SDK primitive (native).
2
- //
3
- // Wraps `@react-native-community/datetimepicker`. That library is imperative
4
- // on Android — rendering it shows the dialog immediately and re-mounting is
5
- // the only way to reopen it — so the primitive owns a small open/closed state
6
- // and renders a tappable, formatted-date trigger (sc-1878). Tapping the
7
- // formatted date (the trigger is an accessible button, reachable by keyboard /
8
- // screen-reader) opens the picker; choosing a value closes it and emits the
9
- // ISO string. This mirrors the web build, where clicking the `<input>` text
10
- // calls `showPicker()` — both hosts: tap the date, the picker opens.
11
- //
12
- // Props (mirrors datetimepicker.js):
13
- // value: string | null — ISO 8601 (`2026-05-28` for date mode,
14
- // `14:30` for time mode,
15
- // `2026-05-28T14:30:00.000Z` for datetime mode).
16
- // `null` / "" shows the placeholder, opens at "now".
17
- // onChange: (iso: string) => void
18
- // mode: "date" | "time" | "datetime" — default "date"
19
- // minimumDate / maximumDate: string | null — ISO bounds
20
- // disabled: boolean
21
- // accessibilityLabel: string | undefined — names the trigger button so the
22
- // shared form-field renderer's column label reaches
23
- // screen readers / test tooling, matching the web
24
- // input's `aria-label`.
1
+ // `<DateTimePicker>` (native). Binds ./datetimepicker-view.js to React Native
2
+ // with the operating system's own picker as its surface, via
3
+ // `@react-native-community/datetimepicker`. That library is imperative on
4
+ // Android — rendering it opens the dialog — so it mounts only while open.
25
5
 
26
- import React, { useMemo, useState } from "react";
6
+ import React from "react";
27
7
  // eslint-disable-next-line no-restricted-syntax
28
8
  import RNDateTimePicker from "@react-native-community/datetimepicker";
29
- import { Pressable, Text, StyleSheet } from "react-native";
30
- import {
31
- parseToDate,
32
- formatToIso,
33
- formatDisplayLabel,
34
- } from "./datetimepicker-format.js";
35
- import { useHostTheme } from "./hooks.js";
36
- import { DEFAULT_THEME_TOKENS } from "./_theme-tokens.js";
37
-
38
- const styles = StyleSheet.create({
39
- trigger: {
40
- minHeight: 44,
41
- paddingVertical: 8,
42
- paddingHorizontal: 12,
43
- borderWidth: 1,
44
- borderRadius: 6,
45
- justifyContent: "center",
46
- },
47
- triggerDisabled: { opacity: 0.5 },
48
- label: { fontSize: 16 },
49
- placeholder: { fontSize: 16, opacity: 0.5 },
50
- });
51
-
52
- export function DateTimePicker({
53
- value,
54
- onChange,
55
- mode,
56
- minimumDate,
57
- maximumDate,
58
- disabled,
59
- accessibilityLabel,
60
- }) {
61
- const theme = useHostTheme() || DEFAULT_THEME_TOKENS;
62
- const effectiveMode = mode === "time" || mode === "datetime" ? mode : "date";
63
- const [open, setOpen] = useState(false);
64
- const dateValue = useMemo(() => parseToDate(value), [value]);
65
- const min = useMemo(
66
- () => (minimumDate ? parseToDate(minimumDate) : undefined),
67
- [minimumDate],
68
- );
69
- const max = useMemo(
70
- () => (maximumDate ? parseToDate(maximumDate) : undefined),
71
- [maximumDate],
72
- );
73
-
74
- const isEmpty = value == null || value === "";
75
-
76
- const handleChange = (event, picked) => {
77
- // Android fires "dismissed" when the user cancels; close without emitting.
78
- setOpen(false);
79
- if (event?.type === "dismissed") return;
80
- if (typeof onChange !== "function") return;
81
- if (!(picked instanceof Date)) return;
82
- const iso = formatToIso(picked, effectiveMode);
83
- if (iso != null) onChange(iso);
84
- };
9
+ import { View, Text, Pressable, Platform } from "react-native";
10
+ import { makeDateTimePicker } from "./datetimepicker-view.js";
11
+ import { nowParts, partsToDate, valueToParts } from "./datetimepicker-format.js";
12
+ import { clampToBounds } from "./datetimepicker-calendar.js";
13
+
14
+ function boundDate(parts, mode) {
15
+ if (!parts || mode === "time") return undefined;
16
+ return partsToDate(parts);
17
+ }
85
18
 
86
- const trigger = React.createElement(
87
- Pressable,
88
- {
89
- onPress: () => { if (!disabled) setOpen(true); },
90
- disabled: !!disabled,
91
- accessibilityRole: "button",
92
- accessibilityLabel,
93
- accessibilityState: { disabled: !!disabled },
94
- // Themed border + text so the trigger is legible on any surface (sc-3370).
95
- style: [
96
- styles.trigger,
97
- { borderColor: theme.colors.border },
98
- disabled && styles.triggerDisabled,
99
- ],
19
+ function OsPicker({ open, onClose, mode, parts, bounds, onPick }) {
20
+ if (!open) return null;
21
+ const start = parts ? { ...nowParts(), ...dropEmpty(parts) } : clampToBounds(nowParts(), bounds);
22
+ return React.createElement(RNDateTimePicker, {
23
+ value: partsToDate(start),
24
+ // iOS honours "datetime"; Android shows its date dialog for it.
25
+ mode,
26
+ minimumDate: boundDate(bounds.min, mode),
27
+ maximumDate: boundDate(bounds.max, mode),
28
+ onChange: (event, picked) => {
29
+ onClose();
30
+ // Android reports a cancelled dialog as "dismissed".
31
+ if (event && event.type === "dismissed") return;
32
+ if (!(picked instanceof Date)) return;
33
+ // Android's time dialog cannot be bounded, so a pick is clamped here.
34
+ onPick(clampToBounds(valueToParts(picked.toISOString(), "datetime"), bounds));
100
35
  },
101
- React.createElement(
102
- Text,
103
- {
104
- style: [
105
- isEmpty ? styles.placeholder : styles.label,
106
- { color: theme.colors.onSurface },
107
- ],
108
- },
109
- formatDisplayLabel(value, effectiveMode),
110
- ),
111
- );
112
-
113
- if (!open) return trigger;
36
+ });
37
+ }
114
38
 
115
- // The RN library's `mode` is "date" / "time"; iOS also honours "datetime",
116
- // Android falls back to date-only for it (a pre-existing limit, unchanged
117
- // here). The web build uses datetimepicker.js, so this file is never web.
118
- return React.createElement(
119
- React.Fragment,
120
- null,
121
- trigger,
122
- React.createElement(RNDateTimePicker, {
123
- value: dateValue,
124
- mode: effectiveMode,
125
- minimumDate: min,
126
- maximumDate: max,
127
- disabled: !!disabled,
128
- onChange: handleChange,
129
- }),
130
- );
39
+ function dropEmpty(parts) {
40
+ return Object.fromEntries(Object.entries(parts).filter(([, v]) => v != null));
131
41
  }
42
+
43
+ export const DateTimePicker = makeDateTimePicker({ View, Text, Pressable, Platform, Surface: OsPicker });
package/dist/hooks.js CHANGED
@@ -238,6 +238,14 @@ export function useHostNavigation() {
238
238
  return ctx ? ctx.navigation : undefined;
239
239
  }
240
240
 
241
+ // @internal — the app's language, read WITHOUT asserting a widget context so
242
+ // `<DateTimePicker>` can name months in it wherever it renders.
243
+ export function useHostLocale() {
244
+ const ctx = useContext(HostWidgetContext);
245
+ const locale = ctx && ctx.i18n ? ctx.i18n.locale : undefined;
246
+ return typeof locale === "string" && locale ? locale : undefined;
247
+ }
248
+
241
249
  // A host translation, or null on a miss: both hosts answer a miss with the
242
250
  // key itself or a `{{t:…}}` placeholder.
243
251
  function hostTranslation(hostT, key) {
package/dist/host.d.ts CHANGED
@@ -626,10 +626,12 @@ export function isoToDateTimeLocalInput(value: unknown): string;
626
626
  export function dateTimeLocalInputToIso(raw: unknown): string;
627
627
 
628
628
  /**
629
- * sc-8448 — the `<DateTimePicker>` trigger label: for `"datetime"` the stored
630
- * instant as `YYYY-MM-DD HH:mm` local time, falling back to the raw string.
629
+ * The `<DateTimePicker>` field's label: the value in `locale` (default: the
630
+ * runtime's), a `"datetime"` instant on the viewer's clock (sc-8448, sc-11777).
631
+ * Empty gives the mode's placeholder; an unreadable value is returned as is.
631
632
  */
632
633
  export function formatDateTimeLabel(
633
634
  value: unknown,
634
635
  mode: "date" | "time" | "datetime",
636
+ locale?: string,
635
637
  ): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.234.0",
3
+ "version": "0.235.0",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "homepage": "https://github.com/Colix-AB/AppStudio",
6
6
  "type": "module",
package/src/contract.js CHANGED
@@ -3123,7 +3123,7 @@ const PRIMITIVES = [
3123
3123
  {
3124
3124
  name: "DateTimePicker",
3125
3125
  description:
3126
- 'Cross-platform date / time / datetime picker. `<DateTimePicker value={iso} onChange={iso => …} mode="date" | "time" | "datetime" />`. The value prop and the onChange callback both speak ISO 8601 strings (the datastore wire format) — authors never round-trip through `new Date()`. Web renders the browser\'s native `<input type="date|time|datetime-local">` directly (react-native-web has no mapping for the RN datetimepicker library); native uses @react-native-community/datetimepicker. The field themes itself from the workspace theme — text uses `colors.onSurface`, the border `colors.border`, and on web `color-scheme` follows the theme so the browser\'s built-in date UI stays legible on a dark surface; never style it with raw CSS or `document`.',
3126
+ 'Cross-platform date / time / datetime field. `<DateTimePicker value={iso} onChange={iso => …} mode="date" | "time" | "datetime" accessibilityLabel="Due" />`. The value prop and the onChange callback both speak ISO 8601 strings (the datastore wire format): `YYYY-MM-DD` for date, `HH:mm` for time, a full UTC instant for datetime — authors never round-trip through `new Date()`. Clearing emits "". The field shows the value in the app user\'s language beside a calendar (or clock) icon, with a clear button once it holds a value. Web opens a themed calendar sheet in `<Overlay>`: a month grid (keyboard: arrows, Home/End, PageUp/PageDown), a month and year jump, hour and minute columns for time, and a Today / Now button. Native opens the operating system\'s own picker (@react-native-community/datetimepicker). `minimumDate` / `maximumDate` (ISO, `HH:mm` in time mode) bound what can be picked; `disabled` locks the field. Optional copy: `placeholder`, `clearLabel` (default "Clear"), `doneLabel` (default "Done", closes the web time sheet), `closeAccessibilityLabel`; `testID` names the field. Colours come from the workspace theme — the field from the text-field tokens, the sheet from the popover scope, the chosen day from `colors.primary`; never style it with raw CSS or `document`.',
3127
3127
  rnComponent: "@react-native-community/datetimepicker",
3128
3128
  docsUrl: "https://github.com/react-native-datetimepicker/datetimepicker",
3129
3129
  },
@@ -6045,7 +6045,11 @@ const CONTRACT = deepFreeze({
6045
6045
  // 1.189.0: additive (sc-11703) — `resolveFooterTokens` gains `padding`,
6046
6046
  // the space around a bottom bar's tabs (0–12, default 4); `FOOTER_SIZES`
6047
6047
  // gains its bound.
6048
- version: "1.189.0",
6048
+ // 1.190.0: additive (sc-11777) — `<DateTimePicker>` on web opens a themed
6049
+ // calendar sheet instead of the browser's date popup; both hosts show
6050
+ // the value in the app's language with a clear button. New optional
6051
+ // props: placeholder, clearLabel, doneLabel, closeAccessibilityLabel, testID.
6052
+ version: "1.190.0",
6049
6053
  sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
6050
6054
  hooks: HOOKS,
6051
6055
  primitives: PRIMITIVES,