@colixsystems/widget-sdk 0.39.0 → 0.40.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -47,7 +47,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
47
47
 
48
48
  ## Status
49
49
 
50
- `v0.39.0` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
50
+ `v0.40.1` — pre-publish. The package surface (types, function names, export paths) is the v1 contract; runtime behaviour for some hooks is stubbed (each hook documents what's wired and what isn't). It is **not yet published to npm**.
51
+
52
+ ### What's new in 0.40.1
53
+
54
+ **`<DateTimePicker>` actually renders on web (sc-1118).** The primitive was a single source that wrapped `@react-native-community/datetimepicker`, but that library ships iOS / Android only and has no react-native-web mapping — on the web Player and Studio the primitive rendered nothing, so date columns in the built-in **Form Input** / **Form Builder** widgets showed a label and required-asterisk with no input beneath them. The implementation is now split: native (`./datetimepicker.native.js`) still wraps the RN library; web (`./datetimepicker.js`) renders the browser's native `<input type="date|time|datetime-local">` directly. **The public contract is unchanged** — same component name, same `{ value, onChange, mode, minimumDate, maximumDate, disabled }` props, same ISO 8601 wire format on the value and the `onChange` callback. `CONTRACT.version` is unchanged.
51
55
 
52
56
  ### What's new in 0.39.0
53
57
 
@@ -233,7 +237,7 @@ The tenant's **Theme Settings** now flow all the way into `useTheme()`.
233
237
 
234
238
  The "split-implementation + vetted package list" pivot.
235
239
 
236
- - **`CONTRACT.vettedImports` (new).** A curated allowlist of bare specifiers a widget may import — `react`, `@colixsystems/widget-sdk`, `react-native`, `axios`, `date-fns`, `react-native-svg`, `lucide-react-native`, `react-native-maps`, `leaflet`, `react-leaflet`, `expo-av`, `@react-native-community/datetimepicker`, `expo-clipboard`, `expo-haptics`. Each entry carries `platforms` (one or both of `"web"` / `"native"`) and a `category` so the linter and the marketplace listing can render honest platform badges. `CONTRACT.allowedBareImports` (the existing field) is now derived from `vettedImports` and stays a plain `string[]` for back-compat.
240
+ - **`CONTRACT.vettedImports` (new).** A curated allowlist of bare specifiers a widget may import — `react`, `@colixsystems/widget-sdk`, `react-native`, `axios`, `date-fns`, `react-native-svg`, `lucide-react-native`, `react-native-maps`, `leaflet`, `react-leaflet`, `expo-audio`, `expo-video`, `@react-native-community/datetimepicker`, `expo-clipboard`, `expo-haptics`. Each entry carries `platforms` (one or both of `"web"` / `"native"`) and a `category` so the linter and the marketplace listing can render honest platform badges. `CONTRACT.allowedBareImports` (the existing field) is now derived from `vettedImports` and stays a plain `string[]` for back-compat.
237
241
  - **`fetch` and `XMLHttpRequest` come off `CONTRACT.bannedApis`.** Widgets may call third-party APIs directly. Calls to the host's own `/api/*` surface will 401 because the JWT token is never shared with widget code; the linter emits a soft `no-host-api-url` warning when it sees host-URL substrings so authors learn the rule statically. Use SDK hooks (`useDatastoreQuery`, `useUsers`, `useAsset`, …) for workspace data; use `axios` / `fetch` for third-party APIs.
238
242
  - **`import-not-vetted` linter rule (new).** Every bare `import` specifier is validated against `CONTRACT.vettedImports`. Relative imports inside the bundle (`./shared.js`) are allowed so split-impl widgets can share helpers; `../` and absolute paths are rejected.
239
243
  - **`import-platform-mismatch` linter rule (new).** A single-source widget that imports a native-only package while `manifest.supportedPlatforms` includes `"web"` fails the lint. The author either drops the platform from the manifest OR ships a `widget.web.jsx` + `widget.native.jsx` pair where the platform-specific import lives in the file that targets its platform.
@@ -340,7 +344,7 @@ import { defineWidget, validateManifest, useDatastoreQuery, Text, View } from "@
340
344
  - `validateManifest(m)` / `validatePropertySchema(s)` / `validateProps(schema, props)` — shape validation; no third-party deps.
