@sincpro/mobile-ui 1.0.1 → 1.1.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.
Files changed (102) hide show
  1. package/README.md +572 -229
  2. package/dist/Actions/Actions.Button.d.ts +7 -2
  3. package/dist/Actions/Actions.Button.js +23 -10
  4. package/dist/Actions/Actions.FAB.d.ts +3 -1
  5. package/dist/Actions/Actions.FAB.js +7 -3
  6. package/dist/Actions/Actions.FilterButton.d.ts +10 -2
  7. package/dist/Actions/Actions.FilterButton.js +21 -6
  8. package/dist/Actions/Actions.IconButton.d.ts +5 -2
  9. package/dist/Actions/Actions.IconButton.js +7 -3
  10. package/dist/Actions/index.d.ts +2 -2
  11. package/dist/Actions/index.js +1 -1
  12. package/dist/Dialog/Dialog.ActionSheet.d.ts +2 -1
  13. package/dist/Dialog/Dialog.ActionSheet.js +15 -5
  14. package/dist/Dialog/Dialog.Confirmation.js +3 -3
  15. package/dist/Dialog/Dialog.NativeSheet.d.ts +10 -0
  16. package/dist/Dialog/Dialog.NativeSheet.js +21 -0
  17. package/dist/Dialog/Dialog.Sheet.d.ts +5 -1
  18. package/dist/Dialog/Dialog.Sheet.js +14 -8
  19. package/dist/Display/Display.CopyableText.d.ts +9 -2
  20. package/dist/Display/Display.CopyableText.js +75 -7
  21. package/dist/Display/Display.Icon.js +2 -1
  22. package/dist/Display/Display.KeyValue.d.ts +11 -4
  23. package/dist/Display/Display.KeyValue.js +6 -4
  24. package/dist/Display/Display.ListGroup.d.ts +23 -0
  25. package/dist/Display/Display.ListGroup.js +17 -0
  26. package/dist/Display/Display.ListItem.d.ts +16 -2
  27. package/dist/Display/Display.ListItem.js +25 -8
  28. package/dist/Display/Display.SectionHeader.d.ts +11 -1
  29. package/dist/Display/Display.SectionHeader.js +11 -2
  30. package/dist/Display/Display.SwipeableRow.d.ts +3 -1
  31. package/dist/Display/Display.SwipeableRow.js +3 -3
  32. package/dist/Display/index.d.ts +4 -0
  33. package/dist/Display/index.js +2 -0
  34. package/dist/Feedback/Feedback.Banner.js +1 -1
  35. package/dist/Feedback/Feedback.EmptyState.d.ts +2 -1
  36. package/dist/Feedback/Feedback.EmptyState.js +2 -2
  37. package/dist/Feedback/Feedback.Error.d.ts +3 -1
  38. package/dist/Feedback/Feedback.Error.js +4 -3
  39. package/dist/Form/Form.Checkbox.d.ts +4 -1
  40. package/dist/Form/Form.Checkbox.js +2 -2
  41. package/dist/Form/Form.DatePicker.d.ts +2 -1
  42. package/dist/Form/Form.DatePicker.js +4 -3
  43. package/dist/Form/Form.FilterButton.d.ts +1 -1
  44. package/dist/Form/Form.Input.d.ts +5 -3
  45. package/dist/Form/Form.Input.js +2 -2
  46. package/dist/Form/Form.KeyboardForm.d.ts +11 -0
  47. package/dist/Form/Form.KeyboardForm.js +9 -0
  48. package/dist/Form/Form.KeyboardToolbar.d.ts +21 -0
  49. package/dist/Form/Form.KeyboardToolbar.js +70 -0
  50. package/dist/Form/Form.Select.d.ts +3 -1
  51. package/dist/Form/Form.Select.js +23 -10
  52. package/dist/Form/index.d.ts +6 -0
  53. package/dist/Form/index.js +4 -0
  54. package/dist/Navigation/Navigation.AppBar.d.ts +6 -2
  55. package/dist/Navigation/Navigation.AppBar.js +7 -7
  56. package/dist/Navigation/Navigation.BottomNav.d.ts +3 -1
  57. package/dist/Navigation/Navigation.BottomNav.js +9 -5
  58. package/dist/Navigation/Navigation.RowItem.d.ts +1 -1
  59. package/dist/Navigation/Navigation.RowItem.js +2 -2
  60. package/dist/Navigation/Navigation.SearchBar.d.ts +6 -1
  61. package/dist/Navigation/Navigation.SearchBar.js +29 -8
  62. package/dist/Navigation/Navigation.SearchSuggestions.d.ts +14 -0
  63. package/dist/Navigation/Navigation.SearchSuggestions.js +7 -0
  64. package/dist/Navigation/Navigation.SegmentedControl.d.ts +3 -1
  65. package/dist/Navigation/Navigation.SegmentedControl.js +2 -1
  66. package/dist/Navigation/index.d.ts +6 -2
  67. package/dist/Navigation/index.js +2 -0
  68. package/dist/Typography/Typography.Heading.js +1 -1
  69. package/dist/Typography/Typography.SelectableText.d.ts +10 -0
  70. package/dist/Typography/Typography.SelectableText.js +16 -0
  71. package/dist/Typography/Typography.Text.js +4 -20
  72. package/dist/Typography/index.d.ts +3 -0
  73. package/dist/Typography/index.js +3 -0
  74. package/dist/layouts/AuthContainer.d.ts +4 -1
  75. package/dist/layouts/AuthContainer.js +3 -3
  76. package/dist/primitives/BaseButton.d.ts +7 -2
  77. package/dist/primitives/BaseButton.js +19 -5
  78. package/dist/theme/typography.d.ts +16 -1
  79. package/dist/theme/typography.js +19 -0
  80. package/dist/utils/criteria.d.ts +56 -0
  81. package/dist/utils/criteria.js +99 -0
  82. package/dist/utils/reorder.d.ts +4 -0
  83. package/dist/utils/reorder.js +32 -0
  84. package/dist/views/AuthFormView.d.ts +4 -1
  85. package/dist/views/AuthFormView.js +2 -2
  86. package/dist/views/FormViewV2.d.ts +7 -2
  87. package/dist/views/FormViewV2.js +35 -16
  88. package/dist/views/ListViewV2.d.ts +9 -6
  89. package/dist/views/ListViewV2.js +34 -21
  90. package/dist/widgets/CollectionOptions.d.ts +15 -0
  91. package/dist/widgets/CollectionOptions.js +127 -0
  92. package/dist/widgets/FilterBar.d.ts +21 -0
  93. package/dist/widgets/FilterBar.js +14 -0
  94. package/dist/widgets/FilterBuilder.d.ts +10 -0
  95. package/dist/widgets/FilterBuilder.js +115 -0
  96. package/dist/widgets/QueryChoice.d.ts +10 -0
  97. package/dist/widgets/QueryChoice.js +13 -0
  98. package/dist/widgets/ReorderableList.d.ts +51 -0
  99. package/dist/widgets/ReorderableList.js +138 -0
  100. package/dist/widgets/index.d.ts +4 -0
  101. package/dist/widgets/index.js +3 -0
  102. package/package.json +15 -1
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @sincpro/mobile-ui
2
2
 
3
- **Sincpro Mobile Design System** — the UI contract between design and engineering for React Native / Expo SDK 57 applications.
3
+ **Sincpro Mobile Design System** — the UI contract between design and engineering for React Native / Expo SDK 58 applications.
4
4
 
5
5
  This package is maintained by Sincpro as part of the mobile framework ecosystem. Anyone can read the source and use the components. What Sincpro provides is the commitment behind it: stable APIs, predictable behavior, long-term support, and a team that ensures this keeps working as platforms evolve. Build on it knowing it will still be there.
6
6
 
@@ -20,18 +20,14 @@ Wire the Tailwind preset, then import by category:
20
20
  // tailwind.config.js
21
21
  module.exports = {
22
22
  presets: [require("@sincpro/mobile-ui/tailwind.preset")],
23
- content: [
24
- "./App.tsx",
25
- "./src/**/*.{ts,tsx}",
26
- "./node_modules/@sincpro/mobile-ui/**/*.js",
27
- ],
23
+ content: ["./App.tsx", "./src/**/*.{ts,tsx}", "./node_modules/@sincpro/mobile-ui/**/*.js"],
28
24
  };
