@forthtilliath/react-native-kit 0.7.1 → 0.8.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/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 Forthtilliath
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Forthtilliath
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,328 +1,338 @@
1
- # @forthtilliath/react-native-kit
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 so consumers only pull in what they use.
4
-
5
- ## Install
6
-
7
- ```bash
8
- npm install @forthtilliath/react-native-kit react react-native
9
- ```
10
-
11
- Or, from within this monorepo, as a workspace dependency:
12
-
13
- ```json
14
- {
15
- "dependencies": {
16
- "@forthtilliath/react-native-kit": "workspace:*"
17
- }
18
- }
19
- ```
20
-
21
- `react` and `react-native` are peer dependencies — install them in the consuming app if not already present. `@expo/vector-icons`, `expo-image-picker`, `expo-speech-recognition`, and `react-native-gesture-handler` are also peer dependencies, needed only if you import the components that use them (`Thumbnail`/`PickerModal`/`SwipeableRow`, `PhotoPicker`, `VoiceSearchButton`/`PickerModal`, `SwipeableRow`, respectively).
22
-
23
- ## Usage
24
-
25
- Each component is its own module — import the file you need directly:
26
-
27
- ```ts
28
- import { ChangelogNotes } from "@forthtilliath/react-native-kit/ChangelogNotes";
29
- ```
30
-
31
- This is the recommended way to import: Metro (React Native's bundler) doesn't reliably tree-shake, so pulling from a single deep-import path keeps peer dependencies you don't use (`expo-image-picker`, `expo-speech-recognition`, `react-native-gesture-handler`...) out of your bundle entirely, rather than merely unused.
32
-
33
- A root barrel is also available for convenience:
34
-
35
- ```ts
36
- import {
37
- ChangelogNotes,
38
- useSubmitGuard,
39
- } from "@forthtilliath/react-native-kit";
40
- ```
41
-
42
- **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
-
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
- ### `<Thumbnail photoUri={...} placeholderIcon="..." />`
87
-
88
- List-row thumbnail: the photo if there is one, otherwise a placeholder icon.
89
-
90
- ```tsx
91
- import { Thumbnail } from "@forthtilliath/react-native-kit/Thumbnail";
92
-
93
- <Thumbnail
94
- photoUri={container.photoUri}
95
- placeholderIcon="cube-outline"
96
- size={48}
97
- />;
98
- ```
99
-
100
- ### `<SwipeableRow onDelete={...} deleteLabel="...">`
101
-
102
- Swipe a list row left to reveal a delete button, on top of a tap to edit it.
103
-
104
- ```tsx
105
- import { SwipeableRow } from "@forthtilliath/react-native-kit/SwipeableRow";
106
-
107
- <SwipeableRow
108
- onDelete={() => remove(item.id)}
109
- deleteLabel={`Delete ${item.name}`}
110
- >
111
- <ItemRow item={item} />
112
- </SwipeableRow>;
113
- ```
114
-
115
- ### `<VoiceSearchButton onResult={...} />`
116
-
117
- Microphone button to dictate a search instead of typing it. Safe to mount more than one at a time (e.g. a name field plus a search picker on the same screen) — only the instance that started listening reacts to its result.
118
-
119
- ```tsx
120
- import { VoiceSearchButton } from "@forthtilliath/react-native-kit/VoiceSearchButton";
121
-
122
- <VoiceSearchButton onResult={setQuery} lang="en-US" />;
123
- ```
124
-
125
- ### `<PhotoPicker photoUri={...} onChange={...} savePhoto={...} photoLabel="..." />`
126
-
127
- Photo picker (camera or library) with a preview and a remove link. `savePhoto` is injected rather than hard-coded, so where/how the picked image gets persisted is entirely up to the caller.
128
-
129
- ```tsx
130
- import { PhotoPicker } from "@forthtilliath/react-native-kit/PhotoPicker";
131
-
132
- <PhotoPicker
133
- photoUri={container.photoUri}
134
- onChange={(uri) => setPhotoUri(uri)}
135
- savePhoto={(sourceUri) => saveContainerPhoto(sourceUri)}
136
- photoLabel="of the container"
137
- />;
138
- ```
139
-
140
- ### `<PickerModal visible title items onSelect onClose />`
141
-
142
- Full-screen picker: search (typed or dictated via `VoiceSearchButton`), optional sections, and "add" actions always visible above the results.
143
-
144
- ```tsx
145
- import {
146
- PickerModal,
147
- type PickerItem,
148
- } from "@forthtilliath/react-native-kit/PickerModal";
149
-
150
- <PickerModal
151
- visible={pickerVisible}
152
- title="Choose a container"
153
- items={containers.map((c): PickerItem => ({
154
- id: c.id,
155
- label: c.name,
156
- imageUri: c.photoUri,
157
- }))}
158
- onSelect={(item) => setContainerId(item.id)}
159
- onClose={() => setPickerVisible(false)}
160
- extraActions={[
161
- {
162
- label: "Add a new container",
163
- onPress: () => router.push("/containers/new"),
164
- },
165
- ]}
166
- />;
167
- ```
168
-
169
- Groups results into sections via each item's `group` (e.g. food groups, ingredients vs recipes) — ignored while searching, where the best global matches are shown instead. Pass `filterItems` for custom ranking (e.g. `rankByNameMatch` from this same package's `utils/`) instead of the default case-insensitive substring match.
170
-
171
- For all 5 components above, styling and (where relevant) copy work the same way as `ChangelogNotes`: an optional `styles` prop (all fields optional, neutral defaults) and, for `PhotoPicker`/`PickerModal`, an optional `labels` prop for the built-in French copy.
172
-
173
- ### `useSubmitGuard()`
174
-
175
- Prevents a second call while a first one is still pending — e.g. a double-tap on a "Save" button before it's had time to disable, which would otherwise create duplicate submissions.
176
-
177
- ```tsx
178
- import { useSubmitGuard } from "@forthtilliath/react-native-kit/useSubmitGuard";
179
-
180
- const { isSaving, guard } = useSubmitGuard();
181
-
182
- <Pressable
183
- disabled={isSaving}
184
- onPress={() =>
185
- guard(async () => {
186
- await save();
187
- })
188
- }
189
- >
190
- <Text>{isSaving ? "Saving…" : "Save"}</Text>
191
- </Pressable>;
192
- ```
193
-
194
- ### `useDebouncedChange(values, delayMs, callback)`
195
-
196
- Calls `callback` `delayMs` after the last change among `values`, ignoring renders where any value is still `undefined` (not loaded yet) and the very first render where they're all defined (no trigger on mount). A new change before the delay elapses resets the timer — a real debounce, not a throttle.
197
-
198
- ```tsx
199
- import { useDebouncedChange } from "@forthtilliath/react-native-kit/useDebouncedChange";
200
-
201
- useDebouncedChange([settingsData, itemsData], 5 * 60 * 1000, () => {
202
- runAutoBackup();
203
- });
204
- ```
205
-
206
- ### `confirmDestructive(title, onConfirm, options?)`
207
-
208
- Generic destructive-action confirmation (title + message + Cancel/Confirm), for anything irreversible (delete, reset...). Ships with French defaults (`message`, `cancelLabel`, `confirmLabel` all overridable).
209
-
210
- ```ts
211
- import { confirmDestructive } from "@forthtilliath/react-native-kit/confirmDestructive";
212
-
213
- confirmDestructive("Delete this item?", () => deleteItem(id), {
214
- message: "This cannot be undone.",
215
- cancelLabel: "Cancel",
216
- confirmLabel: "Delete",
217
- });
218
- ```
219
-
220
- ### `useEffectiveColorScheme(preference)`
221
-
222
- Resolves a `"light" | "dark" | "system"` theme preference against the device's color scheme: `"system"` follows the device, `"light"`/`"dark"` override it regardless of what the device is set to.
223
-
224
- ```tsx
225
- import { useEffectiveColorScheme } from "@forthtilliath/react-native-kit/useEffectiveColorScheme";
226
-
227
- const scheme = useEffectiveColorScheme(themePreference); // "light" | "dark"
228
- const colors = scheme === "dark" ? darkColors : lightColors;
229
- ```
230
-
231
- ### `<ThemeToggle value onChange />`
232
-
233
- 3-way segmented control for a light/dark/system theme preference.
234
-
235
- ```tsx
236
- import { ThemeToggle } from "@forthtilliath/react-native-kit/ThemeToggle";
237
-
238
- <ThemeToggle value={themePreference} onChange={setThemePreference} />;
239
- ```
240
-
241
- ### `useUpdateCheck(options)`
242
-
243
- 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.
244
-
245
- ```tsx
246
- import { useUpdateCheck } from "@forthtilliath/react-native-kit/useUpdateCheck";
247
-
248
- const update = useUpdateCheck({
249
- currentVersion: Constants.expoConfig?.version ?? "0.0.0",
250
- checkForUpdate: fetchLatestRelease,
251
- compareVersions,
252
- getLastCheck: () => ({
253
- lastCheckedAt: settings?.lastUpdateCheckAt ?? null,
254
- dismissedVersion: settings?.dismissedUpdateVersion ?? null,
255
- }),
256
- onChecked: (lastCheckedAt) =>
257
- updateSettings({ lastUpdateCheckAt: lastCheckedAt }),
258
- });
259
-
260
- if (update.status === "available") {
261
- // update.release.version / .notes / .apkUrl
262
- }
263
- ```
264
-
265
- ### `<UpdateAvailableBanner version notes onPress onDismiss />`
266
-
267
- 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.
268
-
269
- ```tsx
270
- import { UpdateAvailableBanner } from "@forthtilliath/react-native-kit/UpdateAvailableBanner";
271
-
272
- {
273
- update.status === "available" && (
274
- <UpdateAvailableBanner
275
- version={update.release.version}
276
- notes={update.release.notes}
277
- onPress={() => router.push("/settings/update")}
278
- onDismiss={() => {
279
- dismissUpdateVersion(update.release.version);
280
- update.dismiss();
281
- }}
282
- />
283
- );
284
- }
285
- ```
286
-
287
- ### Utils (`utils/`)
288
-
289
- Framework-agnostic pure functions — no React or React Native import, usable from Node/web too.
290
-
291
- ```ts
292
- import { getPeriodStartMs } from "@forthtilliath/react-native-kit/utils/getPeriodStartMs";
293
- import { getMostRecentIds } from "@forthtilliath/react-native-kit/utils/getMostRecentIds";
294
- import { nextInCycle } from "@forthtilliath/react-native-kit/utils/nextInCycle";
295
- import { normalizeForSearch } from "@forthtilliath/react-native-kit/utils/normalizeForSearch";
296
- import { rankByNameMatch } from "@forthtilliath/react-native-kit/utils/rankByNameMatch";
297
- import { escapeCsvField } from "@forthtilliath/react-native-kit/utils/escapeCsvField";
298
- import { formatCsvNumber } from "@forthtilliath/react-native-kit/utils/formatCsvNumber";
299
- import { escapeHtml } from "@forthtilliath/react-native-kit/utils/escapeHtml";
300
- ```
301
-
302
- | Function | What it does |
303
- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
304
- | `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. |
305
- | `getMostRecentIds(rows, limit?)` | Most recently occurring distinct ids (`{ id, occurredAt }[]`), most recent first, deduplicated, limited (default 5). |
306
- | `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. |
307
- | `normalizeForSearch(text)` | Lowercases, trims, strips accents, and expands `œ`/`æ` ligatures (which `normalize("NFD")` alone doesn't decompose) — for accent/case-insensitive search matching. |
308
- | `rankByNameMatch(items, query, getName)` | Ranks `items` by relevance to `query`: earlier match position first, then shorter name — for a search-as-you-type list. |
309
- | `escapeCsvField(value)` | Quotes and escapes a CSV field (RFC 4180) only if it contains a `"`, `;`, or newline. |
310
- | `formatCsvNumber(value, decimals?)` | Formats a number with a comma decimal separator (French-locale spreadsheets) instead of JS's dot. |
311
- | `escapeHtml(text)` | Basic HTML entity escaping (`&`, `<`, `>`, `"`) for inserting user text into an HTML template. |
312
-
313
- ## Scripts
314
-
315
- ```bash
316
- pnpm run dev # tsc --watch -> dist/
317
- pnpm run build # tsc -> dist/
318
- pnpm run check-types # tsc --noEmit
319
- pnpm run lint # eslint
320
- pnpm run test # vitest run
321
- pnpm run test:watch # vitest
322
- ```
323
-
324
- Built to `dist/` (see the `exports` field in `package.json`), so run `pnpm run build` (or `dev`) after source changes for consumers to see them.
325
-
326
- ### Testing note
327
-
328
- `react-native`'s package entry uses Flow syntax that `@babel/parser`'s flow plugin can't parse (`as Cast` casts aren't supported there), so Vitest can never load the real package directly. Tests alias `react-native` to a minimal stub (`src/__mocks__/react-native.tsx`) instead — see that file for details. `react-native-gesture-handler` ships the same kind of Flow syntax, and `@expo/vector-icons` has its own unrelated ESM-resolution issue under Vite/Rollup — both are stubbed the same way (`src/__mocks__/react-native-gesture-handler.tsx`, `src/__mocks__/expo-vector-icons.tsx`). `expo-speech-recognition` and `expo-image-picker` wrap native modules unavailable under Vitest regardless, so they're mocked with plain spies (`src/__mocks__/expo-speech-recognition.tsx`, `src/__mocks__/expo-image-picker.ts`).
1
+ # @forthtilliath/react-native-kit
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 so consumers only pull in what they use.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @forthtilliath/react-native-kit react react-native
9
+ ```
10
+
11
+ Or, from within this monorepo, as a workspace dependency:
12
+
13
+ ```json
14
+ {
15
+ "dependencies": {
16
+ "@forthtilliath/react-native-kit": "workspace:*"
17
+ }
18
+ }
19
+ ```
20
+
21
+ `react` and `react-native` are peer dependencies — install them in the consuming app if not already present. `@expo/vector-icons`, `expo-image-picker`, `expo-speech-recognition`, and `react-native-gesture-handler` are also peer dependencies, needed only if you import the components that use them (`Thumbnail`/`PickerModal`/`SwipeableRow`, `PhotoPicker`, `VoiceSearchButton`/`PickerModal`, `SwipeableRow`, respectively).
22
+
23
+ ## Usage
24
+
25
+ Each component is its own module — import the file you need directly:
26
+
27
+ ```ts
28
+ import { ChangelogNotes } from "@forthtilliath/react-native-kit/ChangelogNotes";
29
+ ```
30
+
31
+ This is the recommended way to import: Metro (React Native's bundler) doesn't reliably tree-shake, so pulling from a single deep-import path keeps peer dependencies you don't use (`expo-image-picker`, `expo-speech-recognition`, `react-native-gesture-handler`...) out of your bundle entirely, rather than merely unused.
32
+
33
+ A root barrel is also available for convenience:
34
+
35
+ ```ts
36
+ import {
37
+ ChangelogNotes,
38
+ useSubmitGuard,
39
+ } from "@forthtilliath/react-native-kit";
40
+ ```
41
+
42
+ **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
+
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
+ ### `<Thumbnail photoUri={...} placeholderIcon="..." />`
87
+
88
+ List-row thumbnail: the photo if there is one, otherwise a placeholder icon.
89
+
90
+ ```tsx
91
+ import { Thumbnail } from "@forthtilliath/react-native-kit/Thumbnail";
92
+
93
+ <Thumbnail
94
+ photoUri={container.photoUri}
95
+ placeholderIcon="cube-outline"
96
+ size={48}
97
+ />;
98
+ ```
99
+
100
+ ### `<SwipeableRow onDelete={...} deleteLabel="...">`
101
+
102
+ Swipe a list row left to reveal a delete button, on top of a tap to edit it.
103
+
104
+ ```tsx
105
+ import { SwipeableRow } from "@forthtilliath/react-native-kit/SwipeableRow";
106
+
107
+ <SwipeableRow
108
+ onDelete={() => remove(item.id)}
109
+ deleteLabel={`Delete ${item.name}`}
110
+ >
111
+ <ItemRow item={item} />
112
+ </SwipeableRow>;
113
+ ```
114
+
115
+ ### `<VoiceSearchButton onResult={...} />`
116
+
117
+ Microphone button to dictate a search instead of typing it. Safe to mount more than one at a time (e.g. a name field plus a search picker on the same screen) — only the instance that started listening reacts to its result.
118
+
119
+ ```tsx
120
+ import { VoiceSearchButton } from "@forthtilliath/react-native-kit/VoiceSearchButton";
121
+
122
+ <VoiceSearchButton onResult={setQuery} lang="en-US" />;
123
+ ```
124
+
125
+ ### `<PhotoPicker photoUri={...} onChange={...} savePhoto={...} photoLabel="..." />`
126
+
127
+ Photo picker (camera or library) with a preview and a remove link. `savePhoto` is injected rather than hard-coded, so where/how the picked image gets persisted is entirely up to the caller.
128
+
129
+ ```tsx
130
+ import { PhotoPicker } from "@forthtilliath/react-native-kit/PhotoPicker";
131
+
132
+ <PhotoPicker
133
+ photoUri={container.photoUri}
134
+ onChange={(uri) => setPhotoUri(uri)}
135
+ savePhoto={(sourceUri) => saveContainerPhoto(sourceUri)}
136
+ photoLabel="of the container"
137
+ />;
138
+ ```
139
+
140
+ ### `<PickerModal visible title items onSelect onClose />`
141
+
142
+ Full-screen picker: search (typed or dictated via `VoiceSearchButton`), optional sections, and "add" actions always visible above the results.
143
+
144
+ ```tsx
145
+ import {
146
+ PickerModal,
147
+ type PickerItem,
148
+ } from "@forthtilliath/react-native-kit/PickerModal";
149
+
150
+ <PickerModal
151
+ visible={pickerVisible}
152
+ title="Choose a container"
153
+ items={containers.map((c): PickerItem => ({
154
+ id: c.id,
155
+ label: c.name,
156
+ imageUri: c.photoUri,
157
+ }))}
158
+ onSelect={(item) => setContainerId(item.id)}
159
+ onClose={() => setPickerVisible(false)}
160
+ extraActions={[
161
+ {
162
+ label: "Add a new container",
163
+ onPress: () => router.push("/containers/new"),
164
+ },
165
+ ]}
166
+ />;
167
+ ```
168
+
169
+ Groups results into sections via each item's `group` (e.g. food groups, ingredients vs recipes) — ignored while searching, where the best global matches are shown instead. Pass `filterItems` for custom ranking (e.g. `rankByNameMatch` from this same package's `utils/`) instead of the default case-insensitive substring match.
170
+
171
+ For all 5 components above, styling and (where relevant) copy work the same way as `ChangelogNotes`: an optional `styles` prop (all fields optional, neutral defaults) and, for `PhotoPicker`/`PickerModal`, an optional `labels` prop for the built-in French copy.
172
+
173
+ ### `useSubmitGuard()`
174
+
175
+ Prevents a second call while a first one is still pending — e.g. a double-tap on a "Save" button before it's had time to disable, which would otherwise create duplicate submissions.
176
+
177
+ ```tsx
178
+ import { useSubmitGuard } from "@forthtilliath/react-native-kit/useSubmitGuard";
179
+
180
+ const { isSaving, guard } = useSubmitGuard();
181
+
182
+ <Pressable
183
+ disabled={isSaving}
184
+ onPress={() =>
185
+ guard(async () => {
186
+ await save();
187
+ })
188
+ }
189
+ >
190
+ <Text>{isSaving ? "Saving…" : "Save"}</Text>
191
+ </Pressable>;
192
+ ```
193
+
194
+ ### `useDebouncedChange(values, delayMs, callback)`
195
+
196
+ Calls `callback` `delayMs` after the last change among `values`, ignoring renders where any value is still `undefined` (not loaded yet) and the very first render where they're all defined (no trigger on mount). A new change before the delay elapses resets the timer — a real debounce, not a throttle.
197
+
198
+ ```tsx
199
+ import { useDebouncedChange } from "@forthtilliath/react-native-kit/useDebouncedChange";
200
+
201
+ useDebouncedChange([settingsData, itemsData], 5 * 60 * 1000, () => {
202
+ runAutoBackup();
203
+ });
204
+ ```
205
+
206
+ ### `confirmDestructive(title, onConfirm, options?)`
207
+
208
+ Generic destructive-action confirmation (title + message + Cancel/Confirm), for anything irreversible (delete, reset...). Ships with French defaults (`message`, `cancelLabel`, `confirmLabel` all overridable).
209
+
210
+ ```ts
211
+ import { confirmDestructive } from "@forthtilliath/react-native-kit/confirmDestructive";
212
+
213
+ confirmDestructive("Delete this item?", () => deleteItem(id), {
214
+ message: "This cannot be undone.",
215
+ cancelLabel: "Cancel",
216
+ confirmLabel: "Delete",
217
+ });
218
+ ```
219
+
220
+ ### `useEffectiveColorScheme(preference)`
221
+
222
+ Resolves a `"light" | "dark" | "system"` theme preference against the device's color scheme: `"system"` follows the device, `"light"`/`"dark"` override it regardless of what the device is set to.
223
+
224
+ ```tsx
225
+ import { useEffectiveColorScheme } from "@forthtilliath/react-native-kit/useEffectiveColorScheme";
226
+
227
+ const scheme = useEffectiveColorScheme(themePreference); // "light" | "dark"
228
+ const colors = scheme === "dark" ? darkColors : lightColors;
229
+ ```
230
+
231
+ ### `<ThemeToggle value onChange />`
232
+
233
+ 3-way segmented control for a light/dark/system theme preference.
234
+
235
+ ```tsx
236
+ import { ThemeToggle } from "@forthtilliath/react-native-kit/ThemeToggle";
237
+
238
+ <ThemeToggle value={themePreference} onChange={setThemePreference} />;
239
+ ```
240
+
241
+ ### `<ThemeOptionList value onChange />`
242
+
243
+ Same light/dark/system data contract as `ThemeToggle`, but rendered as a list of full-width rows (icon + label + checkmark on the active one) instead of a segmented control.
244
+
245
+ ```tsx
246
+ import { ThemeOptionList } from "@forthtilliath/react-native-kit/ThemeOptionList";
247
+
248
+ <ThemeOptionList value={themePreference} onChange={setThemePreference} />;
249
+ ```
250
+
251
+ ### `useUpdateCheck(options)`
252
+
253
+ 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.
254
+
255
+ ```tsx
256
+ import { useUpdateCheck } from "@forthtilliath/react-native-kit/useUpdateCheck";
257
+
258
+ const update = useUpdateCheck({
259
+ currentVersion: Constants.expoConfig?.version ?? "0.0.0",
260
+ checkForUpdate: fetchLatestRelease,
261
+ compareVersions,
262
+ getLastCheck: () => ({
263
+ lastCheckedAt: settings?.lastUpdateCheckAt ?? null,
264
+ dismissedVersion: settings?.dismissedUpdateVersion ?? null,
265
+ }),
266
+ onChecked: (lastCheckedAt) =>
267
+ updateSettings({ lastUpdateCheckAt: lastCheckedAt }),
268
+ });
269
+
270
+ if (update.status === "available") {
271
+ // update.release.version / .notes / .apkUrl
272
+ }
273
+ ```
274
+
275
+ ### `<UpdateAvailableBanner version notes onPress onDismiss />`
276
+
277
+ 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.
278
+
279
+ ```tsx
280
+ import { UpdateAvailableBanner } from "@forthtilliath/react-native-kit/UpdateAvailableBanner";
281
+
282
+ {
283
+ update.status === "available" && (
284
+ <UpdateAvailableBanner
285
+ version={update.release.version}
286
+ notes={update.release.notes}
287
+ onPress={() => router.push("/settings/update")}
288
+ onDismiss={() => {
289
+ dismissUpdateVersion(update.release.version);
290
+ update.dismiss();
291
+ }}
292
+ />
293
+ );
294
+ }
295
+ ```
296
+
297
+ ### Utils (`utils/`)
298
+
299
+ Framework-agnostic pure functions — no React or React Native import, usable from Node/web too.
300
+
301
+ ```ts
302
+ import { getPeriodStartMs } from "@forthtilliath/react-native-kit/utils/getPeriodStartMs";
303
+ import { getMostRecentIds } from "@forthtilliath/react-native-kit/utils/getMostRecentIds";
304
+ import { nextInCycle } from "@forthtilliath/react-native-kit/utils/nextInCycle";
305
+ import { normalizeForSearch } from "@forthtilliath/react-native-kit/utils/normalizeForSearch";
306
+ import { rankByNameMatch } from "@forthtilliath/react-native-kit/utils/rankByNameMatch";
307
+ import { escapeCsvField } from "@forthtilliath/react-native-kit/utils/escapeCsvField";
308
+ import { formatCsvNumber } from "@forthtilliath/react-native-kit/utils/formatCsvNumber";
309
+ import { escapeHtml } from "@forthtilliath/react-native-kit/utils/escapeHtml";
310
+ ```
311
+
312
+ | Function | What it does |
313
+ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
314
+ | `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. |
315
+ | `getMostRecentIds(rows, limit?)` | Most recently occurring distinct ids (`{ id, occurredAt }[]`), most recent first, deduplicated, limited (default 5). |
316
+ | `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. |
317
+ | `normalizeForSearch(text)` | Lowercases, trims, strips accents, and expands `œ`/`æ` ligatures (which `normalize("NFD")` alone doesn't decompose) — for accent/case-insensitive search matching. |
318
+ | `rankByNameMatch(items, query, getName)` | Ranks `items` by relevance to `query`: earlier match position first, then shorter name — for a search-as-you-type list. |
319
+ | `escapeCsvField(value)` | Quotes and escapes a CSV field (RFC 4180) only if it contains a `"`, `;`, or newline. |
320
+ | `formatCsvNumber(value, decimals?)` | Formats a number with a comma decimal separator (French-locale spreadsheets) instead of JS's dot. |
321
+ | `escapeHtml(text)` | Basic HTML entity escaping (`&`, `<`, `>`, `"`) for inserting user text into an HTML template. |
322
+
323
+ ## Scripts
324
+
325
+ ```bash
326
+ pnpm run dev # tsc --watch -> dist/
327
+ pnpm run build # tsc -> dist/
328
+ pnpm run check-types # tsc --noEmit
329
+ pnpm run lint # eslint
330
+ pnpm run test # vitest run
331
+ pnpm run test:watch # vitest
332
+ ```
333
+
334
+ Built to `dist/` (see the `exports` field in `package.json`), so run `pnpm run build` (or `dev`) after source changes for consumers to see them.
335
+
336
+ ### Testing note
337
+
338
+ `react-native`'s package entry uses Flow syntax that `@babel/parser`'s flow plugin can't parse (`as Cast` casts aren't supported there), so Vitest can never load the real package directly. Tests alias `react-native` to a minimal stub (`src/__mocks__/react-native.tsx`) instead — see that file for details. `react-native-gesture-handler` ships the same kind of Flow syntax, and `@expo/vector-icons` has its own unrelated ESM-resolution issue under Vite/Rollup — both are stubbed the same way (`src/__mocks__/react-native-gesture-handler.tsx`, `src/__mocks__/expo-vector-icons.tsx`). `expo-speech-recognition` and `expo-image-picker` wrap native modules unavailable under Vitest regardless, so they're mocked with plain spies (`src/__mocks__/expo-speech-recognition.tsx`, `src/__mocks__/expo-image-picker.ts`).
@@ -1 +1 @@
1
- {"version":3,"file":"ChangelogNotes.d.ts","sourceRoot":"","sources":["../src/ChangelogNotes.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAMpE,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/B,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,QAAQ,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC5B,IAAI,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IAClC,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,MAAM,CAAC,EAAE,oBAAoB,CAAC;CAC/B;AA0BD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,mBAAmB,+BAoCpE"}
1
+ {"version":3,"file":"ChangelogNotes.d.ts","sourceRoot":"","sources":["../src/ChangelogNotes.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAMpE,MAAM,WAAW,oBAAoB;IACnC,OAAO,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/B,OAAO,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC/B,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,QAAQ,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAChC,IAAI,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC5B,IAAI,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC7B;AAED,MAAM,WAAW,mBAAmB;IAClC,mEAAmE;IACnE,KAAK,EAAE,MAAM,CAAC;IACd,oEAAoE;IACpE,MAAM,CAAC,EAAE,oBAAoB,CAAC;CAC/B;AA0BD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE,mBAAmB,+BA+CpE"}
@@ -22,7 +22,18 @@ function renderSegments(segments, boldStyle) {
22
22
  * Markdown syntax in a plain `<Text>`.
23
23
  */
24
24
  export function ChangelogNotes({ notes, styles }) {
25
- const merged = { ...defaultStyles, ...styles };
25
+ // Each field is merged as a style array (default, then override) rather
26
+ // than the override replacing the whole default outright — so passing e.g.
27
+ // `styles={{ heading: { color: "red" } }}` only changes the color instead
28
+ // of silently dropping the default's fontSize/fontWeight/marginTop too.
29
+ const merged = {
30
+ heading: [defaultStyles.heading, styles?.heading],
31
+ itemRow: [defaultStyles.itemRow, styles?.itemRow],
32
+ bullet: [defaultStyles.bullet, styles?.bullet],
33
+ itemText: [defaultStyles.itemText, styles?.itemText],
34
+ text: [defaultStyles.text, styles?.text],
35
+ bold: [defaultStyles.bold, styles?.bold],
36
+ };
26
37
  const blocks = parseChangelogNotes(notes);
27
38
  return (_jsx(_Fragment, { children: blocks.map((block, index) => {
28
39
  // Blocks have no stable identity of their own (plain parsed lines),
@@ -1 +1 @@
1
- {"version":3,"file":"PhotoPicker.d.ts","sourceRoot":"","sources":["../src/PhotoPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAKhF,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,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,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;CACtB;AAED,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,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAkDD,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,MAAM,EACN,MAAM,GACP,EAAE,gBAAgB,+BAoGlB"}
1
+ {"version":3,"file":"PhotoPicker.d.ts","sourceRoot":"","sources":["../src/PhotoPicker.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAKhF,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,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,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;CACtB;AAED,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,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AAkDD,wBAAgB,WAAW,CAAC,EAC1B,QAAQ,EACR,QAAQ,EACR,SAAS,EACT,UAAU,EACV,MAAM,EACN,MAAM,GACP,EAAE,gBAAgB,+BAoHlB"}
@@ -47,7 +47,23 @@ const defaultLabels = {
47
47
  };
48
48
  // Photo picker (camera or library) with a preview.
49
49
  export function PhotoPicker({ photoUri, onChange, savePhoto, photoLabel, labels, styles, }) {
50
- const mergedStyles = { ...defaultStyles, ...styles };
50
+ // Style fields are merged as arrays (default, then override) so a partial
51
+ // override only changes the properties it specifies instead of replacing
52
+ // the whole default style object.
53
+ const mergedStyles = {
54
+ photoPicker: [defaultStyles.photoPicker, styles?.photoPicker],
55
+ photoPreview: [defaultStyles.photoPreview, styles?.photoPreview],
56
+ photoPlaceholder: [
57
+ defaultStyles.photoPlaceholder,
58
+ styles?.photoPlaceholder,
59
+ ],
60
+ photoPlaceholderText: [
61
+ defaultStyles.photoPlaceholderText,
62
+ styles?.photoPlaceholderText,
63
+ ],
64
+ clearLink: [defaultStyles.clearLink, styles?.clearLink],
65
+ iconColor: styles?.iconColor ?? defaultStyles.iconColor,
66
+ };
51
67
  const t = { ...defaultLabels, ...labels };
52
68
  async function pickFromCamera() {
53
69
  const permission = await ImagePicker.requestCameraPermissionsAsync();
@@ -1 +1 @@
1
- {"version":3,"file":"PickerModal.d.ts","sourceRoot":"","sources":["../src/PickerModal.tsx"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"PickerModal.d.ts","sourceRoot":"","sources":["../src/PickerModal.tsx"],"names":[],"mappings":"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"}
@@ -88,7 +88,37 @@ const defaultLabels = {
88
88
  // Full-screen picker: search (typed or dictated), optional sections, and
89
89
  // "add" actions always visible above the results.
90
90
  export function PickerModal({ visible, title, items, onSelect, onClose, emptyMessage, initialQuery, filterItems, extraActions, groupOrder, labels, styles, }) {
91
- const merged = useMemo(() => ({ ...defaultStyles, ...styles }), [styles]);
91
+ // Style fields are merged as arrays (default, then override) so a partial
92
+ // override only changes the properties it specifies instead of replacing
93
+ // the whole default style object (e.g. losing rowThumbnailPlaceholder's
94
+ // alignItems/justifyContent by only overriding its backgroundColor).
95
+ const merged = useMemo(() => ({
96
+ container: [defaultStyles.container, styles?.container],
97
+ header: [defaultStyles.header, styles?.header],
98
+ title: [defaultStyles.title, styles?.title],
99
+ close: [defaultStyles.close, styles?.close],
100
+ searchRow: [defaultStyles.searchRow, styles?.searchRow],
101
+ search: [defaultStyles.search, styles?.search],
102
+ row: [defaultStyles.row, styles?.row],
103
+ rowThumbnail: [defaultStyles.rowThumbnail, styles?.rowThumbnail],
104
+ rowThumbnailPlaceholder: [
105
+ defaultStyles.rowThumbnailPlaceholder,
106
+ styles?.rowThumbnailPlaceholder,
107
+ ],
108
+ rowLabel: [defaultStyles.rowLabel, styles?.rowLabel],
109
+ rowSubtitle: [defaultStyles.rowSubtitle, styles?.rowSubtitle],
110
+ empty: [defaultStyles.empty, styles?.empty],
111
+ sectionHeader: [defaultStyles.sectionHeader, styles?.sectionHeader],
112
+ extraActions: [defaultStyles.extraActions, styles?.extraActions],
113
+ extraActionLabel: [
114
+ defaultStyles.extraActionLabel,
115
+ styles?.extraActionLabel,
116
+ ],
117
+ extraActionIconColor: styles?.extraActionIconColor ?? defaultStyles.extraActionIconColor,
118
+ rowThumbnailPlaceholderIconColor: styles?.rowThumbnailPlaceholderIconColor ??
119
+ defaultStyles.rowThumbnailPlaceholderIconColor,
120
+ placeholderTextColor: styles?.placeholderTextColor ?? defaultStyles.placeholderTextColor,
121
+ }), [styles]);
92
122
  const t = useMemo(() => ({ ...defaultLabels, ...labels }), [labels]);
93
123
  const [query, setQuery] = useState(initialQuery ?? "");
94
124
  // Reset the search each time the modal (re-)opens — adjusted during render
@@ -1 +1 @@
1
- {"version":3,"file":"SwipeableRow.d.ts","sourceRoot":"","sources":["../src/SwipeableRow.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAKpE,MAAM,WAAW,kBAAkB;IACjC,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,gBAAgB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,kBAAkB,CAAC;CAC7B;AAiBD,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,UAAqB,EACrB,MAAM,GACP,EAAE,iBAAiB,+BA6BnB"}
1
+ {"version":3,"file":"SwipeableRow.d.ts","sourceRoot":"","sources":["../src/SwipeableRow.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAEvC,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAKpE,MAAM,WAAW,kBAAkB;IACjC,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,gBAAgB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACxC,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,SAAS,CAAC;IACpB,QAAQ,EAAE,MAAM,IAAI,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,MAAM,CAAC,EAAE,kBAAkB,CAAC;CAC7B;AAiBD,wBAAgB,YAAY,CAAC,EAC3B,QAAQ,EACR,QAAQ,EACR,WAAW,EACX,UAAqB,EACrB,MAAM,GACP,EAAE,iBAAiB,+BAuCnB"}
@@ -18,7 +18,17 @@ const defaultStyles = {
18
18
  };
19
19
  // Swipe a list row left to reveal a delete button, on top of a tap to edit it.
20
20
  export function SwipeableRow({ children, onDelete, deleteLabel, deleteText = "Delete", styles, }) {
21
- const merged = { ...defaultStyles, ...styles };
21
+ // Style fields are merged as arrays (default, then override) so a partial
22
+ // override (e.g. just backgroundColor) doesn't drop the default's
23
+ // width/borderRadius/marginBottom/gap too.
24
+ const merged = {
25
+ deleteAction: [defaultStyles.deleteAction, styles?.deleteAction],
26
+ deleteActionText: [
27
+ defaultStyles.deleteActionText,
28
+ styles?.deleteActionText,
29
+ ],
30
+ deleteIconColor: styles?.deleteIconColor ?? defaultStyles.deleteIconColor,
31
+ };
22
32
  const swipeableRef = useRef(null);
23
33
  return (_jsx(Swipeable, { ref: swipeableRef, renderRightActions: () => (_jsxs(Pressable, { style: merged.deleteAction, onPress: () => {
24
34
  swipeableRef.current?.close();
@@ -0,0 +1,28 @@
1
+ import type { StyleProp, TextStyle, ViewStyle } from "react-native";
2
+ import { Ionicons } from "@expo/vector-icons";
3
+ import type { ThemeToggleLabels } from "./ThemeToggle.js";
4
+ import type { ThemePreference } from "./useEffectiveColorScheme.js";
5
+ export interface ThemeOptionListIcons {
6
+ light?: keyof typeof Ionicons.glyphMap;
7
+ dark?: keyof typeof Ionicons.glyphMap;
8
+ system?: keyof typeof Ionicons.glyphMap;
9
+ }
10
+ export interface ThemeOptionListStyles {
11
+ container?: StyleProp<ViewStyle>;
12
+ row?: StyleProp<ViewStyle>;
13
+ rowActive?: StyleProp<ViewStyle>;
14
+ label?: StyleProp<TextStyle>;
15
+ labelActive?: StyleProp<TextStyle>;
16
+ iconColor?: string;
17
+ iconColorActive?: string;
18
+ checkColor?: string;
19
+ }
20
+ export interface ThemeOptionListProps {
21
+ value: ThemePreference;
22
+ onChange: (preference: ThemePreference) => void;
23
+ labels?: ThemeToggleLabels;
24
+ icons?: ThemeOptionListIcons;
25
+ styles?: ThemeOptionListStyles;
26
+ }
27
+ export declare function ThemeOptionList({ value, onChange, labels, icons, styles, }: ThemeOptionListProps): import("react").JSX.Element;
28
+ //# sourceMappingURL=ThemeOptionList.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"ThemeOptionList.d.ts","sourceRoot":"","sources":["../src/ThemeOptionList.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAC1D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAEpE,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,MAAM,OAAO,QAAQ,CAAC,QAAQ,CAAC;IACvC,IAAI,CAAC,EAAE,MAAM,OAAO,QAAQ,CAAC,QAAQ,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,OAAO,QAAQ,CAAC,QAAQ,CAAC;CACzC;AAED,MAAM,WAAW,qBAAqB;IACpC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,GAAG,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC3B,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,WAAW,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,IAAI,CAAC;IAChD,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,MAAM,CAAC,EAAE,qBAAqB,CAAC;CAChC;AA+CD,wBAAgB,eAAe,CAAC,EAC9B,KAAK,EACL,QAAQ,EACR,MAAM,EACN,KAAK,EACL,MAAM,GACP,EAAE,oBAAoB,+BAiDtB"}
@@ -0,0 +1,67 @@
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
+ const OPTIONS = ["light", "dark", "system"];
5
+ const defaultLabels = {
6
+ light: "Clair",
7
+ dark: "Sombre",
8
+ system: "Système",
9
+ };
10
+ const defaultIcons = {
11
+ light: "sunny-outline",
12
+ dark: "moon-outline",
13
+ system: "phone-portrait-outline",
14
+ };
15
+ const defaultStyles = {
16
+ container: { gap: 8 },
17
+ row: {
18
+ flexDirection: "row",
19
+ alignItems: "center",
20
+ gap: 10,
21
+ borderWidth: 1,
22
+ borderColor: "#e5e7eb",
23
+ borderRadius: 10,
24
+ padding: 14,
25
+ backgroundColor: "#ffffff",
26
+ },
27
+ rowActive: {
28
+ borderColor: "#2563eb",
29
+ backgroundColor: "#2563eb1a",
30
+ },
31
+ label: {
32
+ flex: 1,
33
+ fontSize: 15,
34
+ fontWeight: "600",
35
+ color: "#111827",
36
+ },
37
+ labelActive: { color: "#2563eb" },
38
+ iconColor: "#111827",
39
+ iconColorActive: "#2563eb",
40
+ checkColor: "#2563eb",
41
+ };
42
+ // Selectable list of rows (icon + label + checkmark) for a light/dark/system
43
+ // theme preference. Same data contract as ThemeToggle, but a taller row-based
44
+ // layout instead of a 3-way segmented control.
45
+ export function ThemeOptionList({ value, onChange, labels, icons, styles, }) {
46
+ // Style fields are merged as arrays (default, then override) so a partial
47
+ // override only changes the properties it specifies instead of replacing
48
+ // the whole default style object.
49
+ const merged = {
50
+ container: [defaultStyles.container, styles?.container],
51
+ row: [defaultStyles.row, styles?.row],
52
+ rowActive: [defaultStyles.rowActive, styles?.rowActive],
53
+ label: [defaultStyles.label, styles?.label],
54
+ labelActive: [defaultStyles.labelActive, styles?.labelActive],
55
+ };
56
+ const t = { ...defaultLabels, ...labels };
57
+ const i = { ...defaultIcons, ...icons };
58
+ const iconColor = styles?.iconColor ?? defaultStyles.iconColor;
59
+ const iconColorActive = styles?.iconColorActive ?? defaultStyles.iconColorActive;
60
+ const checkColor = styles?.checkColor ?? defaultStyles.checkColor;
61
+ return (_jsx(View, { style: merged.container, children: OPTIONS.map((option) => {
62
+ const active = option === value;
63
+ return (_jsxs(Pressable, { style: [merged.row, active && merged.rowActive], onPress: () => {
64
+ onChange(option);
65
+ }, accessibilityRole: "radio", accessibilityState: { checked: active }, accessibilityLabel: t[option], children: [_jsx(Ionicons, { name: i[option], size: 20, color: active ? iconColorActive : iconColor }), _jsx(Text, { style: [merged.label, active && merged.labelActive], children: t[option] }), active && (_jsx(Ionicons, { name: "checkmark-circle", size: 20, color: checkColor }))] }, option));
66
+ }) }));
67
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"ThemeToggle.d.ts","sourceRoot":"","sources":["../src/ThemeToggle.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAEpE,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,gBAAgB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,IAAI,CAAC;IAChD,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AA2BD,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,MAAM,EACN,MAAM,GACP,EAAE,gBAAgB,+BA6BlB"}
1
+ {"version":3,"file":"ThemeToggle.d.ts","sourceRoot":"","sources":["../src/ThemeToggle.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGpE,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,8BAA8B,CAAC;AAEpE,MAAM,WAAW,iBAAiB;IAChC,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,UAAU,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAClC,gBAAgB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,KAAK,EAAE,eAAe,CAAC;IACvB,QAAQ,EAAE,CAAC,UAAU,EAAE,eAAe,KAAK,IAAI,CAAC;IAChD,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B;AA2BD,wBAAgB,WAAW,CAAC,EAC1B,KAAK,EACL,QAAQ,EACR,MAAM,EACN,MAAM,GACP,EAAE,gBAAgB,+BAyClB"}
@@ -23,7 +23,19 @@ const defaultLabels = {
23
23
  };
24
24
  // 3-way segmented control for a light/dark/system theme preference.
25
25
  export function ThemeToggle({ value, onChange, labels, styles, }) {
26
- const merged = { ...defaultStyles, ...styles };
26
+ // Style fields are merged as arrays (default, then override) so a partial
27
+ // override only changes the properties it specifies instead of replacing
28
+ // the whole default style object.
29
+ const merged = {
30
+ container: [defaultStyles.container, styles?.container],
31
+ option: [defaultStyles.option, styles?.option],
32
+ optionActive: [defaultStyles.optionActive, styles?.optionActive],
33
+ optionText: [defaultStyles.optionText, styles?.optionText],
34
+ optionTextActive: [
35
+ defaultStyles.optionTextActive,
36
+ styles?.optionTextActive,
37
+ ],
38
+ };
27
39
  const t = { ...defaultLabels, ...labels };
28
40
  return (_jsx(View, { style: merged.container, children: OPTIONS.map((option) => {
29
41
  const active = option === value;
@@ -1 +1 @@
1
- {"version":3,"file":"Thumbnail.d.ts","sourceRoot":"","sources":["../src/Thumbnail.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAErE,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IAClC,WAAW,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,iFAAiF;IACjF,eAAe,EAAE,MAAM,OAAO,QAAQ,CAAC,QAAQ,CAAC;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAgBD,wBAAgB,SAAS,CAAC,EACxB,QAAQ,EACR,eAAe,EACf,IAAmB,EACnB,MAAM,GACP,EAAE,cAAc,+BAuBhB"}
1
+ {"version":3,"file":"Thumbnail.d.ts","sourceRoot":"","sources":["../src/Thumbnail.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAErE,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,MAAM,WAAW,eAAe;IAC9B,SAAS,CAAC,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;IAClC,WAAW,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACnC,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,iFAAiF;IACjF,eAAe,EAAE,MAAM,OAAO,QAAQ,CAAC,QAAQ,CAAC;IAChD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,eAAe,CAAC;CAC1B;AAgBD,wBAAgB,SAAS,CAAC,EACxB,QAAQ,EACR,eAAe,EACf,IAAmB,EACnB,MAAM,GACP,EAAE,cAAc,+BA+BhB"}
package/dist/Thumbnail.js CHANGED
@@ -14,7 +14,15 @@ const defaultStyles = {
14
14
  };
15
15
  // List-row thumbnail (photo, or a placeholder icon when there is none).
16
16
  export function Thumbnail({ photoUri, placeholderIcon, size = DEFAULT_SIZE, styles, }) {
17
- const merged = { ...defaultStyles, ...styles };
17
+ // Style fields are merged as arrays (default, then override) so a partial
18
+ // override (e.g. just backgroundColor) doesn't drop the default's
19
+ // borderRadius, or the placeholder's alignItems/justifyContent that center
20
+ // the icon.
21
+ const merged = {
22
+ thumbnail: [defaultStyles.thumbnail, styles?.thumbnail],
23
+ placeholder: [defaultStyles.placeholder, styles?.placeholder],
24
+ iconColor: styles?.iconColor ?? defaultStyles.iconColor,
25
+ };
18
26
  const dimensions = { width: size, height: size };
19
27
  if (photoUri) {
20
28
  return (_jsx(Image, { source: { uri: photoUri }, style: [dimensions, merged.thumbnail], accessibilityIgnoresInvertColors: true }));
@@ -1 +1 @@
1
- {"version":3,"file":"UpdateAvailableBanner.d.ts","sourceRoot":"","sources":["../src/UpdateAvailableBanner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAGhE,MAAM,WAAW,2BAA2B;IAC1C,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,gBAAgB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACxC,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACrC,iBAAiB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,2BAA2B,CAAC;IACrC,MAAM,CAAC,EAAE,2BAA2B,CAAC;CACtC;AAwCD,wBAAgB,qBAAqB,CAAC,EACpC,OAAO,EACP,KAAK,EACL,OAAO,EACP,SAAS,EACT,MAAM,EACN,MAAM,GACP,EAAE,0BAA0B,+BA4B5B"}
1
+ {"version":3,"file":"UpdateAvailableBanner.d.ts","sourceRoot":"","sources":["../src/UpdateAvailableBanner.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAGpE,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAGhE,MAAM,WAAW,2BAA2B;IAC1C,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACjC,MAAM,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC9B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,KAAK,CAAC,EAAE,oBAAoB,CAAC;IAC7B,YAAY,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACpC,gBAAgB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACxC,aAAa,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IACrC,iBAAiB,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC1C;AAED,MAAM,WAAW,2BAA2B;IAC1C,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;IACpC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yBAAyB,CAAC,EAAE,MAAM,CAAC;CACpC;AAED,MAAM,WAAW,0BAA0B;IACzC,OAAO,EAAE,MAAM,CAAC;IAChB,8DAA8D;IAC9D,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,SAAS,EAAE,MAAM,IAAI,CAAC;IACtB,MAAM,CAAC,EAAE,2BAA2B,CAAC;IACrC,MAAM,CAAC,EAAE,2BAA2B,CAAC;CACtC;AAwCD,wBAAgB,qBAAqB,CAAC,EACpC,OAAO,EACP,KAAK,EACL,OAAO,EACP,SAAS,EACT,MAAM,EACN,MAAM,GACP,EAAE,0BAA0B,+BA+C5B"}
@@ -38,7 +38,26 @@ const defaultLabels = {
38
38
  // what "action" does (e.g. navigate to an update screen) or on how/whether
39
39
  // dismissal is persisted — both are left entirely to the caller.
40
40
  export function UpdateAvailableBanner({ version, notes, onPress, onDismiss, labels, styles, }) {
41
- const merged = { ...defaultStyles, ...styles };
41
+ // Style fields are merged as arrays (default, then override) so a partial
42
+ // override only changes the properties it specifies instead of replacing
43
+ // the whole default style object. `notes` is forwarded as-is to
44
+ // ChangelogNotes, which does its own equivalent merge against its defaults.
45
+ const merged = {
46
+ container: [defaultStyles.container, styles?.container],
47
+ header: [defaultStyles.header, styles?.header],
48
+ title: [defaultStyles.title, styles?.title],
49
+ notes: styles?.notes ?? defaultStyles.notes,
50
+ actionButton: [defaultStyles.actionButton, styles?.actionButton],
51
+ actionButtonText: [
52
+ defaultStyles.actionButtonText,
53
+ styles?.actionButtonText,
54
+ ],
55
+ dismissButton: [defaultStyles.dismissButton, styles?.dismissButton],
56
+ dismissButtonText: [
57
+ defaultStyles.dismissButtonText,
58
+ styles?.dismissButtonText,
59
+ ],
60
+ };
42
61
  const t = { ...defaultLabels, ...labels };
43
62
  return (_jsxs(View, { style: merged.container, children: [_jsxs(View, { style: merged.header, children: [_jsx(Text, { style: merged.title, children: t.title(version) }), _jsx(Pressable, { style: merged.dismissButton, onPress: onDismiss, accessibilityRole: "button", accessibilityLabel: t.dismissAccessibilityLabel, children: _jsx(Text, { style: merged.dismissButtonText, children: t.dismiss }) })] }), notes ? _jsx(ChangelogNotes, { notes: notes, styles: merged.notes }) : null, _jsx(Pressable, { style: merged.actionButton, onPress: onPress, accessibilityRole: "button", accessibilityLabel: t.action, children: _jsx(Text, { style: merged.actionButtonText, children: t.action }) })] }));
44
63
  }
package/dist/index.d.ts CHANGED
@@ -3,6 +3,7 @@ export * from "./confirmDestructive.js";
3
3
  export * from "./PhotoPicker.js";
4
4
  export * from "./PickerModal.js";
5
5
  export * from "./SwipeableRow.js";
6
+ export * from "./ThemeOptionList.js";
6
7
  export * from "./ThemeToggle.js";
7
8
  export * from "./Thumbnail.js";
8
9
  export * from "./UpdateAvailableBanner.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,cAAc,qBAAqB,CAAC;AACpC,cAAc,yBAAyB,CAAC;AACxC,cAAc,kBAAkB,CAAC;AACjC,cAAc,kBAAkB,CAAC;AACjC,cAAc,mBAAmB,CAAC;AAClC,cAAc,sBAAsB,CAAC;AACrC,cAAc,kBAAkB,CAAC;AACjC,cAAc,gBAAgB,CAAC;AAC/B,cAAc,4BAA4B,CAAC;AAC3C,cAAc,yBAAyB,CAAC;AACxC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,qBAAqB,CAAC;AACpC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,uBAAuB,CAAC;AACtC,cAAc,4BAA4B,CAAC;AAC3C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,wBAAwB,CAAC;AACvC,cAAc,+BAA+B,CAAC;AAC9C,cAAc,4BAA4B,CAAC;AAC3C,cAAc,wBAAwB,CAAC"}
package/dist/index.js CHANGED
@@ -7,6 +7,7 @@ export * from "./confirmDestructive.js";
7
7
  export * from "./PhotoPicker.js";
8
8
  export * from "./PickerModal.js";
9
9
  export * from "./SwipeableRow.js";
10
+ export * from "./ThemeOptionList.js";
10
11
  export * from "./ThemeToggle.js";
11
12
  export * from "./Thumbnail.js";
12
13
  export * from "./UpdateAvailableBanner.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@forthtilliath/react-native-kit",
3
- "version": "0.7.1",
3
+ "version": "0.8.0",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "publishConfig": {
@@ -8,11 +8,11 @@
8
8
  },
9
9
  "repository": {
10
10
  "type": "git",
11
- "url": "https://github.com/Forthtilliath/turborepo.git",
11
+ "url": "https://github.com/Forthtilliath/forthtilliath-packages.git",
12
12
  "directory": "packages/react-native-kit"
13
13
  },
14
- "homepage": "https://github.com/Forthtilliath/turborepo/tree/main/packages/react-native-kit#readme",
15
- "bugs": "https://github.com/Forthtilliath/turborepo/issues",
14
+ "homepage": "https://github.com/Forthtilliath/forthtilliath-packages/tree/main/packages/react-native-kit#readme",
15
+ "bugs": "https://github.com/Forthtilliath/forthtilliath-packages/issues",
16
16
  "files": [
17
17
  "dist"
18
18
  ],
@@ -21,7 +21,7 @@
21
21
  "./*": "./dist/*.js"
22
22
  },
23
23
  "dependencies": {
24
- "@forthtilliath/expo-release-updates": "0.2.0"
24
+ "@forthtilliath/expo-release-updates": "0.3.0"
25
25
  },
26
26
  "peerDependencies": {
27
27
  "@expo/vector-icons": ">=15.0.0",
@@ -33,19 +33,19 @@
33
33
  },
34
34
  "devDependencies": {
35
35
  "@expo/vector-icons": "^15.0.2",
36
- "@types/react": "^19.2.17",
36
+ "@types/react": "catalog:",
37
37
  "@types/react-test-renderer": "^19.1.0",
38
- "eslint": "10.7.0",
38
+ "eslint": "catalog:",
39
39
  "expo-image-picker": "~57.0.7",
40
40
  "expo-speech-recognition": "^56.0.1",
41
- "react": "^19.2.7",
41
+ "react": "catalog:",
42
42
  "react-native": "0.86.0",
43
43
  "react-native-gesture-handler": "~2.32.0",
44
44
  "react-test-renderer": "19.2.3",
45
- "typescript": "^5.9.3",
45
+ "typescript": "catalog:",
46
46
  "vitest": "^4.1.10",
47
- "@forthtilliath/eslint-config": "0.0.0",
48
- "@forthtilliath/typescript-config": "0.0.0"
47
+ "@forthtilliath/typescript-config": "0.0.0",
48
+ "@forthtilliath/eslint-config": "0.0.0"
49
49
  },
50
50
  "scripts": {
51
51
  "lint": "eslint . --max-warnings 0",