341
345
  - `useDatastoreQuery`, `useDatastoreRecord`, `useDatastoreSchema`, `useDatastoreMutation`, `useDirectory`, `useUsers`, `useGroups`, `useRecordPermissions`, `useAsset`, `useWidgetEvent`, `usePayments`, `useTheme`, `useI18n`, `useUser`, `useNavigation`, `useChildRenderer`, `useClipboard`, `useToast` — hooks that read from the host-provided `WidgetContext` (or, for `useClipboard`, the platform clipboard API directly). `useDirectory(query?)` returns `{ users, loading, error, refetch }` (each user `{ id, name, role }`) and requires the `directory.read:users` scope. `useUsers(query?)` returns `{ users, loading, error, refetch, invite, deactivate, reactivate, remove }` and requires `users.read:*` (mutations also need `users.write:*`); rejections are a `DirectoryError`. `useGroups(query?)` returns `{ groups, loading, error, refetch, create, remove, addMember, removeMember }` and requires `groups.read:*` (mutations also need `groups.write:*`). `usePayments()` returns `{ requestPayment, getPayment }` and requires the `payments.charge:appUser` scope; `requestPayment(...)` rejects with a `PaymentError`. `useUser()` returns the active end-user identity `{ id, email, display_name, roles, group_ids }` (snake_case verbatim; `id` is `null` for anonymous / preview). `useNavigation()` returns `{ goTo, goBack, push, replace, back, currentRoute }` for internal page navigation — for external URLs use the `Linking` primitive (`Linking.openURL(url)`). `useDatastoreRecord(tableId, recordId)` returns `{ data, loading, error, refetch }` for a single record (data is one row or null). `useDatastoreSchema(tableId)` returns `{ schema, loading, error, refetch }` where `schema` is `{ id, name, columns: [{ id, name, data_type, required, relation_type, target_table_id, is_identification }] }` (structure only, no row data; snake_case verbatim) — use it to resolve a stored `columnId` to its column type at runtime; requires the `datastore.read:<table>` scope. `useAsset(fileId)` returns `{ url, file, loading, error, refetch }` — the `url` is an absolute URL composed against the host's API base. `useChildRenderer()` returns `{ renderNode(node) }` — container widgets call it to render arbitrary child page-tree nodes (prefer the `WidgetTree` component for the common case).
342
346
  - `WidgetTree({ node })` — component that renders an author-authored child node through the host's renderer; used by Tabs / Card / custom containers to host arbitrary child widgets.
343
- - `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`, `DateTimePicker` wraps `@react-native-community/datetimepicker`). The web build aliases `react-native` to `react-native-web` so widgets render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
347
+ - `Text`, `View`, `Pressable`, `Image`, `ScrollView`, `TextInput`, `FlatList`, `SectionList`, `ActivityIndicator`, `Switch`, `StyleSheet`, `Linking`, `Icon`, `DateTimePicker` — re-exported from `react-native` (the RN primitives) or implemented in the SDK (`Icon` wraps `lucide-react-native`; `DateTimePicker` wraps `@react-native-community/datetimepicker` on native and renders `<input type="date|time|datetime-local">` directly on web because the RN library has no react-native-web mapping). The web build aliases `react-native` to `react-native-web` so the RN-re-exported primitives render in the browser without any per-platform code; the exported Expo app's Metro bundler resolves the real `react-native` library. `Linking` is a static API (`Linking.openURL(url)`) — use it for external URLs, and use `useNavigation().goTo(pageId)` for internal page navigation. See https://reactnative.dev/docs/ for per-component props.
344
348
  - `WidgetContextProvider` — React context provider that the host (Studio, Player, exported app) wraps widgets with.
345
349
 
346
350
  ## Design & visual polish
