@mrmeg/expo-ui 0.27.0 → 0.28.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/CHANGELOG.md +712 -0
- package/LLM_USAGE.md +260 -32
- package/README.md +558 -101
- package/dist/components/Accordion.js +12 -6
- package/dist/components/AnimatedView.d.ts +1 -1
- package/dist/components/AnimatedView.js +5 -2
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/Avatar.js +2 -2
- package/dist/components/Badge.js +5 -1
- package/dist/components/BottomSheet.d.ts +66 -9
- package/dist/components/BottomSheet.js +143 -39
- package/dist/components/Button.d.ts +20 -4
- package/dist/components/Button.js +64 -43
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +24 -4
- package/dist/components/Carousel.js +2 -2
- package/dist/components/Checkbox.js +51 -44
- package/dist/components/Collapsible.js +7 -3
- package/dist/components/Dialog.js +178 -29
- package/dist/components/Drawer.d.ts +1 -1
- package/dist/components/Drawer.js +65 -55
- package/dist/components/DropdownMenu.js +11 -7
- package/dist/components/EmptyState.d.ts +2 -2
- package/dist/components/EmptyState.js +5 -2
- package/dist/components/Hydrated.d.ts +31 -0
- package/dist/components/Hydrated.js +30 -0
- package/dist/components/Icon.d.ts +17 -5
- package/dist/components/Icon.js +31 -10
- package/dist/components/InputOTP.js +7 -3
- package/dist/components/Item.d.ts +103 -4
- package/dist/components/Item.js +168 -15
- package/dist/components/KeyboardAvoidingView.d.ts +1 -1
- package/dist/components/KeyboardAvoidingView.js +4 -1
- package/dist/components/Label.js +3 -2
- package/dist/components/MaxWidthContainer.js +1 -1
- package/dist/components/Notification.js +45 -27
- package/dist/components/Popover.d.ts +18 -5
- package/dist/components/Popover.js +72 -8
- package/dist/components/Progress.js +8 -6
- package/dist/components/RadioGroup.js +52 -49
- package/dist/components/Screen.d.ts +61 -0
- package/dist/components/Screen.js +62 -0
- package/dist/components/SectionHeader.d.ts +1 -1
- package/dist/components/SegmentedControl.d.ts +12 -4
- package/dist/components/SegmentedControl.js +94 -4
- package/dist/components/Select.js +21 -26
- package/dist/components/Skeleton.js +24 -10
- package/dist/components/Slider.d.ts +11 -3
- package/dist/components/Slider.js +74 -6
- package/dist/components/StatCard.d.ts +4 -2
- package/dist/components/StatCard.js +3 -1
- package/dist/components/StyledText.d.ts +7 -1
- package/dist/components/StyledText.js +5 -3
- package/dist/components/Switch.d.ts +1 -1
- package/dist/components/Switch.js +23 -32
- package/dist/components/Tabs.d.ts +2 -2
- package/dist/components/Tabs.js +13 -24
- package/dist/components/TextInput.d.ts +1 -1
- package/dist/components/TextInput.js +73 -27
- package/dist/components/Toggle.d.ts +2 -42
- package/dist/components/Toggle.js +38 -27
- package/dist/components/ToggleGroup.d.ts +1 -1
- package/dist/components/ToggleGroup.js +37 -32
- package/dist/components/Tooltip.d.ts +1 -1
- package/dist/components/Tooltip.js +9 -3
- package/dist/components/UIProvider.d.ts +22 -2
- package/dist/components/UIProvider.js +15 -3
- package/dist/components/bottomSheetDismiss.d.ts +19 -0
- package/dist/components/bottomSheetDismiss.js +44 -0
- package/dist/components/bottomSheetHostSupport.d.ts +46 -0
- package/dist/components/bottomSheetHostSupport.js +67 -0
- package/dist/components/index.d.ts +46 -44
- package/dist/components/index.js +2 -0
- package/dist/components/keyboardDismiss.d.ts +25 -0
- package/dist/components/keyboardDismiss.js +60 -3
- package/dist/components/nativeTextField.android.js +13 -9
- package/dist/constants/colors.d.ts +40 -5
- package/dist/constants/colors.js +26 -8
- package/dist/constants/fonts.d.ts +41 -2
- package/dist/constants/fonts.js +46 -7
- package/dist/constants/index.d.ts +5 -4
- package/dist/constants/index.js +1 -0
- package/dist/constants/interaction.d.ts +18 -0
- package/dist/constants/interaction.js +18 -0
- package/dist/constants/spacing.d.ts +2 -1
- package/dist/constants/spacing.js +2 -1
- package/dist/design-system.json +154 -2
- package/dist/hooks/index.d.ts +12 -7
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/useDimensions.d.ts +13 -8
- package/dist/hooks/useDimensions.js +123 -48
- package/dist/hooks/useFocusVisible.d.ts +30 -0
- package/dist/hooks/useFocusVisible.js +45 -0
- package/dist/hooks/useFontStyle.d.ts +2 -2
- package/dist/hooks/useFontStyle.js +3 -2
- package/dist/hooks/useHydrated.d.ts +13 -0
- package/dist/hooks/useHydrated.js +19 -0
- package/dist/hooks/useNotificationOffset.d.ts +20 -0
- package/dist/hooks/useNotificationOffset.js +34 -0
- package/dist/hooks/useResources.d.ts +21 -20
- package/dist/hooks/useResources.js +72 -68
- package/dist/hooks/useScalePress.d.ts +8 -5
- package/dist/hooks/useScalePress.js +16 -9
- package/dist/hooks/useShape.d.ts +24 -0
- package/dist/hooks/useShape.js +24 -0
- package/dist/hooks/useStaggeredEntrance.js +9 -7
- package/dist/hooks/useTheme.d.ts +3 -3
- package/dist/hooks/useTheme.js +177 -66
- package/dist/hooks/useWindowInsets.d.ts +13 -0
- package/dist/hooks/useWindowInsets.js +23 -0
- package/dist/index.d.ts +5 -5
- package/dist/lib/composeRefs.d.ts +8 -0
- package/dist/lib/composeRefs.js +20 -0
- package/dist/lib/haptics.d.ts +12 -0
- package/dist/lib/haptics.js +21 -0
- package/dist/lib/index.d.ts +6 -4
- package/dist/lib/index.js +2 -0
- package/dist/lib/interFonts.d.ts +15 -0
- package/dist/lib/interFonts.js +1 -0
- package/dist/lib/interFonts.native.d.ts +3 -0
- package/dist/lib/interFonts.native.js +20 -0
- package/dist/lib/popoverPlacement.d.ts +36 -0
- package/dist/lib/popoverPlacement.js +29 -0
- package/dist/lib/stateSurface.d.ts +28 -0
- package/dist/lib/stateSurface.js +30 -0
- package/dist/lib/themedStyles.d.ts +1 -1
- package/dist/lib/useAnimatedValue.d.ts +15 -0
- package/dist/lib/useAnimatedValue.js +19 -0
- package/dist/lib/webHitTarget.d.ts +40 -0
- package/dist/lib/webHitTarget.js +37 -0
- package/dist/state/feedbackStore.d.ts +24 -0
- package/dist/state/feedbackStore.js +16 -0
- package/dist/state/globalUIStore.d.ts +27 -0
- package/dist/state/globalUIStore.js +23 -0
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +2 -0
- package/dist/state/initialScheme.d.ts +27 -0
- package/dist/state/initialScheme.js +29 -0
- package/dist/state/notify.d.ts +1 -1
- package/dist/state/themeColorScope.d.ts +1 -1
- package/dist/state/themeStore.d.ts +69 -16
- package/dist/state/themeStore.js +65 -38
- package/llms-full.md +56 -8
- package/llms.txt +10 -1
- package/package.json +69 -2
package/dist/design-system.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
|
-
"schemaVersion":
|
|
2
|
+
"schemaVersion": 2,
|
|
3
3
|
"package": "@mrmeg/expo-ui",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.28.0",
|
|
5
5
|
"tokens": {
|
|
6
6
|
"spacing": {
|
|
7
7
|
"entries": [
|
|
@@ -97,6 +97,10 @@
|
|
|
97
97
|
"name": "touchTarget",
|
|
98
98
|
"value": 44
|
|
99
99
|
},
|
|
100
|
+
{
|
|
101
|
+
"name": "minTarget",
|
|
102
|
+
"value": 24
|
|
103
|
+
},
|
|
100
104
|
{
|
|
101
105
|
"name": "buttonPadding",
|
|
102
106
|
"value": 10
|
|
@@ -170,6 +174,50 @@
|
|
|
170
174
|
"value": 48
|
|
171
175
|
}
|
|
172
176
|
]
|
|
177
|
+
},
|
|
178
|
+
"typography": {
|
|
179
|
+
"entries": [
|
|
180
|
+
{
|
|
181
|
+
"name": "xs",
|
|
182
|
+
"value": 11,
|
|
183
|
+
"lineHeight": 16.5
|
|
184
|
+
},
|
|
185
|
+
{
|
|
186
|
+
"name": "sm",
|
|
187
|
+
"value": 12,
|
|
188
|
+
"lineHeight": 18
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
"name": "base",
|
|
192
|
+
"value": 14,
|
|
193
|
+
"lineHeight": 21
|
|
194
|
+
},
|
|
195
|
+
{
|
|
196
|
+
"name": "body",
|
|
197
|
+
"value": 15,
|
|
198
|
+
"lineHeight": 24.75
|
|
199
|
+
},
|
|
200
|
+
{
|
|
201
|
+
"name": "lg",
|
|
202
|
+
"value": 18,
|
|
203
|
+
"lineHeight": 27
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
"name": "xl",
|
|
207
|
+
"value": 22,
|
|
208
|
+
"lineHeight": 26.4
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
"name": "xxl",
|
|
212
|
+
"value": 28,
|
|
213
|
+
"lineHeight": 33.6
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
"name": "display",
|
|
217
|
+
"value": 34,
|
|
218
|
+
"lineHeight": 40.8
|
|
219
|
+
}
|
|
220
|
+
]
|
|
173
221
|
}
|
|
174
222
|
},
|
|
175
223
|
"palette": {
|
|
@@ -450,6 +498,86 @@
|
|
|
450
498
|
"serif",
|
|
451
499
|
"mono"
|
|
452
500
|
],
|
|
501
|
+
"fonts": {
|
|
502
|
+
"families": {
|
|
503
|
+
"serif": {
|
|
504
|
+
"light": [
|
|
505
|
+
"Georgia, 'Times New Roman', serif",
|
|
506
|
+
"Georgia"
|
|
507
|
+
],
|
|
508
|
+
"regular": [
|
|
509
|
+
"Georgia, 'Times New Roman', serif",
|
|
510
|
+
"Georgia"
|
|
511
|
+
],
|
|
512
|
+
"medium": [
|
|
513
|
+
"Georgia, 'Times New Roman', serif",
|
|
514
|
+
"Georgia"
|
|
515
|
+
],
|
|
516
|
+
"semibold": [
|
|
517
|
+
"Georgia, 'Times New Roman', serif",
|
|
518
|
+
"Georgia"
|
|
519
|
+
],
|
|
520
|
+
"bold": [
|
|
521
|
+
"Georgia, 'Times New Roman', serif",
|
|
522
|
+
"Georgia"
|
|
523
|
+
]
|
|
524
|
+
},
|
|
525
|
+
"sansSerif": {
|
|
526
|
+
"light": [
|
|
527
|
+
"\"Inter\", system-ui, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\"",
|
|
528
|
+
"Inter_400Regular",
|
|
529
|
+
"sans-serif"
|
|
530
|
+
],
|
|
531
|
+
"regular": [
|
|
532
|
+
"\"Inter\", system-ui, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\"",
|
|
533
|
+
"Inter_400Regular",
|
|
534
|
+
"sans-serif"
|
|
535
|
+
],
|
|
536
|
+
"medium": [
|
|
537
|
+
"\"Inter\", system-ui, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\"",
|
|
538
|
+
"Inter_500Medium",
|
|
539
|
+
"sans-serif"
|
|
540
|
+
],
|
|
541
|
+
"semibold": [
|
|
542
|
+
"\"Inter\", system-ui, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\"",
|
|
543
|
+
"Inter_600SemiBold",
|
|
544
|
+
"sans-serif"
|
|
545
|
+
],
|
|
546
|
+
"bold": [
|
|
547
|
+
"\"Inter\", system-ui, \"Segoe UI\", Roboto, Helvetica, Arial, sans-serif, \"Apple Color Emoji\", \"Segoe UI Emoji\", \"Segoe UI Symbol\"",
|
|
548
|
+
"Inter_700Bold",
|
|
549
|
+
"sans-serif"
|
|
550
|
+
]
|
|
551
|
+
},
|
|
552
|
+
"mono": {
|
|
553
|
+
"light": [
|
|
554
|
+
"ui-monospace, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace",
|
|
555
|
+
"Menlo",
|
|
556
|
+
"monospace"
|
|
557
|
+
],
|
|
558
|
+
"regular": [
|
|
559
|
+
"ui-monospace, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace",
|
|
560
|
+
"Menlo",
|
|
561
|
+
"monospace"
|
|
562
|
+
],
|
|
563
|
+
"medium": [
|
|
564
|
+
"ui-monospace, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace",
|
|
565
|
+
"Menlo",
|
|
566
|
+
"monospace"
|
|
567
|
+
],
|
|
568
|
+
"semibold": [
|
|
569
|
+
"ui-monospace, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace",
|
|
570
|
+
"Menlo",
|
|
571
|
+
"monospace"
|
|
572
|
+
],
|
|
573
|
+
"bold": [
|
|
574
|
+
"ui-monospace, \"SF Mono\", Menlo, Consolas, \"Liberation Mono\", monospace",
|
|
575
|
+
"Menlo",
|
|
576
|
+
"monospace"
|
|
577
|
+
]
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
},
|
|
453
581
|
"components": [
|
|
454
582
|
{
|
|
455
583
|
"name": "Accordion",
|
|
@@ -1297,6 +1425,14 @@
|
|
|
1297
1425
|
"variantValues": null,
|
|
1298
1426
|
"sizeValues": null
|
|
1299
1427
|
},
|
|
1428
|
+
{
|
|
1429
|
+
"name": "Hydrated",
|
|
1430
|
+
"file": "Hydrated.tsx",
|
|
1431
|
+
"hasSize": false,
|
|
1432
|
+
"variantProp": null,
|
|
1433
|
+
"variantValues": null,
|
|
1434
|
+
"sizeValues": null
|
|
1435
|
+
},
|
|
1300
1436
|
{
|
|
1301
1437
|
"name": "Icon",
|
|
1302
1438
|
"file": "Icon.tsx",
|
|
@@ -1321,6 +1457,14 @@
|
|
|
1321
1457
|
"variantValues": null,
|
|
1322
1458
|
"sizeValues": null
|
|
1323
1459
|
},
|
|
1460
|
+
{
|
|
1461
|
+
"name": "ItemGroup",
|
|
1462
|
+
"file": "Item.tsx",
|
|
1463
|
+
"hasSize": false,
|
|
1464
|
+
"variantProp": null,
|
|
1465
|
+
"variantValues": null,
|
|
1466
|
+
"sizeValues": null
|
|
1467
|
+
},
|
|
1324
1468
|
{
|
|
1325
1469
|
"name": "ItemMedia",
|
|
1326
1470
|
"file": "Item.tsx",
|
|
@@ -1404,6 +1548,14 @@
|
|
|
1404
1548
|
"variantValues": null,
|
|
1405
1549
|
"sizeValues": null
|
|
1406
1550
|
},
|
|
1551
|
+
{
|
|
1552
|
+
"name": "Screen",
|
|
1553
|
+
"file": "Screen.tsx",
|
|
1554
|
+
"hasSize": false,
|
|
1555
|
+
"variantProp": null,
|
|
1556
|
+
"variantValues": null,
|
|
1557
|
+
"sizeValues": null
|
|
1558
|
+
},
|
|
1407
1559
|
{
|
|
1408
1560
|
"name": "Popover",
|
|
1409
1561
|
"file": "Popover.tsx",
|
package/dist/hooks/index.d.ts
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
|
-
export * from "./useDimensions";
|
|
2
|
-
export * from "./
|
|
3
|
-
export * from "./
|
|
4
|
-
export * from "./
|
|
5
|
-
export * from "./
|
|
6
|
-
export * from "./
|
|
7
|
-
export * from "./
|
|
1
|
+
export * from "./useDimensions.js";
|
|
2
|
+
export * from "./useFocusVisible.js";
|
|
3
|
+
export * from "./useFontStyle.js";
|
|
4
|
+
export * from "./useReduceMotion.js";
|
|
5
|
+
export * from "./useResources.js";
|
|
6
|
+
export * from "./useScalePress.js";
|
|
7
|
+
export * from "./useShape.js";
|
|
8
|
+
export * from "./useStaggeredEntrance.js";
|
|
9
|
+
export * from "./useTheme.js";
|
|
10
|
+
export { useHydrated } from "./useHydrated.js";
|
|
11
|
+
export { useNotificationOffset } from "./useNotificationOffset.js";
|
|
12
|
+
export { useWindowInsets } from "./useWindowInsets.js";
|
package/dist/hooks/index.js
CHANGED
|
@@ -1,7 +1,12 @@
|
|
|
1
1
|
export * from "./useDimensions.js";
|
|
2
|
+
export * from "./useFocusVisible.js";
|
|
2
3
|
export * from "./useFontStyle.js";
|
|
3
4
|
export * from "./useReduceMotion.js";
|
|
4
5
|
export * from "./useResources.js";
|
|
5
6
|
export * from "./useScalePress.js";
|
|
7
|
+
export * from "./useShape.js";
|
|
6
8
|
export * from "./useStaggeredEntrance.js";
|
|
7
9
|
export * from "./useTheme.js";
|
|
10
|
+
export { useHydrated } from "./useHydrated.js";
|
|
11
|
+
export { useNotificationOffset } from "./useNotificationOffset.js";
|
|
12
|
+
export { useWindowInsets } from "./useWindowInsets.js";
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 1. A desktop visitor sees no reflow — their real viewport lands in the
|
|
5
5
|
* same breakpoint.
|
|
6
6
|
* 2. A mobile visitor gets one frame of desktop-styled content before the
|
|
7
|
-
* post-
|
|
7
|
+
* post-hydration update snaps to real dimensions — better than the inverse
|
|
8
8
|
* (every desktop visitor seeing mobile-tiny, then snapping).
|
|
9
9
|
* 3. It matches the frame the app hands `SafeAreaProvider`, and it is the
|
|
10
10
|
* value the HTML shell `expo export` renders in Node is built from, so
|
|
@@ -29,13 +29,18 @@ type WindowDimensions = {
|
|
|
29
29
|
* Provides a consistent way to access window dimensions and screen size
|
|
30
30
|
* information across mobile and web.
|
|
31
31
|
*
|
|
32
|
-
* On web the
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* provides a per-request value (server render + first client render
|
|
36
|
-
* provide the same one), falling back to `DEFAULT_VIEWPORT_WIDTH` /
|
|
37
|
-
*
|
|
38
|
-
* there.
|
|
32
|
+
* On web the server render and the browser's hydration pass can't read the
|
|
33
|
+
* window: server-side there is no DOM, and reading it while hydrating would
|
|
34
|
+
* disagree with the markup. Both use the width from `SsrViewportContext` when
|
|
35
|
+
* the host provides a per-request value (server render + first client render
|
|
36
|
+
* must provide the same one), falling back to `DEFAULT_VIEWPORT_WIDTH` /
|
|
37
|
+
* `_HEIGHT`. Right after hydration the real viewport takes over, and resize
|
|
38
|
+
* is followed from there. Every consumer shares one window listener and one
|
|
39
|
+
* snapshot, re-renders only when the width or height changes, and a component
|
|
40
|
+
* that mounts after hydration reads the current viewport straight away.
|
|
41
|
+
*
|
|
42
|
+
* The platform branch is chosen once, at module load (`Platform.OS` never
|
|
43
|
+
* changes at runtime), so each platform calls exactly one set of hooks.
|
|
39
44
|
*/
|
|
40
45
|
export declare const useDimensions: () => WindowDimensions;
|
|
41
46
|
export {};
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { use,
|
|
1
|
+
import { use, useMemo, useSyncExternalStore } from "react";
|
|
2
2
|
import { Platform, useWindowDimensions } from "react-native";
|
|
3
3
|
import { SsrViewportContext } from "../state/SsrViewportContext.js";
|
|
4
4
|
/**
|
|
@@ -7,7 +7,7 @@ import { SsrViewportContext } from "../state/SsrViewportContext.js";
|
|
|
7
7
|
* 1. A desktop visitor sees no reflow — their real viewport lands in the
|
|
8
8
|
* same breakpoint.
|
|
9
9
|
* 2. A mobile visitor gets one frame of desktop-styled content before the
|
|
10
|
-
* post-
|
|
10
|
+
* post-hydration update snaps to real dimensions — better than the inverse
|
|
11
11
|
* (every desktop visitor seeing mobile-tiny, then snapping).
|
|
12
12
|
* 3. It matches the frame the app hands `SafeAreaProvider`, and it is the
|
|
13
13
|
* value the HTML shell `expo export` renders in Node is built from, so
|
|
@@ -34,61 +34,136 @@ const calculateDimensionFlags = (width, height) => {
|
|
|
34
34
|
isLargeScreen: width > SCREEN_SIZES.MEDIUM,
|
|
35
35
|
};
|
|
36
36
|
};
|
|
37
|
-
// Persist the real viewport width as a cookie
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
// server/lib/ssrViewport.ts.
|
|
37
|
+
// Persist the real viewport width as a cookie so subsequent SSR requests can
|
|
38
|
+
// render at the user's actual layout (no reflow on repeat visits). The server
|
|
39
|
+
// reads this cookie via resolveSsrViewportWidth in server/lib/ssrViewport.ts.
|
|
41
40
|
const SSR_VIEWPORT_COOKIE = "mrmeg-vw";
|
|
42
41
|
const SSR_VIEWPORT_COOKIE_MAX_AGE = 60 * 60 * 24 * 365; // 1 year
|
|
42
|
+
// Resize fires continuously while a window is dragged; the cookie only has to
|
|
43
|
+
// hold the width the drag settled on.
|
|
44
|
+
const SSR_VIEWPORT_COOKIE_DEBOUNCE_MS = 250;
|
|
45
|
+
// Round to nearest 10 so resize-driven writes don't bust HTTP caching on
|
|
46
|
+
// every pixel of horizontal movement.
|
|
47
|
+
const roundViewportWidth = (width) => Math.round(width / 10) * 10;
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
// Web: one window store shared by every useDimensions() consumer (same shape
|
|
50
|
+
// as useReduceMotion's). A single `resize` listener, attached while at least
|
|
51
|
+
// one consumer is mounted, updates one snapshot and notifies consumers only
|
|
52
|
+
// when the width or height actually changed.
|
|
53
|
+
// ---------------------------------------------------------------------------
|
|
54
|
+
let windowSnapshot = null;
|
|
55
|
+
const windowListeners = new Set();
|
|
56
|
+
let writtenCookieWidth = null;
|
|
57
|
+
let pendingCookieWidth = null;
|
|
58
|
+
let cookieTimer = null;
|
|
43
59
|
function writeViewportCookie(width) {
|
|
44
60
|
if (typeof document === "undefined")
|
|
45
61
|
return;
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
const rounded = Math.round(width / 10) * 10;
|
|
62
|
+
const rounded = roundViewportWidth(width);
|
|
63
|
+
writtenCookieWidth = rounded;
|
|
49
64
|
document.cookie = `${SSR_VIEWPORT_COOKIE}=${rounded}; path=/; max-age=${SSR_VIEWPORT_COOKIE_MAX_AGE}; SameSite=Lax`;
|
|
50
65
|
}
|
|
66
|
+
/** Reads the window, reusing the current snapshot when nothing changed. */
|
|
67
|
+
function readWindow() {
|
|
68
|
+
const width = typeof window === "undefined" ? DEFAULT_VIEWPORT_WIDTH : window.innerWidth;
|
|
69
|
+
const height = typeof window === "undefined" ? DEFAULT_VIEWPORT_HEIGHT : window.innerHeight;
|
|
70
|
+
if (!windowSnapshot || windowSnapshot.width !== width || windowSnapshot.height !== height) {
|
|
71
|
+
windowSnapshot = calculateDimensionFlags(width, height);
|
|
72
|
+
}
|
|
73
|
+
return windowSnapshot;
|
|
74
|
+
}
|
|
75
|
+
function flushViewportCookie() {
|
|
76
|
+
if (cookieTimer !== null) {
|
|
77
|
+
clearTimeout(cookieTimer);
|
|
78
|
+
cookieTimer = null;
|
|
79
|
+
}
|
|
80
|
+
if (pendingCookieWidth !== null) {
|
|
81
|
+
const width = pendingCookieWidth;
|
|
82
|
+
pendingCookieWidth = null;
|
|
83
|
+
writeViewportCookie(width);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function scheduleViewportCookie(width) {
|
|
87
|
+
if (roundViewportWidth(width) === writtenCookieWidth) {
|
|
88
|
+
pendingCookieWidth = null;
|
|
89
|
+
if (cookieTimer !== null) {
|
|
90
|
+
clearTimeout(cookieTimer);
|
|
91
|
+
cookieTimer = null;
|
|
92
|
+
}
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
pendingCookieWidth = width;
|
|
96
|
+
if (cookieTimer !== null)
|
|
97
|
+
clearTimeout(cookieTimer);
|
|
98
|
+
cookieTimer = setTimeout(flushViewportCookie, SSR_VIEWPORT_COOKIE_DEBOUNCE_MS);
|
|
99
|
+
}
|
|
100
|
+
function handleWindowResize() {
|
|
101
|
+
const previous = windowSnapshot;
|
|
102
|
+
const next = readWindow();
|
|
103
|
+
if (next !== previous) {
|
|
104
|
+
for (const listener of windowListeners)
|
|
105
|
+
listener();
|
|
106
|
+
}
|
|
107
|
+
scheduleViewportCookie(next.width);
|
|
108
|
+
}
|
|
109
|
+
// useSyncExternalStore contract: the first consumer attaches the one window
|
|
110
|
+
// listener (and records the real width in the cookie, once per page view
|
|
111
|
+
// rather than once per consumer); the last one to unmount detaches it.
|
|
112
|
+
function subscribeToWindow(listener) {
|
|
113
|
+
windowListeners.add(listener);
|
|
114
|
+
if (windowListeners.size === 1 && canListenToWindow()) {
|
|
115
|
+
window.addEventListener("resize", handleWindowResize);
|
|
116
|
+
writeViewportCookie(readWindow().width);
|
|
117
|
+
}
|
|
118
|
+
return () => {
|
|
119
|
+
windowListeners.delete(listener);
|
|
120
|
+
if (windowListeners.size === 0 && canListenToWindow()) {
|
|
121
|
+
window.removeEventListener("resize", handleWindowResize);
|
|
122
|
+
flushViewportCookie();
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function canListenToWindow() {
|
|
127
|
+
return typeof window !== "undefined" && typeof window.addEventListener === "function";
|
|
128
|
+
}
|
|
129
|
+
function getWindowSnapshot() {
|
|
130
|
+
// While subscribed, the resize listener keeps the snapshot current, so a
|
|
131
|
+
// render never reads (and forces layout on) the window.
|
|
132
|
+
return windowListeners.size > 0 && windowSnapshot ? windowSnapshot : readWindow();
|
|
133
|
+
}
|
|
134
|
+
// The server has no window, and the browser's hydration pass must render what
|
|
135
|
+
// the server did: `null` selects the seeded frame below for both.
|
|
136
|
+
function getServerWindowSnapshot() {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
function useWebDimensions() {
|
|
140
|
+
const ssrWidth = use(SsrViewportContext);
|
|
141
|
+
const live = useSyncExternalStore(subscribeToWindow, getWindowSnapshot, getServerWindowSnapshot);
|
|
142
|
+
const seeded = useMemo(() => calculateDimensionFlags(ssrWidth ?? DEFAULT_VIEWPORT_WIDTH, DEFAULT_VIEWPORT_HEIGHT), [ssrWidth]);
|
|
143
|
+
return live ?? seeded;
|
|
144
|
+
}
|
|
145
|
+
// Native reads come from useWindowDimensions, which subscribes to rotation /
|
|
146
|
+
// split-screen / resize and tears the listener down for us — no manual
|
|
147
|
+
// Dimensions.addEventListener to leak.
|
|
148
|
+
function useNativeDimensions() {
|
|
149
|
+
const { width, height } = useWindowDimensions();
|
|
150
|
+
return useMemo(() => calculateDimensionFlags(width, height), [width, height]);
|
|
151
|
+
}
|
|
51
152
|
/**
|
|
52
153
|
* Provides a consistent way to access window dimensions and screen size
|
|
53
154
|
* information across mobile and web.
|
|
54
155
|
*
|
|
55
|
-
* On web the
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
* provides a per-request value (server render + first client render
|
|
59
|
-
* provide the same one), falling back to `DEFAULT_VIEWPORT_WIDTH` /
|
|
60
|
-
*
|
|
61
|
-
* there.
|
|
156
|
+
* On web the server render and the browser's hydration pass can't read the
|
|
157
|
+
* window: server-side there is no DOM, and reading it while hydrating would
|
|
158
|
+
* disagree with the markup. Both use the width from `SsrViewportContext` when
|
|
159
|
+
* the host provides a per-request value (server render + first client render
|
|
160
|
+
* must provide the same one), falling back to `DEFAULT_VIEWPORT_WIDTH` /
|
|
161
|
+
* `_HEIGHT`. Right after hydration the real viewport takes over, and resize
|
|
162
|
+
* is followed from there. Every consumer shares one window listener and one
|
|
163
|
+
* snapshot, re-renders only when the width or height changes, and a component
|
|
164
|
+
* that mounts after hydration reads the current viewport straight away.
|
|
165
|
+
*
|
|
166
|
+
* The platform branch is chosen once, at module load (`Platform.OS` never
|
|
167
|
+
* changes at runtime), so each platform calls exactly one set of hooks.
|
|
62
168
|
*/
|
|
63
|
-
export const useDimensions =
|
|
64
|
-
const isWeb = Platform.OS === "web";
|
|
65
|
-
const ssrWidth = use(SsrViewportContext);
|
|
66
|
-
// Native reads come from useWindowDimensions, which subscribes to rotation /
|
|
67
|
-
// split-screen / resize and tears the listener down for us — no manual
|
|
68
|
-
// Dimensions.addEventListener to leak. On web we ignore it and drive layout
|
|
69
|
-
// from the seeded frame + the resize listener below.
|
|
70
|
-
const native = useWindowDimensions();
|
|
71
|
-
const [dimensions, setDimensions] = useState(() => calculateDimensionFlags(ssrWidth ?? DEFAULT_VIEWPORT_WIDTH, DEFAULT_VIEWPORT_HEIGHT));
|
|
72
|
-
// Web: read the real viewport after mount and follow resize events. Keeping
|
|
73
|
-
// this in an effect (not render) keeps the first render identical to the
|
|
74
|
-
// exported markup it hydrates.
|
|
75
|
-
useEffect(() => {
|
|
76
|
-
if (!isWeb)
|
|
77
|
-
return;
|
|
78
|
-
const syncFromWindow = () => {
|
|
79
|
-
setDimensions(calculateDimensionFlags(window.innerWidth, window.innerHeight));
|
|
80
|
-
writeViewportCookie(window.innerWidth);
|
|
81
|
-
};
|
|
82
|
-
syncFromWindow();
|
|
83
|
-
window.addEventListener("resize", syncFromWindow);
|
|
84
|
-
return () => {
|
|
85
|
-
window.removeEventListener("resize", syncFromWindow);
|
|
86
|
-
};
|
|
87
|
-
}, [isWeb]);
|
|
88
|
-
// Native: useWindowDimensions already reacts to changes; mirror it into our
|
|
89
|
-
// enriched flags. (On web `native` is unused — the effect above wins.)
|
|
90
|
-
if (!isWeb) {
|
|
91
|
-
return calculateDimensionFlags(native.width, native.height);
|
|
92
|
-
}
|
|
93
|
-
return dimensions;
|
|
94
|
-
};
|
|
169
|
+
export const useDimensions = Platform.OS === "web" ? useWebDimensions : useNativeDimensions;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type NativeSyntheticEvent, type TargetedEvent } from "react-native";
|
|
2
|
+
export type FocusVisibleEvent = NativeSyntheticEvent<TargetedEvent>;
|
|
3
|
+
export interface UseFocusVisibleOptions<E = FocusVisibleEvent> {
|
|
4
|
+
/** Caller's focus handler, invoked after the ring state updates. */
|
|
5
|
+
onFocus?: ((event: E) => void) | null;
|
|
6
|
+
/** Caller's blur handler, invoked after the ring state updates. */
|
|
7
|
+
onBlur?: ((event: E) => void) | null;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Focus-ring state gated on `:focus-visible`.
|
|
11
|
+
*
|
|
12
|
+
* On web a pointer tap focuses the element too, which left the ring visible
|
|
13
|
+
* after every click; `:focus-visible` is only true for keyboard-driven focus,
|
|
14
|
+
* so the ring follows it and falls back to showing when the target can't be
|
|
15
|
+
* queried (non-DOM targets, older engines). Off web every focus shows the
|
|
16
|
+
* ring. Spread `onFocus`/`onBlur` onto the Pressable and layer
|
|
17
|
+
* `getFocusRingStyle()` while `focused` is true and the control is enabled.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```tsx
|
|
21
|
+
* const focus = useFocusVisible({ onFocus, onBlur });
|
|
22
|
+
* <Pressable onFocus={focus.onFocus} onBlur={focus.onBlur}
|
|
23
|
+
* style={[styles.base, focus.focused && !disabled && getFocusRingStyle()]} />
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export declare function useFocusVisible<E = FocusVisibleEvent>(options?: UseFocusVisibleOptions<E>): {
|
|
27
|
+
focused: boolean;
|
|
28
|
+
onFocus: (event: E) => void;
|
|
29
|
+
onBlur: (event: E) => void;
|
|
30
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
import { Platform } from "react-native";
|
|
3
|
+
/**
|
|
4
|
+
* Focus-ring state gated on `:focus-visible`.
|
|
5
|
+
*
|
|
6
|
+
* On web a pointer tap focuses the element too, which left the ring visible
|
|
7
|
+
* after every click; `:focus-visible` is only true for keyboard-driven focus,
|
|
8
|
+
* so the ring follows it and falls back to showing when the target can't be
|
|
9
|
+
* queried (non-DOM targets, older engines). Off web every focus shows the
|
|
10
|
+
* ring. Spread `onFocus`/`onBlur` onto the Pressable and layer
|
|
11
|
+
* `getFocusRingStyle()` while `focused` is true and the control is enabled.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```tsx
|
|
15
|
+
* const focus = useFocusVisible({ onFocus, onBlur });
|
|
16
|
+
* <Pressable onFocus={focus.onFocus} onBlur={focus.onBlur}
|
|
17
|
+
* style={[styles.base, focus.focused && !disabled && getFocusRingStyle()]} />
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export function useFocusVisible(options = {}) {
|
|
21
|
+
const { onFocus: onFocusProp, onBlur: onBlurProp } = options;
|
|
22
|
+
const [focused, setFocused] = useState(false);
|
|
23
|
+
const onFocus = useCallback((event) => {
|
|
24
|
+
setFocused(isKeyboardFocus(event));
|
|
25
|
+
onFocusProp?.(event);
|
|
26
|
+
}, [onFocusProp]);
|
|
27
|
+
const onBlur = useCallback((event) => {
|
|
28
|
+
setFocused(false);
|
|
29
|
+
onBlurProp?.(event);
|
|
30
|
+
}, [onBlurProp]);
|
|
31
|
+
return { focused, onFocus, onBlur };
|
|
32
|
+
}
|
|
33
|
+
function isKeyboardFocus(event) {
|
|
34
|
+
if (Platform.OS !== "web")
|
|
35
|
+
return true;
|
|
36
|
+
const target = event?.nativeEvent?.target;
|
|
37
|
+
if (!target || typeof target.matches !== "function")
|
|
38
|
+
return true;
|
|
39
|
+
try {
|
|
40
|
+
return target.matches(":focus-visible");
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return true;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FontFamilyWeight, type FontVariant, type ResolvedFontStyle } from "../constants/fonts";
|
|
1
|
+
import { type FontFamilyWeight, type FontStyleOptions, type FontVariant, type ResolvedFontStyle } from "../constants/fonts.js";
|
|
2
2
|
/**
|
|
3
3
|
* Resolve a `{ fontFamily, fontWeight? }` style through the theme store's
|
|
4
4
|
* font overrides, re-rendering when a host app calls `setFonts`.
|
|
@@ -8,4 +8,4 @@ import { type FontFamilyWeight, type FontVariant, type ResolvedFontStyle } from
|
|
|
8
8
|
* is identical to the package's hardcoded families, so layering it after a
|
|
9
9
|
* static themed style changes nothing by default.
|
|
10
10
|
*/
|
|
11
|
-
export declare function useFontStyle(weight?: FontFamilyWeight, variant?: FontVariant): ResolvedFontStyle;
|
|
11
|
+
export declare function useFontStyle(weight?: FontFamilyWeight, variant?: FontVariant, options?: Pick<FontStyleOptions, "italic">): ResolvedFontStyle;
|
|
@@ -9,9 +9,10 @@ import { useThemeStore } from "../state/themeStore.js";
|
|
|
9
9
|
* is identical to the package's hardcoded families, so layering it after a
|
|
10
10
|
* static themed style changes nothing by default.
|
|
11
11
|
*/
|
|
12
|
-
export function useFontStyle(weight = "regular", variant = "sansSerif") {
|
|
12
|
+
export function useFontStyle(weight = "regular", variant = "sansSerif", options = {}) {
|
|
13
13
|
// Subscribed, not read via getState(), so a `setFonts` call after mount
|
|
14
14
|
// re-renders instead of leaving stale families on screen.
|
|
15
15
|
const fontOverrides = useThemeStore((s) => s.fontOverrides);
|
|
16
|
-
|
|
16
|
+
const serifPreset = useThemeStore((s) => s.serifPreset);
|
|
17
|
+
return resolveFontStyle(fontOverrides, variant, weight, { italic: options.italic, serifPreset });
|
|
17
18
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* False during server rendering and the hydration pass, true once the tree
|
|
3
|
+
* is live on the client (and always true on native, which never hydrates).
|
|
4
|
+
*
|
|
5
|
+
* Use it to defer DOM output that cannot be reproduced on the server. The one
|
|
6
|
+
* case in the kit: `useId()`-based `id` / `aria-labelledby` pairs. Under Expo
|
|
7
|
+
* Router's streamed SSR the client's hydration pass computes different tree
|
|
8
|
+
* ids than the server did, so an id emitted on both sides logs "A tree hydrated
|
|
9
|
+
* but some attributes … didn't match" and the link points at a stale id.
|
|
10
|
+
* Adding the attributes in the first client render after hydration is an
|
|
11
|
+
* ordinary update instead.
|
|
12
|
+
*/
|
|
13
|
+
export declare function useHydrated(): boolean;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { useSyncExternalStore } from "react";
|
|
2
|
+
const subscribe = () => () => { };
|
|
3
|
+
const clientSnapshot = () => true;
|
|
4
|
+
const serverSnapshot = () => false;
|
|
5
|
+
/**
|
|
6
|
+
* False during server rendering and the hydration pass, true once the tree
|
|
7
|
+
* is live on the client (and always true on native, which never hydrates).
|
|
8
|
+
*
|
|
9
|
+
* Use it to defer DOM output that cannot be reproduced on the server. The one
|
|
10
|
+
* case in the kit: `useId()`-based `id` / `aria-labelledby` pairs. Under Expo
|
|
11
|
+
* Router's streamed SSR the client's hydration pass computes different tree
|
|
12
|
+
* ids than the server did, so an id emitted on both sides logs "A tree hydrated
|
|
13
|
+
* but some attributes … didn't match" and the link points at a stale id.
|
|
14
|
+
* Adding the attributes in the first client render after hydration is an
|
|
15
|
+
* ordinary update instead.
|
|
16
|
+
*/
|
|
17
|
+
export function useHydrated() {
|
|
18
|
+
return useSyncExternalStore(subscribe, clientSnapshot, serverSnapshot);
|
|
19
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type NotificationOffset } from "../state/globalUIStore.js";
|
|
2
|
+
/**
|
|
3
|
+
* Keep global toasts clear of a bar this layout owns, for as long as it is
|
|
4
|
+
* mounted. The offset is measured from the window edge and includes the
|
|
5
|
+
* safe-area inset there, so a JS tab layout passes `useBottomTabBarHeight()`
|
|
6
|
+
* as-is and a native tab layout passes the bar height plus the bottom inset;
|
|
7
|
+
* `Notification` then sits 8 pt past the larger of the inset and the offset.
|
|
8
|
+
* Pass `null` (or `undefined`) to register nothing — for instance while the
|
|
9
|
+
* tab bar is hidden behind the keyboard. Several layouts may register at once;
|
|
10
|
+
* the largest value per edge wins. A no-op during server rendering.
|
|
11
|
+
*
|
|
12
|
+
* ```tsx
|
|
13
|
+
* // app/(tabs)/_layout.tsx with Expo Router's JS <Tabs>
|
|
14
|
+
* function TabBarOffset() {
|
|
15
|
+
* useNotificationOffset({ bottom: useBottomTabBarHeight() });
|
|
16
|
+
* return null;
|
|
17
|
+
* }
|
|
18
|
+
* ```
|
|
19
|
+
*/
|
|
20
|
+
export declare function useNotificationOffset(offset: NotificationOffset | null | undefined): void;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { useEffect, useId } from "react";
|
|
2
|
+
import { globalUIStore } from "../state/globalUIStore.js";
|
|
3
|
+
/**
|
|
4
|
+
* Keep global toasts clear of a bar this layout owns, for as long as it is
|
|
5
|
+
* mounted. The offset is measured from the window edge and includes the
|
|
6
|
+
* safe-area inset there, so a JS tab layout passes `useBottomTabBarHeight()`
|
|
7
|
+
* as-is and a native tab layout passes the bar height plus the bottom inset;
|
|
8
|
+
* `Notification` then sits 8 pt past the larger of the inset and the offset.
|
|
9
|
+
* Pass `null` (or `undefined`) to register nothing — for instance while the
|
|
10
|
+
* tab bar is hidden behind the keyboard. Several layouts may register at once;
|
|
11
|
+
* the largest value per edge wins. A no-op during server rendering.
|
|
12
|
+
*
|
|
13
|
+
* ```tsx
|
|
14
|
+
* // app/(tabs)/_layout.tsx with Expo Router's JS <Tabs>
|
|
15
|
+
* function TabBarOffset() {
|
|
16
|
+
* useNotificationOffset({ bottom: useBottomTabBarHeight() });
|
|
17
|
+
* return null;
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export function useNotificationOffset(offset) {
|
|
22
|
+
const key = useId();
|
|
23
|
+
const top = offset?.top;
|
|
24
|
+
const bottom = offset?.bottom;
|
|
25
|
+
const active = offset != null && (top !== undefined || bottom !== undefined);
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
if (!active)
|
|
28
|
+
return;
|
|
29
|
+
globalUIStore.getState().setNotificationOffset(key, { top, bottom });
|
|
30
|
+
return () => {
|
|
31
|
+
globalUIStore.getState().clearNotificationOffset(key);
|
|
32
|
+
};
|
|
33
|
+
}, [active, key, top, bottom]);
|
|
34
|
+
}
|