29
25
  ```
30
26
 
31
27
  ```tsx
32
28
  import { Actions } from "@sincpro/mobile-ui/Actions";
33
29
  import { Display } from "@sincpro/mobile-ui/Display";
34
- import { Form } from "@sincpro/mobile-ui/Form";
30
+ import { Form } from "@sincpro/mobile-ui/Form";
35
31
  import { useToast, ToastProvider } from "@sincpro/mobile-ui/Feedback";
36
32
 
37
33
  export default function InvoiceScreen() {
@@ -39,10 +35,12 @@ export default function InvoiceScreen() {
39
35
  return (
40
36
  <Display.Card>
41
37
  <Display.KeyValue label="Invoice #" value="INV-0042" copyable />
42
- <Display.Monetary amount={1500.00} variant="dataLarge" />
43
- <Actions.Button onPress={() => toast.success("Invoice sent")}>
44
- Send Invoice
45
- </Actions.Button>
38
+ <Display.Monetary value={1500.0} variant="dataLarge" />
39
+ <Actions.Button
40
+ accessibleSizing
41
+ onPress={() => toast.success("Invoice sent")}
42
+ title="Send Invoice"
43
+ />
46
44
  </Display.Card>
47
45
  );
48
46
  }
@@ -81,6 +79,338 @@ Without a design system, every developer makes local decisions: which shade of g
81
79
 
82
80
  ## Philosophy
83
81
 
82
+ **Composable behavior, customizable presentation.** Sincpro defaults are a starting
83
+ point, not a mandatory visual style. Preserve existing appearances while exposing public
84
+ APIs for fonts, sizes, shapes, content and composition. See the
85
+ [permanent design-system manifesto](docs/MANIFESTO.md) for the goal, acceptance criteria
86
+ and current limitations; full customizability is an objective, not a completed audit.
87
+
88
+ The [UX/UI evolution plan](docs/UX-UI-PLAN.md) tracks the rollout. The first native pilot
89
+ is in Storybook under `Patterns/Records/Detail`: list, detail, copy, edit, cancel and
90
+ recoverable error. It uses fictional data and local state, not persistence or a backend.
91
+
92
+ ### Accessible interaction
93
+
94
+ - `Actions.Button` (including `.Icon`), `Actions.IconButton`, `BaseButton`,
95
+ `Feedback.EmptyState`, `Feedback.Error` and `Form.Input` accept `accessibleSizing` to
96
+ opt into a minimum 48-point target. Buttons also allow enlarged, multiline labels in
97
+ this mode. Existing sizing remains the default for compatibility.
98
+ - Supply `accessibilityLabel` for icon-only actions. Loading buttons expose busy/disabled
99
+ state and retain their accessible name.
100
+ - `Display.CopyableText` uses a familiar copy icon: tap a short value to copy it.
101
+ A transient checkmark confirms success without adding a “Copiado” block or shifting
102
+ layout; screen readers receive an announcement. Use `selectable` for longer text:
103
+ native text selection stays independent of the 48-point icon action that copies the
104
+ full value. Failures stay explicit and retryable. It accepts `disabled`, `testID`,
105
+ `accessibilityLabel`, `onCopied` and `onCopyError`. Copy targets intentionally reserve 48 points.
106
+ - `Display.KeyValue copyable` enables copying string/number values (enforced by its type).
107
+ Use `align="stacked"` for long content or `align="auto"` to stack below 360 points or
108
+ above a 1.3 font scale. Existing `align="row"` remains unchanged; React nodes are
109
+ display-only.
110
+ - `KeyValue selectable` also supports native text selection, with or without copying.
111
+ Choose the mode by the purpose of the field, not an automatic character-count threshold.
112
+ - `Typography.SelectableText` provides read-only native range selection without opening
113
+ the keyboard. It preserves typography/tokens and backs the longer-text modes above.
114
+ On iOS, `Text selectable` alone only exposes full-paragraph copying, not range handles.
115
+ - Headings and section titles expose native header semantics.
116
+ - Checkbox/Select announce control state; their `accessibleSizing` mode enlarges targets.
117
+ - Sheet supports accessible escape and `showCloseButton`; `SwipeableRow visibleActions`
118
+ supplies a non-gesture alternative. Keep explicit labels for essential actions.
119
+ - `Dialog/NativeSheet` is an optional Expo 58 native presentation (`@expo/ui` peer);
120
+ import it by subpath, not the root barrel. Use short action sheets for choices and a
121
+ sheet for a scoped task; do not stack sheets. Mobile-task stories demonstrate native
122
+ summaries, cancelable selection, filters applied only on confirmation and archive/undo.
123
+ - Form/List view refresh always clears the busy state on failure and accepts
124
+ `onRefreshError` for application recovery without discarding content.
125
+ - AppBar, BottomNav and SegmentedControl support accessible sizing; banners expose status
126
+ semantics, and confirmation defaults use explicit verbs.
127
+
128
+ The [audit and validation report](docs/UX-AUDIT.md) records scope, evidence and device limits.
129
+
130
+ ### Clean Sincpro grouping and customization
131
+
132
+ `Display.ListGroup` groups related settings or a short list on one surface, with a title,
133
+ optional description/footer/action and hairline separators only between rows. Choose
134
+ `surface="plain"` for grouping by proximity instead of a rounded container.
135
+
136
+ ```tsx
137
+ <Display.ListGroup
138
+ title="Cuenta"
139
+ footer="Puedes cambiar estas preferencias en cualquier momento."
140
+ contentClassName="rounded-lg bg-bg-muted"
141
+ >
142
+ <Display.ListItem
143
+ variant="mobile"
144
+ title="Idioma"
145
+ value="Español"
146
+ chevron
147
+ onPress={openLanguageChoices}
148
+ />
149
+ <Display.ListItem
150
+ variant="mobile"
151
+ title="Avisos importantes"
152
+ trailing={notificationControl}
153
+ />
154
+ </Display.ListGroup>
155
+ ```
156
+
157
+ - `ListItem variant="mobile"` opts into wrapping, a 48-point target,
158
+ tonal row press feedback and adaptive accessories. `layout="auto"` stacks values/controls
159
+ below the text on narrow screens or enlarged text; use `layout="stacked"` for narrow
160
+ embedded containers. Navigation values are also exposed as native accessibility values.
161
+ - New mobile rows default to `density="compact"`: regular 14-point titles/values,
162
+ 13-point supporting text and 8-point vertical padding. `density="comfortable"` restores
163
+ the earlier PR's 17/16-point reading presentation and 12-point padding.
164
+ Legacy rows (`variant="default"`) keep their original appearance.
165
+ - Put a toggle/checkbox in a **non-pressable** row; do not nest a second action inside a
166
+ pressable row. The leading/trailing slots remain consumer-owned.
167
+ - `Typography.Text variant="bodyReadable"` and `Typography.SelectableText` with the same
168
+ variant opt into 17 points while retaining `configureFonts` and system font scaling.
169
+ The 14-point `body` and existing ListItem appearance remain unchanged.
170
+ - Customize per instance with `className`, `contentClassName`, slots, surface and separators.
171
+ Rows also accept `style`, `titleTextProps`, `subtitleTextProps` and `valueTextProps`.
172
+ Text props accept typography variants, weights, font families and native style arrays.
173
+ `ListGroup.headerProps` forwards header styling/text props; `footerTextProps` controls
174
+ string footers. Default groups use a 16-point section heading and 13-point description/footer.
175
+ Group `style`, `contentStyle` and `separatorStyle` control spacing, surface and dividers.
176
+ Headers expose `style`, `contentStyle`, `actionStyle` and `actionTextProps` as well.
177
+ For example, `titleTextProps={{ style: { fontFamily: "MyLoadedFont", fontSize: 15 } }}`
178
+ changes only one row, without copying its focus, accessibility or press behavior.
179
+ Customize branding centrally with `buildThemeFromBrand`/`extendTheme`, `setActiveTheme`
180
+ and `themeToVars` on the host, plus `configureFonts` for already-loaded font families.
181
+ Do not introduce a competing palette or hardcode colors in every screen.
182
+ - This group is not virtualized: use FlatList/SectionList for large or unbounded collections.
183
+
184
+ Stories: `Patterns/Forms/Settings` shows Sincpro defaults, long content, customized
185
+ surfaces and text roles. `Foundations/Typography/CompactHierarchy` and
186
+ `ComfortableHierarchy` show the same content at two densities, not two mandatory screens.
187
+ Customization is not a complete visual approval.
188
+
189
+ ### Compose collections, not a prescribed screen
190
+
191
+ `Patterns/Collections/Browsing` shows the same local data as rows, cards and
192
+ long-label variants. It combines icons, bounded 56-point artwork, readable text, metrics,
193
+ independent open/save actions and a native detail sheet. These are recipes to adapt,
194
+ not new domain-specific screen components or a web toolbar squeezed onto a phone.
195
+
196
+ ```tsx
197
+ import { FilterBar } from "@sincpro/mobile-ui/widgets";
198
+
199
+ <FilterBar
200
+ filters={filterOptions}
201
+ onFilterPress={toggleFilter}
202
+ onClearFilters={clearFilters}
203
+ onOpenFilters={openFilterSheet}
204
+ resultsLabel={resultsLabel}
205
+ search={{
206
+ value: query,
207
+ onChangeText: setQuery,
208
+ accessibilityLabel: "Buscar recursos",
209
+ clearAccessibilityLabel: "Limpiar búsqueda",
210
+ }}
211
+ />;
212
+ ```
213
+
214
+ Each filter supplies `id`, `label`, `selected` and optional `disabled`. State, matching,
215
+ result counts and advanced-filter drafts belong to the host. Clearing filters does not
216
+ silently clear the search query. Search, advanced action, result label and clear action
217
+ are optional; customize labels, `className`, theme and `layout="scroll|wrap|auto"`.
218
+ Auto wraps above font scale 1.3, otherwise it scrolls horizontally. Quick chips apply
219
+ immediately; the recipe's advanced sheet applies only on confirmation.
220
+
221
+ `Actions.FilterButton variant="mobile"` adds readable wrapping labels and 48-point targets.
222
+ `FilterButtonGroup layout="scroll|wrap"` and `Navigation.SearchBar accessibleSizing` are
223
+ additive; legacy filter layout and search sizing remain unchanged.
224
+ When placing these controls in a host ScrollView, use `keyboardShouldPersistTaps="handled"`
225
+ so a filter can act on the first tap while the keyboard is open.
226
+ Use virtualized lists for production-sized collections.
227
+
228
+ Dependencies follow the latest stable version compatible with Expo 58, not unbounded
229
+ upgrades. Validate with `CI=1 npx expo install --check` in addition to the quality gates.
230
+
231
+ ### Controlled reordering without a closed row design
232
+
233
+ Optional import: `@sincpro/mobile-ui/widgets/ReorderableList`. Install
234
+ `react-native-sortables@^1.10.1` in the host; it uses the host's Gesture Handler 3.3+,
235
+ Reanimated 4.7+ and Worklets setup. Wrap the app in `GestureHandlerRootView`.
236
+ The adapter is deliberately **not** in the generic widgets/root barrels: applications
237
+ that do not use drag-and-drop do not need to resolve this optional peer.
238
+
239
+ ```tsx
240
+ import ReorderableList from "@sincpro/mobile-ui/widgets/ReorderableList";
241
+
242
+ <ReorderableList
243
+ items={sections}
244
+ keyExtractor={(item) => item.id}
245
+ getItemLabel={(item) => item.name}
246
+ onChange={(next) => setSections(next)}
247
+ renderItem={({ item }) => <MySectionContent section={item} />}
248
+ itemStyle={{ borderRadius: 16, padding: 8 }}
249
+ />;
250
+ ```
251
+
252
+ - A one-column, **non-virtualized short-list** adapter with variable-height content.
253
+ It does not own screen scrolling, persistence, confirmation or domain rules.
254
+ Do not use it as an unbounded feed, kanban board, file drop zone or cross-app drag API.
255
+ - Only the dedicated handle starts drag. Default arrows and the handle's native
256
+ increment/decrement actions offer alternatives; content can retain buttons, inputs
257
+ or any custom layout. Drag/drop uses insertion, not swapping.
258
+ - `onChange(next, { key, fromIndex, toIndex, source })` delivers an immutable order
259
+ (`source` is `drag` or `button`). Update controlled state synchronously; persist later,
260
+ showing save failures/retry in the host. A drop in the original position emits no change.
261
+ Draft/apply/cancel/undo can be built by owning that state, without replacing the gesture.
262
+ - Keys must be unique non-empty strings, stable across renders, never array indices.
263
+ Content updates with the same keys preserve current objects. Insert/remove/external
264
+ reorder or disabling **during** drag cancels the gesture, retains host data and shows
265
+ an accessible error. `onReorderRejected` reports `data-changed` or `disabled`.
266
+ The gesture engine remounts on rejection; keep persistent item state in the host.
267
+ - `renderItem` controls content. `renderControls` receives `dragHandle`, movement
268
+ callbacks and boundary flags; keep that handle and an accessible non-drag alternative
269
+ when replacing controls. `renderHandleContent` replaces **noninteractive** handle
270
+ contents only, keeping the gesture, accessible name and adjustable actions.
271
+ - Root `className`/`style`, `itemStyle`, `contentStyle`, `controlsStyle`, `handleStyle`
272
+ and `actionStyle` accept local layout/shape/spacing. Child content owns typography.
273
+ `labels` localizes controls, announcements and conflict errors.
274
+ - `sortableProps` forwards engine options such as activation delay, row gap, decorations,
275
+ animation settings and autoscroll, while the adapter owns identity, single-column
276
+ insertion and controlled callbacks. Provide a stable Reanimated `scrollableRef` to
277
+ the **actual Animated.ScrollView** for autoscroll; do not nest another scroll container
278
+ or attach the ref to a different view. Native scroll props/ref belong to the host.
279
+ Custom motion, targets and colors still require accessibility validation.
280
+
281
+ `Patterns/Collections/Reordering` demonstrates default, custom content/controls, long
282
+ rows and an external-update conflict. Only this recipe uses the new dependency;
283
+ existing form/list/query templates keep their behavior.
284
+
285
+ ### Keyboard-aware forms and field navigation
286
+
287
+ Expo 58 does not automatically make arbitrary form layouts keyboard-safe.
288
+ The host must include `react-native-keyboard-controller` and its `KeyboardProvider` at
289
+ the app root. Use a development build when the runtime does not include that native module.
290
+
291
+ ```tsx
292
+ <Form.KeyboardForm contentContainerStyle={{ padding: 16, gap: 16 }}>
293
+ {fields}
294
+ </Form.KeyboardForm>
295
+ ```
296
+
297
+ `KeyboardForm` owns the scroll and scopes editable inputs with `KeyboardToolbar.Group`.
298
+ Its toolbar provides previous/next/dismiss actions, including phone/number keyboards.
299
+ The scroll reserves the measured toolbar height plus 24 points (normally 72 total);
300
+ it grows with enlarged text. Override `bottomOffset` for additional accessories.
301
+ `toolbar={false}` keeps automatic avoidance without the toolbar.
302
+ Customize `toolbarLabels` or compose `Form.KeyboardToolbar` with an existing aware scroll,
303
+ using `onHeightChange` to reserve its measured height. The toolbar compensates for space
304
+ below its parent, such as a safe area or form footer, instead of floating over the caret.
305
+ The default toolbar uses compact icons; `toolbarProps={{ showDoneLabel: true }}` opts into
306
+ a text dismiss action. `toolbarProps` also accepts `className`, `style`, `actionStyle`,
307
+ `labelStyle`, `iconSize` and `renderActionContent(action, { label, disabled })`.
308
+ The content slot replaces only non-interactive content: the toolbar retains focus actions,
309
+ accessible labels and disabled states. `toolbarLabels` remains supported;
310
+ `toolbarProps` takes precedence. Font size/family and shape can be changed without
311
+ reimplementing keyboard behavior.
312
+ Do not nest this component inside another vertical ScrollView.
313
+
314
+ For existing templates, opt into `FormViewV2.Content keyboardToolbar` or
315
+ `AuthFormView keyboardToolbar` / `AuthContainer keyboardToolbar`; defaults stay unchanged.
316
+ `Form.Input` forwards its native ref for explicit return-key navigation:
317
+ `onSubmitEditing={() => nextRef.current?.focus()}` with `submitBehavior="submit"`.
318
+ `KeyboardForm` forwards its ref to the native aware scroll for methods such as
319
+ `assureFocusedInputVisible()` after validation/layout changes.
320
+ The three templates accept `keyboardFormProps` (excluding `children` and `toolbar`) for
321
+ the same ref, offsets, toolbar customization, scroll callbacks and content styling.
322
+ `keyboardToolbar` remains the explicit opt-in; omitting it preserves the original
323
+ template layout without a toolbar.
324
+ Multiline Return remains a newline; dismissing the keyboard does not submit the form.
325
+ Story: `Patterns/Forms/Keyboard navigation`.
326
+
327
+ `ListViewV2.Content` accepts native `keyboardShouldPersistTaps` and `keyboardDismissMode`
328
+ across populated, empty and error states. Handled row/retry actions receive the first tap
329
+ while search has focus (`"handled"` by default); pass `"never"` for the old dismiss-first
330
+ behavior. Drag-dismiss remains opt-in (`keyboardDismissMode="on-drag"`).
331
+ This changes interaction, not the list's typography or layout.
332
+
333
+ ```tsx
334
+ <FormViewV2.Content
335
+ keyboardToolbar
336
+ keyboardFormProps={{
337
+ ref: scrollRef,
338
+ toolbarProps: {
339
+ style: { borderRadius: 16 },
340
+ iconSize: 18,
341
+ showDoneLabel: true,
342
+ labelStyle: { fontFamily: "MyFont", fontSize: 13 },
343
+ },
344
+ }}
345
+ >
346
+ {fields}
347
+ </FormViewV2.Content>
348
+ ```
349
+
350
+ Default list rows now honor the template's `onPressItem` and `readonly` settings;
351
+ direct `Navigation.RowItem` usage can still supply the legacy callback on `item`.
352
+
353
+ ### Structured queries, not just quick chips
354
+
355
+ `widgets.FilterBuilder` edits a controlled `CriteriaGroup`: nested `all`/`any`/`none`
356
+ groups and configured conditions. Supply fields with their label, type, permitted
357
+ operators and arity (`none`, `one`, `range`); choice values are explicit. Numbers remain
358
+ editable strings until validated/committed, date fields use the native picker, and invalid
359
+ conditions produce visible errors. The mobile editor opens one group at a time instead of
360
+ squeezing an arbitrarily indented desktop tree into the screen.
361
+
362
+ `widgets.CollectionOptions` is a full-screen, keyboard-aware workspace:
363
+
364
+ ```tsx
365
+ <CollectionOptions
366
+ visible={open}
367
+ value={query}
368
+ fields={filterFields}
369
+ sortFields={sortFields}
370
+ groupFields={groupFields}
371
+ savedQueries={savedQueries}
372
+ onApply={applyQuery}
373
+ onClose={closeOptions}
374
+ onSave={saveQuery}
375
+ />
376
+ ```
377
+
378
+ `CollectionQuery` contains `search`, `filter`, ordered `sort` entries and ordered
379
+ `groupBy` field IDs. Order and grouping priorities have explicit move/remove actions;
380
+ dragging is not required. Loading a saved configuration changes the draft only;
381
+ Apply commits all dimensions together, Cancel discards the draft. Save errors keep the
382
+ draft/name for retry. The host owns saving/loading and closes the workspace after Apply.
383
+
384
+ The DS does not fetch models, evaluate filters, sort a page as if it were the full dataset,
385
+ persist favorites or translate this tree into a backend query language. Relational paths,
386
+ remote model discovery and a JSON editor are not implemented here. The web builder is a
387
+ semantic reference, not a component copied into mobile.
388
+
389
+ `Navigation.SearchBar suggestions={{ items, onSelect, loading, error, onRetry }}` shows
390
+ host-supplied autocomplete while focused. Choosing a suggestion calls `onSelect` without
391
+ silently rewriting the query. A host may complete a title or create a field-specific
392
+ condition; it owns matching, request cancellation and stale responses.
393
+
394
+ Story: `Patterns/Collections/Query configuration` exercises actual local filtering,
395
+ multiple sorting priorities, grouping levels and saving/loading configurations.
396
+
397
+ ### Native E2E
398
+
399
+ With Maestro installed and an iOS simulator running Expo Go, start an isolated test server:
400
+
401
+ ```bash
402
+ EXPO_PUBLIC_STORYBOOK_TEST=1 npx expo start --port 8082
403
+ make storybook-e2e
404
+ ```
405
+
406
+ This disables Storybook's on-device chrome for tests without changing the normal server.
407
+ The flows verify copy, edit, cancel, failed-save recovery and long-content/dark interaction.
408
+ `STORYBOOK_URL` can override the default `exp://127.0.0.1:8082`.
409
+ Maestro analytics are disabled by the Makefile target. These flows target Expo Go on iOS;
410
+ Android needs a connected device and its appropriate app ID/host configuration.
411
+ Tests stop/relaunch Expo Go and switch stories; run them on a dedicated test simulator.
412
+ Enlarged-text screenshots are accessibility stress tests, not the default typography.
413
+
84
414
  ### Maintained for the long term
85
415
 
86
416
  Enterprise software lives for years. The screens built with this DS will be in production long after the initial project is delivered. Sincpro's commitment is to maintain API stability, patch what breaks on platform upgrades, and evolve the system without forcing rewrites on consumers.
@@ -134,7 +464,7 @@ Queued toast system with promise support — covers the full loading → success
134
464
  import { ToastProvider, useToast } from "@sincpro/mobile-ui/Feedback";
135
465
 
136
466
  // 1. Wrap the app once (createAppShell does this automatically)
137
- <ToastProvider position="top">{children}</ToastProvider>
467
+ <ToastProvider position="top">{children}</ToastProvider>;
138
468
 
139
469
  // 2. Call from anywhere in the tree
140
470
  const toast = useToast();
@@ -142,7 +472,7 @@ toast.success("Invoice saved");
142
472
  toast.promise(sendInvoice(), {
143
473
  loading: "Sending…",
144
474
  success: "Invoice sent",
145
- error: "Failed to send",
475
+ error: "Failed to send",
146
476
  });
147
477
  ```
148
478
 
@@ -155,7 +485,7 @@ import { ConfirmationProvider, useConfirmationContext } from "@sincpro/mobile-ui
155
485
  // Controlled sheet — built on RN Modal, no native peer deps
156
486
  <Sheet visible={open} onClose={() => setOpen(false)} title="Options">
157
487
  <Actions.Button onPress={confirm}>Confirm</Actions.Button>
158
- </Sheet>
488
+ </Sheet>;
159
489
 
160
490
  // Imperative confirmation
161
491
  const { confirm } = useConfirmationContext();
@@ -168,10 +498,16 @@ const agreed = await confirm({ title: "Delete invoice?", message: "This cannot b
168
498
  import { Wizard, useWizard } from "@sincpro/mobile-ui/views/Wizard";
169
499
 
170
500
  <Wizard initialStep="customer">
171
- <Wizard.Step id="customer"><CustomerStep /></Wizard.Step>
172
- <Wizard.Step id="items"><ItemsStep /></Wizard.Step>
173
- <Wizard.Step id="review"><ReviewStep /></Wizard.Step>
174
- </Wizard>
501
+ <Wizard.Step id="customer">
502
+ <CustomerStep />
503
+ </Wizard.Step>
504
+ <Wizard.Step id="items">
505
+ <ItemsStep />
506
+ </Wizard.Step>
507
+ <Wizard.Step id="review">
508
+ <ReviewStep />
509
+ </Wizard.Step>
510
+ </Wizard>;
175
511
 
176
512
  // Inside any step
177
513
  const { go, back } = useWizard();
@@ -197,163 +533,163 @@ import { Display } from "@sincpro/mobile-ui/Display";
197
533
 
198
534
  ### Actions
199
535
 
200
- | Component | Description |
201
- |-----------|-------------|
202
- | `Actions.Button` | Primary action — gradient, outline, flat, link variants; optional confirm dialog |
203
- | `Actions.IconButton` | Icon-only tap target — ghost, primary, secondary, tertiary variants |
204
- | `Actions.FAB` | Floating Action Button — fixed position, optional label |
205
- | `Actions.FilterButton` | Chip-style active/inactive toggle |
206
- | `Actions.FilterButtonGroup` | Horizontal scroll container for filter chips |
536
+ | Component | Description |
537
+ | --------------------------- | -------------------------------------------------------------------------------- |
538
+ | `Actions.Button` | Primary action — gradient, outline, flat, link variants; optional confirm dialog |
539
+ | `Actions.IconButton` | Icon-only tap target — ghost, primary, secondary, tertiary variants |
540
+ | `Actions.FAB` | Floating Action Button — fixed position, optional label |
541
+ | `Actions.FilterButton` | Chip-style active/inactive toggle |
542
+ | `Actions.FilterButtonGroup` | Horizontal scroll container for filter chips |
207
543
 
208
544
  ### Display
209
545
 
210
- | Component | Description |
211
- |-----------|-------------|
212
- | `Display.Badge` | Status badge — success, warning, danger, info, neutral |
213
- | `Display.Card` | Surface container with optional shadow and padding |
214
- | `Display.KeyValue` | Label + value row, optional clipboard copy |
215
- | `Display.Stat` | KPI cell — label, value, optional trend indicator |
216
- | `Display.StatCard` | Card-wrapped KPI |
217
- | `Display.Monetary` | Formatted monetary amount using mono typography |
218
- | `Display.PriceTag` | Currency-formatted price with size options |
219
- | `Display.DataTable` | Scrollable table for structured data |
220
- | `Display.ListItem` | Row with icon, label, sublabel, optional action |
221
- | `Display.Chip` | Compact label tag |
222
- | `Display.Avatar` / `AvatarGroup` | User avatar, stacked group |
223
- | `Display.Icon` | Vector icon wrapper (`@expo/vector-icons`) |
224
- | `Display.StatusDot` | Colored presence / state indicator |
225
- | `Display.Divider` | Horizontal separator |
226
- | `Display.GradientSurface` | Container with linear gradient background |
227
- | `Display.Timeline` | Vertical event timeline |
228
- | `Display.SwipeableRow` | Row with swipe-to-reveal actions |
229
- | `Display.Tooltip` | Overlay tooltip |
230
- | `Display.CopyableText` | Text with one-tap clipboard copy |
231
- | `Display.Logo` | Brand logo (falls back to `setBranding` value) |
232
- | `Display.Accordion` | Collapsible content section |
233
- | `Display.Carousel` | Horizontal swipe carousel |
234
- | `Display.Story` / `StoryTray` / `StoryViewer` | Stories UI pattern |
235
- | `Display.MediaCard` | Card with image or video embed |
236
- | `Display.Pattern` | Decorative background pattern |
546
+ | Component | Description |
547
+ | --------------------------------------------- | ------------------------------------------------------ |
548
+ | `Display.Badge` | Status badge — success, warning, danger, info, neutral |
549
+ | `Display.Card` | Surface container with optional shadow and padding |
550
+ | `Display.KeyValue` | Label + value row, optional clipboard copy |
551
+ | `Display.Stat` | KPI cell — label, value, optional trend indicator |
552
+ | `Display.StatCard` | Card-wrapped KPI |
553
+ | `Display.Monetary` | Formatted monetary amount using mono typography |
554
+ | `Display.PriceTag` | Currency-formatted price with size options |
555
+ | `Display.DataTable` | Scrollable table for structured data |
556
+ | `Display.ListItem` | Row with icon, label, sublabel, optional action |
557
+ | `Display.Chip` | Compact label tag |
558
+ | `Display.Avatar` / `AvatarGroup` | User avatar, stacked group |
559
+ | `Display.Icon` | Vector icon wrapper (`@expo/vector-icons`) |
560
+ | `Display.StatusDot` | Colored presence / state indicator |
561
+ | `Display.Divider` | Horizontal separator |
562
+ | `Display.GradientSurface` | Container with linear gradient background |
563
+ | `Display.Timeline` | Vertical event timeline |
564
+ | `Display.SwipeableRow` | Row with swipe-to-reveal actions |
565
+ | `Display.Tooltip` | Overlay tooltip |
566
+ | `Display.CopyableText` | Text with one-tap clipboard copy |
567
+ | `Display.Logo` | Brand logo (falls back to `setBranding` value) |
568
+ | `Display.Accordion` | Collapsible content section |
569
+ | `Display.Carousel` | Horizontal swipe carousel |
570
+ | `Display.Story` / `StoryTray` / `StoryViewer` | Stories UI pattern |
571
+ | `Display.MediaCard` | Card with image or video embed |
572
+ | `Display.Pattern` | Decorative background pattern |
237
573
 
238
574
  ### Form
239
575
 
240
- | Component | Description |
241
- |-----------|-------------|
242
- | `Form.TextInput` | Text field with label, error, and helper text |
243
- | `Form.PasswordInput` | Text input with show/hide toggle |
244
- | `Form.PinInput` | PIN code entry |
245
- | `Form.MonetaryInput` | Currency input with formatting |
246
- | `Form.QuantitySelector` | Integer increment / decrement |
247
- | `Form.FractionalQuantityInput` | Decimal quantity input |
248
- | `Form.Select` | Dropdown select (modal-based) |
249
- | `Form.Dropdown` | Inline dropdown menu |
250
- | `Form.RadioButton` | Single-select radio option |
251
- | `Form.Checkbox` | Multi-select checkbox |
252
- | `Form.Switch` | Toggle switch |
253
- | `Form.Slider` | Range slider (`@react-native-community/slider` peer) |
254
- | `Form.DatePicker` | Date picker (`@react-native-community/datetimepicker` peer) |
255
- | `Form.SearchBar` | Search input with clear button |
256
- | `Form.ControlledInput` | `TextInput` + react-hook-form |
257
- | `Form.ControlledRadioButton` | `RadioButton` + react-hook-form |
258
- | `Form.ControlledCheckbox` | `Checkbox` + react-hook-form |
259
- | `Form.ControlledDropdown` | `Dropdown` + react-hook-form |
260
- | `Form.ControlledMonetaryInput` | `MonetaryInput` + react-hook-form |
576
+ | Component | Description |
577
+ | --------------------------------------- | ------------------------------------------------------------------- |
578
+ | `Form.TextInput` | Text field with label, error, and helper text |
579
+ | `Form.PasswordInput` | Text input with show/hide toggle |
580
+ | `Form.PinInput` | PIN code entry |
581
+ | `Form.MonetaryInput` | Currency input with formatting |
582
+ | `Form.QuantitySelector` | Integer increment / decrement |
583
+ | `Form.FractionalQuantityInput` | Decimal quantity input |
584
+ | `Form.Select` | Dropdown select (modal-based) |
585
+ | `Form.Dropdown` | Inline dropdown menu |
586
+ | `Form.RadioButton` | Single-select radio option |
587
+ | `Form.Checkbox` | Multi-select checkbox |
588
+ | `Form.Switch` | Toggle switch |
589
+ | `Form.Slider` | Range slider (`@react-native-community/slider` peer) |
590
+ | `Form.DatePicker` | Date picker (`@react-native-community/datetimepicker` peer) |
591
+ | `Form.SearchBar` | Search input with clear button |
592
+ | `Form.ControlledInput` | `TextInput` + react-hook-form |
593
+ | `Form.ControlledRadioButton` | `RadioButton` + react-hook-form |
594
+ | `Form.ControlledCheckbox` | `Checkbox` + react-hook-form |
595
+ | `Form.ControlledDropdown` | `Dropdown` + react-hook-form |
596
+ | `Form.ControlledMonetaryInput` | `MonetaryInput` + react-hook-form |
261
597
  | `Actions.Button` / `IconButton` / `FAB` | Action components (preferred over deprecated `Form.Button` aliases) |
262
598
 
263
599
  ### Feedback
264
600
 
265
- | Component | Description |
266
- |-----------|-------------|
267
- | `Feedback.Spinner` | Animated loading spinner |
268
- | `Feedback.Loading` | Full-screen loading overlay |
269
- | `Feedback.Skeleton` | Content placeholder shimmer |
270
- | `Feedback.Progress` | Linear progress bar |
271
- | `Feedback.ProgressCircle` | Circular progress indicator |
272
- | `Feedback.Banner` | Contextual notification banner |
273
- | `Feedback.Empty` / `EmptyState` | Empty list or page placeholder |
274
- | `Feedback.Error` | Error state with optional retry |
275
- | `Feedback.PermissionCard` | Permission request UI |
276
- | `ToastProvider` / `useToast` | Queued toast notification system |
277
- | `ErrorBoundary` | React error boundary wrapper |
601
+ | Component | Description |
602
+ | ------------------------------- | -------------------------------- |
603
+ | `Feedback.Spinner` | Animated loading spinner |
604
+ | `Feedback.Loading` | Full-screen loading overlay |
605
+ | `Feedback.Skeleton` | Content placeholder shimmer |
606
+ | `Feedback.Progress` | Linear progress bar |
607
+ | `Feedback.ProgressCircle` | Circular progress indicator |
608
+ | `Feedback.Banner` | Contextual notification banner |
609
+ | `Feedback.Empty` / `EmptyState` | Empty list or page placeholder |
610
+ | `Feedback.Error` | Error state with optional retry |
611
+ | `Feedback.PermissionCard` | Permission request UI |
612
+ | `ToastProvider` / `useToast` | Queued toast notification system |
613
+ | `ErrorBoundary` | React error boundary wrapper |
278
614
 
279
615
  ### Navigation
280
616
 
281
- | Component | Description |
282
- |-----------|-------------|
283
- | `Navigation.AppBar` | Top navigation bar with action slots |
284
- | `Navigation.BottomNav` | Bottom tab bar |
285
- | `Navigation.Header` | Screen header |
286
- | `Navigation.HeaderToolbar` | Header with left, center, right slots |
287
- | `Navigation.CollapsingHeader` | Header that collapses on scroll |
288
- | `Navigation.FloatingBar` | Floating action bar |
289
- | `Navigation.SearchBar` | Navigation-level search input |
290
- | `Navigation.SegmentedControl` | Segmented control switcher |
291
- | `Navigation.Tabs` | Tab navigation container |
292
- | `Navigation.Steps` | Step indicator / breadcrumb |
293
- | `Navigation.Menu` / `MenuSection` | Dropdown menu with sections |
617
+ | Component | Description |
618
+ | --------------------------------- | ------------------------------------- |
619
+ | `Navigation.AppBar` | Top navigation bar with action slots |
620
+ | `Navigation.BottomNav` | Bottom tab bar |
621
+ | `Navigation.Header` | Screen header |
622
+ | `Navigation.HeaderToolbar` | Header with left, center, right slots |
623
+ | `Navigation.CollapsingHeader` | Header that collapses on scroll |
624
+ | `Navigation.FloatingBar` | Floating action bar |
625
+ | `Navigation.SearchBar` | Navigation-level search input |
626
+ | `Navigation.SegmentedControl` | Segmented control switcher |
627
+ | `Navigation.Tabs` | Tab navigation container |
628
+ | `Navigation.Steps` | Step indicator / breadcrumb |
629
+ | `Navigation.Menu` / `MenuSection` | Dropdown menu with sections |
294
630
 
295
631
  ### Dialog
296
632
 
297
- | Component | Description |
298
- |-----------|-------------|
299
- | `Dialog.BottomSheet` | Bottom drawer (RN Modal, no native peer deps) |
300
- | `Dialog.ActionSheet` | Action sheet with options list |
301
- | `Dialog.Confirmation` | Confirm / cancel modal |
302
- | `Dialog.EditValue` | Inline edit value modal |
303
- | `Sheet` (direct import) | Controlled sheet via `Dialog/Dialog.Sheet` |
304
- | `ConfirmationProvider` / `useConfirmationContext` | Imperative confirmation API |
633
+ | Component | Description |
634
+ | ------------------------------------------------- | --------------------------------------------- |
635
+ | `Dialog.BottomSheet` | Bottom drawer (RN Modal, no native peer deps) |
636
+ | `Dialog.ActionSheet` | Action sheet with options list |
637
+ | `Dialog.Confirmation` | Confirm / cancel modal |
638
+ | `Dialog.EditValue` | Inline edit value modal |
639
+ | `Sheet` (direct import) | Controlled sheet via `Dialog/Dialog.Sheet` |
640
+ | `ConfirmationProvider` / `useConfirmationContext` | Imperative confirmation API |
305
641
 
306
642
  ### Typography
307
643
 
308
- | Component | Description |
309
- |-----------|-------------|
310
- | `Typography.Text` | Main text component — accepts `variant` prop from the full scale |
311
- | `Typography.Heading` | Heading shorthand |
644
+ | Component | Description |
645
+ | -------------------- | ---------------------------------------------------------------- |
646
+ | `Typography.Text` | Main text component — accepts `variant` prop from the full scale |
647
+ | `Typography.Heading` | Heading shorthand |
312
648
 
313
649
  ### Primitives
314
650
 
315
- | Component | Description |
316
- |-----------|-------------|
317
- | `Box` | NativeWind layout container |
318
- | `Row` | Horizontal flex |
319
- | `Stack` | Vertical flex |
320
- | `Spacer` | Flexible space filler |
321
- | `Pressable` | Animated press target |
322
- | `AspectRatio` | Aspect-ratio container |
651
+ | Component | Description |
652
+ | ------------- | --------------------------- |
653
+ | `Box` | NativeWind layout container |
654
+ | `Row` | Horizontal flex |
655
+ | `Stack` | Vertical flex |
656
+ | `Spacer` | Flexible space filler |
657
+ | `Pressable` | Animated press target |
658
+ | `AspectRatio` | Aspect-ratio container |
323
659
 
324
660
  ### Layouts
325
661
 
326
- | Component | Description |
327
- |-----------|-------------|
328
- | `Container` | Padded layout container |
329
- | `ScrollContainer` | Scrollable layout wrapper |
330
- | `GradientContainer` | Container with gradient background |
331
- | `PlainLayout` | Unstyled layout for custom screens |
662
+ | Component | Description |
663
+ | -------------------- | ------------------------------------- |
664
+ | `Container` | Padded layout container |
665
+ | `ScrollContainer` | Scrollable layout wrapper |
666
+ | `GradientContainer` | Container with gradient background |
667
+ | `PlainLayout` | Unstyled layout for custom screens |
332
668
  | `TabNavigatorLayout` | Layout for tab-based screen structure |
333
669
 
334
670
  ### Views
335
671
 
336
- | Component | Description |
337
- |-----------|-------------|
338
- | `ListViewV2` | Paginated list with filtering and empty states |
339
- | `FormViewV2` | Dynamic form view with field management |
340
- | `AuthFormView` | Authentication form layout |
672
+ | Component | Description |
673
+ | ---------------------- | ------------------------------------------------- |
674
+ | `ListViewV2` | Paginated list with filtering and empty states |
675
+ | `FormViewV2` | Dynamic form view with field management |
676
+ | `AuthFormView` | Authentication form layout |
341
677
  | `Wizard` / `useWizard` | Multi-step wizard with guards and lifecycle hooks |
342
- | `AppSplashView` | Branded splash screen with logo and loading state |
678
+ | `AppSplashView` | Branded splash screen with logo and loading state |
343
679
 
344
680
  ### Widgets
345
681
 
346
- | Component | Description |
347
- |-----------|-------------|
348
- | `ScreenHeader` | Composite screen header widget |
349
- | `MenuGrid` / `MenuCard` | Grid and card-based navigation menus |
350
- | `QuickActions` | Quick action icon grid |
351
- | `PromoBanner` | Swipeable promotional carousel |
352
- | `PromoModal` | Promotional modal overlay |
353
- | `HomeHeader` | Home screen hero header |
354
- | `GeoPermissionCard` | Geolocation permission request card |
355
- | `TimeZoneSelector` | Timezone picker widget |
356
- | `JSONViewer` | Collapsible JSON tree (debug / dev tools) |
682
+ | Component | Description |
683
+ | ----------------------- | ----------------------------------------- |
684
+ | `ScreenHeader` | Composite screen header widget |
685
+ | `MenuGrid` / `MenuCard` | Grid and card-based navigation menus |
686
+ | `QuickActions` | Quick action icon grid |
687
+ | `PromoBanner` | Swipeable promotional carousel |
688
+ | `PromoModal` | Promotional modal overlay |
689
+ | `HomeHeader` | Home screen hero header |
690
+ | `GeoPermissionCard` | Geolocation permission request card |
691
+ | `TimeZoneSelector` | Timezone picker widget |
692
+ | `JSONViewer` | Collapsible JSON tree (debug / dev tools) |
357
693
 
358
694
  ---
359
695
 
@@ -374,7 +710,7 @@ setActiveTheme(MY_DARK_THEME);
374
710
 
375
711
  // Inject CSS variables into a subtree
376
712
  const themeStyle = vars(themeToVars(MY_DARK_THEME));
377
- <View style={themeStyle}>{children}</View>
713
+ <View style={themeStyle}>{children}</View>;
378
714
  ```
379
715
 
380
716
  ### Reading the theme in components
@@ -384,7 +720,9 @@ import { useTheme } from "@sincpro/mobile-ui/theme";
384
720
 
385
721
  function PriceCard() {
386
722
  const theme = useTheme();
387
- return <View style={{ backgroundColor: theme.bg.card, borderColor: theme.border.default }} />;
723
+ return (
724
+ <View style={{ backgroundColor: theme.bg.card, borderColor: theme.border.default }} />
725
+ );
388
726
  }
389
727
  ```
390
728
 
@@ -395,33 +733,33 @@ import { extendTheme } from "@sincpro/mobile-ui/theme";
395
733
 
396
734
  // Deep-merges on top of the DS base theme
397
735
  const MY_THEME = extendTheme({
398
- accent: "#00C357",
399
- primary: "#0B0B0B",
736
+ accent: "#00C357",
737
+ primary: "#0B0B0B",
400
738
  secondary: "#6B6963",
401
739
  bg: {
402
740
  page: "#FAFAFA",
403
741
  card: "#FFFFFF",
404
742
  },
405
743
  text: {
406
- accent: "#006E2E", // text on accent-tinted surfaces
744
+ accent: "#006E2E", // text on accent-tinted surfaces
407
745
  },
408
746
  });
409
747
  ```
410
748
 
411
749
  ### ThemeTokens reference
412
750
 
413
- | Group | Keys |
414
- |-------|------|
415
- | `primary / secondary / accent` | Main brand color scales |
416
- | `bg.*` | `page, card, muted, accent, hover, disabled, popover` |
417
- | `text.*` | `primary, secondary, tertiary, muted, accent, inverse, disabled, onPrimary, onSecondary, onAccent, onDanger, onSuccess` |
418
- | `icon.*` | `primary, secondary, tertiary, inverse, disabled` |
419
- | `border.*` | `default, light, strong, focus` |
420
- | `success / warning / danger / info` | Semantic state colors |
421
- | `*Light` | Tinted backgrounds for each state |
422
- | `ring / input` | Focus ring and input border |
423
- | `gradient.*` | `primary, accent` — arrays of color stops |
424
- | `shadow.*` | `sm, md, lg` — React Native shadow objects |
751
+ | Group | Keys |
752
+ | ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
753
+ | `primary / secondary / accent` | Main brand color scales |
754
+ | `bg.*` | `page, card, muted, accent, hover, disabled, popover` |
755
+ | `text.*` | `primary, secondary, tertiary, muted, accent, inverse, disabled, onPrimary, onSecondary, onAccent, onDanger, onSuccess` |
756
+ | `icon.*` | `primary, secondary, tertiary, inverse, disabled` |
757
+ | `border.*` | `default, light, strong, focus` |
758
+ | `success / warning / danger / info` | Semantic state colors |
759
+ | `*Light` | Tinted backgrounds for each state |
760
+ | `ring / input` | Focus ring and input border |
761
+ | `gradient.*` | `primary, accent` — arrays of color stops |
762
+ | `shadow.*` | `sm, md, lg` — React Native shadow objects |
425
763
 
426
764
  ---
427
765
 
@@ -445,16 +783,16 @@ import { Typography } from "@sincpro/mobile-ui/Typography";
445
783
  <Typography.Text variant="button">Action Label</Typography.Text>
446
784
  ```
447
785
 
448
- | Group | Variants |
449
- |-------|----------|
450
- | Display | `display` |
451
- | Headings | `h1` `h2` `h3` `h4` `h5` `h6` |
452
- | Body | `bodyLarge` `body` `bodySmall` |
453
- | Labels | `label` `labelSmall` |
454
- | Data (mono) | `data` `dataLarge` |
455
- | Meta (mono) | `overline` `caption` `captionSmall` |
456
- | Buttons | `button` `buttonSmall` `buttonLarge` |
457
- | Legacy | `title` `subtitle` |
786
+ | Group | Variants |
787
+ | ----------- | ------------------------------------ |
788
+ | Display | `display` |
789
+ | Headings | `h1` `h2` `h3` `h4` `h5` `h6` |
790
+ | Body | `bodyLarge` `body` `bodySmall` |
791
+ | Labels | `label` `labelSmall` |
792
+ | Data (mono) | `data` `dataLarge` |
793
+ | Meta (mono) | `overline` `caption` `captionSmall` |
794
+ | Buttons | `button` `buttonSmall` `buttonLarge` |
795
+ | Legacy | `title` `subtitle` |
458
796
 
459
797
  ### FontRole
460
798
 
@@ -475,9 +813,9 @@ import { configureFonts, resetFonts } from "@sincpro/mobile-ui/theme/typography"
475
813
 
476
814
  configureFonts({
477
815
  titleRegular: "Satoshi-Regular",
478
- titleMedium: "Satoshi-Medium",
479
- title: "Satoshi-Bold",
480
- titleBlack: "Satoshi-Black",
816
+ titleMedium: "Satoshi-Medium",
817
+ title: "Satoshi-Bold",
818
+ titleBlack: "Satoshi-Black",
481
819
  });
482
820
 
483
821
  // Reset to defaults — useful in tests and Storybook stories
@@ -486,15 +824,15 @@ resetFonts();
486
824
 
487
825
  ### Font size scale
488
826
 
489
- | Token | `px` | Token | `px` |
490
- |-------|------|-------|------|
491
- | `xs` | 10 | `4xl` | 28 |
492
- | `sm` | 13 | `5xl` | 32 |
493
- | `base` | 14 | `6xl` | 36 |
494
- | `lg` | 16 | `7xl` | 48 |
495
- | `xl` | 18 | `8xl` | 64 |
496
- | `2xl` | 20 | | |
497
- | `3xl` | 24 | | |
827
+ | Token | `px` | Token | `px` |
828
+ | ------ | ---- | ----- | ---- |
829
+ | `xs` | 10 | `4xl` | 28 |
830
+ | `sm` | 13 | `5xl` | 32 |
831
+ | `base` | 14 | `6xl` | 36 |
832
+ | `lg` | 16 | `7xl` | 48 |
833
+ | `xl` | 18 | `8xl` | 64 |
834
+ | `2xl` | 20 | | |
835
+ | `3xl` | 24 | | |
498
836
 
499
837
  ### Custom text styles
500
838
 
@@ -506,7 +844,7 @@ const errorLabel = createTextStyle("labelSmall", { color: "#FF4D1F" });
506
844
 
507
845
  // Fully custom
508
846
  const amountStyle = createCustomTextStyle("4xl", "monoMedium", {
509
- lineHeight: "none",
847
+ lineHeight: "none",
510
848
  letterSpacing: "tight",
511
849
  });
512
850
  ```
@@ -521,22 +859,22 @@ Centralized animation constants so motion feels consistent across every screen
521
859
  import { duration, easing, spring } from "@sincpro/mobile-ui/tokens";
522
860
 
523
861
  // Durations (ms)
524
- duration.instant // 0 — immediate state swap, no transition
525
- duration.fast // 120 — micro-interactions (button press, toggle)
526
- duration.normal // 200 — standard transitions (modal, drawer)
527
- duration.slow // 320 — large layout shifts
528
- duration.slower // 480 — onboarding, emphasis animations
862
+ duration.instant; // 0 — immediate state swap, no transition
863
+ duration.fast; // 120 — micro-interactions (button press, toggle)
864
+ duration.normal; // 200 — standard transitions (modal, drawer)
865
+ duration.slow; // 320 — large layout shifts
866
+ duration.slower; // 480 — onboarding, emphasis animations
529
867
 
530
868
  // Bezier easing curves
531
- easing.standard // general purpose
532
- easing.decelerate // elements entering the screen
533
- easing.accelerate // elements leaving the screen
534
- easing.emphasized // expressive, attention-drawing transitions
869
+ easing.standard; // general purpose
870
+ easing.decelerate; // elements entering the screen
871
+ easing.accelerate; // elements leaving the screen
872
+ easing.emphasized; // expressive, attention-drawing transitions
535
873
 
536
874
  // Reanimated spring presets
537
- spring.default // balanced bounce
538
- spring.gentle // smooth, no overshoot
539
- spring.snappy // fast with slight bounce
875
+ spring.default; // balanced bounce
876
+ spring.gentle; // smooth, no overshoot
877
+ spring.snappy; // fast with slight bounce
540
878
  ```
541
879
 
542
880
  Usage with Reanimated:
@@ -546,7 +884,7 @@ import { withTiming, withSpring } from "react-native-reanimated";
546
884
  import { duration, easing, spring } from "@sincpro/mobile-ui/tokens";
547
885
 
548
886
  const opacity = withTiming(1, { duration: duration.normal, easing: easing.decelerate });
549
- const scale = withSpring(1, spring.snappy);
887
+ const scale = withSpring(1, spring.snappy);
550
888
  ```
551
889
 
552
890
  ---
@@ -577,19 +915,15 @@ The preset ships the full token class set (`bg-primary`, `text-text-secondary`,
577
915
  ```js
578
916
  module.exports = {
579
917
  presets: [require("@sincpro/mobile-ui/tailwind.preset")],
580
- content: [
581
- "./App.tsx",
582
- "./src/**/*.{ts,tsx}",
583
- "./node_modules/@sincpro/mobile-ui/**/*.js",
584
- ],
918
+ content: ["./App.tsx", "./src/**/*.{ts,tsx}", "./node_modules/@sincpro/mobile-ui/**/*.js"],
585
919
  theme: {
586
920
  extend: {
587
921
  // Override font families to match loaded font names
588
922
  fontFamily: {
589
- light: ["Inter_300Light"],
590
- regular: ["Inter_400Regular"],
591
- medium: ["Inter_500Medium"],
592
- semibold: ["Inter_600SemiBold"],
923
+ light: ["Inter_300Light"],
924
+ regular: ["Inter_400Regular"],
925
+ medium: ["Inter_500Medium"],
926
+ semibold: ["Inter_600SemiBold"],
593
927
  extrabold: ["Inter_800ExtraBold"],
594
928
  },
595
929
  },
@@ -614,13 +948,13 @@ import "@sincpro/mobile-ui/theme/globals.css";
614
948
  Best tree-shaking; makes the component's category visible at the call site:
615
949
 
616
950
  ```tsx
617
- import { Display } from "@sincpro/mobile-ui/Display";
618
- import { Form } from "@sincpro/mobile-ui/Form";
619
- import { Feedback } from "@sincpro/mobile-ui/Feedback";
951
+ import { Display } from "@sincpro/mobile-ui/Display";
952
+ import { Form } from "@sincpro/mobile-ui/Form";
953
+ import { Feedback } from "@sincpro/mobile-ui/Feedback";
620
954
  import { Navigation } from "@sincpro/mobile-ui/Navigation";
621
- import { Dialog } from "@sincpro/mobile-ui/Dialog";
955
+ import { Dialog } from "@sincpro/mobile-ui/Dialog";
622
956
  import { Typography } from "@sincpro/mobile-ui/Typography";
623
- import { Actions } from "@sincpro/mobile-ui/Actions";
957
+ import { Actions } from "@sincpro/mobile-ui/Actions";
624
958
  ```
625
959
 
626
960
  ### Theme and tokens
@@ -636,18 +970,18 @@ For components outside a namespace:
636
970
 
637
971
  ```tsx
638
972
  import Sheet from "@sincpro/mobile-ui/Dialog/Dialog.Sheet";
639
- import { ConfirmationProvider } from "@sincpro/mobile-ui/Dialog";
640
- import { ToastProvider, useToast } from "@sincpro/mobile-ui/Feedback";
641
- import { Wizard, useWizard } from "@sincpro/mobile-ui/views/Wizard";
973
+ import { ConfirmationProvider } from "@sincpro/mobile-ui/Dialog";
974
+ import { ToastProvider, useToast } from "@sincpro/mobile-ui/Feedback";
975
+ import { Wizard, useWizard } from "@sincpro/mobile-ui/views/Wizard";
642
976
  ```
643
977
 
644
978
  ### Layout primitives
645
979
 
646
980
  ```tsx
647
- import { Box, Row, Stack, Spacer } from "@sincpro/mobile-ui/primitives";
648
- import Container from "@sincpro/mobile-ui/layouts/Container";
649
- import PlainLayout from "@sincpro/mobile-ui/layouts/PlainLayout";
650
- import TabNavigatorLayout from "@sincpro/mobile-ui/layouts/TabNavigatorLayout";
981
+ import { Box, Row, Stack, Spacer } from "@sincpro/mobile-ui/primitives";
982
+ import Container from "@sincpro/mobile-ui/layouts/Container";
983
+ import PlainLayout from "@sincpro/mobile-ui/layouts/PlainLayout";
984
+ import TabNavigatorLayout from "@sincpro/mobile-ui/layouts/TabNavigatorLayout";
651
985
  ```
652
986
 
653
987
  ---
@@ -663,26 +997,35 @@ make storybook-android # expo run:android
663
997
 
664
998
  Stories are organized into four groups:
665
999
 
666
- | Group | Contents |
667
- |-------|----------|
668
- | **Foundations** | Tokens, typography scale, color palette, motion |
669
- | **Components** | All compound component namespaces |
670
- | **Patterns** | Composed examples (list screen, form screen, auth flow) |
671
- | **Legacy** | Deprecated components kept for backwards compatibility |
1000
+ | Group | Contents |
1001
+ | --------------- | ------------------------------------------------------- |
1002
+ | **Foundations** | Tokens, typography scale, color palette, motion |
1003
+ | **Components** | All compound component namespaces |
1004
+ | **Patterns** | Composed examples (list screen, form screen, auth flow) |
1005
+ | **Legacy** | Deprecated components kept for backwards compatibility |
672
1006
 
673
1007
  Stories live in `.rnstorybook/stories/**/*.stories.tsx`. The Storybook harness is excluded from the published package (`files: ["dist", ...]`) and never lands in consumer bundles.
674
1008
 
1009
+ UX is a cross-cutting quality, not another catalog category. Component states live with
1010
+ their component (`Actions/Button`, `Data Display/CopyableText`, `Inputs/Select`, `Feedback`);
1011
+ task compositions live under named `Patterns/Forms`, `Patterns/Records`,
1012
+ `Patterns/Collections`, `Patterns/Views` and `Patterns/Profiles`.
1013
+ The former `Patterns/UX`, top-level `Views` and `Templates` story IDs changed;
1014
+ package exports did not. The taxonomy is regression-tested.
1015
+ Stories that own a full-screen scroll set `parameters.ownsScroll = true`; the canvas does
1016
+ not wrap them in a second vertical scroll. Other canvases use keyboard-aware scrolling.
1017
+
675
1018
  ---
676
1019
 
677
1020
  ## Development
678
1021
 
679
- | Command | What it does |
680
- |---------|-------------|
681
- | `make build` | `tsc` + `tsc-alias` → `dist/` (JS + type declarations) |
682
- | `make format` | Auto-fix: ESLint + Prettier + type-check |
683
- | `make verify-format` | CI gate — runs `format`, fails if any file changed |
684
- | `make storybook` | Start Expo dev server for Storybook |
685
- | `make publish` | Bump version + `npm publish` |
1022
+ | Command | What it does |
1023
+ | -------------------- | ------------------------------------------------------ |
1024
+ | `make build` | `tsc` + `tsc-alias` → `dist/` (JS + type declarations) |
1025
+ | `make format` | Auto-fix: ESLint + Prettier + type-check |
1026
+ | `make verify-format` | CI gate — runs `format`, fails if any file changed |
1027
+ | `make storybook` | Start Expo dev server for Storybook |
1028
+ | `make publish` | Bump version + `npm publish` |
686
1029
 
687
1030
  After building, copy the dist to consumers in the local monorepo (no symlinks setup):
688
1031