@forthtilliath/react-native-kit 0.18.0 → 0.19.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 +39 -15
- package/dist/components/picker/PhotoPicker.d.ts +16 -29
- package/dist/components/picker/PhotoPicker.d.ts.map +1 -1
- package/dist/components/picker/PhotoPicker.js +48 -81
- package/dist/components/picker/PhotoPicker.styles.d.ts +63 -0
- package/dist/components/picker/PhotoPicker.styles.d.ts.map +1 -0
- package/dist/components/picker/PhotoPicker.styles.js +59 -0
- package/dist/components/picker/QuantityStepper.d.ts +26 -31
- package/dist/components/picker/QuantityStepper.d.ts.map +1 -1
- package/dist/components/picker/QuantityStepper.js +37 -67
- package/dist/components/picker/QuantityStepper.styles.d.ts +59 -0
- package/dist/components/picker/QuantityStepper.styles.d.ts.map +1 -0
- package/dist/components/picker/QuantityStepper.styles.js +46 -0
- package/dist/components/settings/UpdateSettingsScreen.d.ts +5 -1
- package/dist/components/settings/UpdateSettingsScreen.d.ts.map +1 -1
- package/dist/components/settings/UpdateSettingsScreen.js +3 -3
- package/dist/components/settings/UpdateSettingsScreen.styles.d.ts +2 -0
- package/dist/components/settings/UpdateSettingsScreen.styles.d.ts.map +1 -1
- package/dist/components/theme/BackgroundPicker.d.ts +46 -0
- package/dist/components/theme/BackgroundPicker.d.ts.map +1 -0
- package/dist/components/theme/BackgroundPicker.js +79 -0
- package/dist/components/theme/findBackground.d.ts +20 -0
- package/dist/components/theme/findBackground.d.ts.map +1 -0
- package/dist/components/theme/findBackground.js +12 -0
- package/dist/components/theme/index.d.ts +2 -0
- package/dist/components/theme/index.d.ts.map +1 -1
- package/dist/components/theme/index.js +2 -0
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -211,6 +211,10 @@ import { PhotoPicker } from "@forthtilliath/react-native-kit/components/picker/P
|
|
|
211
211
|
/>;
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
+
- `renderPreview(photoUri, saving)` replaces the square preview and the "Add a photo" placeholder (e.g. a round avatar with initials and a small camera badge); the press, permission and action sheet stay the same.
|
|
215
|
+
- While `savePhoto` runs, the picker ignores presses (`saving` is `true`, `accessibilityState.busy`).
|
|
216
|
+
- A failure (camera, picker or `savePhoto` rejecting) goes to `onError(error)`; without it, an alert says the photo was not saved (`labels.saveErrorTitle` / `saveErrorMessage`).
|
|
217
|
+
|
|
214
218
|
### `<PickerModal visible title items onSelect onClose />`
|
|
215
219
|
|
|
216
220
|
Full-screen picker: search (typed or dictated via `VoiceSearchButton`), optional sections, and "add" actions always visible above the results.
|
|
@@ -336,6 +340,26 @@ import { ThemeOptionList } from "@forthtilliath/react-native-kit/components/them
|
|
|
336
340
|
<ThemeOptionList value={themePreference} onChange={setThemePreference} />;
|
|
337
341
|
```
|
|
338
342
|
|
|
343
|
+
### `<BackgroundPicker options value onChange />` + `findBackground(options, id)`
|
|
344
|
+
|
|
345
|
+
Radio group of background swatches, each drawing the page and a card in the colours on offer (`{ id, label, page, card, cardBorder? }`), the chosen one outlined with a check mark. `onChange` receives the whole option (extra fields kept: the full palette to apply). `findBackground` reads a saved id back, falling back to the first option (the original background) for an unknown id. Check each palette's text contrast in a test with `contrastRatio` from `@forthtilliath/ts-kit/color/contrastRatio`.
|
|
346
|
+
|
|
347
|
+
```tsx
|
|
348
|
+
import { BackgroundPicker } from "@forthtilliath/react-native-kit/components/theme/BackgroundPicker";
|
|
349
|
+
import { findBackground } from "@forthtilliath/react-native-kit/components/theme/findBackground";
|
|
350
|
+
|
|
351
|
+
const background = findBackground(DARK_BACKGROUNDS, settings.background);
|
|
352
|
+
|
|
353
|
+
<BackgroundPicker
|
|
354
|
+
options={DARK_BACKGROUNDS}
|
|
355
|
+
value={background.id}
|
|
356
|
+
onChange={(option) => {
|
|
357
|
+
selectFeedback();
|
|
358
|
+
saveBackground(option.id);
|
|
359
|
+
}}
|
|
360
|
+
/>;
|
|
361
|
+
```
|
|
362
|
+
|
|
339
363
|
### `<ThemePreferenceProvider load save>` + `useThemePreference()` / `useResolvedScheme()`
|
|
340
364
|
|
|
341
365
|
Holds the light/dark/system preference in a context: loaded once on mount through `load`, changed from anywhere with `useThemePreference().setPreference`, persisted through `save`. Storage-agnostic. A choice made before `load` resolves isn't overwritten by the stored value. `useResolvedScheme()` returns the scheme actually applied (`"light" | "dark"`), to pick the palette.
|
|
@@ -358,20 +382,20 @@ const colors = Colors[useResolvedScheme()];
|
|
|
358
382
|
|
|
359
383
|
Every component below has neutral default colors and a `styles` prop whose slots are each merged **after** their default (a partial override keeps the rest of the slot) — pass the app's palette there.
|
|
360
384
|
|
|
361
|
-
| Component | Import path | What it is
|
|
362
|
-
| ----------------------------------------------------------------------- | ----------------------------------- |
|
|
363
|
-
| `<BottomSheet visible onClose bottomInset?>` | `components/overlay/BottomSheet` | Modal sliding up from the bottom, content scrollable past 85 % of the screen. Pass `useSafeAreaInsets().bottom` as `bottomInset` with Android edge-to-edge.
|
|
364
|
-
| `<OptionsModal visible onClose options onSelect>` | `components/picker/OptionsModal` | Centered popup listing `{ value, label }` options; a tap selects and closes.
|
|
365
|
-
| `<SelectField label valueLabel options onSelect>` | `components/picker/SelectField` | Native-looking "select": captioned button + `OptionsModal` (styled through `styles.optionsModal`).
|
|
366
|
-
| `<SegmentedToggle options value onChange label?>` | `components/picker/SegmentedToggle` | Row of equal-width buttons, one active (Calendar / List…), announced as a radio group; each option may carry a smaller `hint` line under its label.
|
|
367
|
-
| `<QuantityStepper value onChange label min? max?>` | `components/picker/QuantityStepper` | Whole-number counter `min…max` (`0…999`, negatives allowed with a negative `min`): −/+ buttons (disabled at the bounds, `longPressStep` to jump on long press, or `repeatOnHold` to repeat faster and faster while held) and a numeric field; screen-reader labels in French or English (`a11yLabels` to override). |
|
|
368
|
-
| `<SwitchRow label description? value onChange>` | `components/settings/SwitchRow` | Settings line "label + description + switch", the whole row pressable and read as one switch; `colors` for the track and thumb.
|
|
369
|
-
| `<SettingsCard title icon?>` + `SettingsCardStatus`, `SettingsCardHint` | `components/settings/SettingsCard` | Settings card: icon + title header, then free content — status lines (small icon + text, `iconColor` per state) and a quieter hint.
|
|
370
|
-
| `<CheckboxGroup options values onChange label?>` | `components/picker/CheckboxGroup` | Wrapping chips that each check and uncheck independently (extensions, tags…), each announced as a checkbox.
|
|
371
|
-
| `<MonthDayGrid days selected onToggle>` | `components/picker/MonthDayGrid` | Monday-first month grid whose days toggle on and off (holidays, days off…); `YYYY-MM-DD` days.
|
|
372
|
-
| `<AddButton label onPress>` | `components/list/AddButton` | Dashed "+ Add …" button closing a list.
|
|
373
|
-
| `<DragHandle onPressIn accessibilityLabel>` | `components/list/DragHandle` | "⠿" grip for `react-native-draggable-flatlist` rows (gesture-handler's `Pressable`).
|
|
374
|
-
| `<ErrorFallback error retry>` | `components/feedback/ErrorFallback` | Readable crash screen with a retry button, for Expo Router's `ErrorBoundary`; texts in French or English (`labels` to override).
|
|
385
|
+
| Component | Import path | What it is |
|
|
386
|
+
| ----------------------------------------------------------------------- | ----------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
387
|
+
| `<BottomSheet visible onClose bottomInset?>` | `components/overlay/BottomSheet` | Modal sliding up from the bottom, content scrollable past 85 % of the screen. Pass `useSafeAreaInsets().bottom` as `bottomInset` with Android edge-to-edge. |
|
|
388
|
+
| `<OptionsModal visible onClose options onSelect>` | `components/picker/OptionsModal` | Centered popup listing `{ value, label }` options; a tap selects and closes. |
|
|
389
|
+
| `<SelectField label valueLabel options onSelect>` | `components/picker/SelectField` | Native-looking "select": captioned button + `OptionsModal` (styled through `styles.optionsModal`). |
|
|
390
|
+
| `<SegmentedToggle options value onChange label?>` | `components/picker/SegmentedToggle` | Row of equal-width buttons, one active (Calendar / List…), announced as a radio group; each option may carry a smaller `hint` line under its label. |
|
|
391
|
+
| `<QuantityStepper value onChange label min? max?>` | `components/picker/QuantityStepper` | Whole-number counter `min…max` (`0…999`, negatives allowed with a negative `min`, `-Infinity`/`Infinity` for no bound): −/+ buttons moving by `step` (1; disabled at the bounds, `longPressStep` to jump on long press, or `repeatOnHold` to repeat faster and faster while held) and a numeric field; `onStep(value)` fires on each −/+ change (e.g. `onStep={tapFeedback}`), not on typing; screen-reader labels in French or English (`a11yLabels` to override). |
|
|
392
|
+
| `<SwitchRow label description? value onChange>` | `components/settings/SwitchRow` | Settings line "label + description + switch", the whole row pressable and read as one switch; `colors` for the track and thumb. |
|
|
393
|
+
| `<SettingsCard title icon?>` + `SettingsCardStatus`, `SettingsCardHint` | `components/settings/SettingsCard` | Settings card: icon + title header, then free content — status lines (small icon + text, `iconColor` per state) and a quieter hint. |
|
|
394
|
+
| `<CheckboxGroup options values onChange label?>` | `components/picker/CheckboxGroup` | Wrapping chips that each check and uncheck independently (extensions, tags…), each announced as a checkbox. |
|
|
395
|
+
| `<MonthDayGrid days selected onToggle>` | `components/picker/MonthDayGrid` | Monday-first month grid whose days toggle on and off (holidays, days off…); `YYYY-MM-DD` days. |
|
|
396
|
+
| `<AddButton label onPress>` | `components/list/AddButton` | Dashed "+ Add …" button closing a list. |
|
|
397
|
+
| `<DragHandle onPressIn accessibilityLabel>` | `components/list/DragHandle` | "⠿" grip for `react-native-draggable-flatlist` rows (gesture-handler's `Pressable`). |
|
|
398
|
+
| `<ErrorFallback error retry>` | `components/feedback/ErrorFallback` | Readable crash screen with a retry button, for Expo Router's `ErrorBoundary`; texts in French or English (`labels` to override). |
|
|
375
399
|
|
|
376
400
|
`SwipeableRow` also takes `fullSwipeToDelete`: swiping past half the row deletes right away (the row slides out over a full-width delete band) instead of revealing a button — pair it with an `UndoToast`.
|
|
377
401
|
|
|
@@ -516,7 +540,7 @@ import { ThemeSettingsScreen } from "@forthtilliath/react-native-kit/components/
|
|
|
516
540
|
|
|
517
541
|
#### `<UpdateSettingsScreen currentVersion checkForUpdate compareVersions downloadAndInstallApk />`
|
|
518
542
|
|
|
519
|
-
The full "check for update" screen: installed version, a manual check button, the available-update box (changelog + install button) or an up-to-date/error message, download progress, and — if `fetchReleaseHistory` is passed — a list of past releases. Checks automatically once on mount. Renders release notes via `@forthtilliath/expo-release-updates-ui`'s `ChangelogNotes`, and pairs naturally with that package's `useUpdateCheck`/`UpdateAvailableBanner` (same `checkForUpdate`/`compareVersions` you already pass those), but takes plain functions so it isn't tied to that package's exact shape. `@forthtilliath/expo-release-updates-ui` is an optional peer dependency: install it to use this screen.
|
|
543
|
+
The full "check for update" screen: installed version, a manual check button, the available-update box (changelog + install button) or an up-to-date/error message, download progress, and — if `fetchReleaseHistory` is passed — a list of past releases. Checks automatically once on mount. Renders release notes via `@forthtilliath/expo-release-updates-ui`'s `ChangelogNotes`, and pairs naturally with that package's `useUpdateCheck`/`UpdateAvailableBanner` (same `checkForUpdate`/`compareVersions` you already pass those), but takes plain functions so it isn't tied to that package's exact shape. `@forthtilliath/expo-release-updates-ui` is an optional peer dependency: install it to use this screen. `downloadAndInstallApk(apkUrl, onProgress, apkSha256)` receives the release's `apkSha256` (from `fetchLatestRelease`): pass it on as `expectedSha256`.
|
|
520
544
|
|
|
521
545
|
```tsx
|
|
522
546
|
import { UpdateSettingsScreen } from "@forthtilliath/react-native-kit/components/settings/UpdateSettingsScreen";
|
|
@@ -1,38 +1,25 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { type ReactNode } from "react";
|
|
2
2
|
import { type KitLocale } from "../../i18n/locale.js";
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
photoPreview?: StyleProp<ImageStyle>;
|
|
6
|
-
photoPlaceholder?: StyleProp<ViewStyle>;
|
|
7
|
-
photoPlaceholderText?: StyleProp<TextStyle>;
|
|
8
|
-
clearLink?: StyleProp<TextStyle>;
|
|
9
|
-
iconColor?: string;
|
|
10
|
-
}
|
|
11
|
-
export interface PhotoPickerLabels {
|
|
12
|
-
photoTitle?: (photoLabel: string) => string;
|
|
13
|
-
editAccessibilityLabel?: (photoLabel: string) => string;
|
|
14
|
-
addAccessibilityLabel?: (photoLabel: string) => string;
|
|
15
|
-
addPlaceholderText?: string;
|
|
16
|
-
cameraPermissionTitle?: string;
|
|
17
|
-
cameraPermissionMessage?: string;
|
|
18
|
-
/**
|
|
19
|
-
* @deprecated Unused: choosing from the library needs no permission (the
|
|
20
|
-
* system photo picker), so no "access denied" alert is ever shown.
|
|
21
|
-
*/
|
|
22
|
-
libraryPermissionTitle?: string;
|
|
23
|
-
/** @deprecated Unused — see `libraryPermissionTitle`. */
|
|
24
|
-
libraryPermissionMessage?: string;
|
|
25
|
-
takePhoto?: string;
|
|
26
|
-
chooseFromLibrary?: string;
|
|
27
|
-
cancel?: string;
|
|
28
|
-
removePhoto?: string;
|
|
29
|
-
}
|
|
3
|
+
import { type PhotoPickerLabels, type PhotoPickerStyles } from "./PhotoPicker.styles.js";
|
|
4
|
+
export type { PhotoPickerLabels, PhotoPickerStyles, } from "./PhotoPicker.styles.js";
|
|
30
5
|
export interface PhotoPickerProps {
|
|
31
6
|
photoUri: string | null;
|
|
32
7
|
onChange: (uri: string | null) => void;
|
|
33
8
|
savePhoto: (sourceUri: string) => Promise<string>;
|
|
34
9
|
/** Used as-is in titles/accessibility labels, e.g. "of the container". */
|
|
35
10
|
photoLabel: string;
|
|
11
|
+
/**
|
|
12
|
+
* Replaces the square preview and the "Add a photo" placeholder, e.g. a
|
|
13
|
+
* round avatar with a small camera badge. `saving` is `true` while
|
|
14
|
+
* `savePhoto` runs (the picker ignores presses meanwhile).
|
|
15
|
+
*/
|
|
16
|
+
renderPreview?: (photoUri: string | null, saving: boolean) => ReactNode;
|
|
17
|
+
/**
|
|
18
|
+
* Called when taking, picking or saving the photo fails (`savePhoto`
|
|
19
|
+
* rejected…). Unset, an alert says the photo was not saved
|
|
20
|
+
* (`labels.saveErrorTitle` / `saveErrorMessage`).
|
|
21
|
+
*/
|
|
22
|
+
onError?: (error: unknown) => void;
|
|
36
23
|
labels?: PhotoPickerLabels;
|
|
37
24
|
/**
|
|
38
25
|
* Language of the built-in labels — French by default, or the nearest
|
|
@@ -41,5 +28,5 @@ export interface PhotoPickerProps {
|
|
|
41
28
|
locale?: KitLocale;
|
|
42
29
|
styles?: PhotoPickerStyles;
|
|
43
30
|
}
|
|
44
|
-
export declare function PhotoPicker({ photoUri, onChange, savePhoto, photoLabel, labels, locale, styles, }: PhotoPickerProps): import("react").JSX.Element;
|
|
31
|
+
export declare function PhotoPicker({ photoUri, onChange, savePhoto, photoLabel, renderPreview, onError, labels, locale, styles, }: PhotoPickerProps): import("react").JSX.Element;
|
|
45
32
|
//# sourceMappingURL=PhotoPicker.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PhotoPicker.d.ts","sourceRoot":"","sources":["../../../src/components/picker/PhotoPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,
|
|
1
|
+
{"version":3,"file":"PhotoPicker.d.ts","sourceRoot":"","sources":["../../../src/components/picker/PhotoPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,SAAS,EAAoB,MAAM,OAAO,CAAC;AAKzD,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,sBAAsB,CAAC;AAGpE,OAAO,EAGL,KAAK,iBAAiB,EACtB,KAAK,iBAAiB,EACvB,MAAM,yBAAyB,CAAC;AAEjC,YAAY,EACV,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,KAAK,IAAI,CAAC;IACvC,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAClD,0EAA0E;IAC1E,UAAU,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,aAAa,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,GAAG,IAAI,EAAE,MAAM,EAAE,OAAO,KAAK,SAAS,CAAC;IACxE;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAUD,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,aAAa,EACb,OAAO,EACP,MAAM,EACN,MAAM,EACN,MAAM,GACP,EAAE,gBAAgB,+BAmHlB"}
|
|
@@ -1,121 +1,88 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useState } from "react";
|
|
2
3
|
import { Alert, Image, Pressable, Text, View } from "react-native";
|
|
3
4
|
import { Ionicons } from "@expo/vector-icons";
|
|
4
5
|
import * as ImagePicker from "expo-image-picker";
|
|
5
6
|
import { useKitLocale } from "../../i18n/locale.js";
|
|
6
7
|
import { mergeSlotStyles } from "../../utils/helpers/mergeSlotStyles.js";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
backgroundColor: "#f9fafb",
|
|
14
|
-
},
|
|
15
|
-
photoPlaceholder: {
|
|
16
|
-
width: 140,
|
|
17
|
-
height: 140,
|
|
18
|
-
borderRadius: 16,
|
|
19
|
-
borderWidth: 1,
|
|
20
|
-
borderStyle: "dashed",
|
|
21
|
-
borderColor: "#e5e7eb",
|
|
22
|
-
backgroundColor: "#f9fafb",
|
|
23
|
-
alignItems: "center",
|
|
24
|
-
justifyContent: "center",
|
|
25
|
-
gap: 6,
|
|
26
|
-
},
|
|
27
|
-
photoPlaceholderText: { fontSize: 12, color: "#6b7280" },
|
|
28
|
-
clearLink: {
|
|
29
|
-
color: "#2563eb",
|
|
30
|
-
fontSize: 13,
|
|
31
|
-
textAlign: "center",
|
|
32
|
-
marginTop: 8,
|
|
33
|
-
},
|
|
34
|
-
iconColor: "#6b7280",
|
|
35
|
-
};
|
|
36
|
-
const defaultLabels = {
|
|
37
|
-
fr: {
|
|
38
|
-
photoTitle: (photoLabel) => `Photo ${photoLabel}`,
|
|
39
|
-
editAccessibilityLabel: (photoLabel) => `Photo ${photoLabel}. Modifier.`,
|
|
40
|
-
addAccessibilityLabel: (photoLabel) => `Ajouter une photo ${photoLabel}`,
|
|
41
|
-
addPlaceholderText: "Ajouter une photo",
|
|
42
|
-
cameraPermissionTitle: "Accès à l'appareil photo refusé",
|
|
43
|
-
cameraPermissionMessage: "Autorise l'accès dans les réglages du téléphone pour prendre une photo.",
|
|
44
|
-
takePhoto: "Prendre une photo",
|
|
45
|
-
chooseFromLibrary: "Choisir dans la galerie",
|
|
46
|
-
cancel: "Annuler",
|
|
47
|
-
removePhoto: "Retirer la photo",
|
|
48
|
-
},
|
|
49
|
-
en: {
|
|
50
|
-
photoTitle: (photoLabel) => `Photo ${photoLabel}`,
|
|
51
|
-
editAccessibilityLabel: (photoLabel) => `Photo ${photoLabel}. Edit.`,
|
|
52
|
-
addAccessibilityLabel: (photoLabel) => `Add a photo ${photoLabel}`,
|
|
53
|
-
addPlaceholderText: "Add a photo",
|
|
54
|
-
cameraPermissionTitle: "Camera access denied",
|
|
55
|
-
cameraPermissionMessage: "Allow access in the phone's settings to take a photo.",
|
|
56
|
-
takePhoto: "Take a photo",
|
|
57
|
-
chooseFromLibrary: "Choose from library",
|
|
58
|
-
cancel: "Cancel",
|
|
59
|
-
removePhoto: "Remove photo",
|
|
60
|
-
},
|
|
8
|
+
import { defaultLabels, defaultStyles, } from "./PhotoPicker.styles.js";
|
|
9
|
+
const PICKER_OPTIONS = {
|
|
10
|
+
mediaTypes: "images",
|
|
11
|
+
quality: 0.5,
|
|
12
|
+
allowsEditing: true,
|
|
13
|
+
aspect: [1, 1],
|
|
61
14
|
};
|
|
62
15
|
// Photo picker (camera or library) with a preview.
|
|
63
|
-
export function PhotoPicker({ photoUri, onChange, savePhoto, photoLabel, labels, locale, styles, }) {
|
|
16
|
+
export function PhotoPicker({ photoUri, onChange, savePhoto, photoLabel, renderPreview, onError, labels, locale, styles, }) {
|
|
64
17
|
const mergedStyles = {
|
|
65
18
|
...mergeSlotStyles(defaultStyles, styles),
|
|
66
19
|
iconColor: styles?.iconColor ?? defaultStyles.iconColor,
|
|
67
20
|
};
|
|
68
21
|
const t = { ...defaultLabels[useKitLocale(locale)], ...labels };
|
|
69
|
-
|
|
22
|
+
const [saving, setSaving] = useState(false);
|
|
23
|
+
// Ahead of `saving`, which only changes on the next render: a second press
|
|
24
|
+
// in the same frame must not start a second save.
|
|
25
|
+
const busyRef = useRef(false);
|
|
26
|
+
async function pick(launch) {
|
|
27
|
+
if (busyRef.current)
|
|
28
|
+
return;
|
|
29
|
+
busyRef.current = true;
|
|
30
|
+
try {
|
|
31
|
+
const result = await launch();
|
|
32
|
+
const asset = result && !result.canceled ? result.assets[0] : undefined;
|
|
33
|
+
if (!asset)
|
|
34
|
+
return;
|
|
35
|
+
setSaving(true);
|
|
36
|
+
onChange(await savePhoto(asset.uri));
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (onError)
|
|
40
|
+
onError(error);
|
|
41
|
+
else
|
|
42
|
+
Alert.alert(t.saveErrorTitle, t.saveErrorMessage);
|
|
43
|
+
}
|
|
44
|
+
finally {
|
|
45
|
+
busyRef.current = false;
|
|
46
|
+
setSaving(false);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function launchCamera() {
|
|
70
50
|
const permission = await ImagePicker.requestCameraPermissionsAsync();
|
|
71
51
|
if (!permission.granted) {
|
|
72
52
|
Alert.alert(t.cameraPermissionTitle, t.cameraPermissionMessage);
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
const result = await ImagePicker.launchCameraAsync({
|
|
76
|
-
mediaTypes: "images",
|
|
77
|
-
quality: 0.5,
|
|
78
|
-
allowsEditing: true,
|
|
79
|
-
aspect: [1, 1],
|
|
80
|
-
});
|
|
81
|
-
if (!result.canceled && result.assets[0]) {
|
|
82
|
-
onChange(await savePhoto(result.assets[0].uri));
|
|
53
|
+
return null;
|
|
83
54
|
}
|
|
55
|
+
return ImagePicker.launchCameraAsync(PICKER_OPTIONS);
|
|
84
56
|
}
|
|
85
57
|
// No permission request: the library is opened through the system photo
|
|
86
58
|
// picker, which needs none — so apps don't have to keep the storage
|
|
87
59
|
// permissions just for this.
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
mediaTypes: "images",
|
|
91
|
-
quality: 0.5,
|
|
92
|
-
allowsEditing: true,
|
|
93
|
-
aspect: [1, 1],
|
|
94
|
-
});
|
|
95
|
-
if (!result.canceled && result.assets[0]) {
|
|
96
|
-
onChange(await savePhoto(result.assets[0].uri));
|
|
97
|
-
}
|
|
60
|
+
function launchLibrary() {
|
|
61
|
+
return ImagePicker.launchImageLibraryAsync(PICKER_OPTIONS);
|
|
98
62
|
}
|
|
99
63
|
function handlePickPhoto() {
|
|
64
|
+
if (busyRef.current)
|
|
65
|
+
return;
|
|
100
66
|
Alert.alert(t.photoTitle(photoLabel), undefined, [
|
|
101
67
|
{
|
|
102
68
|
text: t.takePhoto,
|
|
103
69
|
onPress: () => {
|
|
104
|
-
void
|
|
70
|
+
void pick(launchCamera);
|
|
105
71
|
},
|
|
106
72
|
},
|
|
107
73
|
{
|
|
108
74
|
text: t.chooseFromLibrary,
|
|
109
75
|
onPress: () => {
|
|
110
|
-
void
|
|
76
|
+
void pick(launchLibrary);
|
|
111
77
|
},
|
|
112
78
|
},
|
|
113
79
|
{ text: t.cancel, style: "cancel" },
|
|
114
80
|
]);
|
|
115
81
|
}
|
|
116
|
-
|
|
82
|
+
const defaultPreview = photoUri ? (_jsx(Image, { source: { uri: photoUri }, style: mergedStyles.photoPreview, accessibilityIgnoresInvertColors: true })) : (_jsxs(View, { style: mergedStyles.photoPlaceholder, children: [_jsx(Ionicons, { name: "camera-outline", size: 28, color: mergedStyles.iconColor }), _jsx(Text, { style: mergedStyles.photoPlaceholderText, children: t.addPlaceholderText })] }));
|
|
83
|
+
return (_jsxs(_Fragment, { children: [_jsx(Pressable, { style: mergedStyles.photoPicker, onPress: handlePickPhoto, disabled: saving, accessibilityRole: "button", accessibilityState: { busy: saving, disabled: saving }, accessibilityLabel: photoUri
|
|
117
84
|
? t.editAccessibilityLabel(photoLabel)
|
|
118
|
-
: t.addAccessibilityLabel(photoLabel), children:
|
|
85
|
+
: t.addAccessibilityLabel(photoLabel), children: renderPreview ? renderPreview(photoUri, saving) : defaultPreview }), photoUri ? (_jsx(Pressable, { onPress: () => {
|
|
119
86
|
onChange(null);
|
|
120
|
-
}, accessibilityRole: "button", accessibilityLabel: t.removePhoto, children: _jsx(Text, { style: mergedStyles.clearLink, children: t.removePhoto }) })) : null] }));
|
|
87
|
+
}, disabled: saving, accessibilityRole: "button", accessibilityLabel: t.removePhoto, children: _jsx(Text, { style: mergedStyles.clearLink, children: t.removePhoto }) })) : null] }));
|
|
121
88
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { ImageStyle, StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
2
|
+
export interface PhotoPickerStyles {
|
|
3
|
+
photoPicker?: StyleProp<ViewStyle>;
|
|
4
|
+
photoPreview?: StyleProp<ImageStyle>;
|
|
5
|
+
photoPlaceholder?: StyleProp<ViewStyle>;
|
|
6
|
+
photoPlaceholderText?: StyleProp<TextStyle>;
|
|
7
|
+
clearLink?: StyleProp<TextStyle>;
|
|
8
|
+
iconColor?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface PhotoPickerLabels {
|
|
11
|
+
photoTitle?: (photoLabel: string) => string;
|
|
12
|
+
editAccessibilityLabel?: (photoLabel: string) => string;
|
|
13
|
+
addAccessibilityLabel?: (photoLabel: string) => string;
|
|
14
|
+
addPlaceholderText?: string;
|
|
15
|
+
cameraPermissionTitle?: string;
|
|
16
|
+
cameraPermissionMessage?: string;
|
|
17
|
+
/**
|
|
18
|
+
* @deprecated Unused: choosing from the library needs no permission (the
|
|
19
|
+
* system photo picker), so no "access denied" alert is ever shown.
|
|
20
|
+
*/
|
|
21
|
+
libraryPermissionTitle?: string;
|
|
22
|
+
/** @deprecated Unused — see `libraryPermissionTitle`. */
|
|
23
|
+
libraryPermissionMessage?: string;
|
|
24
|
+
takePhoto?: string;
|
|
25
|
+
chooseFromLibrary?: string;
|
|
26
|
+
cancel?: string;
|
|
27
|
+
removePhoto?: string;
|
|
28
|
+
/** Alert shown when the photo could not be taken or saved (no `onError`). */
|
|
29
|
+
saveErrorTitle?: string;
|
|
30
|
+
saveErrorMessage?: string;
|
|
31
|
+
}
|
|
32
|
+
export declare const defaultStyles: Required<PhotoPickerStyles>;
|
|
33
|
+
export declare const defaultLabels: {
|
|
34
|
+
fr: {
|
|
35
|
+
photoTitle: (photoLabel: string) => string;
|
|
36
|
+
editAccessibilityLabel: (photoLabel: string) => string;
|
|
37
|
+
addAccessibilityLabel: (photoLabel: string) => string;
|
|
38
|
+
addPlaceholderText: string;
|
|
39
|
+
cameraPermissionTitle: string;
|
|
40
|
+
cameraPermissionMessage: string;
|
|
41
|
+
takePhoto: string;
|
|
42
|
+
chooseFromLibrary: string;
|
|
43
|
+
cancel: string;
|
|
44
|
+
removePhoto: string;
|
|
45
|
+
saveErrorTitle: string;
|
|
46
|
+
saveErrorMessage: string;
|
|
47
|
+
};
|
|
48
|
+
en: {
|
|
49
|
+
photoTitle: (photoLabel: string) => string;
|
|
50
|
+
editAccessibilityLabel: (photoLabel: string) => string;
|
|
51
|
+
addAccessibilityLabel: (photoLabel: string) => string;
|
|
52
|
+
addPlaceholderText: string;
|
|
53
|
+
cameraPermissionTitle: string;
|
|
54
|
+
cameraPermissionMessage: string;
|
|
55
|
+
takePhoto: string;
|
|
56
|
+
chooseFromLibrary: string;
|
|
57
|
+
cancel: string;
|
|
58
|
+
removePhoto: string;
|
|
59
|
+
saveErrorTitle: string;
|
|
60
|
+
saveErrorMessage: string;
|
|
61
|
+
};
|
|
62
|
+
};
|
|
63
|
+
//# sourceMappingURL=PhotoPicker.styles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"PhotoPicker.styles.d.ts","sourceRoot":"","sources":["../../../src/components/picker/PhotoPicker.styles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAIhF,MAAM,WAAW,iBAAiB;IAChC,WAAW,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACnC,YAAY,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACrC,gBAAgB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACxC,oBAAoB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC5C,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5C,sBAAsB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IACxD,qBAAqB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IACvD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,uBAAuB,CAAC,EAAE,MAAM,CAAC;IACjC;;;OAGG;IACH,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,yDAAyD;IACzD,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,iBAAiB,CA4BrD,CAAC;AAEF,eAAO,MAAM,aAAa;;iCAEG,MAAM;6CACM,MAAM;4CAEP,MAAM;;;;;;;;;;;;iCAcjB,MAAM;6CACM,MAAM;4CAEP,MAAM;;;;;;;;;;;CAoB7C,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export const defaultStyles = {
|
|
2
|
+
photoPicker: { alignSelf: "center", marginTop: 8 },
|
|
3
|
+
photoPreview: {
|
|
4
|
+
width: 140,
|
|
5
|
+
height: 140,
|
|
6
|
+
borderRadius: 16,
|
|
7
|
+
backgroundColor: "#f9fafb",
|
|
8
|
+
},
|
|
9
|
+
photoPlaceholder: {
|
|
10
|
+
width: 140,
|
|
11
|
+
height: 140,
|
|
12
|
+
borderRadius: 16,
|
|
13
|
+
borderWidth: 1,
|
|
14
|
+
borderStyle: "dashed",
|
|
15
|
+
borderColor: "#e5e7eb",
|
|
16
|
+
backgroundColor: "#f9fafb",
|
|
17
|
+
alignItems: "center",
|
|
18
|
+
justifyContent: "center",
|
|
19
|
+
gap: 6,
|
|
20
|
+
},
|
|
21
|
+
photoPlaceholderText: { fontSize: 12, color: "#6b7280" },
|
|
22
|
+
clearLink: {
|
|
23
|
+
color: "#2563eb",
|
|
24
|
+
fontSize: 13,
|
|
25
|
+
textAlign: "center",
|
|
26
|
+
marginTop: 8,
|
|
27
|
+
},
|
|
28
|
+
iconColor: "#6b7280",
|
|
29
|
+
};
|
|
30
|
+
export const defaultLabels = {
|
|
31
|
+
fr: {
|
|
32
|
+
photoTitle: (photoLabel) => `Photo ${photoLabel}`,
|
|
33
|
+
editAccessibilityLabel: (photoLabel) => `Photo ${photoLabel}. Modifier.`,
|
|
34
|
+
addAccessibilityLabel: (photoLabel) => `Ajouter une photo ${photoLabel}`,
|
|
35
|
+
addPlaceholderText: "Ajouter une photo",
|
|
36
|
+
cameraPermissionTitle: "Accès à l'appareil photo refusé",
|
|
37
|
+
cameraPermissionMessage: "Autorise l'accès dans les réglages du téléphone pour prendre une photo.",
|
|
38
|
+
takePhoto: "Prendre une photo",
|
|
39
|
+
chooseFromLibrary: "Choisir dans la galerie",
|
|
40
|
+
cancel: "Annuler",
|
|
41
|
+
removePhoto: "Retirer la photo",
|
|
42
|
+
saveErrorTitle: "Photo non enregistrée",
|
|
43
|
+
saveErrorMessage: "La photo n'a pas pu être enregistrée. Réessaie.",
|
|
44
|
+
},
|
|
45
|
+
en: {
|
|
46
|
+
photoTitle: (photoLabel) => `Photo ${photoLabel}`,
|
|
47
|
+
editAccessibilityLabel: (photoLabel) => `Photo ${photoLabel}. Edit.`,
|
|
48
|
+
addAccessibilityLabel: (photoLabel) => `Add a photo ${photoLabel}`,
|
|
49
|
+
addPlaceholderText: "Add a photo",
|
|
50
|
+
cameraPermissionTitle: "Camera access denied",
|
|
51
|
+
cameraPermissionMessage: "Allow access in the phone's settings to take a photo.",
|
|
52
|
+
takePhoto: "Take a photo",
|
|
53
|
+
chooseFromLibrary: "Choose from library",
|
|
54
|
+
cancel: "Cancel",
|
|
55
|
+
removePhoto: "Remove photo",
|
|
56
|
+
saveErrorTitle: "Photo not saved",
|
|
57
|
+
saveErrorMessage: "The photo could not be saved. Try again.",
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -1,24 +1,6 @@
|
|
|
1
|
-
import type { StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
2
1
|
import { type KitLocale } from "../../i18n/locale.js";
|
|
3
|
-
|
|
4
|
-
export
|
|
5
|
-
/** The row: − button, field, + button. */
|
|
6
|
-
row?: StyleProp<ViewStyle>;
|
|
7
|
-
/** Both round buttons. */
|
|
8
|
-
button?: StyleProp<ViewStyle>;
|
|
9
|
-
/** Added on top of `button` for "−" at `min` and "+" at `max`. */
|
|
10
|
-
buttonDisabled?: StyleProp<ViewStyle>;
|
|
11
|
-
/** The "−" / "+" glyphs. */
|
|
12
|
-
buttonText?: StyleProp<TextStyle>;
|
|
13
|
-
/** The numeric field in the middle (its text color included). */
|
|
14
|
-
input?: StyleProp<TextStyle>;
|
|
15
|
-
}
|
|
16
|
-
/** Screen-reader labels of the three controls, built from the stepper's `label`. */
|
|
17
|
-
export interface QuantityStepperA11yLabels {
|
|
18
|
-
decrease: (label: string) => string;
|
|
19
|
-
increase: (label: string) => string;
|
|
20
|
-
input: (label: string) => string;
|
|
21
|
-
}
|
|
2
|
+
import { type QuantityStepperA11yLabels, type QuantityStepperStyles } from "./QuantityStepper.styles.js";
|
|
3
|
+
export type { QuantityStepperA11yLabels, QuantityStepperStyles, } from "./QuantityStepper.styles.js";
|
|
22
4
|
export interface QuantityStepperProps {
|
|
23
5
|
value: number;
|
|
24
6
|
/** Receives the new value, already clamped to `min…max`. */
|
|
@@ -27,12 +9,24 @@ export interface QuantityStepperProps {
|
|
|
27
9
|
label: string;
|
|
28
10
|
/**
|
|
29
11
|
* Below 0, negative values are allowed: the field then accepts a leading
|
|
30
|
-
* "-" (and opens the keyboard that has one).
|
|
12
|
+
* "-" (and opens the keyboard that has one). `-Infinity`: no lower bound.
|
|
31
13
|
* @defaultValue 0
|
|
32
14
|
*/
|
|
33
15
|
min?: number;
|
|
34
|
-
/** @defaultValue 999 */
|
|
16
|
+
/** `Infinity`: no upper bound. @defaultValue 999 */
|
|
35
17
|
max?: number;
|
|
18
|
+
/**
|
|
19
|
+
* How much −/+ (and each repeat while held) move the value, e.g. `4` for a
|
|
20
|
+
* category worth 4 points a unit. Typing in the field is not snapped to it.
|
|
21
|
+
* @defaultValue 1
|
|
22
|
+
*/
|
|
23
|
+
step?: number;
|
|
24
|
+
/**
|
|
25
|
+
* Called with the new value each time −/+ change it (tap, long press, each
|
|
26
|
+
* repeat while held) — not on typing. The place for a haptic tick:
|
|
27
|
+
* `onStep={tapFeedback}` (from `utils/helpers/haptics`).
|
|
28
|
+
*/
|
|
29
|
+
onStep?: (value: number) => void;
|
|
36
30
|
/**
|
|
37
31
|
* Step applied by a long press on −/+, to reach large values quickly.
|
|
38
32
|
* Unset, a long press does nothing more than a tap. Ignored with
|
|
@@ -56,13 +50,14 @@ export interface QuantityStepperProps {
|
|
|
56
50
|
styles?: QuantityStepperStyles;
|
|
57
51
|
}
|
|
58
52
|
/**
|
|
59
|
-
* A whole-number counter from `min` (0) to `max`: −/+ buttons to adjust
|
|
60
|
-
* (optionally faster on long press, or repeating while held), and a
|
|
61
|
-
* field in the middle to type a large number in one go (other
|
|
62
|
-
* dropped, the result is clamped). A typed value that is not a
|
|
63
|
-
* below `min` (an empty field, a lone "-", the "2" of "25" with
|
|
64
|
-
* stays as typed until the field loses focus. A negative `min`
|
|
65
|
-
* negative values (penalties, defeat tokens…)
|
|
53
|
+
* A whole-number counter from `min` (0) to `max`: −/+ buttons to adjust by
|
|
54
|
+
* `step` (optionally faster on long press, or repeating while held), and a
|
|
55
|
+
* numeric field in the middle to type a large number in one go (other
|
|
56
|
+
* characters are dropped, the result is clamped). A typed value that is not a
|
|
57
|
+
* number yet or below `min` (an empty field, a lone "-", the "2" of "25" with
|
|
58
|
+
* `min={10}`) stays as typed until the field loses focus. A negative `min`
|
|
59
|
+
* allows negative values (penalties, defeat tokens…); `-Infinity`/`Infinity`
|
|
60
|
+
* remove a bound.
|
|
66
61
|
*
|
|
67
62
|
* @example
|
|
68
63
|
* ```tsx
|
|
@@ -70,8 +65,8 @@ export interface QuantityStepperProps {
|
|
|
70
65
|
*
|
|
71
66
|
* <QuantityStepper value={count} onChange={setCount} label="Croissants" />
|
|
72
67
|
* <QuantityStepper value={bans} onChange={setBans} label="Bannissements" min={1} max={40} longPressStep={5} />
|
|
73
|
-
* <QuantityStepper value={score} onChange={setScore} label="Points" min={-
|
|
68
|
+
* <QuantityStepper value={score} onChange={setScore} label="Points" min={-Infinity} max={Infinity} step={4} repeatOnHold onStep={tapFeedback} />
|
|
74
69
|
* ```
|
|
75
70
|
*/
|
|
76
|
-
export declare function QuantityStepper({ value, onChange, label, min, max, longPressStep, repeatOnHold, locale, a11yLabels, styles, }: QuantityStepperProps): import("react").JSX.Element;
|
|
71
|
+
export declare function QuantityStepper({ value, onChange, label, min, max, step, onStep, longPressStep, repeatOnHold, locale, a11yLabels, styles, }: QuantityStepperProps): import("react").JSX.Element;
|
|
77
72
|
//# sourceMappingURL=QuantityStepper.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"QuantityStepper.d.ts","sourceRoot":"","sources":["../../../src/components/picker/QuantityStepper.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"QuantityStepper.d.ts","sourceRoot":"","sources":["../../../src/components/picker/QuantityStepper.tsx"],"names":[],"mappings":"AAIA,OAAO,EAAE,KAAK,SAAS,EAAgB,MAAM,sBAAsB,CAAC;AAGpE,OAAO,EAGL,KAAK,yBAAyB,EAC9B,KAAK,qBAAqB,EAC3B,MAAM,6BAA6B,CAAC;AAErC,YAAY,EACV,yBAAyB,EACzB,qBAAqB,GACtB,MAAM,6BAA6B,CAAC;AAErC,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,MAAM,CAAC;IACd,4DAA4D;IAC5D,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,gEAAgE;IAChE,KAAK,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,oDAAoD;IACpD,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;;;OAIG;IACH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACjC;;;;OAIG;IACH,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,yDAAyD;IACzD,UAAU,CAAC,EAAE,OAAO,CAAC,yBAAyB,CAAC,CAAC;IAChD,oEAAoE;IACpE,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AAgBD;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,KAAK,EACL,GAAO,EACP,GAAS,EACT,IAAQ,EACR,MAAM,EACN,aAAa,EACb,YAAoB,EACpB,MAAM,EACN,UAAU,EACV,MAAM,GACP,EAAE,oBAAoB,+BA4ItB"}
|
|
@@ -4,52 +4,7 @@ import { Pressable, Text, TextInput, View } from "react-native";
|
|
|
4
4
|
import { usePressRepeat } from "../../hooks/usePressRepeat.js";
|
|
5
5
|
import { useKitLocale } from "../../i18n/locale.js";
|
|
6
6
|
import { mergeSlotStyles } from "../../utils/helpers/mergeSlotStyles.js";
|
|
7
|
-
|
|
8
|
-
fr: {
|
|
9
|
-
decrease: (label) => `Retirer : ${label}`,
|
|
10
|
-
increase: (label) => `Ajouter : ${label}`,
|
|
11
|
-
input: (label) => `Quantité : ${label}`,
|
|
12
|
-
},
|
|
13
|
-
en: {
|
|
14
|
-
decrease: (label) => `Decrease: ${label}`,
|
|
15
|
-
increase: (label) => `Increase: ${label}`,
|
|
16
|
-
input: (label) => `Quantity: ${label}`,
|
|
17
|
-
},
|
|
18
|
-
};
|
|
19
|
-
const defaultStyles = {
|
|
20
|
-
row: {
|
|
21
|
-
flexDirection: "row",
|
|
22
|
-
alignItems: "center",
|
|
23
|
-
gap: 6,
|
|
24
|
-
},
|
|
25
|
-
button: {
|
|
26
|
-
width: 40,
|
|
27
|
-
height: 40,
|
|
28
|
-
borderRadius: 20,
|
|
29
|
-
backgroundColor: "#2563eb",
|
|
30
|
-
alignItems: "center",
|
|
31
|
-
justifyContent: "center",
|
|
32
|
-
},
|
|
33
|
-
buttonDisabled: { opacity: 0.35 },
|
|
34
|
-
buttonText: {
|
|
35
|
-
color: "#ffffff",
|
|
36
|
-
fontSize: 22,
|
|
37
|
-
fontWeight: "700",
|
|
38
|
-
lineHeight: 26,
|
|
39
|
-
},
|
|
40
|
-
input: {
|
|
41
|
-
width: 52,
|
|
42
|
-
height: 40,
|
|
43
|
-
borderRadius: 8,
|
|
44
|
-
borderWidth: 1,
|
|
45
|
-
borderColor: "#d4d4d4",
|
|
46
|
-
color: "#111111",
|
|
47
|
-
fontSize: 18,
|
|
48
|
-
fontWeight: "700",
|
|
49
|
-
textAlign: "center",
|
|
50
|
-
padding: 0,
|
|
51
|
-
},
|
|
52
|
-
};
|
|
7
|
+
import { defaultA11yLabels, defaultStyles, } from "./QuantityStepper.styles.js";
|
|
53
8
|
/** Keeps the digits, and a leading "-" when negatives are allowed. */
|
|
54
9
|
function sanitize(text, allowsNegative) {
|
|
55
10
|
const digits = text.replace(/\D/g, "");
|
|
@@ -57,14 +12,21 @@ function sanitize(text, allowsNegative) {
|
|
|
57
12
|
? `-${digits}`
|
|
58
13
|
: digits;
|
|
59
14
|
}
|
|
15
|
+
/** Characters a bound takes in the field; 15 digits (+ "-") if unbounded. */
|
|
16
|
+
function lengthOf(bound) {
|
|
17
|
+
if (Number.isFinite(bound))
|
|
18
|
+
return String(bound).length;
|
|
19
|
+
return bound < 0 ? 16 : 15;
|
|
20
|
+
}
|
|
60
21
|
/**
|
|
61
|
-
* A whole-number counter from `min` (0) to `max`: −/+ buttons to adjust
|
|
62
|
-
* (optionally faster on long press, or repeating while held), and a
|
|
63
|
-
* field in the middle to type a large number in one go (other
|
|
64
|
-
* dropped, the result is clamped). A typed value that is not a
|
|
65
|
-
* below `min` (an empty field, a lone "-", the "2" of "25" with
|
|
66
|
-
* stays as typed until the field loses focus. A negative `min`
|
|
67
|
-
* negative values (penalties, defeat tokens…)
|
|
22
|
+
* A whole-number counter from `min` (0) to `max`: −/+ buttons to adjust by
|
|
23
|
+
* `step` (optionally faster on long press, or repeating while held), and a
|
|
24
|
+
* numeric field in the middle to type a large number in one go (other
|
|
25
|
+
* characters are dropped, the result is clamped). A typed value that is not a
|
|
26
|
+
* number yet or below `min` (an empty field, a lone "-", the "2" of "25" with
|
|
27
|
+
* `min={10}`) stays as typed until the field loses focus. A negative `min`
|
|
28
|
+
* allows negative values (penalties, defeat tokens…); `-Infinity`/`Infinity`
|
|
29
|
+
* remove a bound.
|
|
68
30
|
*
|
|
69
31
|
* @example
|
|
70
32
|
* ```tsx
|
|
@@ -72,10 +34,10 @@ function sanitize(text, allowsNegative) {
|
|
|
72
34
|
*
|
|
73
35
|
* <QuantityStepper value={count} onChange={setCount} label="Croissants" />
|
|
74
36
|
* <QuantityStepper value={bans} onChange={setBans} label="Bannissements" min={1} max={40} longPressStep={5} />
|
|
75
|
-
* <QuantityStepper value={score} onChange={setScore} label="Points" min={-
|
|
37
|
+
* <QuantityStepper value={score} onChange={setScore} label="Points" min={-Infinity} max={Infinity} step={4} repeatOnHold onStep={tapFeedback} />
|
|
76
38
|
* ```
|
|
77
39
|
*/
|
|
78
|
-
export function QuantityStepper({ value, onChange, label, min = 0, max = 999, longPressStep, repeatOnHold = false, locale, a11yLabels, styles, }) {
|
|
40
|
+
export function QuantityStepper({ value, onChange, label, min = 0, max = 999, step = 1, onStep, longPressStep, repeatOnHold = false, locale, a11yLabels, styles, }) {
|
|
79
41
|
const labels = { ...defaultA11yLabels[useKitLocale(locale)], ...a11yLabels };
|
|
80
42
|
const merged = mergeSlotStyles(defaultStyles, styles);
|
|
81
43
|
const atMin = value <= min;
|
|
@@ -96,32 +58,40 @@ export function QuantityStepper({ value, onChange, label, min = 0, max = 999, lo
|
|
|
96
58
|
setDraft(null);
|
|
97
59
|
onChange(clamp(next));
|
|
98
60
|
}
|
|
61
|
+
/** A −/+ press: moves by `delta`; `false` when a bound stopped it. */
|
|
99
62
|
function stepBy(delta) {
|
|
100
63
|
const next = clamp(valueRef.current + delta);
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
repeat.stop();
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
64
|
+
if (next === valueRef.current)
|
|
65
|
+
return false;
|
|
106
66
|
valueRef.current = next;
|
|
107
67
|
set(next);
|
|
68
|
+
onStep?.(next);
|
|
69
|
+
return true;
|
|
108
70
|
}
|
|
109
|
-
function startRepeat(
|
|
71
|
+
function startRepeat(direction) {
|
|
110
72
|
repeat.start(() => {
|
|
111
|
-
|
|
73
|
+
// A button disabled at its bound may never report the release.
|
|
74
|
+
if (!stepBy(direction * step))
|
|
75
|
+
repeat.stop();
|
|
112
76
|
});
|
|
113
77
|
}
|
|
114
78
|
// Held: repeat from press-in to press-out. Otherwise: one step per tap,
|
|
115
79
|
// and `longPressStep` on long press.
|
|
116
80
|
const tapsOnly = !repeatOnHold;
|
|
117
81
|
const jumps = tapsOnly && longPressStep !== undefined;
|
|
82
|
+
// A tap starts from the rendered value, even if the parent ignored the
|
|
83
|
+
// previous onChange.
|
|
84
|
+
function tap(delta) {
|
|
85
|
+
valueRef.current = value;
|
|
86
|
+
stepBy(delta);
|
|
87
|
+
}
|
|
118
88
|
return (_jsxs(View, { style: merged.row, children: [_jsx(Pressable, { style: [merged.button, atMin && merged.buttonDisabled], onPress: tapsOnly
|
|
119
89
|
? () => {
|
|
120
|
-
|
|
90
|
+
tap(-step);
|
|
121
91
|
}
|
|
122
92
|
: undefined, onLongPress: jumps
|
|
123
93
|
? () => {
|
|
124
|
-
|
|
94
|
+
tap(-longPressStep);
|
|
125
95
|
}
|
|
126
96
|
: undefined, onPressIn: repeatOnHold
|
|
127
97
|
? () => {
|
|
@@ -142,13 +112,13 @@ export function QuantityStepper({ value, onChange, label, min = 0, max = 999, lo
|
|
|
142
112
|
set(draft === "" || draft === "-" ? 0 : Number(draft));
|
|
143
113
|
},
|
|
144
114
|
// "number-pad" has no "-" key on Android.
|
|
145
|
-
keyboardType: allowsNegative ? "numeric" : "number-pad", selectTextOnFocus: true, maxLength: Math.max(
|
|
115
|
+
keyboardType: allowsNegative ? "numeric" : "number-pad", selectTextOnFocus: true, maxLength: Math.max(lengthOf(min), lengthOf(max)), accessibilityLabel: labels.input(label) }), _jsx(Pressable, { style: [merged.button, atMax && merged.buttonDisabled], onPress: tapsOnly
|
|
146
116
|
? () => {
|
|
147
|
-
|
|
117
|
+
tap(step);
|
|
148
118
|
}
|
|
149
119
|
: undefined, onLongPress: jumps
|
|
150
120
|
? () => {
|
|
151
|
-
|
|
121
|
+
tap(longPressStep);
|
|
152
122
|
}
|
|
153
123
|
: undefined, onPressIn: repeatOnHold
|
|
154
124
|
? () => {
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
2
|
+
import type { KitLocale } from "../../i18n/locale.js";
|
|
3
|
+
/** Per-slot style overrides for {@link QuantityStepper}, each merged after its default. */
|
|
4
|
+
export interface QuantityStepperStyles {
|
|
5
|
+
/** The row: − button, field, + button. */
|
|
6
|
+
row?: StyleProp<ViewStyle>;
|
|
7
|
+
/** Both round buttons. */
|
|
8
|
+
button?: StyleProp<ViewStyle>;
|
|
9
|
+
/** Added on top of `button` for "−" at `min` and "+" at `max`. */
|
|
10
|
+
buttonDisabled?: StyleProp<ViewStyle>;
|
|
11
|
+
/** The "−" / "+" glyphs. */
|
|
12
|
+
buttonText?: StyleProp<TextStyle>;
|
|
13
|
+
/** The numeric field in the middle (its text color included). */
|
|
14
|
+
input?: StyleProp<TextStyle>;
|
|
15
|
+
}
|
|
16
|
+
/** Screen-reader labels of the three controls, built from the stepper's `label`. */
|
|
17
|
+
export interface QuantityStepperA11yLabels {
|
|
18
|
+
decrease: (label: string) => string;
|
|
19
|
+
increase: (label: string) => string;
|
|
20
|
+
input: (label: string) => string;
|
|
21
|
+
}
|
|
22
|
+
export declare const defaultA11yLabels: Record<KitLocale, QuantityStepperA11yLabels>;
|
|
23
|
+
export declare const defaultStyles: {
|
|
24
|
+
row: {
|
|
25
|
+
flexDirection: "row";
|
|
26
|
+
alignItems: "center";
|
|
27
|
+
gap: number;
|
|
28
|
+
};
|
|
29
|
+
button: {
|
|
30
|
+
width: number;
|
|
31
|
+
height: number;
|
|
32
|
+
borderRadius: number;
|
|
33
|
+
backgroundColor: string;
|
|
34
|
+
alignItems: "center";
|
|
35
|
+
justifyContent: "center";
|
|
36
|
+
};
|
|
37
|
+
buttonDisabled: {
|
|
38
|
+
opacity: number;
|
|
39
|
+
};
|
|
40
|
+
buttonText: {
|
|
41
|
+
color: string;
|
|
42
|
+
fontSize: number;
|
|
43
|
+
fontWeight: "700";
|
|
44
|
+
lineHeight: number;
|
|
45
|
+
};
|
|
46
|
+
input: {
|
|
47
|
+
width: number;
|
|
48
|
+
height: number;
|
|
49
|
+
borderRadius: number;
|
|
50
|
+
borderWidth: number;
|
|
51
|
+
borderColor: string;
|
|
52
|
+
color: string;
|
|
53
|
+
fontSize: number;
|
|
54
|
+
fontWeight: "700";
|
|
55
|
+
textAlign: "center";
|
|
56
|
+
padding: number;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
//# sourceMappingURL=QuantityStepper.styles.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QuantityStepper.styles.d.ts","sourceRoot":"","sources":["../../../src/components/picker/QuantityStepper.styles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAEtD,2FAA2F;AAC3F,MAAM,WAAW,qBAAqB;IACpC,0CAA0C;IAC1C,GAAG,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC3B,0BAA0B;IAC1B,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,kEAAkE;IAClE,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,4BAA4B;IAC5B,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,iEAAiE;IACjE,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B;AAED,oFAAoF;AACpF,MAAM,WAAW,yBAAyB;IACxC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,QAAQ,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,KAAK,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;CAClC;AAED,eAAO,MAAM,iBAAiB,EAAE,MAAM,CAAC,SAAS,EAAE,yBAAyB,CAW1E,CAAC;AAEF,eAAO,MAAM,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAiCzB,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export const defaultA11yLabels = {
|
|
2
|
+
fr: {
|
|
3
|
+
decrease: (label) => `Retirer : ${label}`,
|
|
4
|
+
increase: (label) => `Ajouter : ${label}`,
|
|
5
|
+
input: (label) => `Quantité : ${label}`,
|
|
6
|
+
},
|
|
7
|
+
en: {
|
|
8
|
+
decrease: (label) => `Decrease: ${label}`,
|
|
9
|
+
increase: (label) => `Increase: ${label}`,
|
|
10
|
+
input: (label) => `Quantity: ${label}`,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
export const defaultStyles = {
|
|
14
|
+
row: {
|
|
15
|
+
flexDirection: "row",
|
|
16
|
+
alignItems: "center",
|
|
17
|
+
gap: 6,
|
|
18
|
+
},
|
|
19
|
+
button: {
|
|
20
|
+
width: 40,
|
|
21
|
+
height: 40,
|
|
22
|
+
borderRadius: 20,
|
|
23
|
+
backgroundColor: "#2563eb",
|
|
24
|
+
alignItems: "center",
|
|
25
|
+
justifyContent: "center",
|
|
26
|
+
},
|
|
27
|
+
buttonDisabled: { opacity: 0.35 },
|
|
28
|
+
buttonText: {
|
|
29
|
+
color: "#ffffff",
|
|
30
|
+
fontSize: 22,
|
|
31
|
+
fontWeight: "700",
|
|
32
|
+
lineHeight: 26,
|
|
33
|
+
},
|
|
34
|
+
input: {
|
|
35
|
+
width: 52,
|
|
36
|
+
height: 40,
|
|
37
|
+
borderRadius: 8,
|
|
38
|
+
borderWidth: 1,
|
|
39
|
+
borderColor: "#d4d4d4",
|
|
40
|
+
color: "#111111",
|
|
41
|
+
fontSize: 18,
|
|
42
|
+
fontWeight: "700",
|
|
43
|
+
textAlign: "center",
|
|
44
|
+
padding: 0,
|
|
45
|
+
},
|
|
46
|
+
};
|
|
@@ -7,7 +7,11 @@ export interface UpdateSettingsScreenProps {
|
|
|
7
7
|
checkForUpdate: () => Promise<UpdateSettingsScreenRelease | null>;
|
|
8
8
|
/** Positive when `a` is newer than `b`, e.g. semver comparison. */
|
|
9
9
|
compareVersions: (a: string, b: string) => number;
|
|
10
|
-
|
|
10
|
+
/**
|
|
11
|
+
* Receives the release's `apkSha256`: pass it on as
|
|
12
|
+
* `expectedSha256` so a swapped or damaged APK is never installed.
|
|
13
|
+
*/
|
|
14
|
+
downloadAndInstallApk: (apkUrl: string, onProgress?: (fraction: number) => void, apkSha256?: string) => Promise<void>;
|
|
11
15
|
/** Omit to not show a version history section at all. */
|
|
12
16
|
fetchReleaseHistory?: () => Promise<UpdateSettingsScreenHistoryEntry[]>;
|
|
13
17
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UpdateSettingsScreen.d.ts","sourceRoot":"","sources":["../../../src/components/settings/UpdateSettingsScreen.tsx"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,SAAS,EAEf,MAAM,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EACV,gCAAgC,EAChC,0BAA0B,EAC1B,2BAA2B,EAC3B,0BAA0B,EAC3B,MAAM,kCAAkC,CAAC;AAI1C,YAAY,EACV,gCAAgC,EAChC,0BAA0B,EAC1B,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,kCAAkC,CAAC;AAU1C,MAAM,WAAW,yBAAyB;IACxC,cAAc,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,cAAc,EAAE,MAAM,OAAO,CAAC,2BAA2B,GAAG,IAAI,CAAC,CAAC;IAClE,mEAAmE;IACnE,eAAe,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD,qBAAqB,EAAE,CACrB,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,
|
|
1
|
+
{"version":3,"file":"UpdateSettingsScreen.d.ts","sourceRoot":"","sources":["../../../src/components/settings/UpdateSettingsScreen.tsx"],"names":[],"mappings":"AAKA,OAAO,EAEL,KAAK,SAAS,EAEf,MAAM,sBAAsB,CAAC;AAG9B,OAAO,KAAK,EACV,gCAAgC,EAChC,0BAA0B,EAC1B,2BAA2B,EAC3B,0BAA0B,EAC3B,MAAM,kCAAkC,CAAC;AAI1C,YAAY,EACV,gCAAgC,EAChC,0BAA0B,EAC1B,2BAA2B,EAC3B,0BAA0B,GAC3B,MAAM,kCAAkC,CAAC;AAU1C,MAAM,WAAW,yBAAyB;IACxC,cAAc,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,cAAc,EAAE,MAAM,OAAO,CAAC,2BAA2B,GAAG,IAAI,CAAC,CAAC;IAClE,mEAAmE;IACnE,eAAe,EAAE,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD;;;OAGG;IACH,qBAAqB,EAAE,CACrB,MAAM,EAAE,MAAM,EACd,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,EACvC,SAAS,CAAC,EAAE,MAAM,KACf,OAAO,CAAC,IAAI,CAAC,CAAC;IACnB,yDAAyD;IACzD,mBAAmB,CAAC,EAAE,MAAM,OAAO,CAAC,gCAAgC,EAAE,CAAC,CAAC;IACxE;;;OAGG;IACH,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,0BAA0B,CAAC;IACpC;;;OAGG;IACH,MAAM,CAAC,EAAE,SAAS,CAAC;IACnB,MAAM,CAAC,EAAE,0BAA0B,CAAC;CACrC;AAQD,wBAAgB,oBAAoB,CAAC,EACnC,cAAc,EACd,cAAc,EACd,eAAe,EACf,qBAAqB,EACrB,mBAAmB,EACnB,UAAU,EAAE,gBAAgB,EAC5B,MAAM,EACN,MAAM,EACN,MAAM,GACP,EAAE,yBAAyB,+BA8J3B"}
|
|
@@ -52,12 +52,12 @@ export function UpdateSettingsScreen({ currentVersion, checkForUpdate, compareVe
|
|
|
52
52
|
void handleCheckForUpdate();
|
|
53
53
|
// eslint-disable-next-line @eslint-react/exhaustive-deps -- runs once on mount by design; handleCheckForUpdate is expected to be stable enough for the screen's lifetime.
|
|
54
54
|
}, []);
|
|
55
|
-
async function handleInstall(apkUrl) {
|
|
55
|
+
async function handleInstall({ apkUrl, apkSha256, }) {
|
|
56
56
|
setUpdateState({ status: "downloading", progress: 0 });
|
|
57
57
|
try {
|
|
58
58
|
await downloadAndInstallApk(apkUrl, (progress) => {
|
|
59
59
|
setUpdateState({ status: "downloading", progress });
|
|
60
|
-
});
|
|
60
|
+
}, apkSha256);
|
|
61
61
|
setUpdateState({ status: "idle" });
|
|
62
62
|
}
|
|
63
63
|
catch {
|
|
@@ -74,6 +74,6 @@ export function UpdateSettingsScreen({ currentVersion, checkForUpdate, compareVe
|
|
|
74
74
|
updateState.status !== "downloading" && (_jsx(Pressable, { style: [merged.button, isBusy && merged.buttonDisabled], onPress: () => {
|
|
75
75
|
void handleCheckForUpdate();
|
|
76
76
|
}, disabled: isBusy, accessibilityRole: "button", accessibilityLabel: t.checkButton, children: updateState.status === "checking" ? (_jsx(ActivityIndicator, { color: activityIndicatorColor })) : (_jsx(Text, { style: merged.buttonText, children: t.checkButton })) })), updateState.status === "up-to-date" && (_jsx(Text, { style: merged.helpText, children: t.upToDate })), updateState.status === "error" && (_jsx(Text, { style: merged.errorText, children: updateState.message })), updateState.status === "available" && (_jsxs(View, { style: merged.updateAvailableBox, children: [_jsx(Text, { style: merged.updateAvailableTitle, children: t.availableTitle(updateState.release.version) }), updateState.release.notes ? (_jsx(ChangelogNotes, { notes: updateState.release.notes, styles: merged.notes })) : null, _jsx(Pressable, { style: merged.button, onPress: () => {
|
|
77
|
-
void handleInstall(updateState.release
|
|
77
|
+
void handleInstall(updateState.release);
|
|
78
78
|
}, accessibilityRole: "button", accessibilityLabel: t.installButtonAccessibilityLabel(updateState.release.version), children: _jsx(Text, { style: merged.buttonText, children: t.installButton(updateState.release.version) }) })] })), updateState.status === "downloading" && (_jsxs(View, { style: merged.updateAvailableBox, children: [_jsx(Text, { style: merged.helpText, children: t.downloadingLabel(Math.round(updateState.progress * 100)) }), _jsx(Text, { style: merged.helpText, children: t.downloadingHint })] })), releaseHistory && releaseHistory.length > 0 && (_jsx(UpdateSettingsScreenHistory, { releaseHistory: releaseHistory, historyTitle: t.historyTitle, dateLocale: dateLocale, notesStyles: notesStyles, styles: merged }))] }));
|
|
79
79
|
}
|
|
@@ -4,6 +4,8 @@ export interface UpdateSettingsScreenRelease {
|
|
|
4
4
|
version: string;
|
|
5
5
|
notes: string;
|
|
6
6
|
apkUrl: string;
|
|
7
|
+
/** SHA-256 of the APK, handed back to `downloadAndInstallApk`. */
|
|
8
|
+
apkSha256?: string;
|
|
7
9
|
}
|
|
8
10
|
export interface UpdateSettingsScreenHistoryEntry {
|
|
9
11
|
version: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"UpdateSettingsScreen.styles.d.ts","sourceRoot":"","sources":["../../../src/components/settings/UpdateSettingsScreen.styles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAC;AAInF,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"UpdateSettingsScreen.styles.d.ts","sourceRoot":"","sources":["../../../src/components/settings/UpdateSettingsScreen.styles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,wCAAwC,CAAC;AAInF,MAAM,WAAW,2BAA2B;IAC1C,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,kEAAkE;IAClE,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,gCAAgC;IAC/C,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CAC7B;AAED,MAAM,WAAW,0BAA0B;IACzC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,cAAc,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAC7C,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAC5C,+BAA+B,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAC9D,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IAC/C,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/B,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,QAAQ,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAChC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,kBAAkB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1C,oBAAoB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC5C,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,oBAAoB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC5C,gBAAgB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACxC,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACrC,KAAK,CAAC,EAAE,oBAAoB,CAAC;CAC9B;AAED,eAAO,MAAM,aAAa;;;;;;;kCAOI,MAAM;;mDAEW,MAAM;oCAErB,MAAM;;;;;;;;;;kCAYR,MAAM;;mDAEW,MAAM;oCAErB,MAAM;;;;CAI6B,CAAC;AAEpE,eAAO,MAAM,aAAa,EAAE,QAAQ,CAClC,IAAI,CAAC,0BAA0B,EAAE,wBAAwB,GAAG,OAAO,CAAC,CACrE,GACC,IAAI,CAAC,0BAA0B,EAAE,wBAAwB,GAAG,OAAO,CA8DpE,CAAC"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { StyleProp, TextStyle, ViewStyle } from "react-native";
|
|
2
|
+
import type { BackgroundOption } from "./findBackground.js";
|
|
3
|
+
export type { BackgroundOption } from "./findBackground.js";
|
|
4
|
+
export interface BackgroundPickerStyles {
|
|
5
|
+
/** The wrapping row of swatches. */
|
|
6
|
+
row?: StyleProp<ViewStyle>;
|
|
7
|
+
/** One option: swatch + label. */
|
|
8
|
+
option?: StyleProp<ViewStyle>;
|
|
9
|
+
swatch?: StyleProp<ViewStyle>;
|
|
10
|
+
/** Added on top of `swatch` for the chosen one. */
|
|
11
|
+
swatchSelected?: StyleProp<ViewStyle>;
|
|
12
|
+
card?: StyleProp<ViewStyle>;
|
|
13
|
+
/** The round badge holding the check mark. */
|
|
14
|
+
check?: StyleProp<ViewStyle>;
|
|
15
|
+
label?: StyleProp<TextStyle>;
|
|
16
|
+
labelSelected?: StyleProp<TextStyle>;
|
|
17
|
+
checkIconColor?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface BackgroundPickerProps<T extends BackgroundOption> {
|
|
20
|
+
options: readonly T[];
|
|
21
|
+
/** Id of the chosen option. */
|
|
22
|
+
value: string;
|
|
23
|
+
/** Receives the chosen option — add a haptic tick here if wanted. */
|
|
24
|
+
onChange: (option: T) => void;
|
|
25
|
+
styles?: BackgroundPickerStyles;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Radio group of background swatches: each one draws the page and a card in
|
|
29
|
+
* the colours on offer, the chosen one outlined with a check mark. Pair it
|
|
30
|
+
* with `findBackground` (`./findBackground`) to read the saved id, and check each palette's
|
|
31
|
+
* contrast with `contrastRatio` from `@forthtilliath/ts-kit/color`.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```tsx
|
|
35
|
+
* <BackgroundPicker
|
|
36
|
+
* options={[
|
|
37
|
+
* { id: "night", label: "Bleu nuit", page: "#0f172a", card: "#1e293b" },
|
|
38
|
+
* { id: "felt", label: "Tapis vert", page: "#0b1f15", card: "#112c1e" },
|
|
39
|
+
* ]}
|
|
40
|
+
* value={settings.background}
|
|
41
|
+
* onChange={(option) => { selectFeedback(); save(option.id); }}
|
|
42
|
+
* />
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function BackgroundPicker<T extends BackgroundOption>({ options, value, onChange, styles, }: BackgroundPickerProps<T>): import("react").JSX.Element;
|
|
46
|
+
//# sourceMappingURL=BackgroundPicker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BackgroundPicker.d.ts","sourceRoot":"","sources":["../../../src/components/theme/BackgroundPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAMpE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,YAAY,EAAE,gBAAgB,EAAE,MAAM,qBAAqB,CAAC;AAE5D,MAAM,WAAW,sBAAsB;IACrC,oCAAoC;IACpC,GAAG,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC3B,kCAAkC;IAClC,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,mDAAmD;IACnD,cAAc,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACtC,IAAI,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC5B,8CAA8C;IAC9C,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACrC,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAED,MAAM,WAAW,qBAAqB,CAAC,CAAC,SAAS,gBAAgB;IAC/D,OAAO,EAAE,SAAS,CAAC,EAAE,CAAC;IACtB,+BAA+B;IAC/B,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,QAAQ,EAAE,CAAC,MAAM,EAAE,CAAC,KAAK,IAAI,CAAC;IAC9B,MAAM,CAAC,EAAE,sBAAsB,CAAC;CACjC;AAwCD;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,gBAAgB,CAAC,CAAC,SAAS,gBAAgB,EAAE,EAC3D,OAAO,EACP,KAAK,EACL,QAAQ,EACR,MAAM,GACP,EAAE,qBAAqB,CAAC,CAAC,CAAC,+BAoD1B"}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Pressable, Text, View } from "react-native";
|
|
3
|
+
import { Ionicons } from "@expo/vector-icons";
|
|
4
|
+
import { mergeSlotStyles } from "../../utils/helpers/mergeSlotStyles.js";
|
|
5
|
+
const defaultStyles = {
|
|
6
|
+
row: { flexDirection: "row", flexWrap: "wrap", gap: 8 },
|
|
7
|
+
option: {
|
|
8
|
+
flexGrow: 1,
|
|
9
|
+
flexBasis: "22%",
|
|
10
|
+
alignItems: "center",
|
|
11
|
+
gap: 4,
|
|
12
|
+
},
|
|
13
|
+
swatch: {
|
|
14
|
+
width: "100%",
|
|
15
|
+
aspectRatio: 1,
|
|
16
|
+
borderRadius: 12,
|
|
17
|
+
borderWidth: 2,
|
|
18
|
+
borderColor: "#d4d4d4",
|
|
19
|
+
padding: 8,
|
|
20
|
+
justifyContent: "flex-end",
|
|
21
|
+
},
|
|
22
|
+
swatchSelected: { borderColor: "#2563eb" },
|
|
23
|
+
card: { height: "45%", borderRadius: 6, borderWidth: 1 },
|
|
24
|
+
check: {
|
|
25
|
+
position: "absolute",
|
|
26
|
+
top: 4,
|
|
27
|
+
right: 4,
|
|
28
|
+
width: 22,
|
|
29
|
+
height: 22,
|
|
30
|
+
borderRadius: 11,
|
|
31
|
+
backgroundColor: "#2563eb",
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
justifyContent: "center",
|
|
34
|
+
},
|
|
35
|
+
label: {
|
|
36
|
+
color: "#6b7280",
|
|
37
|
+
fontSize: 13,
|
|
38
|
+
fontWeight: "600",
|
|
39
|
+
},
|
|
40
|
+
labelSelected: { color: "#111827" },
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Radio group of background swatches: each one draws the page and a card in
|
|
44
|
+
* the colours on offer, the chosen one outlined with a check mark. Pair it
|
|
45
|
+
* with `findBackground` (`./findBackground`) to read the saved id, and check each palette's
|
|
46
|
+
* contrast with `contrastRatio` from `@forthtilliath/ts-kit/color`.
|
|
47
|
+
*
|
|
48
|
+
* @example
|
|
49
|
+
* ```tsx
|
|
50
|
+
* <BackgroundPicker
|
|
51
|
+
* options={[
|
|
52
|
+
* { id: "night", label: "Bleu nuit", page: "#0f172a", card: "#1e293b" },
|
|
53
|
+
* { id: "felt", label: "Tapis vert", page: "#0b1f15", card: "#112c1e" },
|
|
54
|
+
* ]}
|
|
55
|
+
* value={settings.background}
|
|
56
|
+
* onChange={(option) => { selectFeedback(); save(option.id); }}
|
|
57
|
+
* />
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function BackgroundPicker({ options, value, onChange, styles, }) {
|
|
61
|
+
const merged = mergeSlotStyles(defaultStyles, styles);
|
|
62
|
+
const checkIconColor = styles?.checkIconColor ?? "#ffffff";
|
|
63
|
+
return (_jsx(View, { style: merged.row, accessibilityRole: "radiogroup", children: options.map((option) => {
|
|
64
|
+
const selected = option.id === value;
|
|
65
|
+
return (_jsxs(Pressable, { style: merged.option, onPress: () => {
|
|
66
|
+
onChange(option);
|
|
67
|
+
}, accessibilityRole: "radio", accessibilityState: { checked: selected }, accessibilityLabel: option.label, children: [_jsxs(View, { style: [
|
|
68
|
+
merged.swatch,
|
|
69
|
+
{ backgroundColor: option.page },
|
|
70
|
+
selected && merged.swatchSelected,
|
|
71
|
+
], children: [_jsx(View, { style: [
|
|
72
|
+
merged.card,
|
|
73
|
+
{
|
|
74
|
+
backgroundColor: option.card,
|
|
75
|
+
borderColor: option.cardBorder ?? option.card,
|
|
76
|
+
},
|
|
77
|
+
] }), selected && (_jsx(View, { style: merged.check, children: _jsx(Ionicons, { name: "checkmark", size: 14, color: checkIconColor }) }))] }), _jsx(Text, { style: [merged.label, selected && merged.labelSelected], numberOfLines: 1, children: option.label })] }, option.id));
|
|
78
|
+
}) }));
|
|
79
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** One background on offer to `BackgroundPicker`, drawn as a page with a card on it. */
|
|
2
|
+
export interface BackgroundOption {
|
|
3
|
+
id: string;
|
|
4
|
+
/** Shown under the swatch and read by screen readers. */
|
|
5
|
+
label: string;
|
|
6
|
+
/** The page colour. */
|
|
7
|
+
page: string;
|
|
8
|
+
/** The card drawn on the page. */
|
|
9
|
+
card: string;
|
|
10
|
+
/** The card's border. Defaults to `card`. */
|
|
11
|
+
cardBorder?: string;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The option with this id, or the first one (the original background) if the
|
|
15
|
+
* id is unknown — a background removed since it was saved, or none saved yet.
|
|
16
|
+
*
|
|
17
|
+
* @throws {RangeError} If `options` is empty.
|
|
18
|
+
*/
|
|
19
|
+
export declare function findBackground<T extends BackgroundOption>(options: readonly T[], id: string | null | undefined): T;
|
|
20
|
+
//# sourceMappingURL=findBackground.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"findBackground.d.ts","sourceRoot":"","sources":["../../../src/components/theme/findBackground.ts"],"names":[],"mappings":"AAAA,wFAAwF;AACxF,MAAM,WAAW,gBAAgB;IAC/B,EAAE,EAAE,MAAM,CAAC;IACX,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,uBAAuB;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,6CAA6C;IAC7C,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAAC,CAAC,SAAS,gBAAgB,EACvD,OAAO,EAAE,SAAS,CAAC,EAAE,EACrB,EAAE,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,GAC5B,CAAC,CAIH"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The option with this id, or the first one (the original background) if the
|
|
3
|
+
* id is unknown — a background removed since it was saved, or none saved yet.
|
|
4
|
+
*
|
|
5
|
+
* @throws {RangeError} If `options` is empty.
|
|
6
|
+
*/
|
|
7
|
+
export function findBackground(options, id) {
|
|
8
|
+
const found = options.find((option) => option.id === id) ?? options[0];
|
|
9
|
+
if (!found)
|
|
10
|
+
throw new RangeError("findBackground needs at least one option");
|
|
11
|
+
return found;
|
|
12
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/theme/index.ts"],"names":[],"mappings":"AAAA,cAAc,sBAAsB,CAAC;AACrC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kBAAkB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/components/theme/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,cAAc,qBAAqB,CAAC;AACpC,cAAc,sBAAsB,CAAC;AACrC,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,kBAAkB,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forthtilliath/react-native-kit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.19.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
"./*": "./dist/*.js"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
-
"@forthtilliath/ts-kit": "^0.
|
|
25
|
+
"@forthtilliath/ts-kit": "^0.14.0"
|
|
26
26
|
},
|
|
27
27
|
"peerDependencies": {
|
|
28
28
|
"@expo/vector-icons": ">=15.0.0",
|
|
@@ -102,15 +102,15 @@
|
|
|
102
102
|
"size-limit": "^14.1.0",
|
|
103
103
|
"typescript": "^5.9.3",
|
|
104
104
|
"vitest": "^5.0.2",
|
|
105
|
-
"@forthtilliath/
|
|
106
|
-
"@forthtilliath/
|
|
107
|
-
"@forthtilliath/
|
|
105
|
+
"@forthtilliath/expo-release-updates-ui": "^0.5.0",
|
|
106
|
+
"@forthtilliath/typescript-config": "0.1.1",
|
|
107
|
+
"@forthtilliath/eslint-config": "0.9.0"
|
|
108
108
|
},
|
|
109
109
|
"size-limit": [
|
|
110
110
|
{
|
|
111
111
|
"name": "dist (all modules, brotli)",
|
|
112
112
|
"path": "dist/**/*.js",
|
|
113
|
-
"limit": "
|
|
113
|
+
"limit": "47 KB"
|
|
114
114
|
}
|
|
115
115
|
],
|
|
116
116
|
"scripts": {
|