@sincpro/mobile-ui 0.7.0 → 0.7.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +639 -62
- package/dist/Dialog/Dialog.ActionSheet.js +21 -5
- package/dist/Dialog/Dialog.Confirmation.js +1 -1
- package/dist/Dialog/Dialog.EditValue.js +1 -1
- package/dist/Dialog/Dialog.Sheet.js +13 -4
- package/dist/Feedback/Feedback.Progress.js +4 -2
- package/dist/Feedback/Feedback.Skeleton.js +3 -2
- package/dist/Form/Form.Dropdown.js +1 -1
- package/dist/Form/Form.Select.js +28 -13
- package/dist/Navigation/Navigation.Menu.js +1 -1
- package/dist/theme/typography.d.ts +6 -4
- package/dist/theme/typography.js +19 -16
- package/dist/widgets/PromoModal.js +1 -1
- package/package.json +1 -3
package/README.md
CHANGED
|
@@ -1,114 +1,691 @@
|
|
|
1
1
|
# @sincpro/mobile-ui
|
|
2
2
|
|
|
3
|
-
Sincpro Mobile
|
|
3
|
+
**Sincpro Mobile Design System** — the UI contract between design and engineering for React Native / Expo SDK 56 applications.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
- **Standalone** — does not depend on `@sincpro/mobile` (the core depends on this package).
|
|
7
|
-
- **Mobile-first** — built with NativeWind 4 + tailwind-variants, design tokens via CSS variables.
|
|
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.
|
|
8
6
|
|
|
9
|
-
>
|
|
7
|
+
> **AI agent?** Read [`AGENTS.md`](AGENTS.md) and [`docs/GOTCHAS.md`](docs/GOTCHAS.md) first.
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
14
|
npx expo install @sincpro/mobile-ui
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
`react-native-reanimated`, `react-native-svg`, `react-native-safe-area-context`,
|
|
19
|
-
`react-native-keyboard-controller`, `react-native-worklets`, `react-hook-form`,
|
|
20
|
-
`react-native-currency-input`, `@expo/vector-icons`, `@expo-google-fonts/montserrat`, plus the
|
|
21
|
-
expo modules listed in `package.json`. A few components need **optional** peers (install only if
|
|
22
|
-
you use them): `@react-native-community/slider` (`Form/Slider`), `@react-native-community/datetimepicker`
|
|
23
|
-
(`Form/DatePicker`), `@expo/ui` (future native primitives).
|
|
24
|
-
|
|
25
|
-
## Tailwind preset (required)
|
|
26
|
-
|
|
27
|
-
Components use design tokens exposed as Tailwind classes (`bg-primary`, `text-text-secondary`, …)
|
|
28
|
-
backed by CSS variables. Wire them via the shipped preset and content globs:
|
|
17
|
+
Wire the Tailwind preset, then import by category:
|
|
29
18
|
|
|
30
19
|
```js
|
|
31
20
|
// tailwind.config.js
|
|
32
21
|
module.exports = {
|
|
33
22
|
presets: [require("@sincpro/mobile-ui/tailwind.preset")],
|
|
34
|
-
content: [
|
|
35
|
-
|
|
23
|
+
content: [
|
|
24
|
+
"./App.tsx",
|
|
25
|
+
"./src/**/*.{ts,tsx}",
|
|
26
|
+
"./node_modules/@sincpro/mobile-ui/**/*.js",
|
|
27
|
+
],
|
|
36
28
|
};
|
|
37
29
|
```
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
|
|
31
|
+
```tsx
|
|
32
|
+
import { Display } from "@sincpro/mobile-ui/Display";
|
|
33
|
+
import { Form } from "@sincpro/mobile-ui/Form";
|
|
34
|
+
import { useToast, ToastProvider } from "@sincpro/mobile-ui/Feedback";
|
|
35
|
+
|
|
36
|
+
export default function InvoiceScreen() {
|
|
37
|
+
const toast = useToast();
|
|
38
|
+
return (
|
|
39
|
+
<Display.Card>
|
|
40
|
+
<Display.KeyValue label="Invoice #" value="INV-0042" copyable />
|
|
41
|
+
<Display.Monetary amount={1500.00} variant="dataLarge" />
|
|
42
|
+
<Form.Button onPress={() => toast.success("Invoice sent")}>
|
|
43
|
+
Send Invoice
|
|
44
|
+
</Form.Button>
|
|
45
|
+
</Display.Card>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Table of Contents
|
|
53
|
+
|
|
54
|
+
1. [What is a Design System](#what-is-a-design-system)
|
|
55
|
+
2. [Philosophy](#philosophy)
|
|
56
|
+
3. [Key Features](#key-features)
|
|
57
|
+
4. [Component Catalog](#component-catalog)
|
|
58
|
+
5. [Theme System](#theme-system)
|
|
59
|
+
6. [Typography System](#typography-system)
|
|
60
|
+
7. [Motion Tokens](#motion-tokens)
|
|
61
|
+
8. [Branding](#branding)
|
|
62
|
+
9. [Tailwind Setup](#tailwind-setup)
|
|
63
|
+
10. [Import Patterns](#import-patterns)
|
|
64
|
+
11. [Storybook](#storybook)
|
|
65
|
+
12. [Development](#development)
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## What is a Design System
|
|
70
|
+
|
|
71
|
+
A design system is a shared language between the people who design products and the people who build them. It defines how things look, how they behave, and how they are named — once — so every screen built from it is consistent by default.
|
|
72
|
+
|
|
73
|
+
Without a design system, every developer makes local decisions: which shade of green for success, how many pixels for a button press state, which font weight for a label. Those decisions accumulate. Over time the product looks like it was built by ten different teams, because it was.
|
|
74
|
+
|
|
75
|
+
`@sincpro/mobile-ui` is Sincpro's answer to that problem for mobile. It encodes the design decisions once, in components and tokens that the whole team uses. When the brand evolves, the change propagates everywhere. When a new screen needs a data table, it already exists, already handles edge cases, and already matches everything else.
|
|
76
|
+
|
|
77
|
+
**The design system is not a collection of pretty components. It is the baseline every Sincpro app starts from.**
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Philosophy
|
|
82
|
+
|
|
83
|
+
### Maintained for the long term
|
|
84
|
+
|
|
85
|
+
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.
|
|
86
|
+
|
|
87
|
+
Changing a behavior that existing apps depend on is a breaking change. We treat it as one. Additions are additive; deprecations are marked and kept for a full major version cycle before removal.
|
|
88
|
+
|
|
89
|
+
### Presentational only
|
|
90
|
+
|
|
91
|
+
Components in this package are driven entirely by props and callbacks. There is no routing, no domain state, no API calls. This is intentional: it makes components independently testable in Storybook, reusable across different app architectures, and safe to upgrade without side effects in business logic.
|
|
92
|
+
|
|
93
|
+
### Token-driven, not hardcoded
|
|
94
|
+
|
|
95
|
+
No color, no font size, no shadow is hardcoded in a component. Every visual property references a design token — a CSS variable injected at the root. Swapping the entire visual language of an app means changing one token map, not hunting down hex codes across a codebase.
|
|
96
|
+
|
|
97
|
+
### Compound component API
|
|
98
|
+
|
|
99
|
+
Related components are grouped into namespaced objects (`Display`, `Form`, `Navigation`, etc.). One import gives access to an entire category, and the namespace makes the relationship between components visible at the call site.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Key Features
|
|
104
|
+
|
|
105
|
+
### Controlled Form Components
|
|
106
|
+
|
|
107
|
+
Every interactive input ships a `Controlled*` variant that integrates directly with `react-hook-form`. No wrapper, no boilerplate:
|
|
108
|
+
|
|
109
|
+
```tsx
|
|
110
|
+
import { Form } from "@sincpro/mobile-ui/Form";
|
|
111
|
+
import { useForm } from "react-hook-form";
|
|
112
|
+
|
|
113
|
+
const { control } = useForm<{ email: string; amount: number }>();
|
|
114
|
+
|
|
115
|
+
<Form.ControlledInput
|
|
116
|
+
control={control}
|
|
117
|
+
name="email"
|
|
118
|
+
label="Email address"
|
|
119
|
+
rules={{ required: "Required" }}
|
|
120
|
+
/>
|
|
121
|
+
<Form.ControlledMonetaryInput
|
|
122
|
+
control={control}
|
|
123
|
+
name="amount"
|
|
124
|
+
label="Amount (USD)"
|
|
125
|
+
/>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### Toast Notifications
|
|
129
|
+
|
|
130
|
+
Queued toast system with promise support — covers the full loading → success / error lifecycle:
|
|
131
|
+
|
|
132
|
+
```tsx
|
|
133
|
+
import { ToastProvider, useToast } from "@sincpro/mobile-ui/Feedback";
|
|
134
|
+
|
|
135
|
+
// 1. Wrap the app once (createAppShell does this automatically)
|
|
136
|
+
<ToastProvider position="top">{children}</ToastProvider>
|
|
137
|
+
|
|
138
|
+
// 2. Call from anywhere in the tree
|
|
139
|
+
const toast = useToast();
|
|
140
|
+
toast.success("Invoice saved");
|
|
141
|
+
toast.promise(sendInvoice(), {
|
|
142
|
+
loading: "Sending…",
|
|
143
|
+
success: "Invoice sent",
|
|
144
|
+
error: "Failed to send",
|
|
145
|
+
});
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
### Bottom Sheet and Dialogs
|
|
149
|
+
|
|
150
|
+
```tsx
|
|
151
|
+
import Sheet from "@sincpro/mobile-ui/Dialog/Dialog.Sheet";
|
|
152
|
+
import { ConfirmationProvider, useConfirmationContext } from "@sincpro/mobile-ui/Dialog";
|
|
153
|
+
|
|
154
|
+
// Controlled sheet — built on RN Modal, no native peer deps
|
|
155
|
+
<Sheet visible={open} onClose={() => setOpen(false)} title="Options">
|
|
156
|
+
<Form.Button onPress={confirm}>Confirm</Form.Button>
|
|
157
|
+
</Sheet>
|
|
158
|
+
|
|
159
|
+
// Imperative confirmation
|
|
160
|
+
const { confirm } = useConfirmationContext();
|
|
161
|
+
const agreed = await confirm({ title: "Delete invoice?", message: "This cannot be undone." });
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Multi-Step Wizard
|
|
41
165
|
|
|
42
|
-
|
|
166
|
+
```tsx
|
|
167
|
+
import { Wizard, useWizard } from "@sincpro/mobile-ui/views/Wizard";
|
|
168
|
+
|
|
169
|
+
<Wizard initialStep="customer">
|
|
170
|
+
<Wizard.Step id="customer"><CustomerStep /></Wizard.Step>
|
|
171
|
+
<Wizard.Step id="items"><ItemsStep /></Wizard.Step>
|
|
172
|
+
<Wizard.Step id="review"><ReviewStep /></Wizard.Step>
|
|
173
|
+
</Wizard>
|
|
174
|
+
|
|
175
|
+
// Inside any step
|
|
176
|
+
const { go, back } = useWizard();
|
|
177
|
+
go("items");
|
|
178
|
+
```
|
|
43
179
|
|
|
44
|
-
|
|
180
|
+
### Data Display
|
|
45
181
|
|
|
46
182
|
```tsx
|
|
47
|
-
import {
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
183
|
+
import { Display } from "@sincpro/mobile-ui/Display";
|
|
184
|
+
|
|
185
|
+
<Display.Stat label="Sales today" value="$12,400" trend="+8%" />
|
|
186
|
+
<Display.PriceTag amount={299.99} currency="USD" />
|
|
187
|
+
<Display.KeyValue label="Order #" value="ORD-8821" copyable />
|
|
188
|
+
<Display.Badge variant="success">Paid</Display.Badge>
|
|
189
|
+
<Display.Monetary amount={1500.00} variant="dataLarge" />
|
|
190
|
+
<Display.DataTable columns={cols} rows={rows} />
|
|
54
191
|
```
|
|
55
192
|
|
|
56
|
-
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## Component Catalog
|
|
196
|
+
|
|
197
|
+
### Actions
|
|
198
|
+
|
|
199
|
+
| Component | Description |
|
|
200
|
+
|-----------|-------------|
|
|
201
|
+
| `Actions.Button` | Primary action — gradient, outline, flat, link variants; optional confirm dialog |
|
|
202
|
+
| `Actions.IconButton` | Icon-only tap target — ghost, primary, secondary, tertiary variants |
|
|
203
|
+
| `Actions.FAB` | Floating Action Button — fixed position, optional label |
|
|
204
|
+
| `Actions.FilterButton` | Chip-style active/inactive toggle |
|
|
205
|
+
| `Actions.FilterButtonGroup` | Horizontal scroll container for filter chips |
|
|
206
|
+
|
|
207
|
+
### Display
|
|
208
|
+
|
|
209
|
+
| Component | Description |
|
|
210
|
+
|-----------|-------------|
|
|
211
|
+
| `Display.Badge` | Status badge — success, warning, danger, info, neutral |
|
|
212
|
+
| `Display.Card` | Surface container with optional shadow and padding |
|
|
213
|
+
| `Display.KeyValue` | Label + value row, optional clipboard copy |
|
|
214
|
+
| `Display.Stat` | KPI cell — label, value, optional trend indicator |
|
|
215
|
+
| `Display.StatCard` | Card-wrapped KPI |
|
|
216
|
+
| `Display.Monetary` | Formatted monetary amount using mono typography |
|
|
217
|
+
| `Display.PriceTag` | Currency-formatted price with size options |
|
|
218
|
+
| `Display.DataTable` | Scrollable table for structured data |
|
|
219
|
+
| `Display.ListItem` | Row with icon, label, sublabel, optional action |
|
|
220
|
+
| `Display.Chip` | Compact label tag |
|
|
221
|
+
| `Display.Avatar` / `AvatarGroup` | User avatar, stacked group |
|
|
222
|
+
| `Display.Icon` | Vector icon wrapper (`@expo/vector-icons`) |
|
|
223
|
+
| `Display.StatusDot` | Colored presence / state indicator |
|
|
224
|
+
| `Display.Divider` | Horizontal separator |
|
|
225
|
+
| `Display.GradientSurface` | Container with linear gradient background |
|
|
226
|
+
| `Display.Timeline` | Vertical event timeline |
|
|
227
|
+
| `Display.SwipeableRow` | Row with swipe-to-reveal actions |
|
|
228
|
+
| `Display.Tooltip` | Overlay tooltip |
|
|
229
|
+
| `Display.CopyableText` | Text with one-tap clipboard copy |
|
|
230
|
+
| `Display.Logo` | Brand logo (falls back to `setBranding` value) |
|
|
231
|
+
| `Display.Accordion` | Collapsible content section |
|
|
232
|
+
| `Display.Carousel` | Horizontal swipe carousel |
|
|
233
|
+
| `Display.Story` / `StoryTray` / `StoryViewer` | Stories UI pattern |
|
|
234
|
+
| `Display.MediaCard` | Card with image or video embed |
|
|
235
|
+
| `Display.Pattern` | Decorative background pattern |
|
|
236
|
+
|
|
237
|
+
### Form
|
|
238
|
+
|
|
239
|
+
| Component | Description |
|
|
240
|
+
|-----------|-------------|
|
|
241
|
+
| `Form.TextInput` | Text field with label, error, and helper text |
|
|
242
|
+
| `Form.PasswordInput` | Text input with show/hide toggle |
|
|
243
|
+
| `Form.PinInput` | PIN code entry |
|
|
244
|
+
| `Form.MonetaryInput` | Currency input with formatting |
|
|
245
|
+
| `Form.QuantitySelector` | Integer increment / decrement |
|
|
246
|
+
| `Form.FractionalQuantityInput` | Decimal quantity input |
|
|
247
|
+
| `Form.Select` | Dropdown select (modal-based) |
|
|
248
|
+
| `Form.Dropdown` | Inline dropdown menu |
|
|
249
|
+
| `Form.RadioButton` | Single-select radio option |
|
|
250
|
+
| `Form.Checkbox` | Multi-select checkbox |
|
|
251
|
+
| `Form.Switch` | Toggle switch |
|
|
252
|
+
| `Form.Slider` | Range slider (`@react-native-community/slider` peer) |
|
|
253
|
+
| `Form.DatePicker` | Date picker (`@react-native-community/datetimepicker` peer) |
|
|
254
|
+
| `Form.SearchBar` | Search input with clear button |
|
|
255
|
+
| `Form.ControlledInput` | `TextInput` + react-hook-form |
|
|
256
|
+
| `Form.ControlledRadioButton` | `RadioButton` + react-hook-form |
|
|
257
|
+
| `Form.ControlledCheckbox` | `Checkbox` + react-hook-form |
|
|
258
|
+
| `Form.ControlledDropdown` | `Dropdown` + react-hook-form |
|
|
259
|
+
| `Form.ControlledMonetaryInput` | `MonetaryInput` + react-hook-form |
|
|
260
|
+
| `Form.Button` / `IconButton` / `FAB` | Action components in Form namespace |
|
|
261
|
+
|
|
262
|
+
### Feedback
|
|
263
|
+
|
|
264
|
+
| Component | Description |
|
|
265
|
+
|-----------|-------------|
|
|
266
|
+
| `Feedback.Spinner` | Animated loading spinner |
|
|
267
|
+
| `Feedback.Loading` | Full-screen loading overlay |
|
|
268
|
+
| `Feedback.Skeleton` | Content placeholder shimmer |
|
|
269
|
+
| `Feedback.Progress` | Linear progress bar |
|
|
270
|
+
| `Feedback.ProgressCircle` | Circular progress indicator |
|
|
271
|
+
| `Feedback.Banner` | Contextual notification banner |
|
|
272
|
+
| `Feedback.Empty` / `EmptyState` | Empty list or page placeholder |
|
|
273
|
+
| `Feedback.Error` | Error state with optional retry |
|
|
274
|
+
| `Feedback.PermissionCard` | Permission request UI |
|
|
275
|
+
| `ToastProvider` / `useToast` | Queued toast notification system |
|
|
276
|
+
| `ErrorBoundary` | React error boundary wrapper |
|
|
277
|
+
|
|
278
|
+
### Navigation
|
|
279
|
+
|
|
280
|
+
| Component | Description |
|
|
281
|
+
|-----------|-------------|
|
|
282
|
+
| `Navigation.AppBar` | Top navigation bar with action slots |
|
|
283
|
+
| `Navigation.BottomNav` | Bottom tab bar |
|
|
284
|
+
| `Navigation.Header` | Screen header |
|
|
285
|
+
| `Navigation.HeaderToolbar` | Header with left, center, right slots |
|
|
286
|
+
| `Navigation.CollapsingHeader` | Header that collapses on scroll |
|
|
287
|
+
| `Navigation.FloatingBar` | Floating action bar |
|
|
288
|
+
| `Navigation.SearchBar` | Navigation-level search input |
|
|
289
|
+
| `Navigation.SegmentedControl` | Segmented control switcher |
|
|
290
|
+
| `Navigation.Tabs` | Tab navigation container |
|
|
291
|
+
| `Navigation.Steps` | Step indicator / breadcrumb |
|
|
292
|
+
| `Navigation.Menu` / `MenuSection` | Dropdown menu with sections |
|
|
293
|
+
|
|
294
|
+
### Dialog
|
|
57
295
|
|
|
58
|
-
|
|
296
|
+
| Component | Description |
|
|
297
|
+
|-----------|-------------|
|
|
298
|
+
| `Dialog.BottomSheet` | Bottom drawer (RN Modal, no native peer deps) |
|
|
299
|
+
| `Dialog.ActionSheet` | Action sheet with options list |
|
|
300
|
+
| `Dialog.Confirmation` | Confirm / cancel modal |
|
|
301
|
+
| `Dialog.EditValue` | Inline edit value modal |
|
|
302
|
+
| `Sheet` (direct import) | Controlled sheet via `Dialog/Dialog.Sheet` |
|
|
303
|
+
| `ConfirmationProvider` / `useConfirmationContext` | Imperative confirmation API |
|
|
59
304
|
|
|
60
|
-
|
|
61
|
-
(or `createAppShell({ theme })` in the core) and inject the CSS vars with `vars(themeToVars(tokens))`
|
|
62
|
-
on the subtree. `useTheme()` is a reactive hook to read the theme in components.
|
|
305
|
+
### Typography
|
|
63
306
|
|
|
64
|
-
|
|
307
|
+
| Component | Description |
|
|
308
|
+
|-----------|-------------|
|
|
309
|
+
| `Typography.Text` | Main text component — accepts `variant` prop from the full scale |
|
|
310
|
+
| `Typography.Heading` | Heading shorthand |
|
|
311
|
+
|
|
312
|
+
### Primitives
|
|
313
|
+
|
|
314
|
+
| Component | Description |
|
|
315
|
+
|-----------|-------------|
|
|
316
|
+
| `Box` | NativeWind layout container |
|
|
317
|
+
| `Row` | Horizontal flex |
|
|
318
|
+
| `Stack` | Vertical flex |
|
|
319
|
+
| `Spacer` | Flexible space filler |
|
|
320
|
+
| `Pressable` | Animated press target |
|
|
321
|
+
| `AspectRatio` | Aspect-ratio container |
|
|
322
|
+
|
|
323
|
+
### Layouts
|
|
324
|
+
|
|
325
|
+
| Component | Description |
|
|
326
|
+
|-----------|-------------|
|
|
327
|
+
| `Container` | Padded layout container |
|
|
328
|
+
| `ScrollContainer` | Scrollable layout wrapper |
|
|
329
|
+
| `GradientContainer` | Container with gradient background |
|
|
330
|
+
| `PlainLayout` | Unstyled layout for custom screens |
|
|
331
|
+
| `TabNavigatorLayout` | Layout for tab-based screen structure |
|
|
332
|
+
|
|
333
|
+
### Views
|
|
334
|
+
|
|
335
|
+
| Component | Description |
|
|
336
|
+
|-----------|-------------|
|
|
337
|
+
| `ListViewV2` | Paginated list with filtering and empty states |
|
|
338
|
+
| `FormViewV2` | Dynamic form view with field management |
|
|
339
|
+
| `AuthFormView` | Authentication form layout |
|
|
340
|
+
| `Wizard` / `useWizard` | Multi-step wizard with guards and lifecycle hooks |
|
|
341
|
+
|
|
342
|
+
### Widgets
|
|
343
|
+
|
|
344
|
+
| Component | Description |
|
|
345
|
+
|-----------|-------------|
|
|
346
|
+
| `ScreenHeader` | Composite screen header widget |
|
|
347
|
+
| `MenuGrid` / `MenuCard` | Grid and card-based navigation menus |
|
|
348
|
+
| `QuickActions` | Quick action icon grid |
|
|
349
|
+
| `PromoBanner` | Swipeable promotional carousel |
|
|
350
|
+
| `PromoModal` | Promotional modal overlay |
|
|
351
|
+
| `HomeHeader` | Home screen hero header |
|
|
352
|
+
| `GeoPermissionCard` | Geolocation permission request card |
|
|
353
|
+
| `TimeZoneSelector` | Timezone picker widget |
|
|
354
|
+
| `JSONViewer` | Collapsible JSON tree (debug / dev tools) |
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Theme System
|
|
359
|
+
|
|
360
|
+
All component colors reference CSS variables — never hardcoded values. Swapping the theme rewrites the variables and every component updates without touching any component code.
|
|
361
|
+
|
|
362
|
+
### Setting the active theme
|
|
363
|
+
|
|
364
|
+
When using `@sincpro/mobile`, `createAppShell({ theme })` handles this for you. For standalone DS usage:
|
|
365
|
+
|
|
366
|
+
```tsx
|
|
367
|
+
import { setActiveTheme, themeToVars } from "@sincpro/mobile-ui/theme";
|
|
368
|
+
import { vars } from "nativewind";
|
|
369
|
+
|
|
370
|
+
// Set globally
|
|
371
|
+
setActiveTheme(MY_DARK_THEME);
|
|
372
|
+
|
|
373
|
+
// Inject CSS variables into a subtree
|
|
374
|
+
const themeStyle = vars(themeToVars(MY_DARK_THEME));
|
|
375
|
+
<View style={themeStyle}>{children}</View>
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
### Reading the theme in components
|
|
379
|
+
|
|
380
|
+
```tsx
|
|
381
|
+
import { useTheme } from "@sincpro/mobile-ui/theme";
|
|
382
|
+
|
|
383
|
+
function PriceCard() {
|
|
384
|
+
const theme = useTheme();
|
|
385
|
+
return <View style={{ backgroundColor: theme.bg.card, borderColor: theme.border.default }} />;
|
|
386
|
+
}
|
|
387
|
+
```
|
|
65
388
|
|
|
66
|
-
|
|
389
|
+
### Defining a custom theme
|
|
390
|
+
|
|
391
|
+
```tsx
|
|
392
|
+
import { extendTheme } from "@sincpro/mobile-ui/theme";
|
|
393
|
+
|
|
394
|
+
// Deep-merges on top of the DS base theme
|
|
395
|
+
const MY_THEME = extendTheme({
|
|
396
|
+
accent: "#00C357",
|
|
397
|
+
primary: "#0B0B0B",
|
|
398
|
+
secondary: "#6B6963",
|
|
399
|
+
bg: {
|
|
400
|
+
page: "#FAFAFA",
|
|
401
|
+
card: "#FFFFFF",
|
|
402
|
+
},
|
|
403
|
+
text: {
|
|
404
|
+
accent: "#006E2E", // text on accent-tinted surfaces
|
|
405
|
+
},
|
|
406
|
+
});
|
|
407
|
+
```
|
|
408
|
+
|
|
409
|
+
### ThemeTokens reference
|
|
410
|
+
|
|
411
|
+
| Group | Keys |
|
|
412
|
+
|-------|------|
|
|
413
|
+
| `primary / secondary / accent` | Main brand color scales |
|
|
414
|
+
| `bg.*` | `page, card, muted, accent, hover, disabled, popover` |
|
|
415
|
+
| `text.*` | `primary, secondary, tertiary, muted, accent, inverse, disabled, onPrimary, onSecondary, onAccent, onDanger, onSuccess` |
|
|
416
|
+
| `icon.*` | `primary, secondary, tertiary, inverse, disabled` |
|
|
417
|
+
| `border.*` | `default, light, strong, focus` |
|
|
418
|
+
| `success / warning / danger / info` | Semantic state colors |
|
|
419
|
+
| `*Light` | Tinted backgrounds for each state |
|
|
420
|
+
| `ring / input` | Focus ring and input border |
|
|
421
|
+
| `gradient.*` | `primary, accent` — arrays of color stops |
|
|
422
|
+
| `shadow.*` | `sm, md, lg` — React Native shadow objects |
|
|
423
|
+
|
|
424
|
+
---
|
|
425
|
+
|
|
426
|
+
## Typography System
|
|
427
|
+
|
|
428
|
+
Typography is built on a **role → family** indirection. Components never hardcode a font family — they resolve `fontFamilies[role]` at render time. Calling `configureFonts` changes the mapping globally with no re-render required.
|
|
429
|
+
|
|
430
|
+
### Variants
|
|
431
|
+
|
|
432
|
+
24 predefined text style variants via `Typography.Text`:
|
|
433
|
+
|
|
434
|
+
```tsx
|
|
435
|
+
import { Typography } from "@sincpro/mobile-ui/Typography";
|
|
436
|
+
|
|
437
|
+
<Typography.Text variant="display">Hero text</Typography.Text>
|
|
438
|
+
<Typography.Text variant="h1">Page title</Typography.Text>
|
|
439
|
+
<Typography.Text variant="body">Body paragraph at 14px.</Typography.Text>
|
|
440
|
+
<Typography.Text variant="data">1,204.50</Typography.Text> {/* mono, medium weight */}
|
|
441
|
+
<Typography.Text variant="overline">SECTION LABEL</Typography.Text> {/* UPPERCASE mono */}
|
|
442
|
+
<Typography.Text variant="caption">Metadata</Typography.Text>
|
|
443
|
+
<Typography.Text variant="button">Action Label</Typography.Text>
|
|
444
|
+
```
|
|
445
|
+
|
|
446
|
+
| Group | Variants |
|
|
447
|
+
|-------|----------|
|
|
448
|
+
| Display | `display` |
|
|
449
|
+
| Headings | `h1` `h2` `h3` `h4` `h5` `h6` |
|
|
450
|
+
| Body | `bodyLarge` `body` `bodySmall` |
|
|
451
|
+
| Labels | `label` `labelSmall` |
|
|
452
|
+
| Data (mono) | `data` `dataLarge` |
|
|
453
|
+
| Meta (mono) | `overline` `caption` `captionSmall` |
|
|
454
|
+
| Buttons | `button` `buttonSmall` `buttonLarge` |
|
|
455
|
+
| Legacy | `title` `subtitle` |
|
|
456
|
+
|
|
457
|
+
### FontRole
|
|
458
|
+
|
|
459
|
+
Every configurable font slot in the DS:
|
|
460
|
+
|
|
461
|
+
```
|
|
462
|
+
light · regular · medium · semiBold · extraBold ← body roles (Inter by default)
|
|
463
|
+
mono · monoMedium ← data roles (Fira Code by default)
|
|
464
|
+
titleRegular · titleMedium · title · titleBlack ← display roles (Inter fallback)
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
`title*` roles default to Inter. Override them with the app's brand display typeface.
|
|
468
|
+
|
|
469
|
+
### Configuring brand fonts at runtime
|
|
470
|
+
|
|
471
|
+
```tsx
|
|
472
|
+
import { configureFonts, resetFonts } from "@sincpro/mobile-ui/theme/typography";
|
|
473
|
+
|
|
474
|
+
configureFonts({
|
|
475
|
+
titleRegular: "Satoshi-Regular",
|
|
476
|
+
titleMedium: "Satoshi-Medium",
|
|
477
|
+
title: "Satoshi-Bold",
|
|
478
|
+
titleBlack: "Satoshi-Black",
|
|
479
|
+
});
|
|
480
|
+
|
|
481
|
+
// Reset to defaults — useful in tests and Storybook stories
|
|
482
|
+
resetFonts();
|
|
483
|
+
```
|
|
484
|
+
|
|
485
|
+
### Font size scale
|
|
486
|
+
|
|
487
|
+
| Token | `px` | Token | `px` |
|
|
488
|
+
|-------|------|-------|------|
|
|
489
|
+
| `xs` | 10 | `4xl` | 28 |
|
|
490
|
+
| `sm` | 13 | `5xl` | 32 |
|
|
491
|
+
| `base` | 14 | `6xl` | 36 |
|
|
492
|
+
| `lg` | 16 | `7xl` | 48 |
|
|
493
|
+
| `xl` | 18 | `8xl` | 64 |
|
|
494
|
+
| `2xl` | 20 | | |
|
|
495
|
+
| `3xl` | 24 | | |
|
|
496
|
+
|
|
497
|
+
### Custom text styles
|
|
498
|
+
|
|
499
|
+
```tsx
|
|
500
|
+
import { createTextStyle, createCustomTextStyle } from "@sincpro/mobile-ui/theme/typography";
|
|
501
|
+
|
|
502
|
+
// From a named variant with overrides
|
|
503
|
+
const errorLabel = createTextStyle("labelSmall", { color: "#FF4D1F" });
|
|
504
|
+
|
|
505
|
+
// Fully custom
|
|
506
|
+
const amountStyle = createCustomTextStyle("4xl", "monoMedium", {
|
|
507
|
+
lineHeight: "none",
|
|
508
|
+
letterSpacing: "tight",
|
|
509
|
+
});
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
---
|
|
513
|
+
|
|
514
|
+
## Motion Tokens
|
|
515
|
+
|
|
516
|
+
Centralized animation constants so motion feels consistent across every screen — not improvised per component:
|
|
517
|
+
|
|
518
|
+
```tsx
|
|
519
|
+
import { duration, easing, spring } from "@sincpro/mobile-ui/tokens";
|
|
520
|
+
|
|
521
|
+
// Durations (ms)
|
|
522
|
+
duration.instant // 0 — immediate state swap, no transition
|
|
523
|
+
duration.fast // 120 — micro-interactions (button press, toggle)
|
|
524
|
+
duration.normal // 200 — standard transitions (modal, drawer)
|
|
525
|
+
duration.slow // 320 — large layout shifts
|
|
526
|
+
duration.slower // 480 — onboarding, emphasis animations
|
|
527
|
+
|
|
528
|
+
// Bezier easing curves
|
|
529
|
+
easing.standard // general purpose
|
|
530
|
+
easing.decelerate // elements entering the screen
|
|
531
|
+
easing.accelerate // elements leaving the screen
|
|
532
|
+
easing.emphasized // expressive, attention-drawing transitions
|
|
533
|
+
|
|
534
|
+
// Reanimated spring presets
|
|
535
|
+
spring.default // balanced bounce
|
|
536
|
+
spring.gentle // smooth, no overshoot
|
|
537
|
+
spring.snappy // fast with slight bounce
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
Usage with Reanimated:
|
|
541
|
+
|
|
542
|
+
```tsx
|
|
543
|
+
import { withTiming, withSpring } from "react-native-reanimated";
|
|
544
|
+
import { duration, easing, spring } from "@sincpro/mobile-ui/tokens";
|
|
545
|
+
|
|
546
|
+
const opacity = withTiming(1, { duration: duration.normal, easing: easing.decelerate });
|
|
547
|
+
const scale = withSpring(1, spring.snappy);
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
---
|
|
551
|
+
|
|
552
|
+
## Branding
|
|
553
|
+
|
|
554
|
+
Configure the app logo once. DS components (`Display.Logo`, `Navigation.AppBar`, `AuthFormView`) use it as a fallback when no explicit `source` prop is passed:
|
|
67
555
|
|
|
68
556
|
```tsx
|
|
69
557
|
import { setBranding } from "@sincpro/mobile-ui";
|
|
70
|
-
|
|
558
|
+
|
|
559
|
+
// Call before rendering the tree
|
|
560
|
+
setBranding({
|
|
561
|
+
logo: require("../assets/logo.png"),
|
|
562
|
+
});
|
|
71
563
|
```
|
|
72
564
|
|
|
73
|
-
|
|
565
|
+
When using `@sincpro/mobile`, pass `branding` to `createAppShell` — it calls `setBranding` internally.
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
## Tailwind Setup
|
|
570
|
+
|
|
571
|
+
The preset ships the full token class set (`bg-primary`, `text-text-secondary`, `border-border-focus`, …) and the NativeWind v4 configuration.
|
|
572
|
+
|
|
573
|
+
### `tailwind.config.js`
|
|
574
|
+
|
|
575
|
+
```js
|
|
576
|
+
module.exports = {
|
|
577
|
+
presets: [require("@sincpro/mobile-ui/tailwind.preset")],
|
|
578
|
+
content: [
|
|
579
|
+
"./App.tsx",
|
|
580
|
+
"./src/**/*.{ts,tsx}",
|
|
581
|
+
"./node_modules/@sincpro/mobile-ui/**/*.js",
|
|
582
|
+
],
|
|
583
|
+
theme: {
|
|
584
|
+
extend: {
|
|
585
|
+
// Override font families to match loaded font names
|
|
586
|
+
fontFamily: {
|
|
587
|
+
light: ["Inter_300Light"],
|
|
588
|
+
regular: ["Inter_400Regular"],
|
|
589
|
+
medium: ["Inter_500Medium"],
|
|
590
|
+
semibold: ["Inter_600SemiBold"],
|
|
591
|
+
extrabold: ["Inter_800ExtraBold"],
|
|
592
|
+
},
|
|
593
|
+
},
|
|
594
|
+
},
|
|
595
|
+
};
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
### Global CSS
|
|
599
|
+
|
|
600
|
+
Import the DS global stylesheet at the root of the app:
|
|
74
601
|
|
|
75
602
|
```tsx
|
|
76
|
-
import
|
|
77
|
-
// wrap the app in <ToastProvider>; then:
|
|
78
|
-
const toast = useToast();
|
|
79
|
-
toast.promise(save(), { loading: "Saving…", success: "Done", error: "Failed" });
|
|
603
|
+
import "@sincpro/mobile-ui/theme/globals.css";
|
|
80
604
|
```
|
|
81
605
|
|
|
82
|
-
|
|
606
|
+
---
|
|
607
|
+
|
|
608
|
+
## Import Patterns
|
|
609
|
+
|
|
610
|
+
### By category subpath — recommended
|
|
611
|
+
|
|
612
|
+
Best tree-shaking; makes the component's category visible at the call site:
|
|
613
|
+
|
|
614
|
+
```tsx
|
|
615
|
+
import { Display } from "@sincpro/mobile-ui/Display";
|
|
616
|
+
import { Form } from "@sincpro/mobile-ui/Form";
|
|
617
|
+
import { Feedback } from "@sincpro/mobile-ui/Feedback";
|
|
618
|
+
import { Navigation } from "@sincpro/mobile-ui/Navigation";
|
|
619
|
+
import { Dialog } from "@sincpro/mobile-ui/Dialog";
|
|
620
|
+
import { Typography } from "@sincpro/mobile-ui/Typography";
|
|
621
|
+
import { Actions } from "@sincpro/mobile-ui/Actions";
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
### Theme and tokens
|
|
625
|
+
|
|
626
|
+
```tsx
|
|
627
|
+
import { useTheme, setActiveTheme, extendTheme, themeToVars } from "@sincpro/mobile-ui/theme";
|
|
628
|
+
import { duration, easing, spring, motion } from "@sincpro/mobile-ui/tokens";
|
|
629
|
+
```
|
|
630
|
+
|
|
631
|
+
### Direct component imports
|
|
632
|
+
|
|
633
|
+
For components outside a namespace:
|
|
83
634
|
|
|
84
635
|
```tsx
|
|
85
636
|
import Sheet from "@sincpro/mobile-ui/Dialog/Dialog.Sheet";
|
|
86
|
-
|
|
87
|
-
|
|
637
|
+
import { ConfirmationProvider } from "@sincpro/mobile-ui/Dialog";
|
|
638
|
+
import { ToastProvider, useToast } from "@sincpro/mobile-ui/Feedback";
|
|
639
|
+
import { Wizard, useWizard } from "@sincpro/mobile-ui/views/Wizard";
|
|
88
640
|
```
|
|
89
641
|
|
|
90
|
-
|
|
642
|
+
### Layout primitives
|
|
91
643
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
644
|
+
```tsx
|
|
645
|
+
import { Box, Row, Stack, Spacer } from "@sincpro/mobile-ui/primitives";
|
|
646
|
+
import Container from "@sincpro/mobile-ui/layouts/Container";
|
|
647
|
+
import PlainLayout from "@sincpro/mobile-ui/layouts/PlainLayout";
|
|
648
|
+
import TabNavigatorLayout from "@sincpro/mobile-ui/layouts/TabNavigatorLayout";
|
|
649
|
+
```
|
|
650
|
+
|
|
651
|
+
---
|
|
97
652
|
|
|
98
|
-
## Storybook
|
|
653
|
+
## Storybook
|
|
99
654
|
|
|
100
|
-
|
|
655
|
+
Every component in the DS has a Storybook story for on-device preview and manual QA. Run it on simulator or physical device:
|
|
101
656
|
|
|
102
657
|
```bash
|
|
103
|
-
make storybook # expo start
|
|
658
|
+
make storybook # expo start — Expo Go or dev build
|
|
104
659
|
make storybook-android # expo run:android
|
|
105
660
|
```
|
|
106
661
|
|
|
107
|
-
Stories
|
|
108
|
-
|
|
109
|
-
|
|
662
|
+
Stories are organized into four groups:
|
|
663
|
+
|
|
664
|
+
| Group | Contents |
|
|
665
|
+
|-------|----------|
|
|
666
|
+
| **Foundations** | Tokens, typography scale, color palette, motion |
|
|
667
|
+
| **Components** | All compound component namespaces |
|
|
668
|
+
| **Patterns** | Composed examples (list screen, form screen, auth flow) |
|
|
669
|
+
| **Legacy** | Deprecated components kept for backwards compatibility |
|
|
670
|
+
|
|
671
|
+
Stories live in `.rnstorybook/stories/**/*.stories.tsx`. The Storybook harness is excluded from the published package (`files: ["dist", ...]`) and never lands in consumer bundles.
|
|
672
|
+
|
|
673
|
+
---
|
|
110
674
|
|
|
111
|
-
##
|
|
675
|
+
## Development
|
|
676
|
+
|
|
677
|
+
| Command | What it does |
|
|
678
|
+
|---------|-------------|
|
|
679
|
+
| `make build` | `tsc` + `tsc-alias` → `dist/` (JS + type declarations) |
|
|
680
|
+
| `make format` | Auto-fix: ESLint + Prettier + type-check |
|
|
681
|
+
| `make verify-format` | CI gate — runs `format`, fails if any file changed |
|
|
682
|
+
| `make storybook` | Start Expo dev server for Storybook |
|
|
683
|
+
| `make publish` | Bump version + `npm publish` |
|
|
684
|
+
|
|
685
|
+
After building, copy the dist to consumers in the local monorepo (no symlinks setup):
|
|
686
|
+
|
|
687
|
+
```bash
|
|
688
|
+
cp -r dist/* ../sincpro_mobile_tickets/node_modules/@sincpro/mobile-ui/dist/
|
|
689
|
+
```
|
|
112
690
|
|
|
113
|
-
|
|
114
|
-
subpaths (`./Display`, `./Form`, …) plus a catch-all `./*`. See [`CHANGELOG.md`](CHANGELOG.md).
|
|
691
|
+
Architectural decisions, naming conventions, and guardrails: [`AGENTS.md`](AGENTS.md).
|
|
@@ -4,13 +4,29 @@ import Pressable from "../primitives/Pressable";
|
|
|
4
4
|
import { theme } from "../theme";
|
|
5
5
|
import { cn } from "../theme/tw";
|
|
6
6
|
import { Typography } from "../Typography";
|
|
7
|
-
import {
|
|
7
|
+
import { useEffect, useRef } from "react";
|
|
8
|
+
import { Animated, Modal, Pressable as RNPressable, View } from "react-native";
|
|
8
9
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
9
10
|
function ActionSheet({ visible, onClose, actions, title, message, cancelLabel = "Cancelar", }) {
|
|
10
11
|
const insets = useSafeAreaInsets();
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// eslint-disable-next-line react-hooks/refs
|
|
13
|
+
const translateY = useRef(new Animated.Value(800)).current;
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (visible) {
|
|
16
|
+
Animated.spring(translateY, {
|
|
17
|
+
toValue: 0,
|
|
18
|
+
useNativeDriver: true,
|
|
19
|
+
damping: 20,
|
|
20
|
+
stiffness: 300,
|
|
21
|
+
}).start();
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
translateY.setValue(800);
|
|
25
|
+
}
|
|
26
|
+
}, [visible, translateY]);
|
|
27
|
+
return (_jsx(Modal, { animationType: "none", onRequestClose: onClose, presentationStyle: "overFullScreen", transparent: true, visible: visible, children: _jsx(RNPressable, { className: "flex-1 justify-end bg-black/50", onPress: onClose, children: _jsx(Animated.View, { style: { transform: [{ translateY }] }, children: _jsxs(RNPressable, { style: { paddingHorizontal: 12, paddingBottom: insets.bottom + 12 }, children: [title || message ? (_jsxs(View, { className: "rounded-xl mb-2 px-4 py-3 items-center", style: { backgroundColor: theme.bg.card }, children: [title ? (_jsx(Typography.Text, { className: "text-text-primary", semibold: true, children: title })) : null, message ? (_jsx(Typography.Text, { className: "text-text-secondary text-sm", variant: "bodySmall", children: message })) : null] })) : null, _jsx(View, { className: "rounded-xl overflow-hidden mb-2", style: { backgroundColor: theme.bg.card }, children: actions.map((action, index) => (_jsxs(Pressable, { accessibilityRole: "button", accessibilityState: { disabled: action.disabled }, className: cn("flex-row items-center justify-center gap-2 px-4 py-4", index < actions.length - 1 && "border-b border-border-light"), disabled: action.disabled, onPress: () => {
|
|
28
|
+
onClose();
|
|
29
|
+
action.onPress();
|
|
30
|
+
}, children: [action.icon ? (_jsx(Icon, { color: action.destructive ? theme.danger : theme.icon.primary, name: action.icon, size: 20 })) : null, _jsx(Typography.Text, { className: cn(action.destructive ? "text-danger" : "text-text-primary"), semibold: true, children: action.label })] }, action.label))) }), _jsx(Pressable, { accessibilityRole: "button", className: "rounded-xl px-4 py-4 items-center", onPress: onClose, style: { backgroundColor: theme.bg.card }, children: _jsx(Typography.Text, { className: "text-primary", semibold: true, children: cancelLabel }) })] }) }) }) }));
|
|
15
31
|
}
|
|
16
32
|
export default ActionSheet;
|
|
@@ -4,7 +4,7 @@ import { theme } from "../theme";
|
|
|
4
4
|
import Typography from "../Typography/Typography.Text";
|
|
5
5
|
import { Modal, TouchableWithoutFeedback, View } from "react-native";
|
|
6
6
|
const ConfirmationDialog = ({ visible, onCancel, onConfirm, auxTitle = "cambios", subtitle = "¿Estás seguro de que quieres descartar los cambios realizados?", confirmButtonText = "Sí", cancelButtonText = "No, continuar aquí", }) => {
|
|
7
|
-
return (_jsx(Modal, { animationType: "fade", onRequestClose: onCancel, transparent: true, visible: visible, children: _jsx(TouchableWithoutFeedback, { onPress: onCancel, children: _jsx(View, { className: "flex-1 justify-center items-center p-6 bg-black/50", children: _jsx(TouchableWithoutFeedback, { children: _jsxs(View, { className: "p-3 w-full max-w-xs mx-auto", style: {
|
|
7
|
+
return (_jsx(Modal, { animationType: "fade", onRequestClose: onCancel, presentationStyle: "overFullScreen", transparent: true, visible: visible, children: _jsx(TouchableWithoutFeedback, { onPress: onCancel, children: _jsx(View, { className: "flex-1 justify-center items-center p-6 bg-black/50", children: _jsx(TouchableWithoutFeedback, { children: _jsxs(View, { className: "p-3 w-full max-w-xs mx-auto", style: {
|
|
8
8
|
borderRadius: 22,
|
|
9
9
|
backgroundColor: theme.bg.popover,
|
|
10
10
|
...theme.shadow.md,
|
|
@@ -11,6 +11,6 @@ function EditableValueModal({ visible, title, value, onChangeValue, onConfirm, o
|
|
|
11
11
|
return () => clearTimeout(t);
|
|
12
12
|
}
|
|
13
13
|
}, [visible, autoFocus]);
|
|
14
|
-
return (_jsx(Modal, { animationType: "fade", onRequestClose: onCancel, transparent: true, visible: visible, children: _jsx(View, { className: "flex-1 justify-center p-6 bg-black/40", children: _jsxs(View, { className: "p-4", style: { borderRadius: 22, backgroundColor: theme.bg.popover, ...theme.shadow.md }, testID: testID, children: [_jsx(Typography.Text, { className: "mb-3", semibold: true, variant: "h6", children: title }), _jsxs(View, { className: "flex-row items-center", children: [_jsx(TextInput, { className: "flex-1 border border-input rounded-lg p-2.5 text-lg text-text-primary", keyboardType: keyboardType, onChangeText: onChangeValue, onSubmitEditing: onConfirm, placeholder: placeholder, ref: inputRef, returnKeyType: "done", value: value }), rightAdornment && _jsx(View, { className: "ml-2", children: rightAdornment })] }), helperText ? (_jsx(View, { className: "mt-2", children: typeof helperText === "string" ? (_jsx(Typography.Text, { className: "opacity-70", variant: "bodySmall", children: helperText })) : (helperText) })) : null, chips && chips.length > 0 && (_jsx(FlatList, { contentContainerClassName: "gap-2 py-3", data: chips, horizontal: true, keyExtractor: (c, i) => c.key || `${c.label}-${i}`, renderItem: ({ item }) => (_jsx(TouchableOpacity, { className: "px-3 py-1.5 bg-accent/10 rounded-2xl border border-accent/30", onPress: item.onPress, children: _jsx(Typography.Text, { semibold: true, variant: "bodySmall", children: item.label }) })) })), _jsxs(View, { className: "flex-row justify-end gap-3", children: [_jsx(TouchableOpacity, { className: "px-4 py-2.5 rounded-lg bg-bg-hover", onPress: onCancel, children: _jsx(Typography.Text, { semibold: true, variant: "body", children: cancelLabel }) }), _jsx(TouchableOpacity, { className: "px-4 py-2.5 rounded-lg bg-accent", onPress: onConfirm, children: _jsx(Typography.Text, { className: "text-text-inverse", semibold: true, variant: "body", children: confirmLabel }) })] })] }) }) }));
|
|
14
|
+
return (_jsx(Modal, { animationType: "fade", onRequestClose: onCancel, presentationStyle: "overFullScreen", transparent: true, visible: visible, children: _jsx(View, { className: "flex-1 justify-center p-6 bg-black/40", children: _jsxs(View, { className: "p-4", style: { borderRadius: 22, backgroundColor: theme.bg.popover, ...theme.shadow.md }, testID: testID, children: [_jsx(Typography.Text, { className: "mb-3", semibold: true, variant: "h6", children: title }), _jsxs(View, { className: "flex-row items-center", children: [_jsx(TextInput, { className: "flex-1 border border-input rounded-lg p-2.5 text-lg text-text-primary", keyboardType: keyboardType, onChangeText: onChangeValue, onSubmitEditing: onConfirm, placeholder: placeholder, ref: inputRef, returnKeyType: "done", value: value }), rightAdornment && _jsx(View, { className: "ml-2", children: rightAdornment })] }), helperText ? (_jsx(View, { className: "mt-2", children: typeof helperText === "string" ? (_jsx(Typography.Text, { className: "opacity-70", variant: "bodySmall", children: helperText })) : (helperText) })) : null, chips && chips.length > 0 && (_jsx(FlatList, { contentContainerClassName: "gap-2 py-3", data: chips, horizontal: true, keyExtractor: (c, i) => c.key || `${c.label}-${i}`, renderItem: ({ item }) => (_jsx(TouchableOpacity, { className: "px-3 py-1.5 bg-accent/10 rounded-2xl border border-accent/30", onPress: item.onPress, children: _jsx(Typography.Text, { semibold: true, variant: "bodySmall", children: item.label }) })) })), _jsxs(View, { className: "flex-row justify-end gap-3", children: [_jsx(TouchableOpacity, { className: "px-4 py-2.5 rounded-lg bg-bg-hover", onPress: onCancel, children: _jsx(Typography.Text, { semibold: true, variant: "body", children: cancelLabel }) }), _jsx(TouchableOpacity, { className: "px-4 py-2.5 rounded-lg bg-accent", onPress: onConfirm, children: _jsx(Typography.Text, { className: "text-text-inverse", semibold: true, variant: "body", children: confirmLabel }) })] })] }) }) }));
|
|
15
15
|
}
|
|
16
16
|
export default EditableValueModal;
|
|
@@ -18,13 +18,22 @@ export function SheetProvider({ children }) {
|
|
|
18
18
|
*/
|
|
19
19
|
function Sheet({ visible, onClose, children, title, maxHeightRatio = 0.9, dismissOnBackdrop = false, testID, }) {
|
|
20
20
|
const insets = useSafeAreaInsets();
|
|
21
|
-
const
|
|
21
|
+
const translateY = useRef(new Animated.Value(800)).current;
|
|
22
22
|
const onCloseRef = useRef(onClose);
|
|
23
23
|
// eslint-disable-next-line react-hooks/refs
|
|
24
24
|
onCloseRef.current = onClose;
|
|
25
25
|
useEffect(() => {
|
|
26
|
-
if (visible)
|
|
27
|
-
|
|
26
|
+
if (visible) {
|
|
27
|
+
Animated.spring(translateY, {
|
|
28
|
+
toValue: 0,
|
|
29
|
+
useNativeDriver: true,
|
|
30
|
+
damping: 20,
|
|
31
|
+
stiffness: 300,
|
|
32
|
+
}).start();
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
translateY.setValue(800);
|
|
36
|
+
}
|
|
28
37
|
}, [visible, translateY]);
|
|
29
38
|
// eslint-disable-next-line react-hooks/refs
|
|
30
39
|
const [pan] = useState(() => PanResponder.create({
|
|
@@ -47,7 +56,7 @@ function Sheet({ visible, onClose, children, title, maxHeightRatio = 0.9, dismis
|
|
|
47
56
|
}
|
|
48
57
|
},
|
|
49
58
|
}));
|
|
50
|
-
return (_jsx(Modal, { animationType: "
|
|
59
|
+
return (_jsx(Modal, { animationType: "none", onRequestClose: onClose, presentationStyle: "overFullScreen", transparent: true, visible: visible, children: _jsxs(View, { className: "flex-1 justify-end", testID: testID, children: [_jsx(View, { className: "absolute inset-0 bg-black/50", onResponderRelease: dismissOnBackdrop ? onClose : undefined, onStartShouldSetResponder: () => dismissOnBackdrop }), _jsxs(Animated.View, { className: "rounded-t-3xl", style: {
|
|
51
60
|
backgroundColor: theme.bg.card,
|
|
52
61
|
paddingBottom: insets.bottom + 12,
|
|
53
62
|
maxHeight: `${Math.round(maxHeightRatio * 100)}%`,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { jsx as _jsx } from "nativewind/jsx-runtime";
|
|
2
2
|
import { cn } from "../theme/tw";
|
|
3
3
|
import { motion } from "../tokens/motion";
|
|
4
|
-
import { useEffect,
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
5
|
import { Animated, Easing, View } from "react-native";
|
|
6
6
|
function clamp(n) {
|
|
7
7
|
return Math.max(0, Math.min(1, n));
|
|
8
8
|
}
|
|
9
9
|
function Progress({ value = 0, indeterminate = false, height = 6, className, testID, }) {
|
|
10
|
-
|
|
10
|
+
// eslint-disable-next-line react-hooks/refs
|
|
11
|
+
const translate = useRef(new Animated.Value(0)).current;
|
|
11
12
|
useEffect(() => {
|
|
12
13
|
if (!indeterminate)
|
|
13
14
|
return;
|
|
@@ -24,6 +25,7 @@ function Progress({ value = 0, indeterminate = false, height = 6, className, tes
|
|
|
24
25
|
width: "40%",
|
|
25
26
|
transform: [
|
|
26
27
|
{
|
|
28
|
+
// eslint-disable-next-line react-hooks/refs
|
|
27
29
|
translateX: translate.interpolate({
|
|
28
30
|
inputRange: [0, 1],
|
|
29
31
|
outputRange: ["-40%", "250%"],
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx } from "nativewind/jsx-runtime";
|
|
2
2
|
import { cn } from "../theme/tw";
|
|
3
3
|
import { motion } from "../tokens/motion";
|
|
4
|
-
import { useEffect,
|
|
4
|
+
import { useEffect, useRef } from "react";
|
|
5
5
|
import { Animated, Easing } from "react-native";
|
|
6
6
|
function Skeleton({ width = "100%", height = 16, radius = 8, circle = false, className, style, }) {
|
|
7
|
-
|
|
7
|
+
// eslint-disable-next-line react-hooks/refs
|
|
8
|
+
const opacity = useRef(new Animated.Value(0.5)).current;
|
|
8
9
|
useEffect(() => {
|
|
9
10
|
const pulse = Animated.loop(Animated.sequence([
|
|
10
11
|
Animated.timing(opacity, {
|
|
@@ -77,7 +77,7 @@ function DropdownButton({ title, options, variant = "secondary", size = "small",
|
|
|
77
77
|
xs: "bodySmall",
|
|
78
78
|
small: "bodySmall",
|
|
79
79
|
}[size];
|
|
80
|
-
return (_jsxs(_Fragment, { children: [_jsxs(TouchableOpacity, { activeOpacity: 0.7, className: styles.button(), onPress: () => setIsOpen(true), children: [_jsx(Typography.Text, { className: textColorClass, variant: textVariant, children: title }), _jsx(Display.Icon, { color: iconColor, name: "chevron-down", size: 16, type: "feather" })] }), _jsx(Modal, { animationType: "fade", transparent: true, visible: isOpen, children: _jsx(TouchableWithoutFeedback, { onPress: () => setIsOpen(false), children: _jsx(View, { className: styles.overlay(), children: _jsx(TouchableWithoutFeedback, { children: _jsxs(View, { className: styles.dropdownContainer(), style: {
|
|
80
|
+
return (_jsxs(_Fragment, { children: [_jsxs(TouchableOpacity, { activeOpacity: 0.7, className: styles.button(), onPress: () => setIsOpen(true), children: [_jsx(Typography.Text, { className: textColorClass, variant: textVariant, children: title }), _jsx(Display.Icon, { color: iconColor, name: "chevron-down", size: 16, type: "feather" })] }), _jsx(Modal, { animationType: "fade", presentationStyle: "overFullScreen", transparent: true, visible: isOpen, children: _jsx(TouchableWithoutFeedback, { onPress: () => setIsOpen(false), children: _jsx(View, { className: styles.overlay(), children: _jsx(TouchableWithoutFeedback, { children: _jsxs(View, { className: styles.dropdownContainer(), style: {
|
|
81
81
|
backgroundColor: theme.bg.popover,
|
|
82
82
|
...theme.shadow.lg,
|
|
83
83
|
}, children: [_jsxs(View, { className: styles.header(), children: [_jsx(Typography.Text, { className: "text-text-primary", semibold: true, variant: "bodyLarge", children: title }), _jsx(TouchableOpacity, { onPress: () => setIsOpen(false), children: _jsx(Display.Icon, { name: "x", size: 20, type: "feather" }) })] }), _jsx(View, { className: styles.optionsList(), children: options.map((option, index) => (_jsxs(View, { children: [_jsx(TouchableOpacity, { activeOpacity: 0.7, className: dropdown({ disabled: option.disabled }).option(), disabled: option.disabled, onPress: () => handleOptionPress(option), children: _jsx(Typography.Text, { className: option.disabled ? "text-text-disabled" : "text-text-primary", variant: "body", children: option.title }) }), index < options.length - 1 && _jsx(View, { className: styles.separator() })] }, option.id))) })] }) }) }) }) })] }));
|
package/dist/Form/Form.Select.js
CHANGED
|
@@ -4,23 +4,38 @@ import Pressable from "../primitives/Pressable";
|
|
|
4
4
|
import { theme } from "../theme";
|
|
5
5
|
import { cn } from "../theme/tw";
|
|
6
6
|
import { Typography } from "../Typography";
|
|
7
|
-
import { useState } from "react";
|
|
8
|
-
import { Modal, Pressable as RNPressable, ScrollView, View } from "react-native";
|
|
7
|
+
import { useEffect, useRef, useState } from "react";
|
|
8
|
+
import { Animated, Modal, Pressable as RNPressable, ScrollView, View } from "react-native";
|
|
9
9
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
10
10
|
function Select({ options, value, onChange, label, placeholder = "Seleccionar…", title, disabled = false, className, testID, }) {
|
|
11
11
|
const insets = useSafeAreaInsets();
|
|
12
12
|
const [open, setOpen] = useState(false);
|
|
13
13
|
const selected = options.find((o) => o.value === value) ?? null;
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
14
|
+
// eslint-disable-next-line react-hooks/refs
|
|
15
|
+
const translateY = useRef(new Animated.Value(800)).current;
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (open) {
|
|
18
|
+
Animated.spring(translateY, {
|
|
19
|
+
toValue: 0,
|
|
20
|
+
useNativeDriver: true,
|
|
21
|
+
damping: 20,
|
|
22
|
+
stiffness: 300,
|
|
23
|
+
}).start();
|
|
24
|
+
}
|
|
25
|
+
else {
|
|
26
|
+
translateY.setValue(800);
|
|
27
|
+
}
|
|
28
|
+
}, [open, translateY]);
|
|
29
|
+
return (_jsxs(View, { className: cn("gap-1", className), testID: testID, children: [label ? (_jsx(Typography.Text, { className: "font-medium text-text-primary", variant: "bodySmall", children: label })) : null, _jsxs(Pressable, { accessibilityHint: "Abre la lista de opciones", accessibilityLabel: label ?? placeholder, accessibilityRole: "button", accessibilityState: { disabled }, className: "flex-row items-center justify-between rounded-lg border border-input bg-bg-card px-3 min-h-[44px]", disabled: disabled, onPress: () => setOpen(true), children: [_jsx(Typography.Text, { className: cn("flex-1 text-sm", selected ? "text-text-primary" : "text-text-tertiary"), numberOfLines: 1, children: selected ? selected.label : placeholder }), _jsx(Icon, { color: theme.icon.secondary, name: "chevron-down", size: 18 })] }), _jsx(Modal, { animationType: "none", onRequestClose: () => setOpen(false), presentationStyle: "overFullScreen", transparent: true, visible: open, children: _jsx(RNPressable, { className: "flex-1 justify-end bg-black/50", onPress: () => setOpen(false), children: _jsx(Animated.View, { style: { transform: [{ translateY }] }, children: _jsxs(RNPressable, { className: "rounded-t-3xl", style: {
|
|
30
|
+
backgroundColor: theme.bg.card,
|
|
31
|
+
paddingBottom: insets.bottom + 8,
|
|
32
|
+
maxHeight: "70%",
|
|
33
|
+
}, children: [_jsx(View, { className: "items-center pt-3 pb-1", children: _jsx(View, { className: "w-10 h-1 rounded-full bg-border-strong" }) }), title ? (_jsx(Typography.Text, { className: "text-text-primary px-4 py-2", semibold: true, variant: "body", children: title })) : null, _jsx(ScrollView, { children: options.map((option) => {
|
|
34
|
+
const isSelected = option.value === value;
|
|
35
|
+
return (_jsxs(Pressable, { accessibilityRole: "menuitem", accessibilityState: { selected: isSelected }, className: "flex-row items-center justify-between px-4 py-3 border-b border-border-light", onPress: () => {
|
|
36
|
+
onChange(option.value);
|
|
37
|
+
setOpen(false);
|
|
38
|
+
}, children: [_jsx(Typography.Text, { className: cn("flex-1 text-sm", isSelected ? "text-primary font-semibold" : "text-text-primary"), children: option.label }), isSelected ? (_jsx(Icon, { color: theme.primary, name: "checkmark", size: 18 })) : null] }, String(option.value)));
|
|
39
|
+
}) })] }) }) }) })] }));
|
|
25
40
|
}
|
|
26
41
|
export default Select;
|
|
@@ -21,7 +21,7 @@ function Menu({ items, trigger, menuWidth = 220, testID }) {
|
|
|
21
21
|
const top = anchor ? anchor.y + anchor.height + 4 : 0;
|
|
22
22
|
return (_jsxs(View, { collapsable: false, ref: triggerRef, testID: testID, children: [trigger ? (
|
|
23
23
|
// eslint-disable-next-line react-hooks/refs
|
|
24
|
-
cloneElement(trigger, { onPress: open })) : (_jsx(Pressable, { accessibilityLabel: "M\u00E1s opciones", accessibilityRole: "button", onPress: open, children: _jsx(Icon, { color: theme.icon.secondary, name: "ellipsis-vertical", size: 22 }) })), _jsx(Modal, { animationType: "fade", onRequestClose: close, transparent: true, visible: anchor !== null, children: _jsx(RNPressable, { onPress: close, style: { flex: 1 }, children: _jsx(View, { className: "rounded-lg py-1", style: {
|
|
24
|
+
cloneElement(trigger, { onPress: open })) : (_jsx(Pressable, { accessibilityLabel: "M\u00E1s opciones", accessibilityRole: "button", onPress: open, children: _jsx(Icon, { color: theme.icon.secondary, name: "ellipsis-vertical", size: 22 }) })), _jsx(Modal, { animationType: "fade", onRequestClose: close, presentationStyle: "overFullScreen", transparent: true, visible: anchor !== null, children: _jsx(RNPressable, { onPress: close, style: { flex: 1 }, children: _jsx(View, { className: "rounded-lg py-1", style: {
|
|
25
25
|
position: "absolute",
|
|
26
26
|
top,
|
|
27
27
|
right,
|
|
@@ -1,13 +1,14 @@
|
|
|
1
|
+
import { useFonts } from "@expo-google-fonts/inter";
|
|
1
2
|
import type { TextStyle } from "react-native";
|
|
2
3
|
export type FontRole = "light" | "regular" | "medium" | "semiBold" | "extraBold" | "mono" | "monoMedium" | "titleRegular" | "titleMedium" | "title" | "titleBlack";
|
|
3
4
|
export declare const fontFamilies: Record<FontRole, string>;
|
|
4
5
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* `configureFonts({ title: "Satoshi-Bold", titleBlack: "Satoshi-Black" })`.
|
|
6
|
+
* Override DS font family names (role → already-loaded family name).
|
|
7
|
+
* Mutates the singleton in-place so components pick up the change at render time.
|
|
8
|
+
* Example: `configureFonts({ title: "Satoshi-Bold", titleBlack: "Satoshi-Black" })`.
|
|
8
9
|
*/
|
|
9
10
|
export declare function configureFonts(overrides: Partial<Record<FontRole, string>>): void;
|
|
10
|
-
/**
|
|
11
|
+
/** Reset all font roles to the defaults (Inter + Fira Code). Useful in tests and stories. */
|
|
11
12
|
export declare function resetFonts(): void;
|
|
12
13
|
export declare const fontWeights: {
|
|
13
14
|
readonly light: "300";
|
|
@@ -216,6 +217,7 @@ export declare const createCustomTextStyle: (fontSize: keyof typeof fontSizes, f
|
|
|
216
217
|
letterSpacing?: keyof typeof letterSpacing;
|
|
217
218
|
color?: string;
|
|
218
219
|
}) => TextStyle;
|
|
220
|
+
export { useFonts };
|
|
219
221
|
export declare const useAppFonts: () => boolean;
|
|
220
222
|
export declare const Typography: {
|
|
221
223
|
readonly fontFamilies: Record<FontRole, string>;
|
package/dist/theme/typography.js
CHANGED
|
@@ -8,7 +8,7 @@ const DEFAULT_FONT_FAMILIES = {
|
|
|
8
8
|
extraBold: "Inter_800ExtraBold",
|
|
9
9
|
mono: "FiraCode_400Regular",
|
|
10
10
|
monoMedium: "FiraCode_500Medium",
|
|
11
|
-
//
|
|
11
|
+
// Title roles default to Inter until the app wires its brand typeface via configureFonts.
|
|
12
12
|
titleRegular: "Inter_400Regular",
|
|
13
13
|
titleMedium: "Inter_500Medium",
|
|
14
14
|
title: "Inter_600SemiBold",
|
|
@@ -16,14 +16,14 @@ const DEFAULT_FONT_FAMILIES = {
|
|
|
16
16
|
};
|
|
17
17
|
export const fontFamilies = { ...DEFAULT_FONT_FAMILIES };
|
|
18
18
|
/**
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
* `configureFonts({ title: "Satoshi-Bold", titleBlack: "Satoshi-Black" })`.
|
|
19
|
+
* Override DS font family names (role → already-loaded family name).
|
|
20
|
+
* Mutates the singleton in-place so components pick up the change at render time.
|
|
21
|
+
* Example: `configureFonts({ title: "Satoshi-Bold", titleBlack: "Satoshi-Black" })`.
|
|
22
22
|
*/
|
|
23
23
|
export function configureFonts(overrides) {
|
|
24
24
|
Object.assign(fontFamilies, overrides);
|
|
25
25
|
}
|
|
26
|
-
/**
|
|
26
|
+
/** Reset all font roles to the defaults (Inter + Fira Code). Useful in tests and stories. */
|
|
27
27
|
export function resetFonts() {
|
|
28
28
|
Object.assign(fontFamilies, DEFAULT_FONT_FAMILIES);
|
|
29
29
|
}
|
|
@@ -75,7 +75,7 @@ export const letterSpacing = {
|
|
|
75
75
|
};
|
|
76
76
|
/* -------------------- Typography Variants -------------------- */
|
|
77
77
|
export const typographyVariants = {
|
|
78
|
-
// Display (Satoshi Black —
|
|
78
|
+
// Display (Satoshi Black — brand character)
|
|
79
79
|
display: {
|
|
80
80
|
fontSize: fontSizes["7xl"],
|
|
81
81
|
fontFamily: fontFamilies.titleBlack,
|
|
@@ -163,7 +163,7 @@ export const typographyVariants = {
|
|
|
163
163
|
lineHeight: fontSizes.sm * lineHeights.normal,
|
|
164
164
|
letterSpacing: letterSpacing.wide,
|
|
165
165
|
},
|
|
166
|
-
// Data —
|
|
166
|
+
// Data — amounts, SKU, % (Fira Code mono, medium weight). DS role "data".
|
|
167
167
|
data: {
|
|
168
168
|
fontSize: fontSizes.lg,
|
|
169
169
|
fontFamily: fontFamilies.monoMedium,
|
|
@@ -178,7 +178,7 @@ export const typographyVariants = {
|
|
|
178
178
|
lineHeight: fontSizes["4xl"] * lineHeights.none,
|
|
179
179
|
letterSpacing: letterSpacing.tight,
|
|
180
180
|
},
|
|
181
|
-
// Overline — labels/metadata
|
|
181
|
+
// Overline — UPPERCASE labels / metadata in mono with tracking. DS role "caption".
|
|
182
182
|
overline: {
|
|
183
183
|
fontSize: 11,
|
|
184
184
|
fontFamily: fontFamilies.mono,
|
|
@@ -187,7 +187,7 @@ export const typographyVariants = {
|
|
|
187
187
|
letterSpacing: 0.6,
|
|
188
188
|
textTransform: "uppercase",
|
|
189
189
|
},
|
|
190
|
-
// Caption — Fira Code mono (
|
|
190
|
+
// Caption — Fira Code mono (secondary text, metadata, data labels)
|
|
191
191
|
caption: {
|
|
192
192
|
fontSize: fontSizes.sm,
|
|
193
193
|
fontFamily: fontFamilies.mono,
|
|
@@ -241,9 +241,9 @@ export const typographyVariants = {
|
|
|
241
241
|
},
|
|
242
242
|
};
|
|
243
243
|
/* -------------------- Variant → Font Role --------------------
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
*
|
|
244
|
+
* Each variant points to a ROLE (not a fixed family) so `configureFonts` can
|
|
245
|
+
* update it at runtime. `Typography.Text` resolves `fontFamilies[role]` on
|
|
246
|
+
* render. The baked-in family in each variant above is a static fallback only. */
|
|
247
247
|
export const variantFontRole = {
|
|
248
248
|
display: "titleBlack",
|
|
249
249
|
h1: "titleBlack",
|
|
@@ -286,10 +286,13 @@ export const createCustomTextStyle = (fontSize, fontFamily, options) => {
|
|
|
286
286
|
...(options?.color && { color: options.color }),
|
|
287
287
|
};
|
|
288
288
|
};
|
|
289
|
-
/*
|
|
290
|
-
*
|
|
291
|
-
|
|
292
|
-
|
|
289
|
+
/* Re-export useFonts so consumers (e.g. the framework) can load fonts without
|
|
290
|
+
* depending directly on expo-font — it's already a transitive dep via this package. */
|
|
291
|
+
export { useFonts };
|
|
292
|
+
/* -------------------- Font Loader (optional, convenience) ------------------
|
|
293
|
+
* Loads the recommended base fonts (Inter + Fira Code) declared as peer deps.
|
|
294
|
+
* The "title" role (e.g. Satoshi) is loaded and wired by the app/Storybook
|
|
295
|
+
* via `configureFonts` — the DS never bundles font files. */
|
|
293
296
|
export const useAppFonts = () => {
|
|
294
297
|
const [loaded] = useFonts({
|
|
295
298
|
FiraCode_400Regular,
|
|
@@ -25,7 +25,7 @@ export function PromoModal({ visible, onDismiss, title, body, icon: Icon, header
|
|
|
25
25
|
const theme = useTheme();
|
|
26
26
|
const insets = useSafeAreaInsets();
|
|
27
27
|
const accent = accentColor ?? theme.accent;
|
|
28
|
-
return (_jsxs(Modal, { animationType: "fade", onRequestClose: onDismiss, statusBarTranslucent: true, transparent: true, visible: visible, children: [_jsx(TouchableWithoutFeedback, { onPress: onDismiss, children: _jsx(View, { style: {
|
|
28
|
+
return (_jsxs(Modal, { animationType: "fade", onRequestClose: onDismiss, presentationStyle: "overFullScreen", statusBarTranslucent: true, transparent: true, visible: visible, children: [_jsx(TouchableWithoutFeedback, { onPress: onDismiss, children: _jsx(View, { style: {
|
|
29
29
|
flex: 1,
|
|
30
30
|
backgroundColor: "rgba(0,0,0,0.55)",
|
|
31
31
|
alignItems: "center",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sincpro/mobile-ui",
|
|
3
|
-
"version": "0.7.
|
|
3
|
+
"version": "0.7.2",
|
|
4
4
|
"description": "Sincpro Mobile design system: generic, presentational React Native components (no domain logic)",
|
|
5
5
|
"main": "expo/AppEntry.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -50,7 +50,6 @@
|
|
|
50
50
|
"peerDependencies": {
|
|
51
51
|
"@expo-google-fonts/fira-code": "*",
|
|
52
52
|
"@expo-google-fonts/inter": "*",
|
|
53
|
-
"@expo-google-fonts/montserrat": "*",
|
|
54
53
|
"@expo/ui": "*",
|
|
55
54
|
"@expo/vector-icons": "*",
|
|
56
55
|
"@react-native-community/datetimepicker": "*",
|
|
@@ -85,7 +84,6 @@
|
|
|
85
84
|
"devDependencies": {
|
|
86
85
|
"@expo-google-fonts/fira-code": "^0.4.1",
|
|
87
86
|
"@expo-google-fonts/inter": "^0.4.2",
|
|
88
|
-
"@expo-google-fonts/montserrat": "^0.4.2",
|
|
89
87
|
"@expo/ui": "~56.0.18",
|
|
90
88
|
"@expo/vector-icons": "^15.0.3",
|
|
91
89
|
"@gorhom/bottom-sheet": "^5",
|