@forthtilliath/react-native-kit 0.19.0 → 0.20.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 CHANGED
@@ -213,6 +213,7 @@ import { PhotoPicker } from "@forthtilliath/react-native-kit/components/picker/P
213
213
 
214
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
215
  - While `savePhoto` runs, the picker ignores presses (`saving` is `true`, `accessibilityState.busy`).
216
+ - `onOpen()` fires when a press opens the camera/library choice — the place for a haptic tick (`onOpen={tapFeedback}`).
216
217
  - 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
218
 
218
219
  ### `<PickerModal visible title items onSelect onClose />`
@@ -296,6 +297,25 @@ const repeat = usePressRepeat();
296
297
  />;
297
298
  ```
298
299
 
300
+ ### `useViewShare(options?)`
301
+
302
+ Shares as an image a view rendered only for the capture (a scoreboard, a summary card): `share()` sets `capturing` to `true`, waits two frames for the screen to mount and draw the view attached to `ref`, then calls `shareViewAsImage`. A failure shows an alert with the error's message (French or English titles, `errorTitle` / `errorMessage` to override), or goes to `onError(error)`. A second `share()` while one runs is ignored. Takes `shareViewAsImage`'s options (`dialogTitle`…).
303
+
304
+ ```tsx
305
+ import { useViewShare } from "@forthtilliath/react-native-kit/hooks/useViewShare";
306
+
307
+ const ranking = useViewShare({ dialogTitle: "Partager le classement" });
308
+
309
+ <Button title="Partager" onPress={() => void ranking.share()} />;
310
+ {
311
+ ranking.capturing ? (
312
+ <View ref={ranking.ref} collapsable={false} style={offScreen}>
313
+ <RankingCard />
314
+ </View>
315
+ ) : null;
316
+ }
317
+ ```
318
+
299
319
  ### `confirmDestructive(title, onConfirm, options?)`
300
320
 
301
321
  Generic destructive-action confirmation (title + message + Cancel/Confirm), for anything irreversible (delete, reset...). Ships with French defaults, or English ones with `{ locale: "en" }` (`message`, `cancelLabel`, `confirmLabel` all overridable).
@@ -382,20 +402,20 @@ const colors = Colors[useResolvedScheme()];
382
402
 
383
403
  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.
384
404
 
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). |
405
+ | Component | Import path | What it is |
406
+ | ----------------------------------------------------------------------- | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
407
+ | `<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. |
408
+ | `<OptionsModal visible onClose options onSelect>` | `components/picker/OptionsModal` | Centered popup listing `{ value, label }` options; a tap selects and closes. |
409
+ | `<SelectField label valueLabel options onSelect>` | `components/picker/SelectField` | Native-looking "select": captioned button + `OptionsModal` (styled through `styles.optionsModal`). |
410
+ | `<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. |
411
+ | `<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; `maxFontSizeMultiplier` caps the system font scaling of −, + and the field; screen-reader labels in French or English (`a11yLabels` to override). |
412
+ | `<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. |
413
+ | `<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. |
414
+ | `<CheckboxGroup options values onChange label?>` | `components/picker/CheckboxGroup` | Wrapping chips that each check and uncheck independently (extensions, tags…), each announced as a checkbox. |
415
+ | `<MonthDayGrid days selected onToggle>` | `components/picker/MonthDayGrid` | Monday-first month grid whose days toggle on and off (holidays, days off…); `YYYY-MM-DD` days. |
416
+ | `<AddButton label onPress>` | `components/list/AddButton` | Dashed "+ Add …" button closing a list. |
417
+ | `<DragHandle onPressIn accessibilityLabel>` | `components/list/DragHandle` | "⠿" grip for `react-native-draggable-flatlist` rows (gesture-handler's `Pressable`). |
418
+ | `<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). |
399
419
 
400
420
  `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`.
401
421
 
@@ -676,7 +696,7 @@ For all 7 components above, styling works the same way as `ChangelogNotes`: an o
676
696
  - `mergeSlotStyles(defaultStyles, styles)` — merges a component's per-slot `styles` overrides onto its defaults as `[default, override]` style arrays, so overriding one property of a slot keeps the rest of its default. Every component of this package uses it; handy for your own components following the same `styles` convention.
677
697
  - `shareFile(uri, { mimeType, dialogTitle?, locale? })` — opens the native share sheet for a local file; throws a user-facing `Error` (French, or English with `locale: "en"`) when the device can't share. Needs `expo-sharing`.
678
698
  - `shareTextFile(filename, content, options)` — writes the text to a file in the cache (replacing any previous one), then `shareFile`s it: `.ics`, `.csv`, JSON exports… Needs `expo-file-system` and `expo-sharing`.
679
- - `shareViewAsImage(ref, { dialogTitle?, locale? })` (`utils/helpers/shareViewAsImage`) — captures a view as a PNG and shares it (scoreboard, schedule…). Needs `react-native-view-shot` and `expo-sharing`; the capture library is loaded on demand, so a binary built without it only fails this call, with a user-facing `Error`.
699
+ - `shareViewAsImage(ref, { dialogTitle?, locale? })` (`utils/helpers/shareViewAsImage`) — captures a view as a PNG and shares it (scoreboard, schedule…); `ref` is a `RefObject<ComponentRef<typeof View> | null>`, as `useRef<ComponentRef<typeof View>>(null)` gives. Needs `react-native-view-shot` and `expo-sharing`; the capture library is loaded on demand, so a binary built without it only fails this call, with a user-facing `Error`.
680
700
  - `isNativeModuleAvailable(name)` — whether a native module is compiled into this binary (either architecture): check it before loading a library that crashes on import without its module.
681
701
 
682
702
  ### Calendar (`utils/calendar/`)
@@ -20,6 +20,12 @@ export interface PhotoPickerProps {
20
20
  * (`labels.saveErrorTitle` / `saveErrorMessage`).
21
21
  */
22
22
  onError?: (error: unknown) => void;
23
+ /**
24
+ * Called when a press opens the camera/library choice (not while a photo
25
+ * is being saved). The place for a haptic tick: `onOpen={tapFeedback}`
26
+ * (from `utils/helpers/haptics`).
27
+ */
28
+ onOpen?: () => void;
23
29
  labels?: PhotoPickerLabels;
24
30
  /**
25
31
  * Language of the built-in labels — French by default, or the nearest
@@ -28,5 +34,5 @@ export interface PhotoPickerProps {
28
34
  locale?: KitLocale;
29
35
  styles?: PhotoPickerStyles;
30
36
  }
31
- export declare function PhotoPicker({ photoUri, onChange, savePhoto, photoLabel, renderPreview, onError, labels, locale, styles, }: PhotoPickerProps): import("react").JSX.Element;
37
+ export declare function PhotoPicker({ photoUri, onChange, savePhoto, photoLabel, renderPreview, onError, onOpen, labels, locale, styles, }: PhotoPickerProps): import("react").JSX.Element;
32
38
  //# 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,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
+ {"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;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,IAAI,CAAC;IACpB,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,EACN,MAAM,GACP,EAAE,gBAAgB,+BAoHlB"}
@@ -13,7 +13,7 @@ const PICKER_OPTIONS = {
13
13
  aspect: [1, 1],
14
14
  };
15
15
  // Photo picker (camera or library) with a preview.
16
- export function PhotoPicker({ photoUri, onChange, savePhoto, photoLabel, renderPreview, onError, labels, locale, styles, }) {
16
+ export function PhotoPicker({ photoUri, onChange, savePhoto, photoLabel, renderPreview, onError, onOpen, labels, locale, styles, }) {
17
17
  const mergedStyles = {
18
18
  ...mergeSlotStyles(defaultStyles, styles),
19
19
  iconColor: styles?.iconColor ?? defaultStyles.iconColor,
@@ -63,6 +63,7 @@ export function PhotoPicker({ photoUri, onChange, savePhoto, photoLabel, renderP
63
63
  function handlePickPhoto() {
64
64
  if (busyRef.current)
65
65
  return;
66
+ onOpen?.();
66
67
  Alert.alert(t.photoTitle(photoLabel), undefined, [
67
68
  {
68
69
  text: t.takePhoto,
@@ -39,6 +39,12 @@ export interface QuantityStepperProps {
39
39
  * @defaultValue false
40
40
  */
41
41
  repeatOnHold?: boolean;
42
+ /**
43
+ * Caps how much the system font size scales the "−", "+" and the field, so
44
+ * the row keeps fitting at the largest accessibility font sizes (e.g. `1.3`).
45
+ * Unset, they follow the system font size without limit.
46
+ */
47
+ maxFontSizeMultiplier?: number;
42
48
  /**
43
49
  * Language of the default screen-reader labels — French by default, or the
44
50
  * nearest `KitLocaleProvider`'s.
@@ -68,5 +74,5 @@ export interface QuantityStepperProps {
68
74
  * <QuantityStepper value={score} onChange={setScore} label="Points" min={-Infinity} max={Infinity} step={4} repeatOnHold onStep={tapFeedback} />
69
75
  * ```
70
76
  */
71
- export declare function QuantityStepper({ value, onChange, label, min, max, step, onStep, longPressStep, repeatOnHold, locale, a11yLabels, styles, }: QuantityStepperProps): import("react").JSX.Element;
77
+ export declare function QuantityStepper({ value, onChange, label, min, max, step, onStep, longPressStep, repeatOnHold, maxFontSizeMultiplier, locale, a11yLabels, styles, }: QuantityStepperProps): import("react").JSX.Element;
72
78
  //# sourceMappingURL=QuantityStepper.d.ts.map
@@ -1 +1 @@
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"}
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;;;;OAIG;IACH,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;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,qBAAqB,EACrB,MAAM,EACN,UAAU,EACV,MAAM,GACP,EAAE,oBAAoB,+BAuJtB"}
@@ -37,7 +37,7 @@ function lengthOf(bound) {
37
37
  * <QuantityStepper value={score} onChange={setScore} label="Points" min={-Infinity} max={Infinity} step={4} repeatOnHold onStep={tapFeedback} />
38
38
  * ```
39
39
  */
40
- export function QuantityStepper({ value, onChange, label, min = 0, max = 999, step = 1, onStep, longPressStep, repeatOnHold = false, locale, a11yLabels, styles, }) {
40
+ export function QuantityStepper({ value, onChange, label, min = 0, max = 999, step = 1, onStep, longPressStep, repeatOnHold = false, maxFontSizeMultiplier, locale, a11yLabels, styles, }) {
41
41
  const labels = { ...defaultA11yLabels[useKitLocale(locale)], ...a11yLabels };
42
42
  const merged = mergeSlotStyles(defaultStyles, styles);
43
43
  const atMin = value <= min;
@@ -97,7 +97,7 @@ export function QuantityStepper({ value, onChange, label, min = 0, max = 999, st
97
97
  ? () => {
98
98
  startRepeat(-1);
99
99
  }
100
- : undefined, onPressOut: repeatOnHold ? repeat.stop : undefined, disabled: atMin, hitSlop: 6, accessibilityRole: "button", accessibilityLabel: labels.decrease(label), children: _jsx(Text, { style: merged.buttonText, children: "\u2212" }) }), _jsx(TextInput, { style: merged.input, value: draft ?? String(value), onChangeText: (text) => {
100
+ : undefined, onPressOut: repeatOnHold ? repeat.stop : undefined, disabled: atMin, hitSlop: 6, accessibilityRole: "button", accessibilityLabel: labels.decrease(label), children: _jsx(Text, { style: merged.buttonText, maxFontSizeMultiplier: maxFontSizeMultiplier, children: "\u2212" }) }), _jsx(TextInput, { style: merged.input, value: draft ?? String(value), onChangeText: (text) => {
101
101
  const cleaned = sanitize(text, allowsNegative);
102
102
  const typed = Number(cleaned);
103
103
  if (cleaned === "" || cleaned === "-" || typed < min) {
@@ -112,7 +112,7 @@ export function QuantityStepper({ value, onChange, label, min = 0, max = 999, st
112
112
  set(draft === "" || draft === "-" ? 0 : Number(draft));
113
113
  },
114
114
  // "number-pad" has no "-" key on Android.
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
115
+ keyboardType: allowsNegative ? "numeric" : "number-pad", selectTextOnFocus: true, maxFontSizeMultiplier: maxFontSizeMultiplier, maxLength: Math.max(lengthOf(min), lengthOf(max)), accessibilityLabel: labels.input(label) }), _jsx(Pressable, { style: [merged.button, atMax && merged.buttonDisabled], onPress: tapsOnly
116
116
  ? () => {
117
117
  tap(step);
118
118
  }
@@ -124,5 +124,5 @@ export function QuantityStepper({ value, onChange, label, min = 0, max = 999, st
124
124
  ? () => {
125
125
  startRepeat(1);
126
126
  }
127
- : undefined, onPressOut: repeatOnHold ? repeat.stop : undefined, disabled: atMax, hitSlop: 6, accessibilityRole: "button", accessibilityLabel: labels.increase(label), children: _jsx(Text, { style: merged.buttonText, children: "+" }) })] }));
127
+ : undefined, onPressOut: repeatOnHold ? repeat.stop : undefined, disabled: atMax, hitSlop: 6, accessibilityRole: "button", accessibilityLabel: labels.increase(label), children: _jsx(Text, { style: merged.buttonText, maxFontSizeMultiplier: maxFontSizeMultiplier, children: "+" }) })] }));
128
128
  }
@@ -2,4 +2,5 @@ export * from "./useDebouncedChange.js";
2
2
  export * from "./useEffectiveColorScheme.js";
3
3
  export * from "./usePressRepeat.js";
4
4
  export * from "./useSubmitGuard.js";
5
+ export * from "./useViewShare.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/hooks/index.ts"],"names":[],"mappings":"AAAA,cAAc,yBAAyB,CAAC;AACxC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,mBAAmB,CAAC"}
@@ -2,3 +2,4 @@ export * from "./useDebouncedChange.js";
2
2
  export * from "./useEffectiveColorScheme.js";
3
3
  export * from "./usePressRepeat.js";
4
4
  export * from "./useSubmitGuard.js";
5
+ export * from "./useViewShare.js";
@@ -0,0 +1,39 @@
1
+ import { type View } from "react-native";
2
+ import { type ShareViewAsImageOptions } from "../utils/helpers/shareViewAsImage.js";
3
+ export interface UseViewShareOptions extends ShareViewAsImageOptions {
4
+ /**
5
+ * Called when the capture or the share fails (capture missing from this
6
+ * binary, no share sheet…). Unset, an alert shows the error's message.
7
+ */
8
+ onError?: (error: unknown) => void;
9
+ /** Title of the default error alert. */
10
+ errorTitle?: string;
11
+ /** Message of the default error alert, for an error without a message. */
12
+ errorMessage?: string;
13
+ }
14
+ /**
15
+ * Shares as an image a view rendered on demand (a scoreboard, a summary card
16
+ * that isn't on screen): `capturing` is `true` while the capture runs — the
17
+ * screen mounts the view attached to `ref` (off screen, `collapsable={false}`)
18
+ * only meanwhile — and `share()` waits two frames for it to be drawn before
19
+ * capturing it with {@link shareViewAsImage}. A second `share()` while one is
20
+ * running is ignored.
21
+ *
22
+ * @example
23
+ * ```tsx
24
+ * const ranking = useViewShare({ dialogTitle: "Partager le classement" });
25
+ *
26
+ * <Button title="Partager" onPress={() => void ranking.share()} />
27
+ * {ranking.capturing ? (
28
+ * <View ref={ranking.ref} collapsable={false} style={offScreen}>
29
+ * <RankingCard />
30
+ * </View>
31
+ * ) : null}
32
+ * ```
33
+ */
34
+ export declare function useViewShare({ onError, errorTitle, errorMessage, ...options }?: UseViewShareOptions): {
35
+ ref: import("react").RefObject<View | null>;
36
+ capturing: boolean;
37
+ share: () => Promise<void>;
38
+ };
39
+ //# sourceMappingURL=useViewShare.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useViewShare.d.ts","sourceRoot":"","sources":["../../src/hooks/useViewShare.ts"],"names":[],"mappings":"AACA,OAAO,EAAS,KAAK,IAAI,EAAE,MAAM,cAAc,CAAC;AAGhD,OAAO,EAEL,KAAK,uBAAuB,EAC7B,MAAM,sCAAsC,CAAC;AAE9C,MAAM,WAAW,mBAAoB,SAAQ,uBAAuB;IAClE;;;OAGG;IACH,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IACnC,wCAAwC;IACxC,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,0EAA0E;IAC1E,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA2BD;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,YAAY,CAAC,EAC3B,OAAO,EACP,UAAU,EACV,YAAY,EACZ,GAAG,OAAO,EACX,GAAE,mBAAwB;;;;EA+B1B"}
@@ -0,0 +1,75 @@
1
+ import { useRef, useState } from "react";
2
+ import { Alert } from "react-native";
3
+ import { useKitLocale } from "../i18n/locale.js";
4
+ import { shareViewAsImage, } from "../utils/helpers/shareViewAsImage.js";
5
+ const defaultErrorLabels = {
6
+ fr: {
7
+ errorTitle: "Partage impossible",
8
+ errorMessage: "L'image n'a pas pu être créée.",
9
+ },
10
+ en: {
11
+ errorTitle: "Couldn't share",
12
+ errorMessage: "The image couldn't be created.",
13
+ },
14
+ };
15
+ /** Resolves once React has rendered the last state change and native drew it. */
16
+ function nextFrames() {
17
+ return new Promise((resolve) => {
18
+ requestAnimationFrame(() => {
19
+ requestAnimationFrame(() => {
20
+ resolve();
21
+ });
22
+ });
23
+ });
24
+ }
25
+ /**
26
+ * Shares as an image a view rendered on demand (a scoreboard, a summary card
27
+ * that isn't on screen): `capturing` is `true` while the capture runs — the
28
+ * screen mounts the view attached to `ref` (off screen, `collapsable={false}`)
29
+ * only meanwhile — and `share()` waits two frames for it to be drawn before
30
+ * capturing it with {@link shareViewAsImage}. A second `share()` while one is
31
+ * running is ignored.
32
+ *
33
+ * @example
34
+ * ```tsx
35
+ * const ranking = useViewShare({ dialogTitle: "Partager le classement" });
36
+ *
37
+ * <Button title="Partager" onPress={() => void ranking.share()} />
38
+ * {ranking.capturing ? (
39
+ * <View ref={ranking.ref} collapsable={false} style={offScreen}>
40
+ * <RankingCard />
41
+ * </View>
42
+ * ) : null}
43
+ * ```
44
+ */
45
+ export function useViewShare({ onError, errorTitle, errorMessage, ...options } = {}) {
46
+ const ref = useRef(null);
47
+ const [capturing, setCapturing] = useState(false);
48
+ // Ahead of `capturing`, which only changes on the next render.
49
+ const busyRef = useRef(false);
50
+ const locale = useKitLocale(options.locale);
51
+ const defaults = defaultErrorLabels[locale];
52
+ async function share() {
53
+ if (busyRef.current)
54
+ return;
55
+ busyRef.current = true;
56
+ setCapturing(true);
57
+ try {
58
+ await nextFrames();
59
+ await shareViewAsImage(ref, { ...options, locale });
60
+ }
61
+ catch (error) {
62
+ if (onError)
63
+ onError(error);
64
+ else
65
+ Alert.alert(errorTitle ?? defaults.errorTitle, error instanceof Error && error.message
66
+ ? error.message
67
+ : (errorMessage ?? defaults.errorMessage));
68
+ }
69
+ finally {
70
+ busyRef.current = false;
71
+ setCapturing(false);
72
+ }
73
+ }
74
+ return { ref, capturing, share };
75
+ }
@@ -1,4 +1,4 @@
1
- import type { RefObject } from "react";
1
+ import type { ComponentRef, RefObject } from "react";
2
2
  import type { View } from "react-native";
3
3
  import { type ShareFileOptions } from "./shareFile.js";
4
4
  export type ShareViewAsImageOptions = Omit<ShareFileOptions, "mimeType"> & {
@@ -12,10 +12,12 @@ export type ShareViewAsImageOptions = Omit<ShareFileOptions, "mimeType"> & {
12
12
  * module is known to be there: on a binary built without it, only this
13
13
  * feature fails — with a user-facing `Error` — instead of the whole screen.
14
14
  *
15
- * @param view - Ref of the view to capture (`collapsable={false}` on Android).
15
+ * @param view - Ref of the view to capture (`collapsable={false}` on Android),
16
+ * as `useRef<ComponentRef<typeof View>>(null)` gives — `View` is the
17
+ * component, not its instance type.
16
18
  * @param options - See {@link ShareViewAsImageOptions} and `shareFile`.
17
19
  * @example
18
20
  * await shareViewAsImage(rankingRef, { dialogTitle: "Partager le classement" });
19
21
  */
20
- export declare function shareViewAsImage(view: RefObject<View | null>, { captureUnavailableMessage, ...options }?: ShareViewAsImageOptions): Promise<void>;
22
+ export declare function shareViewAsImage(view: RefObject<ComponentRef<typeof View> | null>, { captureUnavailableMessage, ...options }?: ShareViewAsImageOptions): Promise<void>;
21
23
  //# sourceMappingURL=shareViewAsImage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"shareViewAsImage.d.ts","sourceRoot":"","sources":["../../../src/utils/helpers/shareViewAsImage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACvC,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAKzC,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElE,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,GAAG;IACzE,wEAAwE;IACxE,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC,CAAC;AAOF;;;;;;;;;;;GAWG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,SAAS,CAAC,IAAI,GAAG,IAAI,CAAC,EAC5B,EAAE,yBAAyB,EAAE,GAAG,OAAO,EAAE,GAAE,uBAA4B,GACtE,OAAO,CAAC,IAAI,CAAC,CAcf"}
1
+ {"version":3,"file":"shareViewAsImage.d.ts","sourceRoot":"","sources":["../../../src/utils/helpers/shareViewAsImage.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AACrD,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,cAAc,CAAC;AAKzC,OAAO,EAAa,KAAK,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAElE,MAAM,MAAM,uBAAuB,GAAG,IAAI,CAAC,gBAAgB,EAAE,UAAU,CAAC,GAAG;IACzE,wEAAwE;IACxE,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC,CAAC;AAOF;;;;;;;;;;;;;GAaG;AACH,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,SAAS,CAAC,YAAY,CAAC,OAAO,IAAI,CAAC,GAAG,IAAI,CAAC,EACjD,EAAE,yBAAyB,EAAE,GAAG,OAAO,EAAE,GAAE,uBAA4B,GACtE,OAAO,CAAC,IAAI,CAAC,CAcf"}
@@ -12,7 +12,9 @@ const defaultCaptureUnavailableMessages = {
12
12
  * module is known to be there: on a binary built without it, only this
13
13
  * feature fails — with a user-facing `Error` — instead of the whole screen.
14
14
  *
15
- * @param view - Ref of the view to capture (`collapsable={false}` on Android).
15
+ * @param view - Ref of the view to capture (`collapsable={false}` on Android),
16
+ * as `useRef<ComponentRef<typeof View>>(null)` gives — `View` is the
17
+ * component, not its instance type.
16
18
  * @param options - See {@link ShareViewAsImageOptions} and `shareFile`.
17
19
  * @example
18
20
  * await shareViewAsImage(rankingRef, { dialogTitle: "Partager le classement" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forthtilliath/react-native-kit",
3
- "version": "0.19.0",
3
+ "version": "0.20.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.14.0"
25
+ "@forthtilliath/ts-kit": "^0.15.0"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "@expo/vector-icons": ">=15.0.0",
@@ -83,7 +83,7 @@
83
83
  "@size-limit/file": "^14.1.0",
84
84
  "@types/react": "^19.2.18",
85
85
  "@types/react-test-renderer": "^19.1.0",
86
- "@vitest/coverage-v8": "5.0.2",
86
+ "@vitest/coverage-v8": "5.0.5",
87
87
  "eslint": "10.7.0",
88
88
  "expo-calendar": "~57.0.5",
89
89
  "expo-file-system": "~57.0.2",
@@ -101,10 +101,10 @@
101
101
  "react-test-renderer": "19.2.3",
102
102
  "size-limit": "^14.1.0",
103
103
  "typescript": "^5.9.3",
104
- "vitest": "^5.0.2",
105
- "@forthtilliath/expo-release-updates-ui": "^0.5.0",
104
+ "vitest": "^5.0.5",
105
+ "@forthtilliath/eslint-config": "0.10.0",
106
106
  "@forthtilliath/typescript-config": "0.1.1",
107
- "@forthtilliath/eslint-config": "0.9.0"
107
+ "@forthtilliath/expo-release-updates-ui": "^0.5.1"
108
108
  },
109
109
  "size-limit": [
110
110
  {