package/dist/contract.cjs CHANGED
@@ -664,7 +664,7 @@ const PRIMITIVES = [
664
664
  {
665
665
  name: "DateTimePicker",
666
666
  description:
667
- '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 via react-native-web; native uses @react-native-community/datetimepicker.',
667
+ '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.',
668
668
  rnComponent: "@react-native-community/datetimepicker",
669
669
  docsUrl: "https://github.com/react-native-datetimepicker/datetimepicker",
670
670
  },
@@ -1133,11 +1133,18 @@ const VETTED_IMPORTS = [
1133
1133
  description: "React bindings for leaflet. Web-only.",
1134
1134
  },
1135
1135
  {
1136
- specifier: "expo-av",
1136
+ specifier: "expo-audio",
1137
1137
  platforms: ["native"],
1138
1138
  category: "media",
1139
1139
  description:
1140
- "Native audio + video playback. Native-only; pair with browser <audio>/<video> in widget.web.jsx.",
1140
+ "Native audio playback (Expo SDK 56; replaces the removed expo-av). Native-only; pair with the browser <audio> element in widget.web.jsx.",
1141
+ },
1142
+ {
1143
+ specifier: "expo-video",
1144
+ platforms: ["native"],
1145
+ category: "media",
1146
+ description:
1147
+ "Native video playback (Expo SDK 56; replaces the removed expo-av). Native-only; pair with the browser <video> element in widget.web.jsx.",
1141
1148
  },
1142
1149
  {
1143
1150
  specifier: "@react-native-community/datetimepicker",
package/dist/contract.js CHANGED
@@ -664,7 +664,7 @@ const PRIMITIVES = [
664
664
  {
665
665
  name: "DateTimePicker",
666
666
  description:
667
- '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 via react-native-web; native uses @react-native-community/datetimepicker.',
667
+ '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.',
668
668
  rnComponent: "@react-native-community/datetimepicker",
669
669
  docsUrl: "https://github.com/react-native-datetimepicker/datetimepicker",
670
670
  },
@@ -1133,11 +1133,18 @@ const VETTED_IMPORTS = [
1133
1133
  description: "React bindings for leaflet. Web-only.",
1134
1134
  },
1135
1135
  {
1136
- specifier: "expo-av",
1136
+ specifier: "expo-audio",
1137
1137
  platforms: ["native"],
1138
1138
  category: "media",
1139
1139
  description:
1140
- "Native audio + video playback. Native-only; pair with browser <audio>/<video> in widget.web.jsx.",
1140
+ "Native audio playback (Expo SDK 56; replaces the removed expo-av). Native-only; pair with the browser <audio> element in widget.web.jsx.",
1141
+ },
1142
+ {
1143
+ specifier: "expo-video",
1144
+ platforms: ["native"],
1145
+ category: "media",
1146
+ description:
1147
+ "Native video playback (Expo SDK 56; replaces the removed expo-av). Native-only; pair with the browser <video> element in widget.web.jsx.",
1141
1148
  },
1142
1149
  {
1143
1150
  specifier: "@react-native-community/datetimepicker",
@@ -1,64 +1,74 @@
1
- // REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` SDK primitive.
1
+ // REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` SDK primitive (web implementation).
2
2
  //
3
- // Cross-platform date / time / datetime picker. Wraps
4
- // `@react-native-community/datetimepicker` (works on both web and native:
5
- // on web it renders the browser's native `<input type="date|time">`
6
- // surface through react-native-web's mapping). The wire format is ISO
7
- // 8601 strings the same format the datastore speaks, so widget authors
8
- // never round-trip through `new Date()`.
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.
9
10
  //
10
- // Props:
11
+ // Props (mirrors datetimepicker.native.js):
11
12
  // value: string | null — ISO 8601 (`2026-05-28` for date mode,
12
- // `2026-05-28T14:30:00.000Z` for datetime).
13
- // `null` defaults to "now".
13
+ // `14:30` for time mode,
14
+ // `2026-05-28T14:30:00.000Z` for datetime mode).
15
+ // `null` / "" leaves the input blank.
14
16
  // onChange: (iso: string) => void
15
17
  // mode: "date" | "time" | "datetime" — default "date"
16
18
  // minimumDate / maximumDate: string | null — ISO bounds
17
19
  // disabled: boolean
18
- //
19
- // The author writes:
20
- // const [day, setDay] = useState(null);
21
- // <DateTimePicker value={day} onChange={setDay} mode="date" />
22
- //
23
- // …and `day` ends up as an ISO string suitable for storing directly into
24
- // a DATE column. The previous pattern of importing the RN library
25
- // directly and managing `Date` objects in widget state is gone — the
26
- // primitive normalizes both ends.
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.
27
26
 
28
- import React, { useMemo } from "react";
29
- // eslint-disable-next-line no-restricted-syntax
30
- import RNDateTimePicker from "@react-native-community/datetimepicker";
27
+ import React from "react";
31
28
 
32
- function _parseToDate(value) {
33
- if (value == null || value === "") return new Date();
34
- if (value instanceof Date) return Number.isNaN(value.getTime()) ? new Date() : value;
35
- if (typeof value === "string") {
36
- const d = new Date(value);
37
- return Number.isNaN(d.getTime()) ? new Date() : d;
38
- }
39
- return new Date();
40
- }
29
+ const MODE_TO_INPUT_TYPE = {
30
+ date: "date",
31
+ time: "time",
32
+ datetime: "datetime-local",
33
+ };
41
34
 
42
- function _formatToIso(date, mode) {
43
- if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;
35
+ // Render the saved ISO value in the format each `<input>` accepts.
36
+ //
37
+ // `date` and `time` are already local / timezone-free in the SDK contract,
38
+ // so they pass through verbatim. `datetime-local` needs `YYYY-MM-DDTHH:mm`
39
+ // in LOCAL time — we round-trip through Date to convert a stored UTC ISO
40
+ // (e.g. `2026-05-28T13:30:00.000Z`) to the user's local clock.
41
+ function _isoToInputValue(value, mode) {
42
+ if (value == null || value === "") return "";
44
43
  if (mode === "date") {
45
- // Local-date ISO (yyyy-mm-dd) calendar dates should be timezone-free
46
- // so a "May 28" picked in Stockholm doesn't read as "May 27" in NYC.
47
- const y = date.getFullYear();
48
- const m = String(date.getMonth() + 1).padStart(2, "0");
49
- const d = String(date.getDate()).padStart(2, "0");
50
- return `${y}-${m}-${d}`;
44
+ // Strip any time portion if a datetime ISO accidentally lands in a
45
+ // date-mode picker keep the YYYY-MM-DD head.
46
+ return String(value).slice(0, 10);
51
47
  }
52
48
  if (mode === "time") {
53
- // Local-time ISO (hh:mm) — time-of-day is timezone-free for the same
54
- // reason. Authors who want a full datetime get mode="datetime".
55
- const h = String(date.getHours()).padStart(2, "0");
56
- const mm = String(date.getMinutes()).padStart(2, "0");
57
- return `${h}:${mm}`;
49
+ return String(value).slice(0, 5);
58
50
  }
59
- // datetime — full UTC ISO so the wire format round-trips through the
60
- // datastore's DATE column unchanged.
61
- return date.toISOString();
51
+ // datetime-local
52
+ const d = new Date(value);
53
+ if (Number.isNaN(d.getTime())) return "";
54
+ const y = d.getFullYear();
55
+ const m = String(d.getMonth() + 1).padStart(2, "0");
56
+ const day = String(d.getDate()).padStart(2, "0");
57
+ const hh = String(d.getHours()).padStart(2, "0");
58
+ const mm = String(d.getMinutes()).padStart(2, "0");
59
+ return `${y}-${m}-${day}T${hh}:${mm}`;
60
+ }
61
+
62
+ // Translate the input's typed string back to the ISO format the wire wants.
63
+ // `date` and `time` are already in the right shape; `datetime-local` is
64
+ // parsed as local time and serialized as the full UTC ISO so the value
65
+ // round-trips into a DATE column unchanged.
66
+ function _inputValueToIso(raw, mode) {
67
+ if (raw == null || raw === "") return "";
68
+ if (mode === "date" || mode === "time") return raw;
69
+ const d = new Date(raw);
70
+ if (Number.isNaN(d.getTime())) return "";
71
+ return d.toISOString();
62
72
  }
63
73
 
64
74
  export function DateTimePicker({
@@ -68,35 +78,57 @@ export function DateTimePicker({
68
78
  minimumDate,
69
79
  maximumDate,
70
80
  disabled,
81
+ accessibilityLabel,
71
82
  }) {
72
83
  const effectiveMode = mode === "time" || mode === "datetime" ? mode : "date";
73
- const dateValue = useMemo(() => _parseToDate(value), [value]);
74
- const min = useMemo(
75
- () => (minimumDate ? _parseToDate(minimumDate) : undefined),
76
- [minimumDate],
77
- );
78
- const max = useMemo(
79
- () => (maximumDate ? _parseToDate(maximumDate) : undefined),
80
- [maximumDate],
81
- );
84
+ const inputType = MODE_TO_INPUT_TYPE[effectiveMode];
85
+ const inputValue = _isoToInputValue(value, effectiveMode);
82
86
 
83
- const handleChange = (_event, picked) => {
87
+ const handleChange = (event) => {
84
88
  if (typeof onChange !== "function") return;
85
- if (!(picked instanceof Date)) return;
86
- const iso = _formatToIso(picked, effectiveMode);
87
- if (iso != null) onChange(iso);
89
+ const iso = _inputValueToIso(event.target.value, effectiveMode);
90
+ onChange(iso);
88
91
  };
89
92
 
90
- // The RN library's `mode` accepts "date" / "time"; for "datetime" we
91
- // ask for "datetime" on iOS / Android and let the picker's
92
- // implementation handle it. react-native-web's mapping interprets
93
- // "datetime" as `<input type="datetime-local">`.
94
- return React.createElement(RNDateTimePicker, {
95
- value: dateValue,
96
- mode: effectiveMode,
97
- minimumDate: min,
98
- maximumDate: max,
99
- disabled: !!disabled,
93
+ // The min/max props accept ISO strings too pass them through in the
94
+ // input-friendly representation. Undefined when not provided so the
95
+ // browser doesn't show an empty constraint chip.
96
+ const min =
97
+ minimumDate != null && minimumDate !== ""
98
+ ? _isoToInputValue(minimumDate, effectiveMode)
99
+ : undefined;
100
+ const max =
101
+ maximumDate != null && maximumDate !== ""
102
+ ? _isoToInputValue(maximumDate, effectiveMode)
103
+ : undefined;
104
+
105
+ // Inline styles match the SDK's other web-only primitives — the host's
106
+ // form widgets wrap this in their own labelled field, so the input just
107
+ // needs to look like a normal text input.
108
+ const style = {
109
+ boxSizing: "border-box",
110
+ width: "100%",
111
+ minHeight: 44,
112
+ padding: "8px 12px",
113
+ fontSize: 16,
114
+ fontFamily: "inherit",
115
+ color: "inherit",
116
+ backgroundColor: "transparent",
117
+ borderWidth: 1,
118
+ borderStyle: "solid",
119
+ borderColor: "rgba(0, 0, 0, 0.16)",
120
+ borderRadius: 6,
121
+ outline: "none",
122
+ };
123
+
124
+ return React.createElement("input", {
125
+ type: inputType,
126
+ value: inputValue,
100
127
  onChange: handleChange,
128
+ min,
129
+ max,
130
+ disabled: !!disabled,
131
+ style,
132
+ "aria-label": accessibilityLabel || undefined,
101
133
  });
102
134
  }
@@ -0,0 +1,102 @@
1
+ // REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` SDK primitive.
2
+ //
3
+ // Cross-platform date / time / datetime picker. Wraps
4
+ // `@react-native-community/datetimepicker` (works on both web and native:
5
+ // on web it renders the browser's native `<input type="date|time">`
6
+ // surface through react-native-web's mapping). The wire format is ISO
7
+ // 8601 strings — the same format the datastore speaks, so widget authors
8
+ // never round-trip through `new Date()`.
9
+ //
10
+ // Props:
11
+ // value: string | null — ISO 8601 (`2026-05-28` for date mode,
12
+ // `2026-05-28T14:30:00.000Z` for datetime).
13
+ // `null` defaults to "now".
14
+ // onChange: (iso: string) => void
15
+ // mode: "date" | "time" | "datetime" — default "date"
16
+ // minimumDate / maximumDate: string | null — ISO bounds
17
+ // disabled: boolean
18
+ //
19
+ // The author writes:
20
+ // const [day, setDay] = useState(null);
21
+ // <DateTimePicker value={day} onChange={setDay} mode="date" />
22
+ //
23
+ // …and `day` ends up as an ISO string suitable for storing directly into
24
+ // a DATE column. The previous pattern of importing the RN library
25
+ // directly and managing `Date` objects in widget state is gone — the
26
+ // primitive normalizes both ends.
27
+
28
+ import React, { useMemo } from "react";
29
+ // eslint-disable-next-line no-restricted-syntax
30
+ import RNDateTimePicker from "@react-native-community/datetimepicker";
31
+
32
+ function _parseToDate(value) {
33
+ if (value == null || value === "") return new Date();
34
+ if (value instanceof Date) return Number.isNaN(value.getTime()) ? new Date() : value;
35
+ if (typeof value === "string") {
36
+ const d = new Date(value);
37
+ return Number.isNaN(d.getTime()) ? new Date() : d;
38
+ }
39
+ return new Date();
40
+ }
41
+
42
+ function _formatToIso(date, mode) {
43
+ if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;
44
+ if (mode === "date") {
45
+ // Local-date ISO (yyyy-mm-dd) — calendar dates should be timezone-free
46
+ // so a "May 28" picked in Stockholm doesn't read as "May 27" in NYC.
47
+ const y = date.getFullYear();
48
+ const m = String(date.getMonth() + 1).padStart(2, "0");
49
+ const d = String(date.getDate()).padStart(2, "0");
50
+ return `${y}-${m}-${d}`;
51
+ }
52
+ if (mode === "time") {
53
+ // Local-time ISO (hh:mm) — time-of-day is timezone-free for the same
54
+ // reason. Authors who want a full datetime get mode="datetime".
55
+ const h = String(date.getHours()).padStart(2, "0");
56
+ const mm = String(date.getMinutes()).padStart(2, "0");
57
+ return `${h}:${mm}`;
58
+ }
59
+ // datetime — full UTC ISO so the wire format round-trips through the
60
+ // datastore's DATE column unchanged.
61
+ return date.toISOString();
62
+ }
63
+
64
+ export function DateTimePicker({
65
+ value,
66
+ onChange,
67
+ mode,
68
+ minimumDate,
69
+ maximumDate,
70
+ disabled,
71
+ }) {
72
+ const effectiveMode = mode === "time" || mode === "datetime" ? mode : "date";
73
+ const dateValue = useMemo(() => _parseToDate(value), [value]);
74
+ const min = useMemo(
75
+ () => (minimumDate ? _parseToDate(minimumDate) : undefined),
76
+ [minimumDate],
77
+ );
78
+ const max = useMemo(
79
+ () => (maximumDate ? _parseToDate(maximumDate) : undefined),
80
+ [maximumDate],
81
+ );
82
+
83
+ const handleChange = (_event, picked) => {
84
+ if (typeof onChange !== "function") return;
85
+ if (!(picked instanceof Date)) return;
86
+ const iso = _formatToIso(picked, effectiveMode);
87
+ if (iso != null) onChange(iso);
88
+ };
89
+
90
+ // The RN library's `mode` accepts "date" / "time"; for "datetime" we
91
+ // ask for "datetime" on iOS / Android and let the picker's
92
+ // implementation handle it. react-native-web's mapping interprets
93
+ // "datetime" as `<input type="datetime-local">`.
94
+ return React.createElement(RNDateTimePicker, {
95
+ value: dateValue,
96
+ mode: effectiveMode,
97
+ minimumDate: min,
98
+ maximumDate: max,
99
+ disabled: !!disabled,
100
+ onChange: handleChange,
101
+ });
102
+ }
@@ -63,8 +63,10 @@ export const Linking = ReactNative.Linking;
63
63
  // REQ-WSDK-PLATFORM §6 — `<Icon>` wraps lucide-react-native. Same source
64
64
  // runs on both platforms; see ./icon.js.
65
65
  export { Icon } from "./icon.js";
66
- // REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` wraps
67
- // @react-native-community/datetimepicker and normalizes its value to
68
- // ISO 8601 strings. Same source runs on both platforms; see
69
- // ./datetimepicker.js.
66
+ // REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` (web). Renders the browser's
67
+ // native `<input type="date|time|datetime-local">` directly because
68
+ // @react-native-community/datetimepicker (used by the native build in
69
+ // ./datetimepicker.native.js) ships iOS / Android only and has no
70
+ // react-native-web mapping. Both implementations honour the same
71
+ // ISO 8601 value / onChange contract.
70
72
  export { DateTimePicker } from "./datetimepicker.js";
@@ -24,8 +24,10 @@ export {
24
24
  // REQ-WSDK-PLATFORM §6 — `<Icon>` is implemented in one file that runs on
25
25
  // both platforms (lucide-react-native ships a working build for both).
26
26
  export { Icon } from "./icon.js";
27
- // REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` ditto. The underlying
28
- // @react-native-community/datetimepicker handles its own
29
- // per-platform rendering; the SDK wrapper just normalizes the value
30
- // to ISO strings.
31
- export { DateTimePicker } from "./datetimepicker.js";
27
+ // REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` (native). Wraps
28
+ // @react-native-community/datetimepicker, which ships iOS / Android only
29
+ // and has no react-native-web mapping, so the web build uses a separate
30
+ // implementation backed by `<input type="date|time|datetime-local">` in
31
+ // `./datetimepicker.js`. Both files honour the same ISO 8601 value /
32
+ // onChange contract.
33
+ export { DateTimePicker } from "./datetimepicker.native.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colixsystems/widget-sdk",
3
- "version": "0.39.0",
3
+ "version": "0.40.1",
4
4
  "description": "Common widget interface for AppStudio. Implements WidgetManifest, WidgetContext, property schema, and helper hooks.",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",