@notchapp/api 0.1.0 → 0.1.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
@@ -11,27 +11,18 @@ npm install @notchapp/api
11
11
  Use it in a widget:
12
12
 
13
13
  ```tsx
14
- import { Button, Stack, Text } from "@notchapp/api";
15
-
16
- export const initialState = {
17
- count: 0,
18
- };
19
-
20
- export const actions = {
21
- increment(state) {
22
- return {
23
- ...state,
24
- count: (state?.count ?? 0) + 1,
25
- };
26
- },
27
- };
28
-
29
- export default function Widget({ environment, state }) {
14
+ import { Button, Stack, Text, useLocalStorage } from "@notchapp/api";
15
+
16
+ export default function Widget({ environment, logger }) {
17
+ const [count, setCount] = useLocalStorage("count", 0);
18
+
19
+ logger.info(`render hello widget span=${environment.span} count=${count}`);
20
+
30
21
  return (
31
22
  <Stack spacing={10}>
32
23
  <Text>Hello from NotchApp</Text>
33
- <Text tone="secondary">{`Span ${environment.span} • Count ${state.count}`}</Text>
34
- <Button title="Increment" action="increment" />
24
+ <Text tone="secondary">{`Span ${environment.span} • Count ${count}`}</Text>
25
+ <Button title="Increment" onPress={() => setCount((value) => value + 1)} />
35
26
  </Stack>
36
27
  );
37
28
  }
@@ -41,14 +32,47 @@ Current exports:
41
32
 
42
33
  - `Stack`
43
34
  - `Inline`
44
- - `Row`
35
+ - `Spacer`
45
36
  - `Text`
46
37
  - `Icon`
38
+ - `Image`
39
+ - `Button`
40
+ - `Row`
47
41
  - `IconButton`
48
42
  - `Checkbox`
49
43
  - `Input`
50
- - `Button`
44
+ - `ScrollView`
45
+ - `Divider`
46
+ - `Circle`
47
+ - `RoundedRect`
48
+ - `LocalStorage`
49
+ - `getPreferenceValues`
50
+ - `useLocalStorage`
51
+ - `usePromise`
52
+ - `useFetch`
53
+ - `openURL`
54
+
55
+ Widget preferences can be declared in your widget manifest under `notch.preferences` and read at runtime:
56
+
57
+ ```tsx
58
+ import { getPreferenceValues } from "@notchapp/api";
59
+
60
+ export default function Widget() {
61
+ const preferences = getPreferenceValues();
62
+ return <Text>{preferences.mailbox ?? "Inbox"}</Text>;
63
+ }
64
+ ```
51
65
 
52
66
  The SDK source and examples live in the main repository:
53
67
 
54
68
  <https://github.com/itstauq/NotchApp>
69
+
70
+ Local widget images live under your package `assets/` directory and can be referenced with paths like `src="assets/cover.png"`.
71
+
72
+ `Image` supports both local package assets and remote image URLs. `contentMode="fill"` is the default, and `contentMode="fit"` keeps the full image visible inside its frame.
73
+
74
+ Remote image notes:
75
+
76
+ - widgets use `https://` URLs only
77
+ - remote images are fetched by the host, not inside the widget runtime
78
+ - custom headers, cookies, and auth are not supported yet
@@ -0,0 +1,9 @@
1
+ const { callRpc } = require("../runtime");
2
+
3
+ function openURL(url) {
4
+ return callRpc("browser.open", { url });
5
+ }
6
+
7
+ module.exports = {
8
+ openURL,
9
+ };
@@ -0,0 +1,22 @@
1
+ const { usePromise } = require("./usePromise");
2
+
3
+ function useFetch(url, options = {}) {
4
+ const { parseJson = true, ...requestInit } = options;
5
+
6
+ return usePromise(async (signal) => {
7
+ const response = await fetch(url, {
8
+ ...requestInit,
9
+ signal,
10
+ });
11
+
12
+ if (!response.ok) {
13
+ throw new Error(`Request failed with status ${response.status}`);
14
+ }
15
+
16
+ return parseJson ? response.json() : response.text();
17
+ }, [url, JSON.stringify(options)]);
18
+ }
19
+
20
+ module.exports = {
21
+ useFetch,
22
+ };
@@ -0,0 +1,36 @@
1
+ const React = require("react");
2
+
3
+ const { LocalStorage } = require("../runtime");
4
+
5
+ function useLocalStorage(key, defaultValue) {
6
+ const [value, setValue] = React.useState(() => {
7
+ const storedValue = LocalStorage.getItem(key);
8
+ return storedValue === undefined ? defaultValue : storedValue;
9
+ });
10
+
11
+ React.useEffect(() => {
12
+ const storedValue = LocalStorage.getItem(key);
13
+ setValue(storedValue === undefined ? defaultValue : storedValue);
14
+ }, [key]);
15
+
16
+ function setStoredValue(nextValue) {
17
+ setValue((currentValue) => {
18
+ const resolvedValue = typeof nextValue === "function"
19
+ ? nextValue(currentValue)
20
+ : nextValue;
21
+
22
+ if (resolvedValue === undefined) {
23
+ LocalStorage.removeItem(key);
24
+ return defaultValue;
25
+ }
26
+
27
+ return LocalStorage.setItem(key, resolvedValue);
28
+ });
29
+ }
30
+
31
+ return [value, setStoredValue];
32
+ }
33
+
34
+ module.exports = {
35
+ useLocalStorage,
36
+ };
@@ -0,0 +1,96 @@
1
+ const React = require("react");
2
+
3
+ function areDepsEqual(previousDeps, nextDeps) {
4
+ if (!previousDeps || !nextDeps || previousDeps.length !== nextDeps.length) {
5
+ return false;
6
+ }
7
+
8
+ for (let index = 0; index < previousDeps.length; index += 1) {
9
+ if (!Object.is(previousDeps[index], nextDeps[index])) {
10
+ return false;
11
+ }
12
+ }
13
+
14
+ return true;
15
+ }
16
+
17
+ function usePromise(factory, deps = []) {
18
+ const [state, setState] = React.useState({
19
+ data: undefined,
20
+ isLoading: true,
21
+ error: undefined,
22
+ });
23
+ const controllerRef = React.useRef(null);
24
+ const factoryRef = React.useRef(factory);
25
+ const depsRef = React.useRef();
26
+
27
+ factoryRef.current = factory;
28
+
29
+ const revalidate = React.useCallback(() => {
30
+ controllerRef.current?.abort();
31
+ const controller = new AbortController();
32
+ controllerRef.current = controller;
33
+
34
+ setState((currentState) => ({
35
+ data: currentState.data,
36
+ isLoading: true,
37
+ error: undefined,
38
+ }));
39
+
40
+ Promise.resolve()
41
+ .then(() => factoryRef.current(controller.signal))
42
+ .then((data) => {
43
+ if (controller.signal.aborted || controllerRef.current !== controller) {
44
+ return;
45
+ }
46
+
47
+ setState({
48
+ data,
49
+ isLoading: false,
50
+ error: undefined,
51
+ });
52
+ })
53
+ .catch((error) => {
54
+ if (controller.signal.aborted || controllerRef.current !== controller) {
55
+ return;
56
+ }
57
+
58
+ if (error?.name === "AbortError") {
59
+ return;
60
+ }
61
+
62
+ setState({
63
+ data: undefined,
64
+ isLoading: false,
65
+ error,
66
+ });
67
+ });
68
+ }, []);
69
+
70
+ React.useEffect(() => {
71
+ const depsChanged = !areDepsEqual(depsRef.current, deps);
72
+ depsRef.current = deps;
73
+ if (!depsChanged) {
74
+ return;
75
+ }
76
+
77
+ revalidate();
78
+ });
79
+
80
+ React.useEffect(() => {
81
+ return () => {
82
+ controllerRef.current?.abort();
83
+ };
84
+ }, []);
85
+
86
+ return {
87
+ data: state.data,
88
+ isLoading: state.isLoading,
89
+ error: state.error,
90
+ revalidate,
91
+ };
92
+ }
93
+
94
+ module.exports = {
95
+ usePromise,
96
+ };
package/index.js CHANGED
@@ -1,139 +1,161 @@
1
- function flattenChildren(input) {
2
- if (input == null || input === false) return [];
3
- if (Array.isArray(input)) return input.flatMap(flattenChildren);
4
- return [input];
1
+ const React = require("react");
2
+ const { useLocalStorage } = require("./hooks/useLocalStorage");
3
+ const { usePromise } = require("./hooks/usePromise");
4
+ const { useFetch } = require("./hooks/useFetch");
5
+ const { openURL } = require("./functions/openURL");
6
+ const { LocalStorage, getPreferenceValues } = require("./runtime");
7
+
8
+ const OVERLAY_SLOT_TYPE = "__notch_overlay";
9
+ const LEADING_ACCESSORY_SLOT_TYPE = "__notch_leadingAccessory";
10
+ const TRAILING_ACCESSORY_SLOT_TYPE = "__notch_trailingAccessory";
11
+
12
+ function slot(type, props, children, key) {
13
+ return React.createElement(
14
+ type,
15
+ key == null ? props : { ...(props ?? {}), key },
16
+ children
17
+ );
18
+ }
19
+
20
+ function normalizeOverlayChildren(overlay) {
21
+ if (overlay == null || overlay === false) {
22
+ return [];
23
+ }
24
+
25
+ if (Array.isArray(overlay)) {
26
+ return overlay.flatMap(normalizeOverlayChildren);
27
+ }
28
+
29
+ if (React.isValidElement(overlay)) {
30
+ return [slot(OVERLAY_SLOT_TYPE, { alignment: "center" }, overlay, overlay.key)];
31
+ }
32
+
33
+ if (typeof overlay === "object") {
34
+ const node = overlay.element ?? overlay.node;
35
+ if (node != null) {
36
+ return [
37
+ slot(
38
+ OVERLAY_SLOT_TYPE,
39
+ { alignment: typeof overlay.alignment === "string" ? overlay.alignment : "center" },
40
+ node,
41
+ overlay.key ?? node.key
42
+ ),
43
+ ];
44
+ }
45
+ }
46
+
47
+ return [];
5
48
  }
6
49
 
7
- function extractText(input) {
8
- return flattenChildren(input)
9
- .map((item) => {
10
- if (typeof item === "string" || typeof item === "number") return String(item);
11
- if (item && typeof item.text === "string") return item.text;
12
- return "";
13
- })
14
- .join("");
50
+ function normalizeAccessoryChild(type, accessory) {
51
+ if (accessory == null || accessory === false) {
52
+ return [];
53
+ }
54
+
55
+ return [slot(type, null, accessory)];
15
56
  }
16
57
 
17
- function wrapNode(input) {
18
- if (input == null || input === false) return null;
19
- const flattened = flattenChildren(input);
20
- if (flattened.length === 0) return null;
21
- return { node: flattened[0] };
58
+ function createHostElement(type, rawProps = {}) {
59
+ const {
60
+ children,
61
+ overlay,
62
+ leadingAccessory,
63
+ trailingAccessory,
64
+ ...props
65
+ } = rawProps;
66
+ const hostChildren = [];
67
+
68
+ if (children !== undefined) {
69
+ hostChildren.push(children);
70
+ }
71
+
72
+ hostChildren.push(...normalizeOverlayChildren(overlay));
73
+ hostChildren.push(...normalizeAccessoryChild(LEADING_ACCESSORY_SLOT_TYPE, leadingAccessory));
74
+ hostChildren.push(...normalizeAccessoryChild(TRAILING_ACCESSORY_SLOT_TYPE, trailingAccessory));
75
+
76
+ return React.createElement(type, props, ...hostChildren);
22
77
  }
23
78
 
24
79
  function Stack(props = {}) {
25
- return {
26
- id: props.id ?? undefined,
27
- type: "Stack",
28
- direction: props.direction ?? "vertical",
29
- spacing: props.spacing ?? 8,
30
- children: flattenChildren(props.children),
31
- };
80
+ return createHostElement("Stack", props);
32
81
  }
33
82
 
34
83
  function Inline(props = {}) {
35
- return {
36
- id: props.id ?? undefined,
37
- type: "Inline",
38
- spacing: props.spacing ?? 8,
39
- children: flattenChildren(props.children),
40
- };
84
+ return createHostElement("Inline", props);
41
85
  }
42
86
 
43
- function Row(props = {}) {
44
- return {
45
- id: props.id ?? undefined,
46
- type: "Row",
47
- action: props.action ?? null,
48
- payload: props.payload ?? null,
49
- children: flattenChildren(props.children),
50
- };
87
+ function Spacer(props = {}) {
88
+ return createHostElement("Spacer", props);
51
89
  }
52
90
 
53
91
  function Text(props = {}) {
54
- return {
55
- id: props.id ?? undefined,
56
- type: "Text",
57
- text: props.text ?? extractText(props.children),
58
- role: props.role ?? undefined,
59
- tone: props.tone ?? undefined,
60
- lineClamp: props.lineClamp ?? undefined,
61
- strikethrough: props.strikethrough ?? undefined,
62
- children: [],
63
- };
92
+ return createHostElement("Text", props);
64
93
  }
65
94
 
66
95
  function Icon(props = {}) {
67
- return {
68
- id: props.id ?? undefined,
69
- type: "Icon",
70
- symbol: props.symbol ?? props.icon ?? props.name ?? undefined,
71
- tone: props.tone ?? undefined,
72
- children: [],
73
- };
96
+ return createHostElement("Icon", props);
97
+ }
98
+
99
+ function Image(props = {}) {
100
+ return createHostElement("Image", props);
101
+ }
102
+
103
+ function Button(props = {}) {
104
+ return createHostElement("Button", props);
105
+ }
106
+
107
+ function Row(props = {}) {
108
+ return createHostElement("Row", props);
74
109
  }
75
110
 
76
111
  function IconButton(props = {}) {
77
- return {
78
- id: props.id ?? undefined,
79
- type: "IconButton",
80
- symbol: props.symbol ?? props.icon ?? props.name ?? undefined,
81
- action: props.action ?? null,
82
- payload: props.payload ?? null,
83
- tone: props.tone ?? undefined,
84
- disabled: props.disabled ?? false,
85
- children: [],
86
- };
112
+ return createHostElement("IconButton", props);
87
113
  }
88
114
 
89
115
  function Checkbox(props = {}) {
90
- return {
91
- id: props.id ?? undefined,
92
- type: "Checkbox",
93
- checked: props.checked ?? false,
94
- action: props.action ?? null,
95
- payload: props.payload ?? null,
96
- children: [],
97
- };
116
+ return createHostElement("Checkbox", props);
98
117
  }
99
118
 
100
119
  function Input(props = {}) {
101
- return {
102
- id: props.id ?? undefined,
103
- type: "Input",
104
- value: props.value ?? "",
105
- placeholder: props.placeholder ?? "",
106
- changeAction: props.changeAction ?? null,
107
- submitAction: props.submitAction ?? null,
108
- leadingAccessory: wrapNode(props.leadingAccessory),
109
- trailingAccessory: wrapNode(props.trailingAccessory),
110
- children: [],
111
- };
120
+ return createHostElement("Input", props);
112
121
  }
113
122
 
114
- function Button(props = {}) {
115
- return {
116
- id: props.id ?? undefined,
117
- type: "Button",
118
- title: props.title ?? extractText(props.children),
119
- action: props.action ?? null,
120
- payload: props.payload ?? null,
121
- children: [],
122
- };
123
+ function ScrollView(props = {}) {
124
+ return createHostElement("ScrollView", props);
125
+ }
126
+
127
+ function Divider(props = {}) {
128
+ return createHostElement("Divider", props);
129
+ }
130
+
131
+ function Circle(props = {}) {
132
+ return createHostElement("Circle", props);
133
+ }
134
+
135
+ function RoundedRect(props = {}) {
136
+ return createHostElement("RoundedRect", props);
123
137
  }
124
138
 
125
139
  module.exports = {
126
140
  Stack,
127
141
  Inline,
128
- Row,
142
+ Spacer,
129
143
  Text,
130
144
  Icon,
145
+ Image,
146
+ Button,
147
+ Row,
131
148
  IconButton,
132
149
  Checkbox,
133
150
  Input,
134
- Button,
135
- __internal: {
136
- flattenChildren,
137
- extractText,
138
- },
151
+ ScrollView,
152
+ Divider,
153
+ Circle,
154
+ RoundedRect,
155
+ LocalStorage,
156
+ getPreferenceValues,
157
+ useLocalStorage,
158
+ usePromise,
159
+ useFetch,
160
+ openURL,
139
161
  };
package/jsx-runtime.js CHANGED
@@ -1,28 +1,3 @@
1
- const { __internal } = require("./index.js");
2
-
3
- const Fragment = Symbol.for("notch.fragment");
4
-
5
- function jsx(type, props, key) {
6
- if (type === Fragment) {
7
- return __internal.flattenChildren(props?.children);
8
- }
9
-
10
- const merged = { ...(props ?? {}) };
11
- if (key != null && merged.id == null) {
12
- merged.id = String(key);
13
- }
14
-
15
- if (typeof type === "function") {
16
- return type(merged);
17
- }
18
-
19
- throw new Error(`Unsupported JSX type: ${String(type)}`);
20
- }
21
-
22
- const jsxs = jsx;
23
-
24
- module.exports = {
25
- Fragment,
26
- jsx,
27
- jsxs,
28
- };
1
+ // Keep JSX bound to the @notchapp/api namespace so we can evolve this seam
2
+ // later without changing widget build configuration or source imports.
3
+ module.exports = require("react/jsx-runtime");
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@notchapp/api",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Widget component API for building NotchApp widgets",
5
5
  "main": "index.js",
6
6
  "files": [
7
7
  "index.js",
8
- "jsx-runtime.js"
8
+ "jsx-runtime.js",
9
+ "runtime.js",
10
+ "hooks",
11
+ "functions"
9
12
  ],
10
13
  "keywords": [
11
14
  "notchapp",
package/runtime.js ADDED
@@ -0,0 +1,34 @@
1
+ function runtime() {
2
+ if (!globalThis.__NOTCH_RUNTIME__) {
3
+ throw new Error("@notchapp/api must run inside the Notch widget runtime");
4
+ }
5
+
6
+ return globalThis.__NOTCH_RUNTIME__;
7
+ }
8
+
9
+ module.exports = {
10
+ LocalStorage: {
11
+ getItem(key) {
12
+ return runtime().localStorage.getItem(key);
13
+ },
14
+ setItem(key, value) {
15
+ return runtime().localStorage.setItem(key, value);
16
+ },
17
+ removeItem(key) {
18
+ return runtime().localStorage.removeItem(key);
19
+ },
20
+ allItems() {
21
+ return runtime().localStorage.allItems();
22
+ },
23
+ },
24
+ getPreferenceValues() {
25
+ const preferences = runtime().getCurrentProps()?.preferences;
26
+ if (!preferences || typeof preferences !== "object" || Array.isArray(preferences)) {
27
+ return {};
28
+ }
29
+
30
+ return structuredClone(preferences);
31
+ },
32
+ getCurrentProps: () => runtime().getCurrentProps(),
33
+ callRpc: (method, params) => runtime().callRpc(method, params),
34
+ };