@forthtilliath/react-native-kit 0.5.0 → 0.7.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 +78 -0
- package/dist/ThemeToggle.d.ts +22 -0
- package/dist/ThemeToggle.d.ts.map +1 -0
- package/dist/ThemeToggle.js +34 -0
- package/dist/UpdateAvailableBanner.d.ts +29 -0
- package/dist/UpdateAvailableBanner.d.ts.map +1 -0
- package/dist/UpdateAvailableBanner.js +44 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/useEffectiveColorScheme.d.ts +3 -0
- package/dist/useEffectiveColorScheme.d.ts.map +1 -0
- package/dist/useEffectiveColorScheme.js +11 -0
- package/dist/useUpdateCheck.d.ts +35 -0
- package/dist/useUpdateCheck.d.ts.map +1 -0
- package/dist/useUpdateCheck.js +47 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -28,6 +28,17 @@ Each component is its own module — import the file you need directly:
|
|
|
28
28
|
import { ChangelogNotes } from "@forthtilliath/react-native-kit/ChangelogNotes";
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
This is the recommended way to import: Metro (React Native's bundler) doesn't reliably tree-shake, so pulling from a single deep-import path keeps peer dependencies you don't use (`expo-image-picker`, `expo-speech-recognition`, `react-native-gesture-handler`...) out of your bundle entirely, rather than merely unused.
|
|
32
|
+
|
|
33
|
+
A root barrel is also available for convenience when you don't mind that trade-off:
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
import {
|
|
37
|
+
ChangelogNotes,
|
|
38
|
+
useSubmitGuard,
|
|
39
|
+
} from "@forthtilliath/react-native-kit";
|
|
40
|
+
```
|
|
41
|
+
|
|
31
42
|
### `<ChangelogNotes notes={...} styles={...} />`
|
|
32
43
|
|
|
33
44
|
Renders GitHub-style release notes — as produced by `@forthtilliath/expo-release-updates`'s `parseChangelogNotes` — as headings, bulleted items, and bold-aware text, instead of showing the raw Markdown syntax in a plain `Text`.
|
|
@@ -204,6 +215,73 @@ confirmDestructive("Delete this item?", () => deleteItem(id), {
|
|
|
204
215
|
});
|
|
205
216
|
```
|
|
206
217
|
|
|
218
|
+
### `useEffectiveColorScheme(preference)`
|
|
219
|
+
|
|
220
|
+
Resolves a `"light" | "dark" | "system"` theme preference against the device's color scheme: `"system"` follows the device, `"light"`/`"dark"` override it regardless of what the device is set to.
|
|
221
|
+
|
|
222
|
+
```tsx
|
|
223
|
+
import { useEffectiveColorScheme } from "@forthtilliath/react-native-kit/useEffectiveColorScheme";
|
|
224
|
+
|
|
225
|
+
const scheme = useEffectiveColorScheme(themePreference); // "light" | "dark"
|
|
226
|
+
const colors = scheme === "dark" ? darkColors : lightColors;
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### `<ThemeToggle value onChange />`
|
|
230
|
+
|
|
231
|
+
3-way segmented control for a light/dark/system theme preference.
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
import { ThemeToggle } from "@forthtilliath/react-native-kit/ThemeToggle";
|
|
235
|
+
|
|
236
|
+
<ThemeToggle value={themePreference} onChange={setThemePreference} />;
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
### `useUpdateCheck(options)`
|
|
240
|
+
|
|
241
|
+
Checks once per mount (e.g. app launch) whether a newer release is available, throttled to at most one real check per `minIntervalMs` (default 12h) and silent for a release the user already dismissed. Has no opinion on where "when did we last check" / "which version did the user dismiss" are persisted — both are read/written entirely through the options you pass in.
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
import { useUpdateCheck } from "@forthtilliath/react-native-kit/useUpdateCheck";
|
|
245
|
+
|
|
246
|
+
const update = useUpdateCheck({
|
|
247
|
+
currentVersion: Constants.expoConfig?.version ?? "0.0.0",
|
|
248
|
+
checkForUpdate: fetchLatestRelease,
|
|
249
|
+
compareVersions,
|
|
250
|
+
getLastCheck: () => ({
|
|
251
|
+
lastCheckedAt: settings?.lastUpdateCheckAt ?? null,
|
|
252
|
+
dismissedVersion: settings?.dismissedUpdateVersion ?? null,
|
|
253
|
+
}),
|
|
254
|
+
onChecked: (lastCheckedAt) =>
|
|
255
|
+
updateSettings({ lastUpdateCheckAt: lastCheckedAt }),
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
if (update.status === "available") {
|
|
259
|
+
// update.release.version / .notes / .apkUrl
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### `<UpdateAvailableBanner version notes onPress onDismiss />`
|
|
264
|
+
|
|
265
|
+
Dismissible banner announcing an available update: version, release notes (rendered via `ChangelogNotes`), an action button and a dismiss button. Has no opinion on what the action does (e.g. navigate to an update screen) or on how/whether dismissal is persisted.
|
|
266
|
+
|
|
267
|
+
```tsx
|
|
268
|
+
import { UpdateAvailableBanner } from "@forthtilliath/react-native-kit/UpdateAvailableBanner";
|
|
269
|
+
|
|
270
|
+
{
|
|
271
|
+
update.status === "available" && (
|
|
272
|
+
<UpdateAvailableBanner
|
|
273
|
+
version={update.release.version}
|
|
274
|
+
notes={update.release.notes}
|
|
275
|
+
onPress={() => router.push("/settings/update")}
|
|
276
|
+
onDismiss={() => {
|
|
277
|
+
dismissUpdateVersion(update.release.version);
|
|
278
|
+
update.dismiss();
|
|
279
|
+
}}
|
|
280
|
+
/>
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
```
|
|
284
|
+
|
|
207
285
|
### Utils (`utils/`)
|
|
208
286
|
|
|
209
287
|
Framework-agnostic pure functions — no React or React Native import, usable from Node/web too.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
2
|
+
import type { ThemePreference } from "./useEffectiveColorScheme.js";
|
|
3
|
+
export interface ThemeToggleStyles {
|
|
4
|
+
container?: StyleProp<ViewStyle>;
|
|
5
|
+
option?: StyleProp<ViewStyle>;
|
|
6
|
+
optionActive?: StyleProp<ViewStyle>;
|
|
7
|
+
optionText?: StyleProp<TextStyle>;
|
|
8
|
+
optionTextActive?: StyleProp<TextStyle>;
|
|
9
|
+
}
|
|
10
|
+
export interface ThemeToggleLabels {
|
|
11
|
+
light?: string;
|
|
12
|
+
dark?: string;
|
|
13
|
+
system?: string;
|
|
14
|
+
}
|
|
15
|
+
export interface ThemeToggleProps {
|
|
16
|
+
value: ThemePreference;
|
|
17
|
+
onChange: (preference: ThemePreference) => void;
|
|
18
|
+
labels?: ThemeToggleLabels;
|
|
19
|
+
styles?: ThemeToggleStyles;
|
|
20
|
+
}
|
|
21
|
+
export declare function ThemeToggle({ value, onChange, labels, styles, }: ThemeToggleProps): import("react").JSX.Element;
|
|
22
|
+
//# sourceMappingURL=ThemeToggle.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ThemeToggle.d.ts","sourceRoot":"","sources":["../src/ThemeToggle.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAEpE,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,gBAAgB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,IAAI,CAAC;IAChD,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AA2BD,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,MAAM,EACN,MAAM,GACP,EAAE,gBAAgB,+BA6BlB"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { Pressable, Text, View } from "react-native";
|
|
3
|
+
const OPTIONS = ["light", "dark", "system"];
|
|
4
|
+
const defaultStyles = {
|
|
5
|
+
container: { flexDirection: "row", gap: 8 },
|
|
6
|
+
option: {
|
|
7
|
+
flex: 1,
|
|
8
|
+
borderWidth: 1,
|
|
9
|
+
borderColor: "#e5e7eb",
|
|
10
|
+
borderRadius: 10,
|
|
11
|
+
paddingVertical: 10,
|
|
12
|
+
alignItems: "center",
|
|
13
|
+
backgroundColor: "#ffffff",
|
|
14
|
+
},
|
|
15
|
+
optionActive: { backgroundColor: "#2563eb", borderColor: "#2563eb" },
|
|
16
|
+
optionText: { fontSize: 15, fontWeight: "600", color: "#111827" },
|
|
17
|
+
optionTextActive: { color: "#ffffff" },
|
|
18
|
+
};
|
|
19
|
+
const defaultLabels = {
|
|
20
|
+
light: "Clair",
|
|
21
|
+
dark: "Sombre",
|
|
22
|
+
system: "Système",
|
|
23
|
+
};
|
|
24
|
+
// 3-way segmented control for a light/dark/system theme preference.
|
|
25
|
+
export function ThemeToggle({ value, onChange, labels, styles, }) {
|
|
26
|
+
const merged = { ...defaultStyles, ...styles };
|
|
27
|
+
const t = { ...defaultLabels, ...labels };
|
|
28
|
+
return (_jsx(View, { style: merged.container, children: OPTIONS.map((option) => {
|
|
29
|
+
const active = option === value;
|
|
30
|
+
return (_jsx(Pressable, { style: [merged.option, active && merged.optionActive], onPress: () => {
|
|
31
|
+
onChange(option);
|
|
32
|
+
}, accessibilityRole: "button", accessibilityState: { selected: active }, accessibilityLabel: t[option], children: _jsx(Text, { style: [merged.optionText, active && merged.optionTextActive], children: t[option] }) }, option));
|
|
33
|
+
}) }));
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
2
|
+
import type { ChangelogNotesStyles } from "./ChangelogNotes.js";
|
|
3
|
+
export interface UpdateAvailableBannerStyles {
|
|
4
|
+
container?: StyleProp<ViewStyle>;
|
|
5
|
+
header?: StyleProp<ViewStyle>;
|
|
6
|
+
title?: StyleProp<TextStyle>;
|
|
7
|
+
notes?: ChangelogNotesStyles;
|
|
8
|
+
actionButton?: StyleProp<ViewStyle>;
|
|
9
|
+
actionButtonText?: StyleProp<TextStyle>;
|
|
10
|
+
dismissButton?: StyleProp<ViewStyle>;
|
|
11
|
+
dismissButtonText?: StyleProp<TextStyle>;
|
|
12
|
+
}
|
|
13
|
+
export interface UpdateAvailableBannerLabels {
|
|
14
|
+
title?: (version: string) => string;
|
|
15
|
+
action?: string;
|
|
16
|
+
dismiss?: string;
|
|
17
|
+
dismissAccessibilityLabel?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface UpdateAvailableBannerProps {
|
|
20
|
+
version: string;
|
|
21
|
+
/** Raw release notes, passed straight to `ChangelogNotes`. */
|
|
22
|
+
notes: string;
|
|
23
|
+
onPress: () => void;
|
|
24
|
+
onDismiss: () => void;
|
|
25
|
+
labels?: UpdateAvailableBannerLabels;
|
|
26
|
+
styles?: UpdateAvailableBannerStyles;
|
|
27
|
+
}
|
|
28
|
+
export declare function UpdateAvailableBanner({ version, notes, onPress, onDismiss, labels, styles, }: UpdateAvailableBannerProps): import("react").JSX.Element;
|
|
29
|
+
//# sourceMappingURL=UpdateAvailableBanner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"UpdateAvailableBanner.d.ts","sourceRoot":"","sources":["../src/UpdateAvailableBanner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAGhE,MAAM,WAAW,2BAA2B;IAC1C,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,gBAAgB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACxC,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACrC,iBAAiB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,2BAA2B,CAAC;IACrC,MAAM,CAAC,EAAE,2BAA2B,CAAC;CACtC;AAwCD,wBAAgB,qBAAqB,CAAC,EACpC,OAAO,EACP,KAAK,EACL,OAAO,EACP,SAAS,EACT,MAAM,EACN,MAAM,GACP,EAAE,0BAA0B,+BA4B5B"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Pressable, Text, View } from "react-native";
|
|
3
|
+
import { ChangelogNotes } from "./ChangelogNotes.js";
|
|
4
|
+
const defaultStyles = {
|
|
5
|
+
container: {
|
|
6
|
+
backgroundColor: "#ffffff",
|
|
7
|
+
borderWidth: 1,
|
|
8
|
+
borderColor: "#e5e7eb",
|
|
9
|
+
borderRadius: 10,
|
|
10
|
+
padding: 14,
|
|
11
|
+
gap: 8,
|
|
12
|
+
},
|
|
13
|
+
header: {
|
|
14
|
+
flexDirection: "row",
|
|
15
|
+
justifyContent: "space-between",
|
|
16
|
+
alignItems: "center",
|
|
17
|
+
},
|
|
18
|
+
title: { fontSize: 15, fontWeight: "700", color: "#111827" },
|
|
19
|
+
notes: {},
|
|
20
|
+
actionButton: {
|
|
21
|
+
backgroundColor: "#2563eb",
|
|
22
|
+
borderRadius: 10,
|
|
23
|
+
paddingVertical: 10,
|
|
24
|
+
alignItems: "center",
|
|
25
|
+
},
|
|
26
|
+
actionButtonText: { color: "#ffffff", fontSize: 14, fontWeight: "700" },
|
|
27
|
+
dismissButton: { padding: 4 },
|
|
28
|
+
dismissButtonText: { fontSize: 13, color: "#6b7280" },
|
|
29
|
+
};
|
|
30
|
+
const defaultLabels = {
|
|
31
|
+
title: (version) => `Version ${version} disponible`,
|
|
32
|
+
action: "Voir",
|
|
33
|
+
dismiss: "Fermer",
|
|
34
|
+
dismissAccessibilityLabel: "Fermer la notification de mise à jour",
|
|
35
|
+
};
|
|
36
|
+
// Dismissible banner announcing an available update: version, release notes
|
|
37
|
+
// (via `ChangelogNotes`), and an action + dismiss button. Has no opinion on
|
|
38
|
+
// what "action" does (e.g. navigate to an update screen) or on how/whether
|
|
39
|
+
// dismissal is persisted — both are left entirely to the caller.
|
|
40
|
+
export function UpdateAvailableBanner({ version, notes, onPress, onDismiss, labels, styles, }) {
|
|
41
|
+
const merged = { ...defaultStyles, ...styles };
|
|
42
|
+
const t = { ...defaultLabels, ...labels };
|
|
43
|
+
return (_jsxs(View, { style: merged.container, children: [_jsxs(View, { style: merged.header, children: [_jsx(Text, { style: merged.title, children: t.title(version) }), _jsx(Pressable, { style: merged.dismissButton, onPress: onDismiss, accessibilityRole: "button", accessibilityLabel: t.dismissAccessibilityLabel, children: _jsx(Text, { style: merged.dismissButtonText, children: t.dismiss }) })] }), notes ? _jsx(ChangelogNotes, { notes: notes, styles: merged.notes }) : null, _jsx(Pressable, { style: merged.actionButton, onPress: onPress, accessibilityRole: "button", accessibilityLabel: t.action, children: _jsx(Text, { style: merged.actionButtonText, children: t.action }) })] }));
|
|
44
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export * from "./ChangelogNotes.js";
|
|
2
|
+
export * from "./confirmDestructive.js";
|
|
3
|
+
export * from "./PhotoPicker.js";
|
|
4
|
+
export * from "./PickerModal.js";
|
|
5
|
+
export * from "./SwipeableRow.js";
|
|
6
|
+
export * from "./ThemeToggle.js";
|
|
7
|
+
export * from "./Thumbnail.js";
|
|
8
|
+
export * from "./UpdateAvailableBanner.js";
|
|
9
|
+
export * from "./useDebouncedChange.js";
|
|
10
|
+
export * from "./useEffectiveColorScheme.js";
|
|
11
|
+
export * from "./useSubmitGuard.js";
|
|
12
|
+
export * from "./useUpdateCheck.js";
|
|
13
|
+
export * from "./utils/escapeCsvField.js";
|
|
14
|
+
export * from "./utils/escapeHtml.js";
|
|
15
|
+
export * from "./utils/formatCsvNumber.js";
|
|
16
|
+
export * from "./utils/getMostRecentIds.js";
|
|
17
|
+
export * from "./utils/getPeriodStartMs.js";
|
|
18
|
+
export * from "./utils/nextInCycle.js";
|
|
19
|
+
export * from "./utils/normalizeForSearch.js";
|
|
20
|
+
export * from "./utils/rankByNameMatch.js";
|
|
21
|
+
export * from "./VoiceSearchButton.js";
|
|
22
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wBAAwB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// Convenience barrel re-exporting everything below one import path. Deep
|
|
2
|
+
// imports (`@forthtilliath/react-native-kit/PickerModal`, etc.) remain the
|
|
3
|
+
// recommended default for apps that want to avoid pulling in peer deps they
|
|
4
|
+
// don't have installed (Metro doesn't reliably tree-shake) — see the README.
|
|
5
|
+
export * from "./ChangelogNotes.js";
|
|
6
|
+
export * from "./confirmDestructive.js";
|
|
7
|
+
export * from "./PhotoPicker.js";
|
|
8
|
+
export * from "./PickerModal.js";
|
|
9
|
+
export * from "./SwipeableRow.js";
|
|
10
|
+
export * from "./ThemeToggle.js";
|
|
11
|
+
export * from "./Thumbnail.js";
|
|
12
|
+
export * from "./UpdateAvailableBanner.js";
|
|
13
|
+
export * from "./useDebouncedChange.js";
|
|
14
|
+
export * from "./useEffectiveColorScheme.js";
|
|
15
|
+
export * from "./useSubmitGuard.js";
|
|
16
|
+
export * from "./useUpdateCheck.js";
|
|
17
|
+
export * from "./utils/escapeCsvField.js";
|
|
18
|
+
export * from "./utils/escapeHtml.js";
|
|
19
|
+
export * from "./utils/formatCsvNumber.js";
|
|
20
|
+
export * from "./utils/getMostRecentIds.js";
|
|
21
|
+
export * from "./utils/getPeriodStartMs.js";
|
|
22
|
+
export * from "./utils/nextInCycle.js";
|
|
23
|
+
export * from "./utils/normalizeForSearch.js";
|
|
24
|
+
export * from "./utils/rankByNameMatch.js";
|
|
25
|
+
export * from "./VoiceSearchButton.js";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useEffectiveColorScheme.d.ts","sourceRoot":"","sources":["../src/useEffectiveColorScheme.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,eAAe,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAM1D,wBAAgB,uBAAuB,CACrC,UAAU,EAAE,eAAe,GAC1B,OAAO,GAAG,MAAM,CAIlB"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { useColorScheme } from "react-native";
|
|
2
|
+
// Resolves a user's theme preference against the device's color scheme:
|
|
3
|
+
// "system" follows the device, "light"/"dark" override it regardless of what
|
|
4
|
+
// the device is set to. `useColorScheme()` can return `null` (unknown, e.g.
|
|
5
|
+
// during the very first render on some platforms), treated as "light".
|
|
6
|
+
export function useEffectiveColorScheme(preference) {
|
|
7
|
+
const systemScheme = useColorScheme();
|
|
8
|
+
if (preference !== "system")
|
|
9
|
+
return preference;
|
|
10
|
+
return systemScheme === "dark" ? "dark" : "light";
|
|
11
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface UpdateCheckRelease {
|
|
2
|
+
version: string;
|
|
3
|
+
notes: string;
|
|
4
|
+
apkUrl: string;
|
|
5
|
+
}
|
|
6
|
+
export type UpdateCheckState = {
|
|
7
|
+
status: "idle";
|
|
8
|
+
} | {
|
|
9
|
+
status: "available";
|
|
10
|
+
release: UpdateCheckRelease;
|
|
11
|
+
} | {
|
|
12
|
+
status: "error";
|
|
13
|
+
};
|
|
14
|
+
export interface UseUpdateCheckOptions {
|
|
15
|
+
/** The app's currently installed version, e.g. from its own version string. */
|
|
16
|
+
currentVersion: string;
|
|
17
|
+
/** Fetches the latest available release, or `null` if there is none. */
|
|
18
|
+
checkForUpdate: () => Promise<UpdateCheckRelease | null>;
|
|
19
|
+
/** Positive when `a` is newer than `b`, e.g. semver comparison. */
|
|
20
|
+
compareVersions: (a: string, b: string) => number;
|
|
21
|
+
/** Read once on mount: when the last check ran, and which version (if any) the user already dismissed. */
|
|
22
|
+
getLastCheck: () => {
|
|
23
|
+
lastCheckedAt: number | null;
|
|
24
|
+
dismissedVersion: string | null;
|
|
25
|
+
};
|
|
26
|
+
/** Called after each completed check (success or failure) so the caller can persist the timestamp. */
|
|
27
|
+
onChecked: (lastCheckedAt: number) => void;
|
|
28
|
+
/** Minimum delay between two checks. Defaults to 12h. */
|
|
29
|
+
minIntervalMs?: number;
|
|
30
|
+
now?: () => number;
|
|
31
|
+
}
|
|
32
|
+
export declare function useUpdateCheck({ currentVersion, checkForUpdate, compareVersions, getLastCheck, onChecked, minIntervalMs, now, }: UseUpdateCheckOptions): UpdateCheckState & {
|
|
33
|
+
dismiss: () => void;
|
|
34
|
+
};
|
|
35
|
+
//# sourceMappingURL=useUpdateCheck.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"useUpdateCheck.d.ts","sourceRoot":"","sources":["../src/useUpdateCheck.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,MAAM,gBAAgB,GACxB;IAAE,MAAM,EAAE,MAAM,CAAA;CAAE,GAClB;IAAE,MAAM,EAAE,WAAW,CAAC;IAAC,OAAO,EAAE,kBAAkB,CAAA;CAAE,GACpD;IAAE,MAAM,EAAE,OAAO,CAAA;CAAE,CAAC;AAExB,MAAM,WAAW,qBAAqB;IACpC,+EAA+E;IAC/E,cAAc,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,cAAc,EAAE,MAAM,OAAO,CAAC,kBAAkB,GAAG,IAAI,CAAC,CAAC;IACzD,mEAAmE;IACnE,eAAe,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,0GAA0G;IAC1G,YAAY,EAAE,MAAM;QAClB,aAAa,EAAE,MAAM,GAAG,IAAI,CAAC;QAC7B,gBAAgB,EAAE,MAAM,GAAG,IAAI,CAAC;KACjC,CAAC;IACF,sGAAsG;IACtG,SAAS,EAAE,CAAC,aAAa,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3C,yDAAyD;IACzD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB;AAUD,wBAAgB,cAAc,CAAC,EAC7B,cAAc,EACd,cAAc,EACd,eAAe,EACf,YAAY,EACZ,SAAS,EACT,aAAuC,EACvC,GAAc,GACf,EAAE,qBAAqB,GAAG,gBAAgB,GAAG;IAAE,OAAO,EAAE,MAAM,IAAI,CAAA;CAAE,CAyCpE"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { useEffect, useState } from "react";
|
|
2
|
+
const DEFAULT_MIN_INTERVAL_MS = 12 * 60 * 60 * 1000;
|
|
3
|
+
// Checks once per mount (e.g. app launch) whether a newer release is
|
|
4
|
+
// available, throttled to at most one real network check per `minIntervalMs`
|
|
5
|
+
// and silent for a release the user already dismissed. Persistence of
|
|
6
|
+
// "when did we last check" / "which version did the user dismiss" is left
|
|
7
|
+
// entirely to the caller (`getLastCheck`/`onChecked`, plus its own dismiss
|
|
8
|
+
// handler) — this hook has no opinion on where that lives.
|
|
9
|
+
export function useUpdateCheck({ currentVersion, checkForUpdate, compareVersions, getLastCheck, onChecked, minIntervalMs = DEFAULT_MIN_INTERVAL_MS, now = Date.now, }) {
|
|
10
|
+
const [state, setState] = useState({ status: "idle" });
|
|
11
|
+
useEffect(() => {
|
|
12
|
+
const { lastCheckedAt, dismissedVersion } = getLastCheck();
|
|
13
|
+
if (lastCheckedAt != null && now() - lastCheckedAt < minIntervalMs)
|
|
14
|
+
return;
|
|
15
|
+
let cancelled = false;
|
|
16
|
+
checkForUpdate()
|
|
17
|
+
.then((release) => {
|
|
18
|
+
if (cancelled)
|
|
19
|
+
return;
|
|
20
|
+
onChecked(now());
|
|
21
|
+
if (release &&
|
|
22
|
+
compareVersions(release.version, currentVersion) > 0 &&
|
|
23
|
+
release.version !== dismissedVersion) {
|
|
24
|
+
setState({ status: "available", release });
|
|
25
|
+
}
|
|
26
|
+
else {
|
|
27
|
+
setState({ status: "idle" });
|
|
28
|
+
}
|
|
29
|
+
})
|
|
30
|
+
.catch(() => {
|
|
31
|
+
if (cancelled)
|
|
32
|
+
return;
|
|
33
|
+
onChecked(now());
|
|
34
|
+
setState({ status: "error" });
|
|
35
|
+
});
|
|
36
|
+
return () => {
|
|
37
|
+
cancelled = true;
|
|
38
|
+
};
|
|
39
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps, @eslint-react/exhaustive-deps -- runs once per mount by design (e.g. once per app launch); options are expected to be stable for the caller's lifetime.
|
|
40
|
+
}, []);
|
|
41
|
+
return {
|
|
42
|
+
...state,
|
|
43
|
+
dismiss: () => {
|
|
44
|
+
setState({ status: "idle" });
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forthtilliath/react-native-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
"dist"
|
|
18
18
|
],
|
|
19
19
|
"exports": {
|
|
20
|
+
".": "./dist/index.js",
|
|
20
21
|
"./*": "./dist/*.js"
|
|
21
22
|
},
|
|
22
23
|
"dependencies": {
|