@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 +7 -3
- package/dist/contract.cjs +10 -3
- package/dist/contract.js +10 -3
- package/dist/datetimepicker.js +102 -70
- package/dist/datetimepicker.native.js +102 -0
- package/dist/primitives.js +6 -4
- package/dist/primitives.native.js +7 -5
- package/package.json +1 -1
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.
|
|
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-
|
|
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
|
|
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
|
|
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-
|
|
1136
|
+
specifier: "expo-audio",
|
|
1137
1137
|
platforms: ["native"],
|
|
1138
1138
|
category: "media",
|
|
1139
1139
|
description:
|
|
1140
|
-
"Native audio
|
|
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
|
|
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-
|
|
1136
|
+
specifier: "expo-audio",
|
|
1137
1137
|
platforms: ["native"],
|
|
1138
1138
|
category: "media",
|
|
1139
1139
|
description:
|
|
1140
|
-
"Native audio
|
|
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/datetimepicker.js
CHANGED
|
@@ -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
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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
|
-
// `
|
|
13
|
-
// `
|
|
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
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
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
|
|
29
|
-
// eslint-disable-next-line no-restricted-syntax
|
|
30
|
-
import RNDateTimePicker from "@react-native-community/datetimepicker";
|
|
27
|
+
import React from "react";
|
|
31
28
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
43
|
-
|
|
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
|
-
//
|
|
46
|
-
//
|
|
47
|
-
|
|
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
|
-
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
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
|
|
74
|
-
const
|
|
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 = (
|
|
87
|
+
const handleChange = (event) => {
|
|
84
88
|
if (typeof onChange !== "function") return;
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
if (iso != null) onChange(iso);
|
|
89
|
+
const iso = _inputValueToIso(event.target.value, effectiveMode);
|
|
90
|
+
onChange(iso);
|
|
88
91
|
};
|
|
89
92
|
|
|
90
|
-
// The
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
maximumDate
|
|
99
|
-
|
|
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
|
+
}
|
package/dist/primitives.js
CHANGED
|
@@ -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>`
|
|
67
|
-
//
|
|
68
|
-
//
|
|
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>`
|
|
28
|
-
// @react-native-community/datetimepicker
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
|
|
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.
|
|
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",
|