@colixsystems/widget-sdk 0.48.0 → 0.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -2
- package/dist/contract.cjs +38 -1
- package/dist/contract.js +38 -1
- package/dist/datetimepicker-format.js +53 -0
- package/dist/datetimepicker.js +28 -1
- package/dist/datetimepicker.native.js +85 -68
- package/dist/hooks.js +135 -0
- package/dist/index.d.ts +64 -0
- package/dist/index.js +2 -0
- package/dist/index.native.js +2 -0
- package/dist/lucideIconNames.cjs +1 -0
- package/dist/lucideIconNames.js +1 -0
- package/package.json +2 -7
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @colixsystems/widget-sdk
|
|
2
2
|
|
|
3
|
-
Common widget interface for
|
|
3
|
+
Common widget interface for AppStudio. This package is **core only** — it implements the contract that every widget (built-in or third-party, web or native) speaks: a `WidgetManifest`, a `WidgetContext`, a property schema, the primitives + rendering surface, the helper hooks, events, theme/i18n, and the static linter that gates submissions. **It owns no HTTP and depends on none of the data SDK packages.**
|
|
4
4
|
|
|
5
5
|
The data layer lives in **four separate domain-client packages**, each instantiated by the host and **injected into `WidgetContext`**. Widgets never import those packages — they reach the data surface only through this SDK's hooks, which read the injected client instances:
|
|
6
6
|
|
|
@@ -27,6 +27,7 @@ The data layer lives in **four separate domain-client packages**, each instantia
|
|
|
27
27
|
| **CORE** | `useRefresh(handler)` | `void` | `ctx.refresh.subscribe` — no scope. Subscribes the handler to the page-level refresh tick (pull-to-refresh on mobile). Handler may return a Promise — the host waits for `allSettled` before clearing the spinner. The three datastore hooks auto-subscribe their own `refetch`; widgets only call this directly to re-run non-datastore work. No-op on a host that doesn't implement refresh. |
|
|
28
28
|
| **CORE** | `useClipboard()` | `{ copy, paste, hasContent }` | platform clipboard (web `navigator.clipboard` / native `expo-clipboard`); rejects with `ClipboardError` — no scope |
|
|
29
29
|
| **CORE** | `useToast()` | `{ showToast }` | `ctx.toast.showToast` (falls back to a CustomEvent / console) — no scope |
|
|
30
|
+
| **CORE** | `useGeolocation(options?)` | `{ latitude, longitude, accuracy, loading, error, getCurrentPosition }` | `ctx.device.geolocation` — no scope. Capture is IMPERATIVE: call `getCurrentPosition()` from a user gesture (a tap), never on mount. Resolves to `{ latitude, longitude, accuracy }`; rejects with `GeolocationError` (`.code` in `PERMISSION_DENIED \| UNAVAILABLE \| TIMEOUT \| UNSUPPORTED \| INTERNAL`). Identical on web (`navigator.geolocation`) and the Expo export (`expo-location`). |
|
|
30
31
|
| **CORE** | `useI18n()` | `{ t, locale }` | `ctx.i18n` — no scope. `t(key)` resolves the widget-namespaced key (`widget.<id>.<key>`, declared in `manifest.translations`) first, then a **predefined shared key** (`shared.<key>`) when `key` is one of the standard strings (`submit`, `cancel`, `save`, `loading`, …), then the raw key. Use a shared key for an identical default string so it translates once and any per-instance `widget.<id>.<key>` override still wins. |
|
|
31
32
|
| **DATASTORE** (`ctx.datastore`) | `useDatastoreQuery(table, options?)` | `{ data, loading, error, refetch }` | `records(table).list` (unwraps `{ data, meta }` to `data: []`) — `datastore.read:*` |
|
|
32
33
|
| **DATASTORE** | `useDatastoreRecord(table, id)` | `{ data, loading, error, refetch }` | `records(table).get` — `datastore.read:<table>` |
|
|
@@ -50,7 +51,11 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
50
51
|
|
|
51
52
|
## Status
|
|
52
53
|
|
|
53
|
-
`v0.
|
|
54
|
+
`v0.49.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**.
|
|
55
|
+
|
|
56
|
+
### What's new in 0.49.0
|
|
57
|
+
|
|
58
|
+
**New `useGeolocation()` hook — read the device's current position (sc-1584).** A new CORE hook reading a newly-injected `ctx.device` slice (host-brokered device capabilities). Returns `{ latitude, longitude, accuracy, loading, error, getCurrentPosition }`. Capture is **imperative** — call `getCurrentPosition()` from a user gesture (a `Pressable.onPress`); browsers and the mobile OS gate the permission prompt on a gesture, so it NEVER fires on mount. The promise resolves to `{ latitude, longitude, accuracy }` and mirrors the same values onto the hook; `options` (`{ enableHighAccuracy, timeout, maximumAge }`) pass through to the host. Rejections surface as a structured `GeolocationError` (new named export) with a stable `.code` (`PERMISSION_DENIED` / `UNAVAILABLE` / `TIMEOUT` / `UNSUPPORTED` / `INTERNAL`). It needs **no manifest scope** and **no `requestedScopes` entry**. The web Player brokers it via `navigator.geolocation`; the Expo export via `expo-location` — so device access is identical on both platforms. The `ctx.device` slice is optional: a host that can't broker the sensor omits it and the hook degrades to an `UNSUPPORTED` error rather than throwing at render. `CONTRACT.version` → `1.36.0`. Additive — one new hook, one new optional context slice, one new error class; no existing export changed signature.
|
|
54
59
|
|
|
55
60
|
### What's new in 0.48.0
|
|
56
61
|
|
|
@@ -107,6 +112,10 @@ See the design reference for the full architecture: [`docs/architecture/widget-m
|
|
|
107
112
|
|
|
108
113
|
**`useI18n().t()` no longer leaks the host's `{{t:key}}` miss placeholder.** Resolution steps 1–2 (per-widget and shared namespaces) already treated a `{{t:…}}` return from the host as a miss, but the final raw-key step did not — on the web Player (whose host resolver returns the placeholder form on a miss and ignores the fallback argument) a key absent from the tenant dictionary rendered as literal `{{t:key}}` text instead of degrading to `fallback ?? key`. The same guard now applies to all three steps. **The public contract is unchanged** — this is the behaviour `useI18n` always documented. `CONTRACT.version` is unchanged.
|
|
109
114
|
|
|
115
|
+
### What's new in 0.50.0
|
|
116
|
+
|
|
117
|
+
**`<DateTimePicker>` — tap the formatted date to open the picker (sc-1878).** The displayed date is now the affordance to change it on both hosts: on web, clicking the `<input>` text (or focusing it and pressing Enter / Space) calls `showPicker()` so the calendar opens from the text, not only the small built-in calendar icon; on native, the primitive renders an accessible, tappable formatted-date trigger that opens `@react-native-community/datetimepicker` on press (the RN library is imperative on Android, so the primitive owns the open/closed state — this also fixes the dialog auto-opening on mount). Both builds gained an `accessibilityLabel` prop that names the field for screen readers / test tooling. **The value contract is unchanged** — same `mode` values and ISO 8601 wire format on `value` / `onChange`; `CONTRACT.version` is unchanged.
|
|
118
|
+
|
|
110
119
|
### What's new in 0.40.1
|
|
111
120
|
|
|
112
121
|
**`<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.
|
|
@@ -369,6 +378,7 @@ The "split-implementation + vetted package list" pivot.
|
|
|
369
378
|
### What was in 0.4.1
|
|
370
379
|
|
|
371
380
|
- **`useDatastoreQuery` returns a stable `refetch` identity.** The hook no longer rebinds the underlying callback when the host's `WidgetContext` value (a fresh object identity on every render in Studio + PageRenderer) changes. Widgets that put `refetch` in a `useEffect` dep array no longer loop.
|
|
381
|
+
- **Keep the `useDatastoreQuery` argument stable across renders.** The hook re-fetches whenever `[table, JSON.stringify(query)]` changes, so a query whose serialized value differs every render refetches forever and the widget is stuck on its loading state. The classic trap is a time-relative filter built with `new Date()` / `Date.now()` inline in the query (a "this week" / "today" range) — the timestamp advances each render, so the key is never the same twice. Compute the date/time bound once with `useMemo(() => …, [])` (round to the day if you only need day granularity) and pass the stable value in. The same applies to any per-render value (a freshly built object, `Math.random()`): memoize it before it enters the query.
|
|
372
382
|
|
|
373
383
|
### What's new in 0.4.0
|
|
374
384
|
|
package/dist/contract.cjs
CHANGED
|
@@ -637,6 +637,30 @@ const HOOKS = [
|
|
|
637
637
|
requiredContextSlice: ["toast.showToast"],
|
|
638
638
|
scopes: null,
|
|
639
639
|
},
|
|
640
|
+
// sc-1584 — host-brokered device geolocation. Optional slice; the hook
|
|
641
|
+
// degrades to an UNSUPPORTED error rather than throwing at render.
|
|
642
|
+
{
|
|
643
|
+
name: "useGeolocation",
|
|
644
|
+
signature: "useGeolocation(options?)",
|
|
645
|
+
description:
|
|
646
|
+
"Read the device's current position. Returns { latitude, longitude, accuracy, loading, error, getCurrentPosition }. " +
|
|
647
|
+
"Capture is IMPERATIVE — call getCurrentPosition() from a user gesture (a tap); browsers and the mobile OS gate the " +
|
|
648
|
+
"permission prompt on a gesture, so it NEVER fires on mount. The promise resolves to { latitude, longitude, accuracy } " +
|
|
649
|
+
"and stores the same values on the hook; it rejects with a GeolocationError whose .code is one of PERMISSION_DENIED | " +
|
|
650
|
+
"UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL. options pass through to the host ({ enableHighAccuracy, timeout, " +
|
|
651
|
+
"maximumAge }). Identical on web (navigator.geolocation) and the Expo export (expo-location).",
|
|
652
|
+
returnShape: {
|
|
653
|
+
latitude: "number | null",
|
|
654
|
+
longitude: "number | null",
|
|
655
|
+
accuracy: "number | null // metres, best-effort",
|
|
656
|
+
loading: "boolean",
|
|
657
|
+
error: "GeolocationError | null",
|
|
658
|
+
getCurrentPosition:
|
|
659
|
+
"() => Promise<{ latitude, longitude, accuracy }> // rejects with GeolocationError",
|
|
660
|
+
},
|
|
661
|
+
requiredContextSlice: [],
|
|
662
|
+
scopes: null,
|
|
663
|
+
},
|
|
640
664
|
];
|
|
641
665
|
|
|
642
666
|
// REQ-WSDK-RN-WEB: the SDK exposes the React Native primitive API
|
|
@@ -1083,6 +1107,19 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1083
1107
|
required: false,
|
|
1084
1108
|
fields: { showToast: "function" },
|
|
1085
1109
|
},
|
|
1110
|
+
// sc-1584 — host-brokered device capabilities. Optional: a host that can't
|
|
1111
|
+
// broker a sensor omits the slice and useGeolocation() surfaces UNSUPPORTED.
|
|
1112
|
+
// Both the web Player (navigator.geolocation) and the Expo export
|
|
1113
|
+
// (expo-location) inject it, so device access is identical on both platforms.
|
|
1114
|
+
device: {
|
|
1115
|
+
description:
|
|
1116
|
+
"Optional host-brokered device capabilities. " +
|
|
1117
|
+
"{ geolocation: { getCurrentPosition(options?) -> Promise<{ latitude, longitude, accuracy }> } }. " +
|
|
1118
|
+
"Backs useGeolocation(). The web Player brokers it via navigator.geolocation; the Expo export via expo-location. " +
|
|
1119
|
+
"getCurrentPosition rejects with a GeolocationError (.code PERMISSION_DENIED | UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL).",
|
|
1120
|
+
required: false,
|
|
1121
|
+
fields: { geolocation: "object" },
|
|
1122
|
+
},
|
|
1086
1123
|
};
|
|
1087
1124
|
|
|
1088
1125
|
const BUNDLE_EXPORT_CONTRACT = [
|
|
@@ -1778,7 +1815,7 @@ const CONTRACT = deepFreeze({
|
|
|
1778
1815
|
// `notifications.send:appUser` scope it gates on. No existing hook,
|
|
1779
1816
|
// primitive, manifest field, or token changed shape — minor bump on the
|
|
1780
1817
|
// pre-1.0 channel.
|
|
1781
|
-
version: "1.
|
|
1818
|
+
version: "1.36.0",
|
|
1782
1819
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1783
1820
|
hooks: HOOKS,
|
|
1784
1821
|
primitives: PRIMITIVES,
|
package/dist/contract.js
CHANGED
|
@@ -637,6 +637,30 @@ const HOOKS = [
|
|
|
637
637
|
requiredContextSlice: ["toast.showToast"],
|
|
638
638
|
scopes: null,
|
|
639
639
|
},
|
|
640
|
+
// sc-1584 — host-brokered device geolocation. Optional slice; the hook
|
|
641
|
+
// degrades to an UNSUPPORTED error rather than throwing at render.
|
|
642
|
+
{
|
|
643
|
+
name: "useGeolocation",
|
|
644
|
+
signature: "useGeolocation(options?)",
|
|
645
|
+
description:
|
|
646
|
+
"Read the device's current position. Returns { latitude, longitude, accuracy, loading, error, getCurrentPosition }. " +
|
|
647
|
+
"Capture is IMPERATIVE — call getCurrentPosition() from a user gesture (a tap); browsers and the mobile OS gate the " +
|
|
648
|
+
"permission prompt on a gesture, so it NEVER fires on mount. The promise resolves to { latitude, longitude, accuracy } " +
|
|
649
|
+
"and stores the same values on the hook; it rejects with a GeolocationError whose .code is one of PERMISSION_DENIED | " +
|
|
650
|
+
"UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL. options pass through to the host ({ enableHighAccuracy, timeout, " +
|
|
651
|
+
"maximumAge }). Identical on web (navigator.geolocation) and the Expo export (expo-location).",
|
|
652
|
+
returnShape: {
|
|
653
|
+
latitude: "number | null",
|
|
654
|
+
longitude: "number | null",
|
|
655
|
+
accuracy: "number | null // metres, best-effort",
|
|
656
|
+
loading: "boolean",
|
|
657
|
+
error: "GeolocationError | null",
|
|
658
|
+
getCurrentPosition:
|
|
659
|
+
"() => Promise<{ latitude, longitude, accuracy }> // rejects with GeolocationError",
|
|
660
|
+
},
|
|
661
|
+
requiredContextSlice: [],
|
|
662
|
+
scopes: null,
|
|
663
|
+
},
|
|
640
664
|
];
|
|
641
665
|
|
|
642
666
|
// REQ-WSDK-RN-WEB: the SDK exposes the React Native primitive API
|
|
@@ -1083,6 +1107,19 @@ const WIDGET_CONTEXT_SHAPE = {
|
|
|
1083
1107
|
required: false,
|
|
1084
1108
|
fields: { showToast: "function" },
|
|
1085
1109
|
},
|
|
1110
|
+
// sc-1584 — host-brokered device capabilities. Optional: a host that can't
|
|
1111
|
+
// broker a sensor omits the slice and useGeolocation() surfaces UNSUPPORTED.
|
|
1112
|
+
// Both the web Player (navigator.geolocation) and the Expo export
|
|
1113
|
+
// (expo-location) inject it, so device access is identical on both platforms.
|
|
1114
|
+
device: {
|
|
1115
|
+
description:
|
|
1116
|
+
"Optional host-brokered device capabilities. " +
|
|
1117
|
+
"{ geolocation: { getCurrentPosition(options?) -> Promise<{ latitude, longitude, accuracy }> } }. " +
|
|
1118
|
+
"Backs useGeolocation(). The web Player brokers it via navigator.geolocation; the Expo export via expo-location. " +
|
|
1119
|
+
"getCurrentPosition rejects with a GeolocationError (.code PERMISSION_DENIED | UNAVAILABLE | TIMEOUT | UNSUPPORTED | INTERNAL).",
|
|
1120
|
+
required: false,
|
|
1121
|
+
fields: { geolocation: "object" },
|
|
1122
|
+
},
|
|
1086
1123
|
};
|
|
1087
1124
|
|
|
1088
1125
|
const BUNDLE_EXPORT_CONTRACT = [
|
|
@@ -1778,7 +1815,7 @@ const CONTRACT = deepFreeze({
|
|
|
1778
1815
|
// `notifications.send:appUser` scope it gates on. No existing hook,
|
|
1779
1816
|
// primitive, manifest field, or token changed shape — minor bump on the
|
|
1780
1817
|
// pre-1.0 channel.
|
|
1781
|
-
version: "1.
|
|
1818
|
+
version: "1.36.0",
|
|
1782
1819
|
sharedTranslationKeys: SHARED_TRANSLATION_KEYS,
|
|
1783
1820
|
hooks: HOOKS,
|
|
1784
1821
|
primitives: PRIMITIVES,
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// REQ-WSDK-PLATFORM §6 — pure date/ISO helpers for the native `<DateTimePicker>`.
|
|
2
|
+
//
|
|
3
|
+
// Split out of datetimepicker.native.js so the formatting contract can be
|
|
4
|
+
// unit-tested without importing react-native / the RN datetimepicker library
|
|
5
|
+
// (neither is installed in this package's node tree). No React, no RN here —
|
|
6
|
+
// just Date math and string shaping.
|
|
7
|
+
|
|
8
|
+
export function parseToDate(value) {
|
|
9
|
+
if (value == null || value === "") return new Date();
|
|
10
|
+
if (value instanceof Date) return Number.isNaN(value.getTime()) ? new Date() : value;
|
|
11
|
+
if (typeof value === "string") {
|
|
12
|
+
const d = new Date(value);
|
|
13
|
+
return Number.isNaN(d.getTime()) ? new Date() : d;
|
|
14
|
+
}
|
|
15
|
+
return new Date();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function formatToIso(date, mode) {
|
|
19
|
+
if (!(date instanceof Date) || Number.isNaN(date.getTime())) return null;
|
|
20
|
+
if (mode === "date") {
|
|
21
|
+
// Local-date ISO (yyyy-mm-dd) — calendar dates are timezone-free so a
|
|
22
|
+
// "May 28" picked in Stockholm doesn't read as "May 27" in NYC.
|
|
23
|
+
const y = date.getFullYear();
|
|
24
|
+
const m = String(date.getMonth() + 1).padStart(2, "0");
|
|
25
|
+
const d = String(date.getDate()).padStart(2, "0");
|
|
26
|
+
return `${y}-${m}-${d}`;
|
|
27
|
+
}
|
|
28
|
+
if (mode === "time") {
|
|
29
|
+
// Local-time ISO (hh:mm) — time-of-day is timezone-free for the same reason.
|
|
30
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
31
|
+
const mm = String(date.getMinutes()).padStart(2, "0");
|
|
32
|
+
return `${h}:${mm}`;
|
|
33
|
+
}
|
|
34
|
+
// datetime — full UTC ISO so the wire format round-trips through a DATE column.
|
|
35
|
+
return date.toISOString();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// The label shown on the tappable trigger. Empty value → a mode-specific
|
|
39
|
+
// placeholder so the field reads as "tap to pick" rather than blank.
|
|
40
|
+
export function formatDisplayLabel(value, mode) {
|
|
41
|
+
const effectiveMode = mode === "time" || mode === "datetime" ? mode : "date";
|
|
42
|
+
if (value == null || value === "") {
|
|
43
|
+
if (effectiveMode === "time") return "Select time";
|
|
44
|
+
if (effectiveMode === "datetime") return "Select date & time";
|
|
45
|
+
return "Select date";
|
|
46
|
+
}
|
|
47
|
+
if (effectiveMode === "date") return String(value).slice(0, 10);
|
|
48
|
+
if (effectiveMode === "time") return String(value).slice(0, 5);
|
|
49
|
+
// datetime — render the stored UTC ISO in the user's local clock.
|
|
50
|
+
const d = new Date(value);
|
|
51
|
+
if (Number.isNaN(d.getTime())) return String(value);
|
|
52
|
+
return `${formatToIso(d, "date")} ${formatToIso(d, "time")}`;
|
|
53
|
+
}
|
package/dist/datetimepicker.js
CHANGED
|
@@ -102,9 +102,33 @@ export function DateTimePicker({
|
|
|
102
102
|
? _isoToInputValue(maximumDate, effectiveMode)
|
|
103
103
|
: undefined;
|
|
104
104
|
|
|
105
|
+
// sc-1878 — clicking the formatted date text (or focusing it and pressing
|
|
106
|
+
// Enter / Space) opens the native calendar, not just the small built-in
|
|
107
|
+
// calendar icon. `showPicker()` requires a user gesture; click/keydown both
|
|
108
|
+
// qualify. Guarded: not every browser/build exposes it, and calling it while
|
|
109
|
+
// the picker is already open throws — neither should break the field.
|
|
110
|
+
const openPicker = (target) => {
|
|
111
|
+
if (disabled || !target || typeof target.showPicker !== "function") return;
|
|
112
|
+
try {
|
|
113
|
+
target.showPicker();
|
|
114
|
+
} catch {
|
|
115
|
+
// Already open, or blocked outside a user gesture — leave the input as-is.
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const handleClick = (event) => openPicker(event.currentTarget);
|
|
120
|
+
|
|
121
|
+
const handleKeyDown = (event) => {
|
|
122
|
+
if (event.key === "Enter" || event.key === " " || event.key === "Spacebar") {
|
|
123
|
+
event.preventDefault();
|
|
124
|
+
openPicker(event.currentTarget);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
|
|
105
128
|
// Inline styles match the SDK's other web-only primitives — the host's
|
|
106
129
|
// form widgets wrap this in their own labelled field, so the input just
|
|
107
|
-
// needs to look like a normal text input.
|
|
130
|
+
// needs to look like a normal text input. `cursor: pointer` signals the
|
|
131
|
+
// whole field is the click affordance (sc-1878).
|
|
108
132
|
const style = {
|
|
109
133
|
boxSizing: "border-box",
|
|
110
134
|
width: "100%",
|
|
@@ -119,12 +143,15 @@ export function DateTimePicker({
|
|
|
119
143
|
borderColor: "rgba(0, 0, 0, 0.16)",
|
|
120
144
|
borderRadius: 6,
|
|
121
145
|
outline: "none",
|
|
146
|
+
cursor: disabled ? "default" : "pointer",
|
|
122
147
|
};
|
|
123
148
|
|
|
124
149
|
return React.createElement("input", {
|
|
125
150
|
type: inputType,
|
|
126
151
|
value: inputValue,
|
|
127
152
|
onChange: handleChange,
|
|
153
|
+
onClick: handleClick,
|
|
154
|
+
onKeyDown: handleKeyDown,
|
|
128
155
|
min,
|
|
129
156
|
max,
|
|
130
157
|
disabled: !!disabled,
|
|
@@ -1,65 +1,52 @@
|
|
|
1
|
-
// REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` SDK primitive.
|
|
1
|
+
// REQ-WSDK-PLATFORM §6 — `<DateTimePicker>` SDK primitive (native).
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
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.
|
|
9
11
|
//
|
|
10
|
-
// Props:
|
|
12
|
+
// Props (mirrors datetimepicker.js):
|
|
11
13
|
// value: string | null — ISO 8601 (`2026-05-28` for date mode,
|
|
12
|
-
// `
|
|
13
|
-
// `
|
|
14
|
+
// `14:30` for time mode,
|
|
15
|
+
// `2026-05-28T14:30:00.000Z` for datetime mode).
|
|
16
|
+
// `null` / "" shows the placeholder, opens at "now".
|
|
14
17
|
// onChange: (iso: string) => void
|
|
15
18
|
// mode: "date" | "time" | "datetime" — default "date"
|
|
16
19
|
// minimumDate / maximumDate: string | null — ISO bounds
|
|
17
20
|
// disabled: boolean
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
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.
|
|
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`.
|
|
27
25
|
|
|
28
|
-
import React, { useMemo } from "react";
|
|
26
|
+
import React, { useMemo, useState } from "react";
|
|
29
27
|
// eslint-disable-next-line no-restricted-syntax
|
|
30
28
|
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";
|
|
31
35
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
}
|
|
36
|
+
const styles = StyleSheet.create({
|
|
37
|
+
trigger: {
|
|
38
|
+
minHeight: 44,
|
|
39
|
+
paddingVertical: 8,
|
|
40
|
+
paddingHorizontal: 12,
|
|
41
|
+
borderWidth: 1,
|
|
42
|
+
borderColor: "rgba(0, 0, 0, 0.16)",
|
|
43
|
+
borderRadius: 6,
|
|
44
|
+
justifyContent: "center",
|
|
45
|
+
},
|
|
46
|
+
triggerDisabled: { opacity: 0.5 },
|
|
47
|
+
label: { fontSize: 16 },
|
|
48
|
+
placeholder: { fontSize: 16, opacity: 0.5 },
|
|
49
|
+
});
|
|
63
50
|
|
|
64
51
|
export function DateTimePicker({
|
|
65
52
|
value,
|
|
@@ -68,35 +55,65 @@ export function DateTimePicker({
|
|
|
68
55
|
minimumDate,
|
|
69
56
|
maximumDate,
|
|
70
57
|
disabled,
|
|
58
|
+
accessibilityLabel,
|
|
71
59
|
}) {
|
|
72
60
|
const effectiveMode = mode === "time" || mode === "datetime" ? mode : "date";
|
|
73
|
-
const
|
|
61
|
+
const [open, setOpen] = useState(false);
|
|
62
|
+
const dateValue = useMemo(() => parseToDate(value), [value]);
|
|
74
63
|
const min = useMemo(
|
|
75
|
-
() => (minimumDate ?
|
|
64
|
+
() => (minimumDate ? parseToDate(minimumDate) : undefined),
|
|
76
65
|
[minimumDate],
|
|
77
66
|
);
|
|
78
67
|
const max = useMemo(
|
|
79
|
-
() => (maximumDate ?
|
|
68
|
+
() => (maximumDate ? parseToDate(maximumDate) : undefined),
|
|
80
69
|
[maximumDate],
|
|
81
70
|
);
|
|
82
71
|
|
|
83
|
-
const
|
|
72
|
+
const isEmpty = value == null || value === "";
|
|
73
|
+
|
|
74
|
+
const handleChange = (event, picked) => {
|
|
75
|
+
// Android fires "dismissed" when the user cancels; close without emitting.
|
|
76
|
+
setOpen(false);
|
|
77
|
+
if (event?.type === "dismissed") return;
|
|
84
78
|
if (typeof onChange !== "function") return;
|
|
85
79
|
if (!(picked instanceof Date)) return;
|
|
86
|
-
const iso =
|
|
80
|
+
const iso = formatToIso(picked, effectiveMode);
|
|
87
81
|
if (iso != null) onChange(iso);
|
|
88
82
|
};
|
|
89
83
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
84
|
+
const trigger = React.createElement(
|
|
85
|
+
Pressable,
|
|
86
|
+
{
|
|
87
|
+
onPress: () => { if (!disabled) setOpen(true); },
|
|
88
|
+
disabled: !!disabled,
|
|
89
|
+
accessibilityRole: "button",
|
|
90
|
+
accessibilityLabel,
|
|
91
|
+
accessibilityState: { disabled: !!disabled },
|
|
92
|
+
style: [styles.trigger, disabled && styles.triggerDisabled],
|
|
93
|
+
},
|
|
94
|
+
React.createElement(
|
|
95
|
+
Text,
|
|
96
|
+
{ style: isEmpty ? styles.placeholder : styles.label },
|
|
97
|
+
formatDisplayLabel(value, effectiveMode),
|
|
98
|
+
),
|
|
99
|
+
);
|
|
100
|
+
|
|
101
|
+
if (!open) return trigger;
|
|
102
|
+
|
|
103
|
+
// The RN library's `mode` is "date" / "time"; iOS also honours "datetime",
|
|
104
|
+
// Android falls back to date-only for it (a pre-existing limit, unchanged
|
|
105
|
+
// here). The web build uses datetimepicker.js, so this file is never web.
|
|
106
|
+
return React.createElement(
|
|
107
|
+
React.Fragment,
|
|
108
|
+
null,
|
|
109
|
+
trigger,
|
|
110
|
+
React.createElement(RNDateTimePicker, {
|
|
111
|
+
value: dateValue,
|
|
112
|
+
mode: effectiveMode,
|
|
113
|
+
minimumDate: min,
|
|
114
|
+
maximumDate: max,
|
|
115
|
+
disabled: !!disabled,
|
|
116
|
+
onChange: handleChange,
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
102
119
|
}
|
package/dist/hooks.js
CHANGED
|
@@ -336,6 +336,141 @@ export function useI18n() {
|
|
|
336
336
|
return { t, locale };
|
|
337
337
|
}
|
|
338
338
|
|
|
339
|
+
/* ============================================================================
|
|
340
|
+
* DEVICE — ctx.device (host-brokered device capabilities)
|
|
341
|
+
*
|
|
342
|
+
* Sensor / hardware the host brokers for the widget — geolocation today. The
|
|
343
|
+
* host injects `ctx.device.<cap>` on BOTH platforms (web Player via
|
|
344
|
+
* navigator.geolocation; the Expo export via expo-location), so a widget reads
|
|
345
|
+
* the device identically on web and native (widget-parity skill). The slice is
|
|
346
|
+
* optional — a host that cannot broker a capability omits it and the hook
|
|
347
|
+
* surfaces an UNSUPPORTED error instead of throwing at render. Covers:
|
|
348
|
+
* useGeolocation.
|
|
349
|
+
* ==========================================================================*/
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Error thrown by `useGeolocation().getCurrentPosition()` (and surfaced in the
|
|
353
|
+
* hook's `error` slot). Carries a stable `code` so widgets branch on the error
|
|
354
|
+
* class without parsing message strings.
|
|
355
|
+
*
|
|
356
|
+
* `code` is one of:
|
|
357
|
+
* - "PERMISSION_DENIED" — the user (or OS) refused location access.
|
|
358
|
+
* - "UNAVAILABLE" — position could not be determined (no fix / sensor).
|
|
359
|
+
* - "TIMEOUT" — the request exceeded the host/option timeout.
|
|
360
|
+
* - "UNSUPPORTED" — this host does not broker geolocation.
|
|
361
|
+
* - "INTERNAL" — anything else.
|
|
362
|
+
*/
|
|
363
|
+
export class GeolocationError extends Error {
|
|
364
|
+
constructor(code, message, opts) {
|
|
365
|
+
super(message);
|
|
366
|
+
this.name = "GeolocationError";
|
|
367
|
+
this.code = code;
|
|
368
|
+
if (opts && opts.cause) this.cause = opts.cause;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* Coerce a thrown value into a GeolocationError with a stable `.code`. Maps the
|
|
374
|
+
* browser PositionError numeric codes (1/2/3) and the host clients' string
|
|
375
|
+
* codes onto one vocabulary.
|
|
376
|
+
*/
|
|
377
|
+
function toGeolocationError(err) {
|
|
378
|
+
if (err instanceof GeolocationError) return err;
|
|
379
|
+
const raw = err && err.code !== undefined ? err.code : null;
|
|
380
|
+
let code = "INTERNAL";
|
|
381
|
+
if (raw === 1 || raw === "PERMISSION_DENIED") code = "PERMISSION_DENIED";
|
|
382
|
+
else if (raw === 2 || raw === "UNAVAILABLE" || raw === "POSITION_UNAVAILABLE")
|
|
383
|
+
code = "UNAVAILABLE";
|
|
384
|
+
else if (raw === 3 || raw === "TIMEOUT") code = "TIMEOUT";
|
|
385
|
+
else if (raw === "UNSUPPORTED") code = "UNSUPPORTED";
|
|
386
|
+
const message =
|
|
387
|
+
(err && typeof err.message === "string" && err.message) ||
|
|
388
|
+
"Geolocation request failed";
|
|
389
|
+
return new GeolocationError(code, message, { cause: err });
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* Read the device's current position. Returns
|
|
394
|
+
* `{ latitude, longitude, accuracy, loading, error, getCurrentPosition }`.
|
|
395
|
+
*
|
|
396
|
+
* Capture is IMPERATIVE — call `getCurrentPosition()` from a user gesture (a
|
|
397
|
+
* tap on a button). Browsers and the mobile OS gate the permission prompt on a
|
|
398
|
+
* user gesture, so the hook never fires on mount. The promise resolves to
|
|
399
|
+
* `{ latitude, longitude, accuracy }` and the same values are stored on the
|
|
400
|
+
* hook; it rejects with a `GeolocationError` carrying a stable `.code`.
|
|
401
|
+
*
|
|
402
|
+
* `options` pass through to the host (`{ enableHighAccuracy, timeout,
|
|
403
|
+
* maximumAge }`). The SAME hook drives both platforms: the web Player brokers
|
|
404
|
+
* it through `navigator.geolocation`, the Expo export through `expo-location`.
|
|
405
|
+
*
|
|
406
|
+
* Safe-by-default: on a host that does not inject `ctx.device.geolocation`,
|
|
407
|
+
* `getCurrentPosition()` rejects with `code: "UNSUPPORTED"` rather than
|
|
408
|
+
* throwing at render, so a widget can call the hook unconditionally.
|
|
409
|
+
*/
|
|
410
|
+
export function useGeolocation(options) {
|
|
411
|
+
const ctx = useWidgetContextOrThrow("useGeolocation");
|
|
412
|
+
const [coords, setCoords] = useState(null);
|
|
413
|
+
const [loading, setLoading] = useState(false);
|
|
414
|
+
const [error, setError] = useState(null);
|
|
415
|
+
|
|
416
|
+
// `ctx` is a fresh identity every host render — hold the live client +
|
|
417
|
+
// options in refs so getCurrentPosition is a stable callback.
|
|
418
|
+
const clientRef = useRef(ctx.device && ctx.device.geolocation);
|
|
419
|
+
clientRef.current = ctx.device && ctx.device.geolocation;
|
|
420
|
+
const optionsRef = useRef(options);
|
|
421
|
+
optionsRef.current = options;
|
|
422
|
+
const runRef = useRef(0);
|
|
423
|
+
|
|
424
|
+
const getCurrentPosition = useCallback(async () => {
|
|
425
|
+
const myRun = ++runRef.current;
|
|
426
|
+
const client = clientRef.current;
|
|
427
|
+
if (!client || typeof client.getCurrentPosition !== "function") {
|
|
428
|
+
const e = new GeolocationError(
|
|
429
|
+
"UNSUPPORTED",
|
|
430
|
+
"This host does not provide device geolocation.",
|
|
431
|
+
);
|
|
432
|
+
if (runRef.current === myRun) {
|
|
433
|
+
setError(e);
|
|
434
|
+
setLoading(false);
|
|
435
|
+
}
|
|
436
|
+
throw e;
|
|
437
|
+
}
|
|
438
|
+
setLoading(true);
|
|
439
|
+
setError(null);
|
|
440
|
+
try {
|
|
441
|
+
const pos = await client.getCurrentPosition(optionsRef.current);
|
|
442
|
+
const next = {
|
|
443
|
+
latitude:
|
|
444
|
+
pos && typeof pos.latitude === "number" ? pos.latitude : null,
|
|
445
|
+
longitude:
|
|
446
|
+
pos && typeof pos.longitude === "number" ? pos.longitude : null,
|
|
447
|
+
accuracy:
|
|
448
|
+
pos && typeof pos.accuracy === "number" ? pos.accuracy : null,
|
|
449
|
+
};
|
|
450
|
+
if (runRef.current !== myRun) return next;
|
|
451
|
+
setCoords(next);
|
|
452
|
+
setLoading(false);
|
|
453
|
+
return next;
|
|
454
|
+
} catch (err) {
|
|
455
|
+
const ge = toGeolocationError(err);
|
|
456
|
+
if (runRef.current === myRun) {
|
|
457
|
+
setError(ge);
|
|
458
|
+
setLoading(false);
|
|
459
|
+
}
|
|
460
|
+
throw ge;
|
|
461
|
+
}
|
|
462
|
+
}, []);
|
|
463
|
+
|
|
464
|
+
return {
|
|
465
|
+
latitude: coords ? coords.latitude : null,
|
|
466
|
+
longitude: coords ? coords.longitude : null,
|
|
467
|
+
accuracy: coords ? coords.accuracy : null,
|
|
468
|
+
loading,
|
|
469
|
+
error,
|
|
470
|
+
getCurrentPosition,
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
339
474
|
/* ============================================================================
|
|
340
475
|
* DATASTORE CLIENT — ctx.datastore (@colixsystems/datastore-client)
|
|
341
476
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -464,6 +464,16 @@ export interface WidgetContext<TProps = unknown> {
|
|
|
464
464
|
toast?: {
|
|
465
465
|
showToast(args: { kind?: string; message: string }): void;
|
|
466
466
|
};
|
|
467
|
+
/** Optional host-brokered device capabilities; backs useGeolocation. */
|
|
468
|
+
device?: {
|
|
469
|
+
geolocation?: {
|
|
470
|
+
getCurrentPosition(options?: GeolocationOptions): Promise<{
|
|
471
|
+
latitude: number;
|
|
472
|
+
longitude: number;
|
|
473
|
+
accuracy: number;
|
|
474
|
+
}>;
|
|
475
|
+
};
|
|
476
|
+
};
|
|
467
477
|
}
|
|
468
478
|
|
|
469
479
|
/**
|
|
@@ -921,6 +931,60 @@ export function useRefresh(
|
|
|
921
931
|
handler: () => void | Promise<unknown>,
|
|
922
932
|
): void;
|
|
923
933
|
|
|
934
|
+
/** Pass-through options for `useGeolocation().getCurrentPosition(...)`. */
|
|
935
|
+
export interface GeolocationOptions {
|
|
936
|
+
enableHighAccuracy?: boolean;
|
|
937
|
+
timeout?: number;
|
|
938
|
+
maximumAge?: number;
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
export interface GeolocationResult {
|
|
942
|
+
latitude: number | null;
|
|
943
|
+
longitude: number | null;
|
|
944
|
+
/** Best-effort accuracy in metres. */
|
|
945
|
+
accuracy: number | null;
|
|
946
|
+
loading: boolean;
|
|
947
|
+
error: GeolocationError | null;
|
|
948
|
+
/**
|
|
949
|
+
* Imperatively read the device position — call from a user gesture. Resolves
|
|
950
|
+
* to `{ latitude, longitude, accuracy }` and stores the same on the hook;
|
|
951
|
+
* rejects with a `GeolocationError`.
|
|
952
|
+
*/
|
|
953
|
+
getCurrentPosition(): Promise<{
|
|
954
|
+
latitude: number;
|
|
955
|
+
longitude: number;
|
|
956
|
+
accuracy: number;
|
|
957
|
+
}>;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
/**
|
|
961
|
+
* sc-1584 — read the device's current position. Capture is imperative (call
|
|
962
|
+
* `getCurrentPosition()` from a user gesture; it never fires on mount). The
|
|
963
|
+
* same hook drives both platforms — the web Player brokers it via
|
|
964
|
+
* `navigator.geolocation`, the Expo export via `expo-location`. Safe to call on
|
|
965
|
+
* a host that doesn't broker geolocation: `getCurrentPosition()` then rejects
|
|
966
|
+
* with `code: "UNSUPPORTED"`.
|
|
967
|
+
*/
|
|
968
|
+
export function useGeolocation(options?: GeolocationOptions): GeolocationResult;
|
|
969
|
+
|
|
970
|
+
/**
|
|
971
|
+
* sc-1584 — error thrown by `useGeolocation().getCurrentPosition()` and
|
|
972
|
+
* surfaced in the hook's `error` slot. `code` is a stable categorisation.
|
|
973
|
+
*/
|
|
974
|
+
export class GeolocationError extends Error {
|
|
975
|
+
code:
|
|
976
|
+
| "PERMISSION_DENIED"
|
|
977
|
+
| "UNAVAILABLE"
|
|
978
|
+
| "TIMEOUT"
|
|
979
|
+
| "UNSUPPORTED"
|
|
980
|
+
| "INTERNAL";
|
|
981
|
+
constructor(
|
|
982
|
+
code: GeolocationError["code"],
|
|
983
|
+
message: string,
|
|
984
|
+
opts?: { cause?: unknown },
|
|
985
|
+
);
|
|
986
|
+
}
|
|
987
|
+
|
|
924
988
|
/**
|
|
925
989
|
* Error class thrown by useDatastoreMutation callbacks (and surfaced by
|
|
926
990
|
* useDatastoreQuery in its `error` slot). The `code` is a stable
|
package/dist/index.js
CHANGED
package/dist/index.native.js
CHANGED
package/dist/lucideIconNames.cjs
CHANGED
package/dist/lucideIconNames.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colixsystems/widget-sdk",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.50.0",
|
|
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",
|
|
@@ -42,7 +42,7 @@
|
|
|
42
42
|
],
|
|
43
43
|
"scripts": {
|
|
44
44
|
"build": "node scripts/build.js",
|
|
45
|
-
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js"
|
|
45
|
+
"test": "node --test src/__tests__/contract.test.js src/__tests__/hooks-users.test.js src/__tests__/hooks-groups.test.js src/__tests__/hooks-schema.test.js src/__tests__/hooks-assets-by-tag.test.js src/__tests__/hooks-filestore-upload.test.js src/__tests__/hooks-mutation.test.js src/__tests__/hooks-record-permissions.test.js src/__tests__/hooks-geolocation.test.js src/__tests__/hooks-subscription.test.js src/__tests__/linter-users-scope.test.js src/__tests__/linter-comments.test.js src/__tests__/lucide-icon-names.test.js src/__tests__/manifest-actions.test.js src/__tests__/widget-translations.test.js src/__tests__/devserver.test.js src/__tests__/host-externals.test.js src/__tests__/datetimepicker.test.js"
|
|
46
46
|
},
|
|
47
47
|
"engines": {
|
|
48
48
|
"node": ">=18"
|
|
@@ -51,11 +51,6 @@
|
|
|
51
51
|
"access": "public",
|
|
52
52
|
"registry": "https://registry.npmjs.org/"
|
|
53
53
|
},
|
|
54
|
-
"repository": {
|
|
55
|
-
"type": "git",
|
|
56
|
-
"url": "git+https://github.com/colixwestin/appstudio.git",
|
|
57
|
-
"directory": "packages/widget-sdk"
|
|
58
|
-
},
|
|
59
54
|
"peerDependencies": {
|
|
60
55
|
"react": ">=18.0.0",
|
|
61
56
|
"react-native": "*",
|