@forthtilliath/react-native-kit 0.13.0 → 0.14.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/README.md +18 -116
  2. package/dist/components/index.d.ts +0 -1
  3. package/dist/components/index.d.ts.map +1 -1
  4. package/dist/components/index.js +0 -1
  5. package/dist/components/picker/PickerModal.d.ts +2 -29
  6. package/dist/components/picker/PickerModal.d.ts.map +1 -1
  7. package/dist/components/picker/PickerModal.js +10 -112
  8. package/dist/components/picker/PickerModal.styles.d.ts +32 -0
  9. package/dist/components/picker/PickerModal.styles.d.ts.map +1 -0
  10. package/dist/components/picker/PickerModal.styles.js +82 -0
  11. package/dist/components/picker/PickerModalRow.d.ts +17 -0
  12. package/dist/components/picker/PickerModalRow.d.ts.map +1 -0
  13. package/dist/components/picker/PickerModalRow.js +11 -0
  14. package/dist/components/picker/usePickerModalSections.d.ts +7 -0
  15. package/dist/components/picker/usePickerModalSections.d.ts.map +1 -0
  16. package/dist/components/picker/usePickerModalSections.js +28 -0
  17. package/dist/components/settings/UpdateSettingsScreen.d.ts +2 -46
  18. package/dist/components/settings/UpdateSettingsScreen.d.ts.map +1 -1
  19. package/dist/components/settings/UpdateSettingsScreen.js +4 -78
  20. package/dist/components/settings/UpdateSettingsScreen.styles.d.ts +49 -0
  21. package/dist/components/settings/UpdateSettingsScreen.styles.d.ts.map +1 -0
  22. package/dist/components/settings/UpdateSettingsScreen.styles.js +76 -0
  23. package/dist/components/settings/UpdateSettingsScreenHistory.d.ts +20 -0
  24. package/dist/components/settings/UpdateSettingsScreenHistory.d.ts.map +1 -0
  25. package/dist/components/settings/UpdateSettingsScreenHistory.js +10 -0
  26. package/dist/hooks/index.d.ts +0 -1
  27. package/dist/hooks/index.d.ts.map +1 -1
  28. package/dist/hooks/index.js +0 -1
  29. package/dist/index.d.ts +8 -11
  30. package/dist/index.d.ts.map +1 -1
  31. package/dist/index.js +10 -11
  32. package/dist/utils/helpers/index.d.ts +1 -6
  33. package/dist/utils/helpers/index.d.ts.map +1 -1
  34. package/dist/utils/helpers/index.js +1 -6
  35. package/dist/utils/index.d.ts +0 -1
  36. package/dist/utils/index.d.ts.map +1 -1
  37. package/dist/utils/index.js +0 -1
  38. package/package.json +4 -3
  39. package/dist/components/update/ChangelogNotes.d.ts +0 -22
  40. package/dist/components/update/ChangelogNotes.d.ts.map +0 -1
  41. package/dist/components/update/ChangelogNotes.js +0 -51
  42. package/dist/components/update/UpdateAvailableBanner.d.ts +0 -29
  43. package/dist/components/update/UpdateAvailableBanner.d.ts.map +0 -1
  44. package/dist/components/update/UpdateAvailableBanner.js +0 -63
  45. package/dist/components/update/index.d.ts +0 -3
  46. package/dist/components/update/index.d.ts.map +0 -1
  47. package/dist/components/update/index.js +0 -2
  48. package/dist/hooks/useUpdateCheck.d.ts +0 -35
  49. package/dist/hooks/useUpdateCheck.d.ts.map +0 -1
  50. package/dist/hooks/useUpdateCheck.js +0 -47
  51. package/dist/utils/format/escapeCsvField.d.ts +0 -2
  52. package/dist/utils/format/escapeCsvField.d.ts.map +0 -1
  53. package/dist/utils/format/escapeCsvField.js +0 -8
  54. package/dist/utils/format/escapeHtml.d.ts +0 -2
  55. package/dist/utils/format/escapeHtml.d.ts.map +0 -1
  56. package/dist/utils/format/escapeHtml.js +0 -9
  57. package/dist/utils/format/formatCsvNumber.d.ts +0 -2
  58. package/dist/utils/format/formatCsvNumber.d.ts.map +0 -1
  59. package/dist/utils/format/formatCsvNumber.js +0 -6
  60. package/dist/utils/format/index.d.ts +0 -4
  61. package/dist/utils/format/index.d.ts.map +0 -1
  62. package/dist/utils/format/index.js +0 -3
  63. package/dist/utils/helpers/getMostRecentIds.d.ts +0 -6
  64. package/dist/utils/helpers/getMostRecentIds.d.ts.map +0 -1
  65. package/dist/utils/helpers/getMostRecentIds.js +0 -19
  66. package/dist/utils/helpers/getPeriodStartMs.d.ts +0 -3
  67. package/dist/utils/helpers/getPeriodStartMs.d.ts.map +0 -1
  68. package/dist/utils/helpers/getPeriodStartMs.js +0 -14
  69. package/dist/utils/helpers/nextInCycle.d.ts +0 -2
  70. package/dist/utils/helpers/nextInCycle.d.ts.map +0 -1
  71. package/dist/utils/helpers/nextInCycle.js +0 -9
  72. package/dist/utils/helpers/normalizeForSearch.d.ts +0 -2
  73. package/dist/utils/helpers/normalizeForSearch.d.ts.map +0 -1
  74. package/dist/utils/helpers/normalizeForSearch.js +0 -15
  75. package/dist/utils/helpers/rankByNameMatch.d.ts +0 -2
  76. package/dist/utils/helpers/rankByNameMatch.d.ts.map +0 -1
  77. package/dist/utils/helpers/rankByNameMatch.js +0 -33
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @forthtilliath/react-native-kit
2
2
 
