@retray-dev/ui-kit 9.1.0 → 9.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/COMPONENTS.md +165 -4
- package/CONSUMER.md +247 -0
- package/DESIGN.md +668 -0
- package/FONTS.md +107 -0
- package/README.md +3 -3
- package/dist/AlertBanner.d.mts +3 -1
- package/dist/AlertBanner.d.ts +3 -1
- package/dist/AlertBanner.js +18 -2
- package/dist/AlertBanner.mjs +1 -1
- package/dist/ConfirmDialog.d.mts +3 -1
- package/dist/ConfirmDialog.d.ts +3 -1
- package/dist/ConfirmDialog.js +3 -0
- package/dist/ConfirmDialog.mjs +1 -1
- package/dist/CurrencyInput.d.mts +3 -1
- package/dist/CurrencyInput.d.ts +3 -1
- package/dist/CurrencyInput.js +31 -4
- package/dist/CurrencyInput.mjs +2 -2
- package/dist/ImageUpload.d.mts +27 -0
- package/dist/ImageUpload.d.ts +27 -0
- package/dist/ImageUpload.js +399 -0
- package/dist/ImageUpload.mjs +9 -0
- package/dist/Input.d.mts +3 -1
- package/dist/Input.d.ts +3 -1
- package/dist/Input.js +27 -2
- package/dist/Input.mjs +1 -1
- package/dist/ListItem.d.mts +3 -1
- package/dist/ListItem.d.ts +3 -1
- package/dist/ListItem.js +2 -1
- package/dist/ListItem.mjs +1 -1
- package/dist/SheetSelect.d.mts +25 -0
- package/dist/SheetSelect.d.ts +25 -0
- package/dist/SheetSelect.js +440 -0
- package/dist/SheetSelect.mjs +9 -0
- package/dist/{chunk-M6ZXVBTK.mjs → chunk-6MKGPAR2.mjs} +21 -5
- package/dist/{chunk-7QHVVCB3.mjs → chunk-FZZLPJ6B.mjs} +3 -0
- package/dist/{chunk-MAC465BB.mjs → chunk-KNSENOV4.mjs} +5 -3
- package/dist/{chunk-756RAKE4.mjs → chunk-LVYEU5ZK.mjs} +27 -2
- package/dist/{chunk-BNP626TY.mjs → chunk-T4I5WVHA.mjs} +2 -1
- package/dist/chunk-URI2WBIV.mjs +147 -0
- package/dist/chunk-Y4GL2MHX.mjs +112 -0
- package/dist/index.d.mts +26 -1
- package/dist/index.d.ts +26 -1
- package/dist/index.js +327 -8
- package/dist/index.mjs +51 -12
- package/package.json +18 -5
- package/src/components/AlertBanner/AlertBanner.tsx +21 -3
- package/src/components/ConfirmDialog/ConfirmDialog.tsx +5 -0
- package/src/components/CurrencyInput/CurrencyInput.tsx +4 -0
- package/src/components/ImageUpload/ImageUpload.tsx +158 -0
- package/src/components/ImageUpload/index.ts +1 -0
- package/src/components/Input/Input.tsx +51 -23
- package/src/components/ListItem/ListItem.tsx +4 -1
- package/src/components/SheetSelect/SheetSelect.tsx +192 -0
- package/src/components/SheetSelect/index.ts +1 -0
- package/src/hooks/useConfirmDialog.ts +67 -0
- package/src/index.ts +6 -0
package/DESIGN.md
ADDED
|
@@ -0,0 +1,668 @@
|
|
|
1
|
+
---
|
|
2
|
+
version: 1.0
|
|
3
|
+
name: Retray Design System
|
|
4
|
+
description: A warm, generous consumer-app system anchored on a white canvas and deep ink (#1a1a1a) as primary, with a single coral accent (#d4561d) carrying primary CTAs and interactive moments. Type runs Sohne at modest weights — display sits at 20–28px in weight 600/700 rather than heavy weights; the system trusts whitespace and imagery over typographic muscle. Soft rounded shapes throughout — no hard corners on interactive elements. Built for React Native / Expo.
|
|
5
|
+
|
|
6
|
+
colors:
|
|
7
|
+
primary: "#1a1a1a"
|
|
8
|
+
primary-foreground: "#ffffff"
|
|
9
|
+
accent: "#d4561d"
|
|
10
|
+
accent-active: "#b8431a"
|
|
11
|
+
accent-disabled: "#f0c4b4"
|
|
12
|
+
accent-foreground: "#ffffff"
|
|
13
|
+
background: "#ffffff"
|
|
14
|
+
foreground: "#1a1a1a"
|
|
15
|
+
card: "#ffffff"
|
|
16
|
+
border: "#dddddd"
|
|
17
|
+
foreground-subtle: "#646464" # derived — ~70% foreground mix
|
|
18
|
+
foreground-muted: "#767676" # derived — ~62% foreground mix (WCAG AA 4.5:1)
|
|
19
|
+
surface: "#f5f5f5" # derived — background slightly off-canvas
|
|
20
|
+
surface-strong: "#ebebeb" # derived — pressed/hover states
|
|
21
|
+
destructive: "#c72828"
|
|
22
|
+
destructive-foreground: "#ffffff"
|
|
23
|
+
success: "#1a7a45"
|
|
24
|
+
success-foreground: "#ffffff"
|
|
25
|
+
warning: "#9a5200"
|
|
26
|
+
warning-foreground: "#ffffff"
|
|
27
|
+
overlay: "rgba(0,0,0,0.45)"
|
|
28
|
+
|
|
29
|
+
typography:
|
|
30
|
+
display-hero:
|
|
31
|
+
fontFamily: "Sohne-Bold"
|
|
32
|
+
fontSize: 64px
|
|
33
|
+
fontWeight: 700
|
|
34
|
+
lineHeight: 70px
|
|
35
|
+
letterSpacing: -1px
|
|
36
|
+
display-xl:
|
|
37
|
+
fontFamily: "Sohne-Bold"
|
|
38
|
+
fontSize: 28px
|
|
39
|
+
fontWeight: 700
|
|
40
|
+
lineHeight: 40px
|
|
41
|
+
letterSpacing: 0
|
|
42
|
+
display-lg:
|
|
43
|
+
fontFamily: "Sohne-SemiBold"
|
|
44
|
+
fontSize: 24px
|
|
45
|
+
fontWeight: 600
|
|
46
|
+
lineHeight: 32px
|
|
47
|
+
letterSpacing: -0.3px
|
|
48
|
+
display-md:
|
|
49
|
+
fontFamily: "Sohne-SemiBold"
|
|
50
|
+
fontSize: 20px
|
|
51
|
+
fontWeight: 600
|
|
52
|
+
lineHeight: 28px
|
|
53
|
+
letterSpacing: 0
|
|
54
|
+
display-sm:
|
|
55
|
+
fontFamily: "Sohne-SemiBold"
|
|
56
|
+
fontSize: 18px
|
|
57
|
+
fontWeight: 600
|
|
58
|
+
lineHeight: 24px
|
|
59
|
+
letterSpacing: -0.18px
|
|
60
|
+
title-md:
|
|
61
|
+
fontFamily: "Sohne-SemiBold"
|
|
62
|
+
fontSize: 17px
|
|
63
|
+
fontWeight: 600
|
|
64
|
+
lineHeight: 22px
|
|
65
|
+
letterSpacing: 0
|
|
66
|
+
title-sm:
|
|
67
|
+
fontFamily: "Sohne-Medium"
|
|
68
|
+
fontSize: 15px
|
|
69
|
+
fontWeight: 500
|
|
70
|
+
lineHeight: 20px
|
|
71
|
+
letterSpacing: 0
|
|
72
|
+
body-md:
|
|
73
|
+
fontFamily: "Sohne-Regular"
|
|
74
|
+
fontSize: 16px
|
|
75
|
+
fontWeight: 400
|
|
76
|
+
lineHeight: 24px
|
|
77
|
+
letterSpacing: 0
|
|
78
|
+
body-sm:
|
|
79
|
+
fontFamily: "Sohne-Regular"
|
|
80
|
+
fontSize: 14px
|
|
81
|
+
fontWeight: 400
|
|
82
|
+
lineHeight: 20px
|
|
83
|
+
letterSpacing: 0
|
|
84
|
+
caption:
|
|
85
|
+
fontFamily: "Sohne-Medium"
|
|
86
|
+
fontSize: 14px
|
|
87
|
+
fontWeight: 500
|
|
88
|
+
lineHeight: 18px
|
|
89
|
+
letterSpacing: 0
|
|
90
|
+
caption-sm:
|
|
91
|
+
fontFamily: "Sohne-Regular"
|
|
92
|
+
fontSize: 13px
|
|
93
|
+
fontWeight: 400
|
|
94
|
+
lineHeight: 16px
|
|
95
|
+
letterSpacing: 0
|
|
96
|
+
badge-text:
|
|
97
|
+
fontFamily: "Sohne-SemiBold"
|
|
98
|
+
fontSize: 11px
|
|
99
|
+
fontWeight: 600
|
|
100
|
+
lineHeight: 14px
|
|
101
|
+
letterSpacing: 0
|
|
102
|
+
badge-text-md:
|
|
103
|
+
fontFamily: "Sohne-SemiBold"
|
|
104
|
+
fontSize: 13px
|
|
105
|
+
fontWeight: 600
|
|
106
|
+
lineHeight: 16px
|
|
107
|
+
letterSpacing: 0
|
|
108
|
+
micro-label:
|
|
109
|
+
fontFamily: "Sohne-Bold"
|
|
110
|
+
fontSize: 12px
|
|
111
|
+
fontWeight: 700
|
|
112
|
+
lineHeight: 16px
|
|
113
|
+
letterSpacing: 0
|
|
114
|
+
uppercase-tag:
|
|
115
|
+
fontFamily: "Sohne-Bold"
|
|
116
|
+
fontSize: 11px
|
|
117
|
+
fontWeight: 700
|
|
118
|
+
lineHeight: 14px
|
|
119
|
+
letterSpacing: 0.6px
|
|
120
|
+
textTransform: uppercase
|
|
121
|
+
button-lg:
|
|
122
|
+
fontFamily: "Sohne-Medium"
|
|
123
|
+
fontSize: 16px
|
|
124
|
+
fontWeight: 500
|
|
125
|
+
lineHeight: 22px
|
|
126
|
+
letterSpacing: 0
|
|
127
|
+
button-sm:
|
|
128
|
+
fontFamily: "Sohne-Medium"
|
|
129
|
+
fontSize: 14px
|
|
130
|
+
fontWeight: 500
|
|
131
|
+
lineHeight: 18px
|
|
132
|
+
letterSpacing: 0
|
|
133
|
+
|
|
134
|
+
rounded:
|
|
135
|
+
none: 0px
|
|
136
|
+
xs: 4px
|
|
137
|
+
sm: 8px
|
|
138
|
+
md: 14px
|
|
139
|
+
lg: 20px
|
|
140
|
+
xl: 32px
|
|
141
|
+
full: 9999px
|
|
142
|
+
|
|
143
|
+
spacing:
|
|
144
|
+
xxs: 2px
|
|
145
|
+
xs: 4px
|
|
146
|
+
sm: 8px
|
|
147
|
+
md: 12px
|
|
148
|
+
base: 16px
|
|
149
|
+
lg: 24px
|
|
150
|
+
xl: 32px
|
|
151
|
+
xxl: 48px
|
|
152
|
+
section: 64px
|
|
153
|
+
|
|
154
|
+
components:
|
|
155
|
+
button-primary:
|
|
156
|
+
backgroundColor: "{colors.primary}"
|
|
157
|
+
textColor: "{colors.primary-foreground}"
|
|
158
|
+
typography: "{typography.button-lg}"
|
|
159
|
+
rounded: "{rounded.md}" # 14px — soft rounded rect, not pill
|
|
160
|
+
paddingV: 10px
|
|
161
|
+
paddingH: 24px
|
|
162
|
+
minHeight: 44px
|
|
163
|
+
button-primary-lg:
|
|
164
|
+
backgroundColor: "{colors.primary}"
|
|
165
|
+
textColor: "{colors.primary-foreground}"
|
|
166
|
+
rounded: "{rounded.md}"
|
|
167
|
+
paddingV: 12px
|
|
168
|
+
minHeight: 48px
|
|
169
|
+
button-primary-disabled:
|
|
170
|
+
backgroundColor: "{colors.surface-strong}"
|
|
171
|
+
textColor: "{colors.foreground-muted}"
|
|
172
|
+
rounded: "{rounded.md}"
|
|
173
|
+
button-accent:
|
|
174
|
+
backgroundColor: "{colors.accent}"
|
|
175
|
+
textColor: "{colors.accent-foreground}"
|
|
176
|
+
typography: "{typography.button-lg}"
|
|
177
|
+
rounded: "{rounded.md}"
|
|
178
|
+
paddingV: 10px
|
|
179
|
+
minHeight: 44px
|
|
180
|
+
button-secondary:
|
|
181
|
+
backgroundColor: "{colors.background}"
|
|
182
|
+
textColor: "{colors.foreground}"
|
|
183
|
+
typography: "{typography.button-lg}"
|
|
184
|
+
rounded: "{rounded.md}"
|
|
185
|
+
borderWidth: 1px
|
|
186
|
+
borderColor: "{colors.border}"
|
|
187
|
+
paddingV: 10px
|
|
188
|
+
minHeight: 44px
|
|
189
|
+
button-ghost:
|
|
190
|
+
backgroundColor: transparent
|
|
191
|
+
textColor: "{colors.foreground}"
|
|
192
|
+
typography: "{typography.button-lg}"
|
|
193
|
+
button-destructive:
|
|
194
|
+
backgroundColor: "{colors.destructive}"
|
|
195
|
+
textColor: "{colors.destructive-foreground}"
|
|
196
|
+
rounded: "{rounded.md}"
|
|
197
|
+
paddingV: 10px
|
|
198
|
+
minHeight: 44px
|
|
199
|
+
icon-button:
|
|
200
|
+
backgroundColor: "{colors.surface}"
|
|
201
|
+
textColor: "{colors.foreground}"
|
|
202
|
+
rounded: "{rounded.full}"
|
|
203
|
+
size: 40px
|
|
204
|
+
card:
|
|
205
|
+
backgroundColor: "{colors.card}"
|
|
206
|
+
rounded: "{rounded.md}"
|
|
207
|
+
borderWidth: 1px
|
|
208
|
+
borderColor: "{colors.border}"
|
|
209
|
+
padding: 16px
|
|
210
|
+
card-pressable:
|
|
211
|
+
backgroundColor: "{colors.card}"
|
|
212
|
+
rounded: "{rounded.md}"
|
|
213
|
+
pressScale: 0.98
|
|
214
|
+
text-input:
|
|
215
|
+
backgroundColor: "{colors.background}"
|
|
216
|
+
textColor: "{colors.foreground}"
|
|
217
|
+
typography: "{typography.body-md}"
|
|
218
|
+
rounded: "{rounded.sm}"
|
|
219
|
+
paddingV: 11px
|
|
220
|
+
paddingH: 14px
|
|
221
|
+
minHeight: 44px
|
|
222
|
+
borderWidth: 1px
|
|
223
|
+
borderColor: "{colors.border}"
|
|
224
|
+
focusBorderColor: "{colors.primary}"
|
|
225
|
+
badge:
|
|
226
|
+
rounded: "{rounded.sm}"
|
|
227
|
+
paddingV: 2px
|
|
228
|
+
paddingH: 6px
|
|
229
|
+
chip:
|
|
230
|
+
backgroundColor: "{colors.surface}"
|
|
231
|
+
textColor: "{colors.foreground}"
|
|
232
|
+
typography: "{typography.button-sm}"
|
|
233
|
+
rounded: "{rounded.full}"
|
|
234
|
+
pressScale: 0.94
|
|
235
|
+
chip-selected:
|
|
236
|
+
backgroundColor: "{colors.primary}"
|
|
237
|
+
textColor: "{colors.primary-foreground}"
|
|
238
|
+
rounded: "{rounded.full}"
|
|
239
|
+
list-item:
|
|
240
|
+
backgroundColor: "{colors.background}"
|
|
241
|
+
textColor: "{colors.foreground}"
|
|
242
|
+
typography: "{typography.body-md}"
|
|
243
|
+
minHeight: 44px
|
|
244
|
+
pressScale: 0.97
|
|
245
|
+
menu-item:
|
|
246
|
+
backgroundColor: "{colors.background}"
|
|
247
|
+
textColor: "{colors.foreground}"
|
|
248
|
+
typography: "{typography.body-md}"
|
|
249
|
+
minHeight: 44px
|
|
250
|
+
pressScale: 0.97
|
|
251
|
+
tab-trigger:
|
|
252
|
+
backgroundColor: transparent
|
|
253
|
+
textColor: "{colors.foreground-muted}"
|
|
254
|
+
typography: "{typography.button-sm}"
|
|
255
|
+
pressScale: 0.95
|
|
256
|
+
tab-trigger-active:
|
|
257
|
+
textColor: "{colors.foreground}"
|
|
258
|
+
typography: "{typography.button-sm}"
|
|
259
|
+
app-header:
|
|
260
|
+
backgroundColor: "{colors.background}"
|
|
261
|
+
textColor: "{colors.foreground}"
|
|
262
|
+
typography: "{typography.title-md}"
|
|
263
|
+
borderColor: "{colors.border}"
|
|
264
|
+
height: 56px
|
|
265
|
+
tab-bar:
|
|
266
|
+
backgroundColor: "{colors.background}"
|
|
267
|
+
textColor: "{colors.foreground-muted}"
|
|
268
|
+
activeColor: "{colors.primary}"
|
|
269
|
+
borderColor: "{colors.border}"
|
|
270
|
+
sheet:
|
|
271
|
+
backgroundColor: "{colors.card}"
|
|
272
|
+
rounded: "{rounded.xl}" # 32px top corners only
|
|
273
|
+
handleColor: "{colors.border}"
|
|
274
|
+
toast:
|
|
275
|
+
backgroundColor: "{colors.card}"
|
|
276
|
+
textColor: "{colors.foreground}"
|
|
277
|
+
rounded: "{rounded.lg}"
|
|
278
|
+
duration: 4000ms
|
|
279
|
+
alert-banner:
|
|
280
|
+
rounded: "{rounded.lg}"
|
|
281
|
+
padding: 16px
|
|
282
|
+
checkbox:
|
|
283
|
+
size: 24px
|
|
284
|
+
rounded: "{rounded.sm}"
|
|
285
|
+
pressScale: 0.95
|
|
286
|
+
switch:
|
|
287
|
+
trackWidth: 52px
|
|
288
|
+
trackHeight: 30px
|
|
289
|
+
thumbSize: 24px
|
|
290
|
+
toggle:
|
|
291
|
+
rounded: "{rounded.md}"
|
|
292
|
+
minHeight-sm: 40px
|
|
293
|
+
minHeight-md: 44px
|
|
294
|
+
minHeight-lg: 48px
|
|
295
|
+
pressScale: 0.95
|
|
296
|
+
slider:
|
|
297
|
+
containerHeight: 40px
|
|
298
|
+
thumbSize: 28px
|
|
299
|
+
progress:
|
|
300
|
+
height: 6px
|
|
301
|
+
rounded: "{rounded.full}"
|
|
302
|
+
backgroundColor: "{colors.surface-strong}"
|
|
303
|
+
fillColor: "{colors.primary}"
|
|
304
|
+
skeleton:
|
|
305
|
+
backgroundColor: "{colors.surface}"
|
|
306
|
+
shimmerColor: "{colors.surface-strong}"
|
|
307
|
+
rounded: "{rounded.md}"
|
|
308
|
+
separator:
|
|
309
|
+
color: "{colors.border}"
|
|
310
|
+
thickness: 1px
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Overview
|
|
314
|
+
|
|
315
|
+
Retray Design System is a warm, generous mobile-first UI kit for React Native / Expo consumer apps. The base canvas is **pure white** (`{colors.background}` — #ffffff) with deep near-black ink (`{colors.foreground}` — #1a1a1a) for all text and primary interactive elements, and a single coral voltage (`{colors.accent}` — #d4561d) available for brand moments, highlights, and accent CTAs. Most surfaces are 90% white + ink with one accent moment.
|
|
316
|
+
|
|
317
|
+
Type runs **Sohne** exclusively — a refined geometric sans-serif with 14 weights. Display sits at modest 18–28px / 600–700 weight rather than heavy 800+. The system trusts whitespace and component density for visual hierarchy, not typographic muscle. The full Sohne family ships as `.otf` assets; consuming apps load them via `useFonts(SohneFonts)` from `@retray-dev/ui-kit/fonts`.
|
|
318
|
+
|
|
319
|
+
Shape language is **soft everywhere**. Buttons use `{rounded.md}` (14px) — a friendly rounded rect, never pill-shaped for primary CTAs. Cards use `{rounded.md}` (14px). Sheet modals use `{rounded.xl}` (32px) on top corners only. Icon buttons are circles (`{rounded.full}`). No hard corner on any interactive element.
|
|
320
|
+
|
|
321
|
+
**Key characteristics:**
|
|
322
|
+
- Primary (`{colors.primary}` — #1a1a1a) carries all primary CTAs, toggle active states, focus rings, and the default tab indicator.
|
|
323
|
+
- Accent (`{colors.accent}` — #d4561d) is optional — used for brand CTAs, highlighted badges, and semantic color moments. Falls back to primary when unset.
|
|
324
|
+
- Sohne type family. Display weights 600–700. Body weight 400. Modest weight is intentional.
|
|
325
|
+
- 8pt spacing grid with 4pt micro-steps. Section bands at `{spacing.section}` (64px).
|
|
326
|
+
- Maximum two elevation tiers — flat baseline (95% of surfaces) plus one card float tier.
|
|
327
|
+
- Haptics on every interaction via `react-native-pulsar` with `expo-haptics` fallback.
|
|
328
|
+
- Press animations via `pressto` — main-thread, gesture-handler worklets. Color transitions via `react-native-reanimated` v4.
|
|
329
|
+
|
|
330
|
+
## Colors
|
|
331
|
+
|
|
332
|
+
### Primary & Foreground
|
|
333
|
+
- **Primary** (`{colors.primary}` — #1a1a1a): Default text color and primary interactive color. Button fills, active tab indicators, focus rings, toggle on-states.
|
|
334
|
+
- **Primary Foreground** (`{colors.primary-foreground}` — #ffffff): Text on primary surfaces.
|
|
335
|
+
- **Foreground** (`{colors.foreground}` — #1a1a1a): All body text and headlines. Same value as primary — intentional.
|
|
336
|
+
- **Foreground Subtle** (`{colors.foreground-subtle}` — ~#646464): Sub-titles, secondary labels, inactive tab text. 5.9:1 contrast on white — WCAG AA ✓.
|
|
337
|
+
- **Foreground Muted** (`{colors.foreground-muted}` — ~#767676): Captions, timestamps, placeholders, empty-state labels. 4.5:1 — WCAG AA minimum ✓.
|
|
338
|
+
|
|
339
|
+
### Accent
|
|
340
|
+
- **Accent** (`{colors.accent}` — #d4561d): The single brand voltage. Used sparingly — one or two moments per screen. Primary CTAs on accent-themed flows, highlighted chips, brand badges.
|
|
341
|
+
- **Accent Active** (`{colors.accent-active}` — #b8431a): Press / pointer-down variant.
|
|
342
|
+
- **Accent Disabled** (`{colors.accent-disabled}` — #f0c4b4): Pale tint for disabled accent CTAs.
|
|
343
|
+
- **Accent Foreground** (`{colors.accent-foreground}` — #ffffff): White text on accent surfaces.
|
|
344
|
+
|
|
345
|
+
### Surfaces
|
|
346
|
+
- **Background** (`{colors.background}` — #ffffff): Page/screen floor. Default for all views.
|
|
347
|
+
- **Card** (`{colors.card}` — #ffffff): Card surfaces. Same value as background — components separate via border + shadow, not fill.
|
|
348
|
+
- **Surface** (`{colors.surface}` — ~#f5f5f5): Unselected chip fills, input backgrounds on subtle forms, skeleton placeholder base.
|
|
349
|
+
- **Surface Strong** (`{colors.surface-strong}` — ~#ebebeb): Pressed/hover fill for icon buttons and row highlights.
|
|
350
|
+
|
|
351
|
+
### Borders
|
|
352
|
+
- **Border** (`{colors.border}` — #dddddd): Default 1px stroke — card borders, input outlines, tab-bar top edge, separator lines.
|
|
353
|
+
|
|
354
|
+
### Semantic
|
|
355
|
+
- **Destructive** (`{colors.destructive}` — #c72828): Error and destructive states. 5.59:1 on white — WCAG AA ✓.
|
|
356
|
+
- **Success** (`{colors.success}` — #1a7a45): Positive confirmation states.
|
|
357
|
+
- **Warning** (`{colors.warning}` — #9a5200): Caution states. 5.86:1 on white — WCAG AA ✓.
|
|
358
|
+
- All semantic colors have matching `-foreground` (white text) and derived `Tint` / `Border` variants computed by `deriveColors()`.
|
|
359
|
+
|
|
360
|
+
### Overlay
|
|
361
|
+
- **Overlay** (`{colors.overlay}` — rgba(0,0,0,0.45)): Sheet and dialog backdrop. Applied by `@gorhom/bottom-sheet` and `ConfirmDialog`.
|
|
362
|
+
|
|
363
|
+
## Typography
|
|
364
|
+
|
|
365
|
+
### Font Family
|
|
366
|
+
Sohne is the exclusive type family — display, body, captions, buttons, badges. No separate display family. 14 weights ship as `.otf` files in `src/assets/fonts/`. Fallback on web: `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif`.
|
|
367
|
+
|
|
368
|
+
**Consuming apps must load fonts before rendering any component:**
|
|
369
|
+
```tsx
|
|
370
|
+
import { useFonts } from 'expo-font'
|
|
371
|
+
import { SohneFonts } from '@retray-dev/ui-kit/fonts'
|
|
372
|
+
|
|
373
|
+
const [fontsLoaded] = useFonts(SohneFonts)
|
|
374
|
+
if (!fontsLoaded) return null
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### Hierarchy
|
|
378
|
+
|
|
379
|
+
| Token | Size | Weight | Line Height | Letter Spacing | Use |
|
|
380
|
+
|---|---|---|---|---|---|
|
|
381
|
+
| `display-hero` | 64px | 700 | 70px | -1px | Hero numbers, large stat displays |
|
|
382
|
+
| `display-xl` | 28px | 700 | 40px | 0 | Screen titles, onboarding headlines |
|
|
383
|
+
| `display-lg` | 24px | 600 | 32px | -0.3px | Section display heads |
|
|
384
|
+
| `display-md` | 20px | 600 | 28px | 0 | Sub-section heads, sheet titles |
|
|
385
|
+
| `display-sm` | 18px | 600 | 24px | -0.18px | Card primary labels, list group heads |
|
|
386
|
+
| `title-md` | 17px | 600 | 22px | 0 | Item titles, row primary text |
|
|
387
|
+
| `title-sm` | 15px | 500 | 20px | 0 | Secondary titles, sub-labels |
|
|
388
|
+
| `body-md` | 16px | 400 | 24px | 0 | Default running text inside content |
|
|
389
|
+
| `body-sm` | 14px | 400 | 20px | 0 | Card meta, dates, prices, distance text |
|
|
390
|
+
| `caption` | 14px | 500 | 18px | 0 | Input labels, form field labels |
|
|
391
|
+
| `caption-sm` | 13px | 400 | 16px | 0 | Legal text, copyright, footer copy |
|
|
392
|
+
| `badge-text` | 11px | 600 | 14px | 0 | Small badge labels |
|
|
393
|
+
| `badge-text-md` | 13px | 600 | 16px | 0 | Medium badge labels |
|
|
394
|
+
| `micro-label` | 12px | 700 | 16px | 0 | Chip category labels, micro-tags |
|
|
395
|
+
| `uppercase-tag` | 11px | 700 | 14px | 0.6px + uppercase | Section tags, status labels (uppercase) |
|
|
396
|
+
| `button-lg` | 16px | 500 | 22px | 0 | Primary/secondary CTA labels |
|
|
397
|
+
| `button-sm` | 14px | 500 | 18px | 0 | Small button and pill labels |
|
|
398
|
+
|
|
399
|
+
### Principles
|
|
400
|
+
Display weights stay modest. Screen titles at 28px / 700 feel spacious but not aggressive. Most hierarchy comes from spacing and surface separation, not weight extremes. `display-hero` (64px / 700) is reserved for isolated stat moments — rating displays, large numeric callouts.
|
|
401
|
+
|
|
402
|
+
All `<Text>` components set `allowFontScaling={true}` for Dynamic Type compliance.
|
|
403
|
+
|
|
404
|
+
## Spacing
|
|
405
|
+
|
|
406
|
+
**Base unit:** 8pt grid (4pt micro-steps for tight UI).
|
|
407
|
+
|
|
408
|
+
| Token | Value | Use |
|
|
409
|
+
|---|---|---|
|
|
410
|
+
| `spacing.xxs` | 2px | Micro gap — icon + label pair |
|
|
411
|
+
| `spacing.xs` | 4px | Badge padding, inline icon gap |
|
|
412
|
+
| `spacing.sm` | 8px | Row internal padding, caption gap |
|
|
413
|
+
| `spacing.md` | 12px | Button vertical padding (sm), card internal gap |
|
|
414
|
+
| `spacing.base` | 16px | Card padding, screen horizontal inset |
|
|
415
|
+
| `spacing.lg` | 24px | Section internal padding, dialog padding |
|
|
416
|
+
| `spacing.xl` | 32px | Between major sections within a screen |
|
|
417
|
+
| `spacing.xxl` | 48px | Between top-level page bands |
|
|
418
|
+
| `spacing.section` | 64px | Major vertical breathing room between primary sections |
|
|
419
|
+
|
|
420
|
+
## Border Radius
|
|
421
|
+
|
|
422
|
+
| Token | Value | Use |
|
|
423
|
+
|---|---|---|
|
|
424
|
+
| `rounded.none` | 0 | Separators, divider lines |
|
|
425
|
+
| `rounded.xs` | 4px | — |
|
|
426
|
+
| `rounded.sm` | 8px | Inputs, Textarea, Checkbox, Select list, AlertBanner |
|
|
427
|
+
| `rounded.md` | 14px | Buttons, Cards, Tabs pill, Toggle, Badge (lg) |
|
|
428
|
+
| `rounded.lg` | 20px | Toast, EmptyState, large cards |
|
|
429
|
+
| `rounded.xl` | 32px | Sheet modal top corners |
|
|
430
|
+
| `rounded.full` | 9999px | Chips, IconButton, Avatar, Progress track, PagerDots |
|
|
431
|
+
|
|
432
|
+
## Elevation
|
|
433
|
+
|
|
434
|
+
Two tiers — flat baseline and one float:
|
|
435
|
+
|
|
436
|
+
- **Flat:** All screens, forms, list items, tab bars — 95% of surfaces. No shadow.
|
|
437
|
+
- **Card float:** `shadowColor: #000 / shadowOffset: {0, 2} / shadowOpacity: 0.10 / shadowRadius: 8 / elevation: 5` — applied to Cards on press-release (hover on web), Sheets, ConfirmDialogs, and the account dropdown. This is the `SHADOWS.md` preset.
|
|
438
|
+
- **Modal backdrop:** `{colors.overlay}` (rgba(0,0,0,0.45)) — applied by `@gorhom/bottom-sheet` behind Sheet and ConfirmDialog.
|
|
439
|
+
|
|
440
|
+
Depth comes from `{rounded.md}` corner clipping and white-on-white surface border separation, not layered shadows.
|
|
441
|
+
|
|
442
|
+
## Components
|
|
443
|
+
|
|
444
|
+
### Buttons
|
|
445
|
+
|
|
446
|
+
**Primary** — Ink fill, white text, 14px radius, 10px vertical padding, 44px min-height. Main screen action: "Continue", "Save", "Confirm". Uses `PressableButton` (scale 0.95) with `impactLight()` haptic.
|
|
447
|
+
|
|
448
|
+
**Primary (lg)** — Same ink fill, 12px vertical padding, 48px min-height. Full-width CTAs on onboarding and forms.
|
|
449
|
+
|
|
450
|
+
**Accent** — Coral fill (`{colors.accent}`), white text, 14px radius. Brand moments and highlighted CTA flows where one coral moment per screen is appropriate.
|
|
451
|
+
|
|
452
|
+
**Secondary** — White fill, ink border (1px), ink text. "Cancel", "Back", inverse actions.
|
|
453
|
+
|
|
454
|
+
**Ghost** — Transparent, ink text only. Underlined on press. "Show more" links, tertiary actions.
|
|
455
|
+
|
|
456
|
+
**Destructive** — Red fill, white text. Irreversible actions — delete, remove, block.
|
|
457
|
+
|
|
458
|
+
**IconButton** — Circle (`{rounded.full}`), surface fill, 40px size. Toolbar actions, floating action buttons.
|
|
459
|
+
|
|
460
|
+
### Inputs
|
|
461
|
+
|
|
462
|
+
**Input / Textarea** — White fill, 1px hairline border, 8px radius, 11px vertical / 14px horizontal padding, 44px min-height. On focus: border thickens to 2px and color transitions to `{colors.primary}` via 140ms `useColorTransition`. No glow ring — border is the focus signal.
|
|
463
|
+
|
|
464
|
+
**Select** — Same dimensions as Input. Opens `@react-native-picker/picker` (native sheet). 1px hairline border, 8px radius.
|
|
465
|
+
|
|
466
|
+
**Slider** — Native `@react-native-community/slider`, 40px container height, 28px thumb. `selectionAsync()` haptic per step.
|
|
467
|
+
|
|
468
|
+
### Cards
|
|
469
|
+
|
|
470
|
+
**Card** — White surface, 1px border, 14px radius, 16px internal padding. Static by default. Use `pressScale: 0.98` when interactive.
|
|
471
|
+
|
|
472
|
+
**MediaCard** — Photo-first card. Image fills top with 14px corner clipping. Meta block below in body-sm. Uses `PressableCard` (scale 0.98).
|
|
473
|
+
|
|
474
|
+
**PricingCard** — Highlighted card variant with accent border and feature list.
|
|
475
|
+
|
|
476
|
+
### Selection Controls
|
|
477
|
+
|
|
478
|
+
**Checkbox** — 24×24px square, 8px radius, ink fill when checked. Press: `PressableButton` scale 0.95. Haptic: `selectionAsync()`.
|
|
479
|
+
|
|
480
|
+
**Switch** — 52px track / 30px height / 24px thumb. Single `progress` shared value (0→1) drives thumb translateX via `SPRINGS.elastic`, track color via `interpolateColor`. Haptic: `selectionAsync()`.
|
|
481
|
+
|
|
482
|
+
**RadioGroup** — 24px circle items. Dot scale + opacity driven by `SPRINGS.elastic`. Haptic: `selectionAsync()`.
|
|
483
|
+
|
|
484
|
+
**Toggle** — Segmented control. Min-heights: sm=40, md=44, lg=48. Border and fill color via `useColorTransition(TIMINGS.state)`. Haptic: `selectionAsync()`.
|
|
485
|
+
|
|
486
|
+
**Checkbox / Switch / RadioGroup / Toggle:** All use `PressableButton` (scale 0.95), `enabled={!disabled}`, `rippleColor="transparent"`, `touchSoundDisabled`.
|
|
487
|
+
|
|
488
|
+
### Chips
|
|
489
|
+
|
|
490
|
+
**Chip** — `{rounded.full}` pill, surface fill unselected, ink fill selected. `PressableChip` scale 0.94. `selectionAsync()` haptic. `ChipGroup` handles multi-select state.
|
|
491
|
+
|
|
492
|
+
**CategoryStrip** — Horizontal scrolling strip of chips. Active chip: ink background, white text. Inactive: muted text, transparent fill. Color transitions via `COLOR_TRANSITION`.
|
|
493
|
+
|
|
494
|
+
### Navigation
|
|
495
|
+
|
|
496
|
+
**AppHeader** — White background, 56px height, 1px bottom border. Title in `title-md`. Back/action buttons use `IconButton`.
|
|
497
|
+
|
|
498
|
+
**TabBar** — White background, 1px top border. Active icon/label in `{colors.primary}`. Inactive in `{colors.foreground-muted}`. `selectionAsync()` on tab switch.
|
|
499
|
+
|
|
500
|
+
**Tabs** — Horizontal segmented tabs with sliding ink pill indicator. Pill slides via `withSpring(SPRINGS.glide)`. `PressableTab` scale 0.95.
|
|
501
|
+
|
|
502
|
+
**PagerDots** — Row of `{rounded.full}` dots. Active dot scales up and fills with `{colors.primary}`.
|
|
503
|
+
|
|
504
|
+
### Overlays
|
|
505
|
+
|
|
506
|
+
**Sheet** — `@gorhom/bottom-sheet` `BottomSheetModal` with `enableDynamicSizing`. Top corners `{rounded.xl}` (32px). `keyboardBehavior="interactive"`. Always use `<SheetTextInput>` (not `<TextInput>`) inside sheets. Requires `BottomSheetModalProvider` at app root.
|
|
507
|
+
|
|
508
|
+
**ConfirmDialog** — Sheet variant for destructive confirmations. `notificationSuccess()` / `notificationError()` haptic on confirm. `selectionAsync()` on cancel.
|
|
509
|
+
|
|
510
|
+
**ImageViewer** — Full-screen overlay for photo viewing. Pinch-to-zoom + swipe-to-dismiss via gesture handler.
|
|
511
|
+
|
|
512
|
+
### Feedback
|
|
513
|
+
|
|
514
|
+
**Toast** — `sonner-native` `Toaster`. `richColors: false` (colored icon, neutral background). `swipeToDismissDirection="up"`, `duration: 4000`. `{rounded.lg}` (20px). Requires `ToastProvider` at app root.
|
|
515
|
+
|
|
516
|
+
**AlertBanner** — Inline semantic banner. `{rounded.sm}` (8px), 16px padding. Four variants: default / destructive / success / warning. Uses semantic tint fills from `deriveColors()`.
|
|
517
|
+
|
|
518
|
+
**Spinner** — Activity indicator. Three sizes matching icon scale.
|
|
519
|
+
|
|
520
|
+
**Skeleton** — Animated shimmer placeholder. `{rounded.md}` base, 1400ms shimmer cycle via `expo-linear-gradient`. Sub-components: `Skeleton.MediaCard`, `Skeleton.ListItem`.
|
|
521
|
+
|
|
522
|
+
**Progress** — 6px track, `{rounded.full}`, ink fill. Width animated via `withSpring(SPRINGS.glide)` on value change.
|
|
523
|
+
|
|
524
|
+
### Data Display
|
|
525
|
+
|
|
526
|
+
**ListItem / ListGroup** — Row list items, 44px min-height, 0.97 press scale. `selectionAsync()` haptic. `ListGroup` adds Header and Footer sub-components.
|
|
527
|
+
|
|
528
|
+
**MenuItem / MenuGroup** — Same dimensions as ListItem. Used inside Sheets and dropdowns.
|
|
529
|
+
|
|
530
|
+
**LabelValue** — Two-column key-value row. Label in `foreground-muted`, value in `foreground`.
|
|
531
|
+
|
|
532
|
+
**DetailRow** — Single key-value with optional icon. Used in summary and detail screens.
|
|
533
|
+
|
|
534
|
+
**Badge** — Inline label pill. Sizes: sm (`badge-text` 11px) and md (`badge-text-md` 13px). 8px radius. Four variants: default / success / destructive / warning.
|
|
535
|
+
|
|
536
|
+
**Avatar** — `{rounded.full}` circle. Three sizes: sm/md/lg. Falls back to initials when no image.
|
|
537
|
+
|
|
538
|
+
**Separator** — 1px horizontal rule in `{colors.border}`. Inset variant for list spacing.
|
|
539
|
+
|
|
540
|
+
**VirtualList** — Virtualized scroll list via `FlashList`. Use for long data sets.
|
|
541
|
+
|
|
542
|
+
**MonthPicker** — Month/year scroll picker using native sheet.
|
|
543
|
+
|
|
544
|
+
**CurrencyDisplay** — Formatted monetary value with currency symbol. Uses `display-md` for amount.
|
|
545
|
+
|
|
546
|
+
**CurrencyInput** — Numeric input with currency formatting. Same dimensions as `Input`.
|
|
547
|
+
|
|
548
|
+
### Form Utilities
|
|
549
|
+
|
|
550
|
+
**Form / Form.Field / Form.Section / Form.Footer** — Layout wrappers for form screens. Section provides label + helper text above fields. Field wraps an input with error state. Footer holds the primary CTA + legal copy.
|
|
551
|
+
|
|
552
|
+
### Composition
|
|
553
|
+
|
|
554
|
+
**Accordion** — Collapsible section. Height animated via `withTiming(TIMINGS.expand/collapse)`. Content uses `position: absolute` for `onLayout` measurement while parent is at `height: 0`. Rotate chevron 180° on expand.
|
|
555
|
+
|
|
556
|
+
**SelectableGrid** — Grid of tappable option cells. Used for multi-select category/tag picking.
|
|
557
|
+
|
|
558
|
+
## Animation System
|
|
559
|
+
|
|
560
|
+
### Press Scales (`PRESS_SCALE` in `animations.ts`)
|
|
561
|
+
| Context | Scale | Components |
|
|
562
|
+
|---|---|---|
|
|
563
|
+
| Button / Toggle / Checkbox | 0.95 | `PressableButton` |
|
|
564
|
+
| Card / MediaCard | 0.98 | `PressableCard` |
|
|
565
|
+
| ListItem / MenuItem | 0.97 | `PressableRow` |
|
|
566
|
+
| Chip | 0.94 | `PressableChip` |
|
|
567
|
+
| Tab trigger | 0.95 | `PressableTab` |
|
|
568
|
+
|
|
569
|
+
All pressables: `enabled={!disabled}`, `rippleColor="transparent"`, `touchSoundDisabled`.
|
|
570
|
+
|
|
571
|
+
### Spring Presets (`SPRINGS`)
|
|
572
|
+
| Preset | Stiffness | Damping | Mass | Use |
|
|
573
|
+
|---|---|---|---|---|
|
|
574
|
+
| `pressIn` | 600 | 35 | 0.8 | Button / Toggle / Tabs fast compression |
|
|
575
|
+
| `pressOut` | 280 | 22 | 0.8 | Elastic rebound after press |
|
|
576
|
+
| `surfacePressIn` | 380 | 30 | 0.95 | Card / ListItem softer compression |
|
|
577
|
+
| `surfacePressOut` | 220 | 20 | 0.95 | Card / ListItem softer rebound |
|
|
578
|
+
| `glide` | 380 | 38 | 1.0 | Tabs indicator slide, Progress fill |
|
|
579
|
+
| `elastic` | 320 | 22 | 0.7 | Switch thumb, RadioGroup dot |
|
|
580
|
+
|
|
581
|
+
### Timing Presets (`TIMINGS`)
|
|
582
|
+
| Preset | Duration | Use |
|
|
583
|
+
|---|---|---|
|
|
584
|
+
| `state` | 160ms | Checkbox/toggle color transitions |
|
|
585
|
+
| `focusIn` | 140ms | Input focus ring appear |
|
|
586
|
+
| `focusOut` | 100ms | Input focus ring dismiss |
|
|
587
|
+
| `expand` | 240ms | Accordion open |
|
|
588
|
+
| `collapse` | 200ms | Accordion close |
|
|
589
|
+
| `shimmer` | 1400ms | Skeleton shimmer pass |
|
|
590
|
+
|
|
591
|
+
### Easing Presets (`EASINGS`)
|
|
592
|
+
| Preset | Curve | Use |
|
|
593
|
+
|---|---|---|
|
|
594
|
+
| `standard` | bezier(0.2, 0, 0, 1) | General state transitions |
|
|
595
|
+
| `expand` | bezier(0.23, 1, 0.32, 1) | Accordion expand, panel slide |
|
|
596
|
+
| `collapse` | Easing.in(ease) | Accordion collapse |
|
|
597
|
+
|
|
598
|
+
### EaseView Transitions (for `react-native-ease`)
|
|
599
|
+
- `COLOR_TRANSITION` — 160ms timing, standard easing. Chip/Toggle/CategoryStrip color state.
|
|
600
|
+
- `OPACITY_TRANSITION` — 160ms timing, standard easing. Icon/content crossfades.
|
|
601
|
+
- `SPRING_ELASTIC` — spring stiffness=320, damping=22, mass=0.7. Switch thumb, RadioGroup dot.
|
|
602
|
+
|
|
603
|
+
## Haptic Feedback
|
|
604
|
+
|
|
605
|
+
All haptics via `src/utils/haptics.ts` — `react-native-pulsar` with `expo-haptics` fallback. Pulsar loads dynamically via `import()` inside a try/catch; absent in Expo Go, graceful fallback.
|
|
606
|
+
|
|
607
|
+
| Function | Use |
|
|
608
|
+
|---|---|
|
|
609
|
+
| `selectionAsync()` | Checkbox, Switch, Toggle, RadioGroup, Select (open + pick), Slider (per step), Accordion toggle, ListItem press, MenuItem press, ConfirmDialog cancel |
|
|
610
|
+
| `impactLight()` | Button tap, small action confirmations |
|
|
611
|
+
| `impactMedium()` | Sheet open |
|
|
612
|
+
| `impactHeavy()` | ConfirmDialog open |
|
|
613
|
+
| `notificationSuccess()` | ConfirmDialog confirm (success action) |
|
|
614
|
+
| `notificationError()` | ConfirmDialog confirm (destructive action) |
|
|
615
|
+
| `notificationWarning()` | Form validation error |
|
|
616
|
+
| `richHaptics.hammer()` / `.pulse()` / `.buzz()` / `.flick()` | Enhanced Pulsar presets (fallback to basic haptics in Expo Go) |
|
|
617
|
+
|
|
618
|
+
## Touch Targets (Apple HIG)
|
|
619
|
+
|
|
620
|
+
All interactive elements maintain ≥44pt touch height:
|
|
621
|
+
|
|
622
|
+
| Component | Min Height | Enforcement |
|
|
623
|
+
|---|---|---|
|
|
624
|
+
| Button sm/md | 44px | `paddingV: vs(10)` |
|
|
625
|
+
| Button lg | 48px | `paddingV: vs(12)` |
|
|
626
|
+
| Input / Textarea / Select | 44px | `paddingV: vs(11)` |
|
|
627
|
+
| Checkbox / Radio circle | 24×24px | fixed size |
|
|
628
|
+
| Switch thumb | 24px | `THUMB_SIZE: 24` |
|
|
629
|
+
| Switch track | 52×30px | `TRACK_WIDTH/HEIGHT` |
|
|
630
|
+
| Toggle sm/md/lg | 40/44/48px | `minHeight` |
|
|
631
|
+
| Slider | 40px container | `height: 40` |
|
|
632
|
+
| Slider thumb | 28×28px | native thumb |
|
|
633
|
+
|
|
634
|
+
## Provider Setup (Consuming Apps)
|
|
635
|
+
|
|
636
|
+
```tsx
|
|
637
|
+
import { SafeAreaProvider, initialWindowMetrics } from 'react-native-safe-area-context'
|
|
638
|
+
import { GestureHandlerRootView } from 'react-native-gesture-handler'
|
|
639
|
+
import { ThemeProvider, BottomSheetModalProvider, ToastProvider } from '@retray-dev/ui-kit'
|
|
640
|
+
|
|
641
|
+
<SafeAreaProvider initialMetrics={initialWindowMetrics}>
|
|
642
|
+
<GestureHandlerRootView style={{ flex: 1 }}>
|
|
643
|
+
<ThemeProvider>
|
|
644
|
+
<BottomSheetModalProvider>
|
|
645
|
+
<ToastProvider>
|
|
646
|
+
{/* app */}
|
|
647
|
+
</ToastProvider>
|
|
648
|
+
</BottomSheetModalProvider>
|
|
649
|
+
</ThemeProvider>
|
|
650
|
+
</GestureHandlerRootView>
|
|
651
|
+
</SafeAreaProvider>
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
`SafeAreaProvider` must be outermost. `initialMetrics={initialWindowMetrics}` required on Android. `GestureHandlerRootView` must wrap all gesture-enabled components.
|
|
655
|
+
|
|
656
|
+
## Design Philosophy
|
|
657
|
+
|
|
658
|
+
**Warmth over precision.** Consumer-first. Generous spacing, rounded shapes, friendly weight. Not B2B grid-and-data precision — human marketplace feel.
|
|
659
|
+
|
|
660
|
+
**Single accent.** 90% of screens are ink + white. One coral moment. Never two accent-colored elements competing.
|
|
661
|
+
|
|
662
|
+
**Haptics always.** Every interactive element has a haptic. No reduced-motion toggle — animations always run.
|
|
663
|
+
|
|
664
|
+
**Icon everywhere.** All interactive components support `iconName` (auto-resolved across 6 `@expo/vector-icons` families) and `icon` (custom ReactNode). Icons are first-class — not decorative. They improve scannability and reinforce hierarchy.
|
|
665
|
+
|
|
666
|
+
**Flat and honest.** Maximum two shadow tiers. Depth from rounded corners + border separation + photography, not layered shadows.
|
|
667
|
+
|
|
668
|
+
**Mobile HIG.** 44pt minimum touch targets, Dynamic Type (`allowFontScaling={true}`), swipe-to-dismiss on sheets, keyboard-interactive sheet behavior.
|