3
- Small React Native building blocks — components, hooks, and framework-agnostic utils — with no opinion on your app's theme, distributed one file per export (grouped under `components/{picker,theme,update,list,settings}/`, `hooks/`, `utils/{format,helpers}/`) so consumers only pull in what they use.
3
+ Small React Native building blocks — components, hooks, and a framework-agnostic util — with no opinion on your app's theme, distributed one file per export (grouped under `components/{picker,theme,list,settings}/`, `hooks/`, `utils/helpers/`) so consumers only pull in what they use. Update-checking UI (`ChangelogNotes`, `UpdateAvailableBanner`, `useUpdateCheck`) lives in the sibling `@forthtilliath/expo-release-updates-ui` package instead.
4
4
 
5
5
  ## Install
6
6
 
@@ -25,7 +25,7 @@ Or, from within this monorepo, as a workspace dependency:
25
25
  Each component is its own module — import the file you need directly:
26
26
 
27
27
  ```ts
28
- import { ChangelogNotes } from "@forthtilliath/react-native-kit/components/update/ChangelogNotes";
28
+ import { PhotoPicker } from "@forthtilliath/react-native-kit/components/picker/PhotoPicker";
29
29
  ```
30
30
 
31
31
  This is the recommended way to import: Metro (React Native's bundler) doesn't reliably tree-shake, so pulling from a single deep-import path keeps peer dependencies you don't use (`expo-image-picker`, `expo-speech-recognition`, `react-native-gesture-handler`...) out of your bundle entirely, rather than merely unused.
@@ -33,56 +33,11 @@ This is the recommended way to import: Metro (React Native's bundler) doesn't re
33
33
  A root barrel is also available for convenience:
34
34
 
35
35
  ```ts
36
- import {
37
- ChangelogNotes,
38
- useSubmitGuard,
39
- } from "@forthtilliath/react-native-kit";
36
+ import { PhotoPicker, useSubmitGuard } from "@forthtilliath/react-native-kit";
40
37
  ```
41
38
 
42
39
  **Avoid the barrel under Jest (or any other CommonJS `require` consumer).** `export * from` re-exports are evaluated eagerly on `require()` — unlike Metro's ESM bundling, there's no tree-shaking to skip the unused ones. Requiring the barrel from _any_ file, even one that only wants a framework-agnostic util like `getMostRecentIds`, pulls in every component's module graph, including native-module imports (`expo-speech-recognition` via `VoiceSearchButton`/`PickerModal`) that don't exist in a Jest environment — this throws `Cannot find native module '...'` at require time, not just at runtime for an unrendered component. Deep imports only ever load the one module you asked for, so they don't have this problem in any environment.
43
40
 
44
- ### `<ChangelogNotes notes={...} styles={...} />`
45
-
46
- Renders GitHub-style release notes — as produced by `@forthtilliath/expo-release-updates`'s `parseChangelogNotes` — as headings, bulleted items, and bold-aware text, instead of showing the raw Markdown syntax in a plain `Text`.
47
-
48
- ```tsx
49
- <ChangelogNotes notes={release.notes} />
50
- ```
51
-
52
- It understands the small subset of Markdown GitHub release notes actually use:
53
-
54
- | `notes` input | Rendered as |
55
- | ----------------------------------------- | --------------------------------------------------------------------------- |
56
- | `### Added` | a heading `Text` — `"Added"` |
57
- | `- Export history to CSV.` | a row (`View`) with a `•` bullet `Text` and an item `Text` |
58
- | `**Auto-backup**: saves every 5 minutes.` | a plain `Text`, with the bold segment split into its own bold-styled `Text` |
59
-
60
- A full example, mixing all three:
61
-
62
- ```tsx
63
- <ChangelogNotes
64
- notes={`### Added\n- **Auto-backup**: saves every 5 minutes.\n- Export history to CSV.\n\nSee the full changelog for details.`}
65
- />
66
- ```
67
-
68
- The component ships with neutral default styles and no opinion on your app's theme. Override any subset via the `styles` prop to match your colors/dark mode:
69
-
70
- ```tsx
71
- <ChangelogNotes
72
- notes={release.notes}
73
- styles={{
74
- heading: { color: colors.text, fontWeight: "700" },
75
- itemRow: { gap: 8 },
76
- bullet: { color: colors.primary },
77
- itemText: { color: colors.textMuted },
78
- text: { color: colors.textMuted },
79
- bold: { color: colors.text, fontWeight: "700" },
80
- }}
81
- />
82
- ```
83
-
84
- Every field of `styles` is optional — pass only the ones you want to override; the rest fall back to the defaults (`ChangelogNotesStyles` in `ChangelogNotes.tsx`).
85
-
86
41
  ### `<Thumbnail photoUri={...} placeholderIcon="..." />`
87
42
 
88
43
  List-row thumbnail: the photo if there is one, otherwise a placeholder icon.
@@ -345,51 +300,9 @@ import { ThemeOptionList } from "@forthtilliath/react-native-kit/components/them
345
300
  <ThemeOptionList value={themePreference} onChange={setThemePreference} />;
346
301
  ```
347
302
 
348
- ### `useUpdateCheck(options)`
349
-
350
- Checks once per mount (e.g. app launch) whether a newer release is available, throttled to at most one real check per `minIntervalMs` (default 12h) and silent for a release the user already dismissed. Has no opinion on where "when did we last check" / "which version did the user dismiss" are persisted — both are read/written entirely through the options you pass in.
351
-
352
- ```tsx
353
- import { useUpdateCheck } from "@forthtilliath/react-native-kit/hooks/useUpdateCheck";
354
-
355
- const update = useUpdateCheck({
356
- currentVersion: Constants.expoConfig?.version ?? "0.0.0",
357
- checkForUpdate: fetchLatestRelease,
358
- compareVersions,
359
- getLastCheck: () => ({
360
- lastCheckedAt: settings?.lastUpdateCheckAt ?? null,
361
- dismissedVersion: settings?.dismissedUpdateVersion ?? null,
362
- }),
363
- onChecked: (lastCheckedAt) =>
364
- updateSettings({ lastUpdateCheckAt: lastCheckedAt }),
365
- });
366
-
367
- if (update.status === "available") {
368
- // update.release.version / .notes / .apkUrl
369
- }
370
- ```
371
-
372
- ### `<UpdateAvailableBanner version notes onPress onDismiss />`
373
-
374
- Dismissible banner announcing an available update: version, release notes (rendered via `ChangelogNotes`), an action button and a dismiss button. Has no opinion on what the action does (e.g. navigate to an update screen) or on how/whether dismissal is persisted.
375
-
376
- ```tsx
377
- import { UpdateAvailableBanner } from "@forthtilliath/react-native-kit/components/update/UpdateAvailableBanner";
378
-
379
- {
380
- update.status === "available" && (
381
- <UpdateAvailableBanner
382
- version={update.release.version}
383
- notes={update.release.notes}
384
- onPress={() => router.push("/settings/update")}
385
- onDismiss={() => {
386
- dismissUpdateVersion(update.release.version);
387
- update.dismiss();
388
- }}
389
- />
390
- );
391
- }
392
- ```
303
+ > `useUpdateCheck`, `<UpdateAvailableBanner>` and `<ChangelogNotes>` moved to
304
+ > [`@forthtilliath/expo-release-updates-ui`](../expo-release-updates-ui#readme) —
305
+ > install that package alongside this one if you need them.
393
306
 
394
307
  ### Settings screens (`components/settings/`)
395
308
 
@@ -528,7 +441,7 @@ import { ThemeSettingsScreen } from "@forthtilliath/react-native-kit/components/
528
441
 
529
442
  #### `<UpdateSettingsScreen currentVersion checkForUpdate compareVersions downloadAndInstallApk />`
530
443
 
531
- 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. Pairs naturally with `@forthtilliath/expo-release-updates` and `useUpdateCheck`/`UpdateAvailableBanner` above (same `checkForUpdate`/`compareVersions` you already pass those), but takes plain functions so it isn't tied to that package's exact shape.
444
+ 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.
532
445
 
533
446
  ```tsx
534
447
  import { UpdateSettingsScreen } from "@forthtilliath/react-native-kit/components/settings/UpdateSettingsScreen";
@@ -657,31 +570,20 @@ import { PrivacySettingsScreen } from "@forthtilliath/react-native-kit/component
657
570
 
658
571
  For all 7 components above, styling works the same way as `ChangelogNotes`: an optional `styles` prop (all fields optional, neutral defaults), and where relevant an optional `labels` prop for the built-in French copy.
659
572
 
660
- ### Utils (`utils/`)
573
+ ### Utils (`utils/helpers/`)
574
+
575
+ `confirmDestructive` is the only remaining util here — it wraps React Native's `Alert`, so it isn't framework-agnostic (see its own section above).
661
576
 
662
- Framework-agnostic pure functions — no React or React Native import, usable from Node/web too.
577
+ The framework-agnostic helpers that used to live here (`getPeriodStartMs`, `getMostRecentIds`, `nextInCycle`, `normalizeForSearch`, `rankByNameMatch`, `escapeCsvField`, `formatCsvNumber`, `escapeHtml`) moved to [`@forthtilliath/ts-kit`](../ts-kit#readme) — no React Native dependency on them, so they're usable from Node/web too. They're still re-exported from this package's root barrel for convenience:
663
578
 
664
579
  ```ts
665
- import { getPeriodStartMs } from "@forthtilliath/react-native-kit/utils/helpers/getPeriodStartMs";
666
- import { getMostRecentIds } from "@forthtilliath/react-native-kit/utils/helpers/getMostRecentIds";
667
- import { nextInCycle } from "@forthtilliath/react-native-kit/utils/helpers/nextInCycle";
668
- import { normalizeForSearch } from "@forthtilliath/react-native-kit/utils/helpers/normalizeForSearch";
669
- import { rankByNameMatch } from "@forthtilliath/react-native-kit/utils/helpers/rankByNameMatch";
670
- import { escapeCsvField } from "@forthtilliath/react-native-kit/utils/format/escapeCsvField";
671
- import { formatCsvNumber } from "@forthtilliath/react-native-kit/utils/format/formatCsvNumber";
672
- import { escapeHtml } from "@forthtilliath/react-native-kit/utils/format/escapeHtml";
673
- ```
674
-
675
- | Function | What it does |
676
- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
677
- | `getPeriodStartMs(period, now?)` | Start timestamp (ms) for a `"today" \| "7d" \| "30d" \| "all"` period filter, or `null` for `"all"`. `"today"` is the current calendar day, not a rolling 24h. |
678
- | `getMostRecentIds(rows, limit?)` | Most recently occurring distinct ids (`{ id, occurredAt }[]`), most recent first, deduplicated, limited (default 5). |
679
- | `nextInCycle(ids, currentId)` | Next id in a short list (e.g. cycling through recents on tap) — wraps to the first if `currentId` is at the end or no longer in the list. |
680
- | `normalizeForSearch(text)` | Lowercases, trims, strips accents, and expands `œ`/`æ` ligatures (which `normalize("NFD")` alone doesn't decompose) — for accent/case-insensitive search matching. |
681
- | `rankByNameMatch(items, query, getName)` | Ranks `items` by relevance to `query`: earlier match position first, then shorter name — for a search-as-you-type list. |
682
- | `escapeCsvField(value)` | Quotes and escapes a CSV field (RFC 4180) only if it contains a `"`, `;`, or newline. |
683
- | `formatCsvNumber(value, decimals?)` | Formats a number with a comma decimal separator (French-locale spreadsheets) instead of JS's dot. |
684
- | `escapeHtml(text)` | Basic HTML entity escaping (`&`, `<`, `>`, `"`) for inserting user text into an HTML template. |
580
+ import {
581
+ getPeriodStartMs,
582
+ getMostRecentIds,
583
+ } from "@forthtilliath/react-native-kit";
584
+ ```
585
+
586
+ Prefer importing straight from `ts-kit` (deep import, see its README) if you don't otherwise need this package.
685
587
 
686
588
  ## Scripts
687
589
 
@@ -2,5 +2,4 @@ export * from "./list";
2
2
  export * from "./picker";
3
3
  export * from "./settings";
4
4
  export * from "./theme";
5
- export * from "./update";
6
5
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC;AACxB,cAAc,UAAU,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/components/index.ts"],"names":[],"mappings":"AAAA,cAAc,QAAQ,CAAC;AACvB,cAAc,UAAU,CAAC;AACzB,cAAc,YAAY,CAAC;AAC3B,cAAc,SAAS,CAAC"}
@@ -2,4 +2,3 @@ export * from "./list";
2
2
  export * from "./picker";
3
3
  export * from "./settings";
4
4
  export * from "./theme";
5
- export * from "./update";
@@ -1,4 +1,5 @@
1
- import type { ImageStyle, StyleProp, TextStyle, ViewStyle } from "react-native";
1
+ import type { PickerModalLabels, PickerModalStyles } from "./PickerModal.styles.js";
2
+ export type { PickerModalLabels, PickerModalStyles, } from "./PickerModal.styles.js";
2
3
  export interface PickerItem {
3
4
  id: number;
4
5
  label: string;
@@ -11,34 +12,6 @@ export interface PickerItem {
11
12
  */
12
13
  group?: string;
13
14
  }
14
- export interface PickerModalStyles {
15
- container?: StyleProp<ViewStyle>;
16
- header?: StyleProp<ViewStyle>;
17
- title?: StyleProp<TextStyle>;
18
- close?: StyleProp<TextStyle>;
19
- searchRow?: StyleProp<ViewStyle>;
20
- search?: StyleProp<TextStyle>;
21
- row?: StyleProp<ViewStyle>;
22
- rowThumbnail?: StyleProp<ImageStyle>;
23
- rowThumbnailPlaceholder?: StyleProp<ViewStyle>;
24
- rowLabel?: StyleProp<TextStyle>;
25
- rowSubtitle?: StyleProp<TextStyle>;
26
- empty?: StyleProp<TextStyle>;
27
- sectionHeader?: StyleProp<TextStyle>;
28
- extraActions?: StyleProp<ViewStyle>;
29
- extraActionLabel?: StyleProp<TextStyle>;
30
- extraActionIconColor?: string;
31
- rowThumbnailPlaceholderIconColor?: string;
32
- placeholderTextColor?: string;
33
- }
34
- export interface PickerModalLabels {
35
- close?: string;
36
- searchPlaceholder?: string;
37
- searchAccessibilityLabel?: string;
38
- voiceSearchAccessibilityLabel?: string;
39
- defaultEmptyMessage?: string;
40
- otherGroupLabel?: string;
41
- }
42
15
  export interface PickerModalProps {
43
16
  visible: boolean;
44
17
  title: string;
@@ -1 +1 @@
1
- {"version":3,"file":"PickerModal.d.ts","sourceRoot":"","sources":["../../../src/components/picker/PickerModal.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PickerModal.d.ts","sourceRoot":"","sources":["../../../src/components/picker/PickerModal.tsx"],"names":[],"mappings":"AAWA,OAAO,KAAK,EACV,iBAAiB,EACjB,iBAAiB,EAClB,MAAM,yBAAyB,CAAC;AAMjC,YAAY,EACV,iBAAiB,EACjB,iBAAiB,GAClB,MAAM,yBAAyB,CAAC;AAEjC,MAAM,WAAW,UAAU;IACzB,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,OAAO,CAAC;IACjB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,UAAU,EAAE,CAAC;IACpB,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;IACrC,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,sDAAsD;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,8FAA8F;IAC9F,WAAW,CAAC,EAAE,CAAC,KAAK,EAAE,UAAU,EAAE,EAAE,KAAK,EAAE,MAAM,KAAK,UAAU,EAAE,CAAC;IACnE,qFAAqF;IACrF,YAAY,CAAC,EAAE;QAAE,KAAK,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,IAAI,CAAA;KAAE,EAAE,CAAC;IACxD;;;;OAIG;IACH,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAID,wBAAgB,WAAW,CAAC,EAC1B,OAAO,EACP,KAAK,EACL,KAAK,EACL,QAAQ,EACR,OAAO,EACP,YAAY,EACZ,YAAY,EACZ,WAAW,EACX,YAAY,EACZ,UAAU,EACV,MAAM,EACN,MAAM,GACP,EAAE,gBAAgB,+BAwJlB"}
@@ -1,90 +1,11 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import { useMemo, useState } from "react";
3
- import { Image, Modal, Pressable, SectionList, Text, TextInput, View, } from "react-native";
3
+ import { Modal, Pressable, SectionList, Text, TextInput, View, } from "react-native";
4
4
  import { Ionicons } from "@expo/vector-icons";
5
+ import { defaultLabels, defaultStyles } from "./PickerModal.styles.js";
6
+ import { PickerModalRow } from "./PickerModalRow.js";
7
+ import { usePickerModalSections } from "./usePickerModalSections.js";
5
8
  import { VoiceSearchButton } from "./VoiceSearchButton.js";
6
- const defaultStyles = {
7
- container: {
8
- flex: 1,
9
- backgroundColor: "#ffffff",
10
- paddingTop: 60,
11
- paddingHorizontal: 16,
12
- },
13
- header: {
14
- flexDirection: "row",
15
- justifyContent: "space-between",
16
- alignItems: "center",
17
- marginBottom: 12,
18
- },
19
- title: { fontSize: 20, fontWeight: "700", color: "#111827" },
20
- close: { fontSize: 16, color: "#2563eb" },
21
- searchRow: {
22
- flexDirection: "row",
23
- alignItems: "center",
24
- gap: 10,
25
- marginBottom: 8,
26
- },
27
- search: {
28
- backgroundColor: "#f9fafb",
29
- borderWidth: 1,
30
- borderColor: "#e5e7eb",
31
- borderRadius: 10,
32
- paddingHorizontal: 12,
33
- paddingVertical: 10,
34
- fontSize: 16,
35
- color: "#111827",
36
- flex: 1,
37
- },
38
- row: {
39
- flexDirection: "row",
40
- alignItems: "center",
41
- gap: 12,
42
- backgroundColor: "#f9fafb",
43
- borderWidth: 1,
44
- borderColor: "#e5e7eb",
45
- borderRadius: 10,
46
- padding: 14,
47
- marginBottom: 8,
48
- },
49
- rowThumbnail: {
50
- width: 40,
51
- height: 40,
52
- borderRadius: 8,
53
- backgroundColor: "#ffffff",
54
- },
55
- rowThumbnailPlaceholder: {
56
- width: 40,
57
- height: 40,
58
- borderRadius: 8,
59
- backgroundColor: "#ffffff",
60
- alignItems: "center",
61
- justifyContent: "center",
62
- },
63
- rowLabel: { fontSize: 16, fontWeight: "600", color: "#111827" },
64
- rowSubtitle: { fontSize: 13, color: "#6b7280", marginTop: 2 },
65
- empty: { textAlign: "center", color: "#6b7280", marginTop: 24 },
66
- sectionHeader: {
67
- fontSize: 13,
68
- fontWeight: "700",
69
- color: "#6b7280",
70
- textTransform: "uppercase",
71
- marginTop: 8,
72
- marginBottom: 6,
73
- },
74
- extraActions: { marginBottom: 4 },
75
- extraActionLabel: { color: "#2563eb" },
76
- extraActionIconColor: "#2563eb",
77
- rowThumbnailPlaceholderIconColor: "#6b7280",
78
- placeholderTextColor: "#6b7280",
79
- };
80
- const defaultLabels = {
81
- close: "Fermer",
82
- searchPlaceholder: "Rechercher…",
83
- searchAccessibilityLabel: "Rechercher",
84
- voiceSearchAccessibilityLabel: "Dicter la recherche",
85
- defaultEmptyMessage: "Aucun résultat.",
86
- otherGroupLabel: "Autres",
87
- };
88
9
  // Full-screen picker: search (typed or dictated), optional sections, and
89
10
  // "add" actions always visible above the results.
90
11
  export function PickerModal({ visible, title, items, onSelect, onClose, emptyMessage, initialQuery, filterItems, extraActions, groupOrder, labels, styles, }) {
@@ -139,33 +60,10 @@ export function PickerModal({ visible, title, items, onSelect, onClose, emptyMes
139
60
  const q = query.trim().toLowerCase();
140
61
  return items.filter((item) => item.label.toLowerCase().includes(q));
141
62
  }, [items, query, filterItems, isSearching]);
142
- // Sections sorted by title: stable order, independent of the source
143
- // groups' order. No sections during a search (results ranked by relevance,
144
- // not by group), nor if no item has a group (uncategorized pickers,
145
- // unchanged behavior).
146
- const sections = useMemo(() => {
147
- if (isSearching || !filtered.some((item) => item.group)) {
148
- return [{ title: null, data: filtered }];
149
- }
150
- const byGroup = new Map();
151
- for (const item of filtered) {
152
- const key = item.group ?? t.otherGroupLabel;
153
- const group = byGroup.get(key);
154
- if (group)
155
- group.push(item);
156
- else
157
- byGroup.set(key, [item]);
158
- }
159
- function sortIndex(groupTitle) {
160
- const index = groupOrder?.indexOf(groupTitle) ?? -1;
161
- return index === -1 ? (groupOrder?.length ?? 0) : index;
162
- }
163
- return [...byGroup.entries()]
164
- .sort(([a], [b]) => sortIndex(a) - sortIndex(b) || a.localeCompare(b))
165
- .map(([sectionTitle, data]) => ({ title: sectionTitle, data }));
166
- }, [filtered, isSearching, groupOrder, t.otherGroupLabel]);
167
- return (_jsx(Modal, { visible: visible, animationType: "slide", onRequestClose: onClose, children: _jsxs(View, { style: merged.container, children: [_jsxs(View, { style: merged.header, children: [_jsx(Text, { style: merged.title, children: title }), _jsx(Pressable, { onPress: onClose, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: t.close, children: _jsx(Text, { style: merged.close, children: t.close }) })] }), _jsxs(View, { style: merged.searchRow, children: [_jsx(TextInput, { style: merged.search, placeholder: t.searchPlaceholder, placeholderTextColor: merged.placeholderTextColor, value: query, onChangeText: setQuery, autoFocus: true, accessibilityLabel: t.searchAccessibilityLabel }), _jsx(VoiceSearchButton, { onResult: setQuery, accessibilityLabel: t.voiceSearchAccessibilityLabel })] }), _jsx(SectionList, { sections: sections, keyExtractor: (item) => String(item.id), keyboardShouldPersistTaps: "handled", stickySectionHeadersEnabled: false, ListHeaderComponent: extraActions && extraActions.length > 0 ? (_jsx(View, { style: merged.extraActions, children: extraActions.map((action) => (_jsxs(Pressable, { style: merged.row, onPress: action.onPress, accessibilityRole: "button", accessibilityLabel: action.label, children: [_jsx(Ionicons, { name: "add-circle-outline", size: 20, color: merged.extraActionIconColor }), _jsx(Text, { style: [merged.rowLabel, merged.extraActionLabel], children: action.label })] }, action.label))) })) : null, ListEmptyComponent: _jsx(Text, { style: merged.empty, children: emptyMessage ?? t.defaultEmptyMessage }), renderSectionHeader: ({ section: { title: sectionTitle }, }) => sectionTitle ? (_jsx(Text, { style: merged.sectionHeader, children: sectionTitle })) : null, renderItem: ({ item }) => (_jsxs(Pressable, { style: merged.row, onPress: () => {
168
- onSelect(item);
169
- setQuery("");
170
- }, accessibilityRole: "button", accessibilityLabel: item.subtitle ? `${item.label}, ${item.subtitle}` : item.label, children: [item.imageUri ? (_jsx(Image, { source: { uri: item.imageUri }, style: merged.rowThumbnail, accessibilityIgnoresInvertColors: true })) : item.imageUri === null ? (_jsx(View, { style: merged.rowThumbnailPlaceholder, children: _jsx(Ionicons, { name: "cube-outline", size: 18, color: merged.rowThumbnailPlaceholderIconColor }) })) : null, _jsxs(View, { style: { flex: 1 }, children: [_jsx(Text, { style: merged.rowLabel, children: item.label }), item.subtitle ? (_jsx(Text, { style: merged.rowSubtitle, children: item.subtitle })) : null] })] })) })] }) }));
63
+ const sections = usePickerModalSections(filtered, isSearching, groupOrder, t.otherGroupLabel);
64
+ function handleSelect(item) {
65
+ onSelect(item);
66
+ setQuery("");
67
+ }
68
+ return (_jsx(Modal, { visible: visible, animationType: "slide", onRequestClose: onClose, children: _jsxs(View, { style: merged.container, children: [_jsxs(View, { style: merged.header, children: [_jsx(Text, { style: merged.title, children: title }), _jsx(Pressable, { onPress: onClose, hitSlop: 12, accessibilityRole: "button", accessibilityLabel: t.close, children: _jsx(Text, { style: merged.close, children: t.close }) })] }), _jsxs(View, { style: merged.searchRow, children: [_jsx(TextInput, { style: merged.search, placeholder: t.searchPlaceholder, placeholderTextColor: merged.placeholderTextColor, value: query, onChangeText: setQuery, autoFocus: true, accessibilityLabel: t.searchAccessibilityLabel }), _jsx(VoiceSearchButton, { onResult: setQuery, accessibilityLabel: t.voiceSearchAccessibilityLabel })] }), _jsx(SectionList, { sections: sections, keyExtractor: (item) => String(item.id), keyboardShouldPersistTaps: "handled", stickySectionHeadersEnabled: false, ListHeaderComponent: extraActions && extraActions.length > 0 ? (_jsx(View, { style: merged.extraActions, children: extraActions.map((action) => (_jsxs(Pressable, { style: merged.row, onPress: action.onPress, accessibilityRole: "button", accessibilityLabel: action.label, children: [_jsx(Ionicons, { name: "add-circle-outline", size: 20, color: merged.extraActionIconColor }), _jsx(Text, { style: [merged.rowLabel, merged.extraActionLabel], children: action.label })] }, action.label))) })) : null, ListEmptyComponent: _jsx(Text, { style: merged.empty, children: emptyMessage ?? t.defaultEmptyMessage }), renderSectionHeader: ({ section: { title: sectionTitle }, }) => sectionTitle ? (_jsx(Text, { style: merged.sectionHeader, children: sectionTitle })) : null, renderItem: ({ item }) => (_jsx(PickerModalRow, { item: item, styles: merged, onSelect: handleSelect })) })] }) }));
171
69
  }
@@ -0,0 +1,32 @@
1
+ import type { ImageStyle, StyleProp, TextStyle, ViewStyle } from "react-native";
2
+ export interface PickerModalStyles {
3
+ container?: StyleProp<ViewStyle>;
4
+ header?: StyleProp<ViewStyle>;
5
+ title?: StyleProp<TextStyle>;
6
+ close?: StyleProp<TextStyle>;
7
+ searchRow?: StyleProp<ViewStyle>;
8
+ search?: StyleProp<TextStyle>;
9
+ row?: StyleProp<ViewStyle>;
10
+ rowThumbnail?: StyleProp<ImageStyle>;
11
+ rowThumbnailPlaceholder?: StyleProp<ViewStyle>;
12
+ rowLabel?: StyleProp<TextStyle>;
13
+ rowSubtitle?: StyleProp<TextStyle>;
14
+ empty?: StyleProp<TextStyle>;
15
+ sectionHeader?: StyleProp<TextStyle>;
16
+ extraActions?: StyleProp<ViewStyle>;
17
+ extraActionLabel?: StyleProp<TextStyle>;
18
+ extraActionIconColor?: string;
19
+ rowThumbnailPlaceholderIconColor?: string;
20
+ placeholderTextColor?: string;
21
+ }
22
+ export interface PickerModalLabels {
23
+ close?: string;
24
+ searchPlaceholder?: string;
25
+ searchAccessibilityLabel?: string;
26
+ voiceSearchAccessibilityLabel?: string;
27
+ defaultEmptyMessage?: string;
28
+ otherGroupLabel?: string;
29
+ }
30
+ export declare const defaultStyles: Required<PickerModalStyles>;
31
+ export declare const defaultLabels: Required<PickerModalLabels>;
32
+ //# sourceMappingURL=PickerModal.styles.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PickerModal.styles.d.ts","sourceRoot":"","sources":["../../../src/components/picker/PickerModal.styles.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEhF,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,GAAG,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC3B,YAAY,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACrC,uBAAuB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/C,QAAQ,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAChC,WAAW,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACnC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACrC,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,gBAAgB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACxC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,gCAAgC,CAAC,EAAE,MAAM,CAAC;IAC1C,oBAAoB,CAAC,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,6BAA6B,CAAC,EAAE,MAAM,CAAC;IACvC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,iBAAiB,CAyErD,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,QAAQ,CAAC,iBAAiB,CAOrD,CAAC"}
@@ -0,0 +1,82 @@
1
+ export const defaultStyles = {
2
+ container: {
3
+ flex: 1,
4
+ backgroundColor: "#ffffff",
5
+ paddingTop: 60,
6
+ paddingHorizontal: 16,
7
+ },
8
+ header: {
9
+ flexDirection: "row",
10
+ justifyContent: "space-between",
11
+ alignItems: "center",
12
+ marginBottom: 12,
13
+ },
14
+ title: { fontSize: 20, fontWeight: "700", color: "#111827" },
15
+ close: { fontSize: 16, color: "#2563eb" },
16
+ searchRow: {
17
+ flexDirection: "row",
18
+ alignItems: "center",
19
+ gap: 10,
20
+ marginBottom: 8,
21
+ },
22
+ search: {
23
+ backgroundColor: "#f9fafb",
24
+ borderWidth: 1,
25
+ borderColor: "#e5e7eb",
26
+ borderRadius: 10,
27
+ paddingHorizontal: 12,
28
+ paddingVertical: 10,
29
+ fontSize: 16,
30
+ color: "#111827",
31
+ flex: 1,
32
+ },
33
+ row: {
34
+ flexDirection: "row",
35
+ alignItems: "center",
36
+ gap: 12,
37
+ backgroundColor: "#f9fafb",
38
+ borderWidth: 1,
39
+ borderColor: "#e5e7eb",
40
+ borderRadius: 10,
41
+ padding: 14,
42
+ marginBottom: 8,
43
+ },
44
+ rowThumbnail: {
45
+ width: 40,
46
+ height: 40,
47
+ borderRadius: 8,
48
+ backgroundColor: "#ffffff",
49
+ },
50
+ rowThumbnailPlaceholder: {
51
+ width: 40,
52
+ height: 40,
53
+ borderRadius: 8,
54
+ backgroundColor: "#ffffff",
55
+ alignItems: "center",
56
+ justifyContent: "center",
57
+ },
58
+ rowLabel: { fontSize: 16, fontWeight: "600", color: "#111827" },
59
+ rowSubtitle: { fontSize: 13, color: "#6b7280", marginTop: 2 },
60
+ empty: { textAlign: "center", color: "#6b7280", marginTop: 24 },
61
+ sectionHeader: {
62
+ fontSize: 13,
63
+ fontWeight: "700",
64
+ color: "#6b7280",
65
+ textTransform: "uppercase",
66
+ marginTop: 8,
67
+ marginBottom: 6,
68
+ },
69
+ extraActions: { marginBottom: 4 },
70
+ extraActionLabel: { color: "#2563eb" },
71
+ extraActionIconColor: "#2563eb",
72
+ rowThumbnailPlaceholderIconColor: "#6b7280",
73
+ placeholderTextColor: "#6b7280",
74
+ };
75
+ export const defaultLabels = {
76
+ close: "Fermer",
77
+ searchPlaceholder: "Rechercher…",
78
+ searchAccessibilityLabel: "Rechercher",
79
+ voiceSearchAccessibilityLabel: "Dicter la recherche",
80
+ defaultEmptyMessage: "Aucun résultat.",
81
+ otherGroupLabel: "Autres",
82
+ };
@@ -0,0 +1,17 @@
1
+ import type { ImageStyle, StyleProp, TextStyle, ViewStyle } from "react-native";
2
+ import type { PickerItem } from "./PickerModal.js";
3
+ export interface PickerModalRowStyles {
4
+ row: StyleProp<ViewStyle>;
5
+ rowThumbnail: StyleProp<ImageStyle>;
6
+ rowThumbnailPlaceholder: StyleProp<ViewStyle>;
7
+ rowThumbnailPlaceholderIconColor: string;
8
+ rowLabel: StyleProp<TextStyle>;
9
+ rowSubtitle: StyleProp<TextStyle>;
10
+ }
11
+ export interface PickerModalRowProps {
12
+ item: PickerItem;
13
+ styles: PickerModalRowStyles;
14
+ onSelect: (item: PickerItem) => void;
15
+ }
16
+ export declare function PickerModalRow({ item, styles, onSelect, }: PickerModalRowProps): import("react").JSX.Element;
17
+ //# sourceMappingURL=PickerModalRow.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PickerModalRow.d.ts","sourceRoot":"","sources":["../../../src/components/picker/PickerModalRow.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAIhF,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1B,YAAY,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IACpC,uBAAuB,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9C,gCAAgC,EAAE,MAAM,CAAC;IACzC,QAAQ,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/B,WAAW,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CACnC;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,UAAU,CAAC;IACjB,MAAM,EAAE,oBAAoB,CAAC;IAC7B,QAAQ,EAAE,CAAC,IAAI,EAAE,UAAU,KAAK,IAAI,CAAC;CACtC;AAKD,wBAAgB,cAAc,CAAC,EAC7B,IAAI,EACJ,MAAM,EACN,QAAQ,GACT,EAAE,mBAAmB,+BAmCrB"}
@@ -0,0 +1,11 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Image, Pressable, Text, View } from "react-native";
3
+ import { Ionicons } from "@expo/vector-icons";
4
+ // One result row: thumbnail (photo, placeholder icon, or nothing depending
5
+ // on `imageUri`), label, and optional subtitle. Extracted from PickerModal's
6
+ // `renderItem` to keep that file under the project's 300-line-per-file rule.
7
+ export function PickerModalRow({ item, styles, onSelect, }) {
8
+ return (_jsxs(Pressable, { style: styles.row, onPress: () => {
9
+ onSelect(item);
10
+ }, accessibilityRole: "button", accessibilityLabel: item.subtitle ? `${item.label}, ${item.subtitle}` : item.label, children: [item.imageUri ? (_jsx(Image, { source: { uri: item.imageUri }, style: styles.rowThumbnail, accessibilityIgnoresInvertColors: true })) : item.imageUri === null ? (_jsx(View, { style: styles.rowThumbnailPlaceholder, children: _jsx(Ionicons, { name: "cube-outline", size: 18, color: styles.rowThumbnailPlaceholderIconColor }) })) : null, _jsxs(View, { style: { flex: 1 }, children: [_jsx(Text, { style: styles.rowLabel, children: item.label }), item.subtitle ? (_jsx(Text, { style: styles.rowSubtitle, children: item.subtitle })) : null] })] }));
11
+ }
@@ -0,0 +1,7 @@
1
+ import type { PickerItem } from "./PickerModal.js";
2
+ export interface PickerModalSection {
3
+ title: string | null;
4
+ data: PickerItem[];
5
+ }
6
+ export declare function usePickerModalSections(filtered: PickerItem[], isSearching: boolean, groupOrder: string[] | undefined, otherGroupLabel: string): PickerModalSection[];
7
+ //# sourceMappingURL=usePickerModalSections.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"usePickerModalSections.d.ts","sourceRoot":"","sources":["../../../src/components/picker/usePickerModalSections.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAEnD,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,IAAI,EAAE,UAAU,EAAE,CAAC;CACpB;AAMD,wBAAgB,sBAAsB,CACpC,QAAQ,EAAE,UAAU,EAAE,EACtB,WAAW,EAAE,OAAO,EACpB,UAAU,EAAE,MAAM,EAAE,GAAG,SAAS,EAChC,eAAe,EAAE,MAAM,GACtB,kBAAkB,EAAE,CAoBtB"}
@@ -0,0 +1,28 @@
1
+ import { useMemo } from "react";
2
+ // Sections sorted by title: stable order, independent of the source groups'
3
+ // order. No sections during a search (results ranked by relevance, not by
4
+ // group), nor if no item has a group (uncategorized pickers, unchanged
5
+ // behavior).
6
+ export function usePickerModalSections(filtered, isSearching, groupOrder, otherGroupLabel) {
7
+ return useMemo(() => {
8
+ if (isSearching || !filtered.some((item) => item.group)) {
9
+ return [{ title: null, data: filtered }];
10
+ }
11
+ const byGroup = new Map();
12
+ for (const item of filtered) {
13
+ const key = item.group ?? otherGroupLabel;
14
+ const group = byGroup.get(key);
15
+ if (group)
16
+ group.push(item);
17
+ else
18
+ byGroup.set(key, [item]);
19
+ }
20
+ function sortIndex(groupTitle) {
21
+ const index = groupOrder?.indexOf(groupTitle) ?? -1;
22
+ return index === -1 ? (groupOrder?.length ?? 0) : index;
23
+ }
24
+ return [...byGroup.entries()]
25
+ .sort(([a], [b]) => sortIndex(a) - sortIndex(b) || a.localeCompare(b))
26
+ .map(([sectionTitle, data]) => ({ title: sectionTitle, data }));
27
+ }, [filtered, isSearching, groupOrder, otherGroupLabel]);
28
+ }
@@ -1,49 +1,5 @@
1
- import type { StyleProp, TextStyle, ViewStyle } from "react-native";
2
- import type { ChangelogNotesStyles } from "../update/ChangelogNotes.js";
3
- export interface UpdateSettingsScreenRelease {
4
- version: string;
5
- notes: string;
6
- apkUrl: string;
7
- }
8
- export interface UpdateSettingsScreenHistoryEntry {
9
- version: string;
10
- notes: string;
11
- publishedAt?: string | null;
12
- }
13
- export interface UpdateSettingsScreenLabels {
14
- installedVersionLabel?: string;
15
- checkButton?: string;
16
- upToDate?: string;
17
- checkError?: string;
18
- downloadError?: string;
19
- availableTitle?: (version: string) => string;
20
- installButton?: (version: string) => string;
21
- installButtonAccessibilityLabel?: (version: string) => string;
22
- downloadingLabel?: (percent: number) => string;
23
- downloadingHint?: string;
24
- historyTitle?: string;
25
- }
26
- export interface UpdateSettingsScreenStyles {
27
- container?: StyleProp<ViewStyle>;
28
- infoBox?: StyleProp<ViewStyle>;
29
- infoLabel?: StyleProp<TextStyle>;
30
- infoValue?: StyleProp<TextStyle>;
31
- helpText?: StyleProp<TextStyle>;
32
- errorText?: StyleProp<TextStyle>;
33
- button?: StyleProp<ViewStyle>;
34
- buttonDisabled?: StyleProp<ViewStyle>;
35
- buttonText?: StyleProp<TextStyle>;
36
- activityIndicatorColor?: string;
37
- updateAvailableBox?: StyleProp<ViewStyle>;
38
- updateAvailableTitle?: StyleProp<TextStyle>;
39
- changelog?: StyleProp<ViewStyle>;
40
- changelogTitle?: StyleProp<TextStyle>;
41
- changelogEntry?: StyleProp<ViewStyle>;
42
- changelogEntryHeader?: StyleProp<ViewStyle>;
43
- changelogVersion?: StyleProp<TextStyle>;
44
- changelogDate?: StyleProp<TextStyle>;
45
- notes?: ChangelogNotesStyles;
46
- }
1
+ import type { UpdateSettingsScreenHistoryEntry, UpdateSettingsScreenLabels, UpdateSettingsScreenRelease, UpdateSettingsScreenStyles } from "./UpdateSettingsScreen.styles.js";
2
+ export type { UpdateSettingsScreenHistoryEntry, UpdateSettingsScreenLabels, UpdateSettingsScreenRelease, UpdateSettingsScreenStyles, } from "./UpdateSettingsScreen.styles.js";
47
3
  export interface UpdateSettingsScreenProps {
48
4
  currentVersion: string;
49
5
  /** Fetches the latest available release, or `null` if there is none. */