@ankhorage/surface 4.2.1 → 4.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. package/CHANGELOG.md +12 -0
  2. package/README.md +4 -4
  3. package/dist/components/tabs/Tabs.js +2 -2
  4. package/dist/components/tabs/Tabs.js.map +1 -1
  5. package/dist/components/tabs/context.js +1 -1
  6. package/dist/components/tabs/context.js.map +1 -1
  7. package/dist/context/FontContext.d.ts.map +1 -1
  8. package/dist/context/FontContext.js +3 -3
  9. package/dist/context/FontContext.js.map +1 -1
  10. package/dist/context/TranslationContext.d.ts.map +1 -1
  11. package/dist/context/TranslationContext.js +3 -4
  12. package/dist/context/TranslationContext.js.map +1 -1
  13. package/dist/core/responsive/ResponsiveProvider.d.ts.map +1 -1
  14. package/dist/core/responsive/ResponsiveProvider.js +3 -3
  15. package/dist/core/responsive/ResponsiveProvider.js.map +1 -1
  16. package/dist/examples/DocsExamples.js +1 -1
  17. package/dist/examples/DocsExamples.js.map +1 -1
  18. package/dist/features/accordion/adapters/inbound/Accordion.d.ts +5 -0
  19. package/dist/features/accordion/adapters/inbound/Accordion.d.ts.map +1 -0
  20. package/dist/features/accordion/adapters/inbound/Accordion.js +46 -0
  21. package/dist/features/accordion/adapters/inbound/Accordion.js.map +1 -0
  22. package/dist/features/accordion/adapters/inbound/AccordionContent.d.ts +5 -0
  23. package/dist/features/accordion/adapters/inbound/AccordionContent.d.ts.map +1 -0
  24. package/dist/features/accordion/adapters/inbound/AccordionContent.js +16 -0
  25. package/dist/features/accordion/adapters/inbound/AccordionContent.js.map +1 -0
  26. package/dist/features/accordion/adapters/inbound/AccordionContext.d.ts +4 -0
  27. package/dist/features/accordion/adapters/inbound/AccordionContext.d.ts.map +1 -0
  28. package/dist/features/accordion/adapters/inbound/AccordionContext.js +3 -0
  29. package/dist/features/accordion/adapters/inbound/AccordionContext.js.map +1 -0
  30. package/dist/features/accordion/adapters/inbound/AccordionItem.d.ts +5 -0
  31. package/dist/features/accordion/adapters/inbound/AccordionItem.d.ts.map +1 -0
  32. package/dist/features/accordion/adapters/inbound/AccordionItem.js +28 -0
  33. package/dist/features/accordion/adapters/inbound/AccordionItem.js.map +1 -0
  34. package/dist/features/accordion/adapters/inbound/AccordionItemContext.d.ts +4 -0
  35. package/dist/features/accordion/adapters/inbound/AccordionItemContext.d.ts.map +1 -0
  36. package/dist/features/accordion/adapters/inbound/AccordionItemContext.js +3 -0
  37. package/dist/features/accordion/adapters/inbound/AccordionItemContext.js.map +1 -0
  38. package/dist/features/accordion/adapters/inbound/AccordionTrigger.d.ts +5 -0
  39. package/dist/features/accordion/adapters/inbound/AccordionTrigger.d.ts.map +1 -0
  40. package/dist/features/accordion/adapters/inbound/AccordionTrigger.js +17 -0
  41. package/dist/features/accordion/adapters/inbound/AccordionTrigger.js.map +1 -0
  42. package/dist/features/accordion/public.d.ts +6 -0
  43. package/dist/features/accordion/public.d.ts.map +1 -0
  44. package/dist/features/accordion/public.js +5 -0
  45. package/dist/features/accordion/public.js.map +1 -0
  46. package/dist/features/accordion/utils/resolveAccordionToggle.d.ts +11 -0
  47. package/dist/features/accordion/utils/resolveAccordionToggle.d.ts.map +1 -0
  48. package/dist/features/accordion/utils/resolveAccordionToggle.js +13 -0
  49. package/dist/features/accordion/utils/resolveAccordionToggle.js.map +1 -0
  50. package/dist/features/bottom-sheet/BottomSheetProvider.js +2 -2
  51. package/dist/features/bottom-sheet/BottomSheetProvider.js.map +1 -1
  52. package/dist/features/bottom-sheet/useBottomSheet.js +1 -1
  53. package/dist/features/bottom-sheet/useBottomSheet.js.map +1 -1
  54. package/dist/features/popover/adapters/inbound/Popover.d.ts +5 -0
  55. package/dist/features/popover/adapters/inbound/Popover.d.ts.map +1 -0
  56. package/dist/features/popover/adapters/inbound/Popover.js +31 -0
  57. package/dist/features/popover/adapters/inbound/Popover.js.map +1 -0
  58. package/dist/features/popover/adapters/inbound/PopoverOverlay.d.ts +17 -0
  59. package/dist/features/popover/adapters/inbound/PopoverOverlay.d.ts.map +1 -0
  60. package/dist/features/popover/adapters/inbound/PopoverOverlay.js +34 -0
  61. package/dist/features/popover/adapters/inbound/PopoverOverlay.js.map +1 -0
  62. package/dist/features/popover/adapters/inbound/usePopoverLayout.d.ts +21 -0
  63. package/dist/features/popover/adapters/inbound/usePopoverLayout.d.ts.map +1 -0
  64. package/dist/features/popover/adapters/inbound/usePopoverLayout.js +33 -0
  65. package/dist/features/popover/adapters/inbound/usePopoverLayout.js.map +1 -0
  66. package/dist/features/popover/public.d.ts +3 -0
  67. package/dist/features/popover/public.d.ts.map +1 -0
  68. package/dist/features/popover/public.js +2 -0
  69. package/dist/features/popover/public.js.map +1 -0
  70. package/dist/features/popover/utils/resolvePopoverPosition.d.ts +25 -0
  71. package/dist/features/popover/utils/resolvePopoverPosition.d.ts.map +1 -0
  72. package/dist/features/popover/utils/resolvePopoverPosition.js +23 -0
  73. package/dist/features/popover/utils/resolvePopoverPosition.js.map +1 -0
  74. package/dist/{components/toast → features/toast/adapters/inbound}/Toast.d.ts +2 -1
  75. package/dist/features/toast/adapters/inbound/Toast.d.ts.map +1 -0
  76. package/dist/{components/toast → features/toast/adapters/inbound}/Toast.js +37 -32
  77. package/dist/features/toast/adapters/inbound/Toast.js.map +1 -0
  78. package/dist/features/toast/adapters/inbound/ToastProvider.d.ts +5 -0
  79. package/dist/features/toast/adapters/inbound/ToastProvider.d.ts.map +1 -0
  80. package/dist/features/toast/adapters/inbound/ToastProvider.js +29 -0
  81. package/dist/features/toast/adapters/inbound/ToastProvider.js.map +1 -0
  82. package/dist/features/toast/adapters/inbound/useToast.d.ts +4 -0
  83. package/dist/features/toast/adapters/inbound/useToast.d.ts.map +1 -0
  84. package/dist/features/toast/adapters/inbound/useToast.js +11 -0
  85. package/dist/features/toast/adapters/inbound/useToast.js.map +1 -0
  86. package/dist/features/toast/composition/ToastContext.d.ts +4 -0
  87. package/dist/features/toast/composition/ToastContext.d.ts.map +1 -0
  88. package/dist/features/toast/composition/ToastContext.js +4 -0
  89. package/dist/features/toast/composition/ToastContext.js.map +1 -0
  90. package/dist/features/toast/composition/useToastRuntime.d.ts +11 -0
  91. package/dist/features/toast/composition/useToastRuntime.d.ts.map +1 -0
  92. package/dist/features/toast/composition/useToastRuntime.js +50 -0
  93. package/dist/features/toast/composition/useToastRuntime.js.map +1 -0
  94. package/dist/features/toast/public.d.ts +5 -0
  95. package/dist/features/toast/public.d.ts.map +1 -0
  96. package/dist/features/toast/public.js +4 -0
  97. package/dist/features/toast/public.js.map +1 -0
  98. package/dist/index.d.ts +6 -2
  99. package/dist/index.d.ts.map +1 -1
  100. package/dist/index.js +3 -1
  101. package/dist/index.js.map +1 -1
  102. package/dist/internal/overlay/OverlayProvider.d.ts +1 -0
  103. package/dist/internal/overlay/OverlayProvider.d.ts.map +1 -1
  104. package/dist/internal/overlay/OverlayProvider.js +9 -7
  105. package/dist/internal/overlay/OverlayProvider.js.map +1 -1
  106. package/dist/internal/overlay/useOverlayStack.js +1 -1
  107. package/dist/internal/overlay/useOverlayStack.js.map +1 -1
  108. package/dist/internal/resolvers/resolveOverlayZIndex.d.ts +1 -1
  109. package/dist/internal/resolvers/resolveOverlayZIndex.d.ts.map +1 -1
  110. package/dist/internal/resolvers/resolveOverlayZIndex.js +1 -0
  111. package/dist/internal/resolvers/resolveOverlayZIndex.js.map +1 -1
  112. package/dist/layout/AppBar.js +1 -1
  113. package/dist/layout/AppBar.js.map +1 -1
  114. package/dist/theme/ThemeContext.d.ts.map +1 -1
  115. package/dist/theme/ThemeContext.js +4 -5
  116. package/dist/theme/ThemeContext.js.map +1 -1
  117. package/dist/types/accordion.d.ts +54 -0
  118. package/dist/types/accordion.d.ts.map +1 -0
  119. package/dist/types/accordion.js +2 -0
  120. package/dist/types/accordion.js.map +1 -0
  121. package/dist/types/popover.d.ts +21 -0
  122. package/dist/types/popover.d.ts.map +1 -0
  123. package/dist/types/popover.js +2 -0
  124. package/dist/types/popover.js.map +1 -0
  125. package/dist/{components/toast/types.d.ts → types/toast.d.ts} +11 -3
  126. package/dist/types/toast.d.ts.map +1 -0
  127. package/dist/types/toast.js +2 -0
  128. package/dist/types/toast.js.map +1 -0
  129. package/package.json +1 -1
  130. package/src/components/tabs/Tabs.tsx +2 -2
  131. package/src/components/tabs/context.tsx +1 -1
  132. package/src/context/FontContext.tsx +3 -3
  133. package/src/context/TranslationContext.tsx +3 -4
  134. package/src/core/responsive/ResponsiveProvider.tsx +3 -3
  135. package/src/examples/DocsExamples.tsx +1 -1
  136. package/src/features/accordion/adapters/inbound/Accordion.tsx +83 -0
  137. package/src/features/accordion/adapters/inbound/AccordionContent.tsx +30 -0
  138. package/src/features/accordion/adapters/inbound/AccordionContext.ts +5 -0
  139. package/src/features/accordion/adapters/inbound/AccordionItem.tsx +42 -0
  140. package/src/features/accordion/adapters/inbound/AccordionItemContext.ts +5 -0
  141. package/src/features/accordion/adapters/inbound/AccordionTrigger.tsx +34 -0
  142. package/src/features/accordion/public.ts +13 -0
  143. package/src/features/accordion/utils/resolveAccordionToggle.test.ts +57 -0
  144. package/src/features/accordion/utils/resolveAccordionToggle.ts +28 -0
  145. package/src/features/bottom-sheet/BottomSheetProvider.tsx +2 -2
  146. package/src/features/bottom-sheet/useBottomSheet.ts +1 -1
  147. package/src/features/popover/adapters/inbound/Popover.tsx +49 -0
  148. package/src/features/popover/adapters/inbound/PopoverOverlay.tsx +72 -0
  149. package/src/features/popover/adapters/inbound/usePopoverLayout.ts +51 -0
  150. package/src/features/popover/public.ts +2 -0
  151. package/src/features/popover/utils/resolvePopoverPosition.test.ts +31 -0
  152. package/src/features/popover/utils/resolvePopoverPosition.ts +40 -0
  153. package/src/{components/toast → features/toast/adapters/inbound}/Toast.tsx +58 -54
  154. package/src/features/toast/adapters/inbound/ToastProvider.tsx +47 -0
  155. package/src/features/toast/adapters/inbound/useToast.test.tsx +18 -0
  156. package/src/features/toast/adapters/inbound/useToast.ts +15 -0
  157. package/src/features/toast/composition/ToastContext.ts +6 -0
  158. package/src/features/toast/composition/useToastRuntime.ts +76 -0
  159. package/src/features/toast/public.ts +10 -0
  160. package/src/index.test.ts +3 -1
  161. package/src/index.ts +29 -2
  162. package/src/internal/overlay/OverlayProvider.tsx +10 -8
  163. package/src/internal/overlay/useOverlayStack.ts +1 -1
  164. package/src/internal/resolvers/resolveOverlayZIndex.test.ts +2 -1
  165. package/src/internal/resolvers/resolveOverlayZIndex.ts +2 -1
  166. package/src/layout/AppBar.tsx +1 -1
  167. package/src/theme/ThemeContext.tsx +4 -5
  168. package/src/types/accordion.ts +69 -0
  169. package/src/types/popover.ts +25 -0
  170. package/src/{components/toast/types.ts → types/toast.ts} +12 -2
  171. package/dist/components/toast/Toast.d.ts.map +0 -1
  172. package/dist/components/toast/Toast.js.map +0 -1
  173. package/dist/components/toast/ToastProvider.d.ts +0 -13
  174. package/dist/components/toast/ToastProvider.d.ts.map +0 -1
  175. package/dist/components/toast/ToastProvider.js +0 -70
  176. package/dist/components/toast/ToastProvider.js.map +0 -1
  177. package/dist/components/toast/index.d.ts +0 -4
  178. package/dist/components/toast/index.d.ts.map +0 -1
  179. package/dist/components/toast/index.js +0 -3
  180. package/dist/components/toast/index.js.map +0 -1
  181. package/dist/components/toast/types.d.ts.map +0 -1
  182. package/dist/components/toast/types.js +0 -2
  183. package/dist/components/toast/types.js.map +0 -1
  184. package/src/components/toast/ToastProvider.tsx +0 -115
  185. package/src/components/toast/index.ts +0 -3
@@ -0,0 +1,40 @@
1
+ import type { PopoverPlacement } from '../../../types/popover';
2
+
3
+ /*** Resolves a viewport-clamped anchored position for popover content. */
4
+ export function resolvePopoverPosition({
5
+ anchor,
6
+ contentSize,
7
+ offset,
8
+ placement,
9
+ viewport,
10
+ viewportPadding,
11
+ }: {
12
+ anchor: { height: number; width: number; x: number; y: number };
13
+ contentSize: { height: number; width: number };
14
+ offset: number;
15
+ placement: PopoverPlacement;
16
+ viewport: { height: number; width: number };
17
+ viewportPadding: number;
18
+ }): { left: number; top: number } {
19
+ const anchorCenterX = anchor.x + anchor.width / 2;
20
+ const rawLeft = placement.endsWith('-start')
21
+ ? anchor.x
22
+ : placement.endsWith('-end')
23
+ ? anchor.x + anchor.width - contentSize.width
24
+ : anchorCenterX - contentSize.width / 2;
25
+ const rawTop = placement.startsWith('top')
26
+ ? anchor.y - contentSize.height - offset
27
+ : anchor.y + anchor.height + offset;
28
+ const maxLeft = Math.max(viewportPadding, viewport.width - contentSize.width - viewportPadding);
29
+ const maxTop = Math.max(viewportPadding, viewport.height - contentSize.height - viewportPadding);
30
+
31
+ return {
32
+ left: clamp(rawLeft, viewportPadding, maxLeft),
33
+ top: clamp(rawTop, viewportPadding, maxTop),
34
+ };
35
+ }
36
+
37
+ /*** Clamps one numeric position into the available viewport range. */
38
+ function clamp(value: number, minimum: number, maximum: number): number {
39
+ return Math.min(Math.max(value, minimum), maximum);
40
+ }
@@ -1,45 +1,15 @@
1
1
  import React from 'react';
2
- import { Pressable } from 'react-native';
2
+ import { Pressable, StyleSheet, type ViewStyle } from 'react-native';
3
3
 
4
- import { Box } from '../../features/layout/public';
5
- import { Surface } from '../../features/surface/public';
6
- import { Text } from '../../features/typography/public';
7
- import { Inline } from '../../layout';
8
- import type { SurfaceColor } from '../../surfaceColor';
9
- import { useTheme } from '../../theme/ThemeContext';
10
- import type { SurfaceTheme } from '../../theme/types';
11
- import type { ToastProps } from './types';
12
-
13
- function resolveToastStatusColor(theme: SurfaceTheme, status: ToastProps['status']) {
14
- switch (status) {
15
- case 'success':
16
- return theme.semantics.success.base;
17
- case 'warning':
18
- return theme.semantics.warning.base;
19
- case 'error':
20
- return theme.semantics.error.base;
21
- case 'info':
22
- return theme.semantics.info.base;
23
- default:
24
- return theme.semantics.action.primary.base;
25
- }
26
- }
27
-
28
- function resolveToastStatusTextColor(status: ToastProps['status']): SurfaceColor {
29
- switch (status) {
30
- case 'success':
31
- return 'success';
32
- case 'warning':
33
- return 'warning';
34
- case 'error':
35
- return 'error';
36
- case 'info':
37
- return 'info';
38
- default:
39
- return 'primary';
40
- }
41
- }
4
+ import type { SurfaceColor } from '../../../../surfaceColor';
5
+ import { useTheme } from '../../../../theme/ThemeContext';
6
+ import type { SurfaceTheme } from '../../../../theme/types';
7
+ import type { ToastProps } from '../../../../types/toast';
8
+ import { Box, Stack } from '../../../layout/public';
9
+ import { Surface } from '../../../surface/public';
10
+ import { Text } from '../../../typography/public';
42
11
 
12
+ /*** Renders one transient toast notification. */
43
13
  export function Toast({
44
14
  title,
45
15
  description,
@@ -52,22 +22,14 @@ export function Toast({
52
22
  const statusColor = resolveToastStatusColor(theme, status);
53
23
  const statusTextColor = resolveToastStatusTextColor(status);
54
24
  const passive = interactionPolicy === 'passive';
25
+ const statusStyle = React.useMemo<ViewStyle>(
26
+ () => ({ borderLeftColor: statusColor }),
27
+ [statusColor],
28
+ );
55
29
 
56
30
  return (
57
- <Surface
58
- p="m"
59
- style={{
60
- borderLeftColor: statusColor,
61
- borderLeftWidth: 3,
62
- minWidth: 280,
63
- shadowOpacity: 0.14,
64
- shadowRadius: 12,
65
- shadowOffset: { width: 0, height: 6 },
66
- }}
67
- testID={testID}
68
- variant="raised"
69
- >
70
- <Inline align="center" justify="space-between">
31
+ <Surface p="m" style={[styles.root, statusStyle]} testID={testID} variant="raised">
32
+ <Stack align="center" direction="row" justify="space-between">
71
33
  <Box flex={1}>
72
34
  {title ? (
73
35
  <Text variant="label" weight="medium">
@@ -86,7 +48,49 @@ export function Toast({
86
48
  <Text color={statusTextColor}>×</Text>
87
49
  </Pressable>
88
50
  ) : null}
89
- </Inline>
51
+ </Stack>
90
52
  </Surface>
91
53
  );
92
54
  }
55
+
56
+ /*** Resolves the border color representing a toast status. */
57
+ function resolveToastStatusColor(theme: SurfaceTheme, status: ToastProps['status']) {
58
+ switch (status) {
59
+ case 'success':
60
+ return theme.semantics.success.base;
61
+ case 'warning':
62
+ return theme.semantics.warning.base;
63
+ case 'error':
64
+ return theme.semantics.error.base;
65
+ case 'info':
66
+ return theme.semantics.info.base;
67
+ default:
68
+ return theme.semantics.action.primary.base;
69
+ }
70
+ }
71
+
72
+ /*** Resolves the semantic text color representing a toast status. */
73
+ function resolveToastStatusTextColor(status: ToastProps['status']): SurfaceColor {
74
+ switch (status) {
75
+ case 'success':
76
+ return 'success';
77
+ case 'warning':
78
+ return 'warning';
79
+ case 'error':
80
+ return 'error';
81
+ case 'info':
82
+ return 'info';
83
+ default:
84
+ return 'primary';
85
+ }
86
+ }
87
+
88
+ const styles = StyleSheet.create({
89
+ root: {
90
+ borderLeftWidth: 3,
91
+ minWidth: 280,
92
+ shadowOffset: { height: 6, width: 0 },
93
+ shadowOpacity: 0.14,
94
+ shadowRadius: 12,
95
+ },
96
+ });
@@ -0,0 +1,47 @@
1
+ import React from 'react';
2
+ import { StyleSheet, type ViewStyle } from 'react-native';
3
+
4
+ import { Portal } from '../../../../internal/overlay/Portal';
5
+ import { resolveOverlayAnimation } from '../../../../internal/resolvers';
6
+ import type { ToastProviderProps } from '../../../../types/toast';
7
+ import { Stack } from '../../../layout/public';
8
+ import { ToastContext } from '../../composition/ToastContext';
9
+ import { useToastRuntime } from '../../composition/useToastRuntime';
10
+ import { Toast } from './Toast';
11
+
12
+ /*** Provides the toast runtime context and renders its shared portal host. */
13
+ export function ToastProvider({ children, defaultDuration = 4000 }: ToastProviderProps) {
14
+ const animation = resolveOverlayAnimation('toast');
15
+ const { controller, toasts } = useToastRuntime(defaultDuration);
16
+ const animationStyle = React.useMemo<ViewStyle>(
17
+ () => ({ paddingTop: 16 + animation.offset }),
18
+ [animation.offset],
19
+ );
20
+
21
+ return (
22
+ <ToastContext value={controller}>
23
+ {children}
24
+ <Portal layer="toast" visible={toasts.length > 0}>
25
+ <Stack gap="s" pointerEvents="box-none" style={[styles.container, animationStyle]}>
26
+ {toasts.map((toast) => (
27
+ <Toast
28
+ description={toast.description}
29
+ key={toast.id}
30
+ onDismiss={() => controller.dismissToast(toast.id)}
31
+ status={toast.status}
32
+ testID={toast.testID}
33
+ title={toast.title}
34
+ />
35
+ ))}
36
+ </Stack>
37
+ </Portal>
38
+ </ToastContext>
39
+ );
40
+ }
41
+
42
+ const styles = StyleSheet.create({
43
+ container: {
44
+ alignItems: 'flex-end',
45
+ padding: 16,
46
+ },
47
+ });
@@ -0,0 +1,18 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import React from 'react';
3
+ import { renderToStaticMarkup } from 'react-dom/server';
4
+
5
+ import { useToast } from './useToast';
6
+
7
+ describe('useToast', () => {
8
+ it('requires ToastProvider', () => {
9
+ function Consumer() {
10
+ useToast();
11
+ return null;
12
+ }
13
+
14
+ expect(() => renderToStaticMarkup(<Consumer />)).toThrow(
15
+ 'useToast must be used within <ToastProvider>.',
16
+ );
17
+ });
18
+ });
@@ -0,0 +1,15 @@
1
+ import { use } from 'react';
2
+
3
+ import type { ToastController } from '../../../../types/toast';
4
+ import { ToastContext } from '../../composition/ToastContext';
5
+
6
+ /*** Returns the toast controller installed by ToastProvider. */
7
+ export function useToast(): ToastController {
8
+ const controller = use(ToastContext);
9
+
10
+ if (!controller) {
11
+ throw new Error('useToast must be used within <ToastProvider>.');
12
+ }
13
+
14
+ return controller;
15
+ }
@@ -0,0 +1,6 @@
1
+ import { createContext } from 'react';
2
+
3
+ import type { ToastController } from '../../../types/toast';
4
+
5
+ /*** Stores the toast controller shared by ToastProvider and useToast. */
6
+ export const ToastContext = createContext<ToastController | null>(null);
@@ -0,0 +1,76 @@
1
+ import React from 'react';
2
+
3
+ import type { ToastController, ToastOptions } from '../../../types/toast';
4
+
5
+ /*** Owns the toast queue, dismissal timers, and imperative controller state. */
6
+ export function useToastRuntime(defaultDuration: number) {
7
+ const [toasts, setToasts] = React.useState<ToastEntry[]>([]);
8
+ const timersRef = React.useRef(new Map<string, ReturnType<typeof setTimeout>>());
9
+ const counterRef = React.useRef(0);
10
+
11
+ const dismissToast = React.useCallback((id: string) => {
12
+ const timer = timersRef.current.get(id);
13
+ if (timer) {
14
+ clearTimeout(timer);
15
+ timersRef.current.delete(id);
16
+ }
17
+ setToasts((current) => current.filter((toast) => toast.id !== id));
18
+ }, []);
19
+
20
+ const showToast = React.useCallback((options: ToastOptions) => {
21
+ const id = options.id ?? `toast-${counterRef.current++}`;
22
+ setToasts((current) => [...current, { ...options, id }]);
23
+ return id;
24
+ }, []);
25
+
26
+ useToastTimers({ defaultDuration, dismissToast, timersRef, toasts });
27
+
28
+ const controller = React.useMemo<ToastController>(
29
+ () => ({ dismissToast, showToast }),
30
+ [dismissToast, showToast],
31
+ );
32
+
33
+ return { controller, toasts };
34
+ }
35
+
36
+ interface ToastEntry extends ToastOptions {
37
+ id: string;
38
+ }
39
+
40
+ interface UseToastTimersInput {
41
+ defaultDuration: number;
42
+ dismissToast: (id: string) => void;
43
+ timersRef: React.RefObject<Map<string, ReturnType<typeof setTimeout>>>;
44
+ toasts: readonly ToastEntry[];
45
+ }
46
+
47
+ /*** Synchronizes toast lifetimes with timer side effects and cleans them up on unmount. */
48
+ function useToastTimers({ defaultDuration, dismissToast, timersRef, toasts }: UseToastTimersInput) {
49
+ React.useEffect(() => {
50
+ const timers = timersRef.current;
51
+
52
+ toasts.forEach((toast) => {
53
+ if (timers.has(toast.id)) return;
54
+
55
+ const timer = setTimeout(() => dismissToast(toast.id), toast.duration ?? defaultDuration);
56
+ timers.set(toast.id, timer);
57
+ });
58
+
59
+ const activeToastIds = new Set(toasts.map((toast) => toast.id));
60
+ timers.forEach((timer, id) => {
61
+ if (!activeToastIds.has(id)) {
62
+ clearTimeout(timer);
63
+ timers.delete(id);
64
+ }
65
+ });
66
+ }, [defaultDuration, dismissToast, timersRef, toasts]);
67
+
68
+ React.useEffect(() => {
69
+ const timers = timersRef.current;
70
+
71
+ return () => {
72
+ timers.forEach((timer) => clearTimeout(timer));
73
+ timers.clear();
74
+ };
75
+ }, [timersRef]);
76
+ }
@@ -0,0 +1,10 @@
1
+ export type {
2
+ ToastController,
3
+ ToastOptions,
4
+ ToastProps,
5
+ ToastProviderProps,
6
+ ToastStatus,
7
+ } from '../../types/toast';
8
+ export { Toast } from './adapters/inbound/Toast';
9
+ export { ToastProvider } from './adapters/inbound/ToastProvider';
10
+ export { useToast } from './adapters/inbound/useToast';
package/src/index.test.ts CHANGED
@@ -28,7 +28,9 @@ const expectedFeatureRootExports = [
28
28
  "export { Image } from './features/image/public';",
29
29
  "export { KeyboardAvoidingView } from './features/keyboard-avoiding-view/public';",
30
30
  "export { Box, Container, Divider, Grid, Stack } from './features/layout/public';",
31
+ "export { Popover } from './features/popover/public';",
31
32
  "export { Surface } from './features/surface/public';",
33
+ "export { Toast, ToastProvider, useToast } from './features/toast/public';",
32
34
  "export { Heading, Text } from './features/typography/public';",
33
35
  ] as const;
34
36
 
@@ -42,7 +44,6 @@ const expectedLegacyRootExports = [
42
44
  "export { Switch } from './components/switch';",
43
45
  "export { Tab, TabList, TabPanel, Tabs } from './components/tabs';",
44
46
  "export { Textarea } from './components/textarea';",
45
- "export { Toast, ToastProvider, useToast } from './components/toast';",
46
47
  "export { Tooltip } from './components/tooltip';",
47
48
  "export type { InteractionPolicy, InteractionPolicyProps } from './interactionPolicy';",
48
49
  "export * from './core/responsive';",
@@ -63,6 +64,7 @@ describe('feature-owned root barrel contract', () => {
63
64
  './components/icon-button',
64
65
  './components/radio',
65
66
  './components/text-input',
67
+ './components/toast',
66
68
  './primitives/heading',
67
69
  './primitives/icon',
68
70
  './primitives/image',
package/src/index.ts CHANGED
@@ -16,8 +16,6 @@ export type { TabListProps, TabPanelProps, TabProps, TabsProps } from './compone
16
16
  export { Tab, TabList, TabPanel, Tabs } from './components/tabs';
17
17
  export type { TextareaProps } from './components/textarea';
18
18
  export { Textarea } from './components/textarea';
19
- export type { ToastOptions, ToastProps, ToastStatus } from './components/toast';
20
- export { Toast, ToastProvider, useToast } from './components/toast';
21
19
  export type { TooltipProps } from './components/tooltip';
22
20
  export { Tooltip } from './components/tooltip';
23
21
  export type { FontRuntime } from './context/FontContext';
@@ -25,6 +23,21 @@ export { FontProvider, useFontContext } from './context/FontContext';
25
23
  export type { I18nInstance, TranslationRuntime, Translator } from './context/TranslationContext';
26
24
  export { TranslationProvider, useTranslationContext } from './context/TranslationContext';
27
25
  export * from './core/responsive';
26
+ export type {
27
+ AccordionContentProps,
28
+ AccordionItemProps,
29
+ AccordionMode,
30
+ AccordionMultipleProps,
31
+ AccordionProps,
32
+ AccordionSingleProps,
33
+ AccordionTriggerProps,
34
+ } from './features/accordion/public';
35
+ export {
36
+ Accordion,
37
+ AccordionContent,
38
+ AccordionItem,
39
+ AccordionTrigger,
40
+ } from './features/accordion/public';
28
41
  export type { BadgeProps } from './features/badge/public';
29
42
  export { Badge } from './features/badge/public';
30
43
  export type { ButtonIconSpec, ButtonProps, IconButtonProps } from './features/button/public';
@@ -61,8 +74,22 @@ export type {
61
74
  StackProps,
62
75
  } from './features/layout/public';
63
76
  export { Box, Container, Divider, Grid, Stack } from './features/layout/public';
77
+ export type {
78
+ PopoverAnchorRenderProps,
79
+ PopoverPlacement,
80
+ PopoverProps,
81
+ } from './features/popover/public';
82
+ export { Popover } from './features/popover/public';
64
83
  export type { SurfaceProps, SurfaceVariant } from './features/surface/public';
65
84
  export { Surface } from './features/surface/public';
85
+ export type {
86
+ ToastController,
87
+ ToastOptions,
88
+ ToastProps,
89
+ ToastProviderProps,
90
+ ToastStatus,
91
+ } from './features/toast/public';
92
+ export { Toast, ToastProvider, useToast } from './features/toast/public';
66
93
  export type { HeadingLevel, HeadingProps, TextProps } from './features/typography/public';
67
94
  export { Heading, Text } from './features/typography/public';
68
95
  export type { InteractionPolicy, InteractionPolicyProps } from './interactionPolicy';
@@ -22,10 +22,7 @@ const styles = StyleSheet.create({
22
22
  },
23
23
  });
24
24
 
25
- function overlayZIndex(zIndex: number): ViewStyle {
26
- return { zIndex };
27
- }
28
-
25
+ /*** Installs the shared overlay stack used by portal-based Surface features. */
29
26
  export function OverlayProvider({ children }: { children: React.ReactNode }) {
30
27
  const orderRef = React.useRef(0);
31
28
  const [overlays, setOverlays] = React.useState<OverlayEntry[]>([]);
@@ -65,8 +62,8 @@ export function OverlayProvider({ children }: { children: React.ReactNode }) {
65
62
  );
66
63
 
67
64
  return (
68
- <OverlayStackActionsContext.Provider value={actions}>
69
- <OverlayStackContext.Provider value={value}>
65
+ <OverlayStackActionsContext value={actions}>
66
+ <OverlayStackContext value={value}>
70
67
  {children}
71
68
  <View {...boxNonePointerEvents.props} style={[boxNonePointerEvents.style, styles.fill]}>
72
69
  {overlays.map((overlay) => (
@@ -79,7 +76,12 @@ export function OverlayProvider({ children }: { children: React.ReactNode }) {
79
76
  </View>
80
77
  ))}
81
78
  </View>
82
- </OverlayStackContext.Provider>
83
- </OverlayStackActionsContext.Provider>
79
+ </OverlayStackContext>
80
+ </OverlayStackActionsContext>
84
81
  );
85
82
  }
83
+
84
+ /*** Resolves a per-entry z-index style for the overlay host. */
85
+ function overlayZIndex(zIndex: number): ViewStyle {
86
+ return { zIndex };
87
+ }
@@ -48,7 +48,7 @@ export const OverlayStackContext = React.createContext<OverlayStackRuntime | nul
48
48
  export const OverlayStackActionsContext = React.createContext<OverlayStackActions | null>(null);
49
49
 
50
50
  export function useOverlayStackActions(): OverlayStackActions | null {
51
- return React.useContext(OverlayStackActionsContext);
51
+ return React.use(OverlayStackActionsContext);
52
52
  }
53
53
 
54
54
  export function createOverlayEntry(
@@ -5,7 +5,8 @@ import { resolveOverlayZIndex } from './resolveOverlayZIndex';
5
5
  describe('resolveOverlayZIndex', () => {
6
6
  it('assigns higher base layers to later overlay types', () => {
7
7
  expect(resolveOverlayZIndex('modal')).toBeLessThan(resolveOverlayZIndex('menu'));
8
- expect(resolveOverlayZIndex('menu')).toBeLessThan(resolveOverlayZIndex('tooltip'));
8
+ expect(resolveOverlayZIndex('menu')).toBeLessThan(resolveOverlayZIndex('popover'));
9
+ expect(resolveOverlayZIndex('popover')).toBeLessThan(resolveOverlayZIndex('tooltip'));
9
10
  expect(resolveOverlayZIndex('tooltip')).toBeLessThan(resolveOverlayZIndex('toast'));
10
11
  });
11
12
 
@@ -1,9 +1,10 @@
1
- export type OverlayLayer = 'modal' | 'drawer' | 'menu' | 'tooltip' | 'toast';
1
+ export type OverlayLayer = 'modal' | 'drawer' | 'menu' | 'popover' | 'tooltip' | 'toast';
2
2
 
3
3
  const BASE_Z_INDEX: Record<OverlayLayer, number> = {
4
4
  modal: 1000,
5
5
  drawer: 1100,
6
6
  menu: 1200,
7
+ popover: 1250,
7
8
  tooltip: 1300,
8
9
  toast: 1400,
9
10
  };
@@ -26,7 +26,7 @@ export function AppBar({
26
26
  ...props
27
27
  }: AppBarProps) {
28
28
  const { theme } = useTheme();
29
- const insets = React.useContext(SafeAreaInsetsContext);
29
+ const insets = React.use(SafeAreaInsetsContext);
30
30
  const topInset = safeAreaTop ? (insets?.top ?? 0) : 0;
31
31
  const safeAreaStyle: ViewStyle | undefined =
32
32
  topInset === 0 ? undefined : { paddingTop: topInset };
@@ -1,4 +1,4 @@
1
- import React, { createContext, useContext, useMemo } from 'react';
1
+ import React, { createContext, use, useMemo } from 'react';
2
2
 
3
3
  import { useFontContext } from '../context/FontContext';
4
4
  import { ResponsiveProvider } from '../core/responsive/ResponsiveProvider';
@@ -43,7 +43,6 @@ export const ThemeProvider = ({
43
43
  const [mode, setMode] = React.useState<'light' | 'dark'>(initialMode);
44
44
  const { activeFontId } = useFontContext();
45
45
 
46
- // Keep state in sync with prop for real-time Studio updates
47
46
  React.useEffect(() => {
48
47
  if (initialConfig) {
49
48
  setConfig((prev) => {
@@ -73,15 +72,15 @@ export const ThemeProvider = ({
73
72
 
74
73
  return (
75
74
  <ResponsiveProvider>
76
- <ThemeContext.Provider value={value}>
75
+ <ThemeContext value={value}>
77
76
  <OverlayProvider>{children}</OverlayProvider>
78
- </ThemeContext.Provider>
77
+ </ThemeContext>
79
78
  </ResponsiveProvider>
80
79
  );
81
80
  };
82
81
 
83
82
  export const useTheme = () => {
84
- return useContext(ThemeContext);
83
+ return use(ThemeContext);
85
84
  };
86
85
 
87
86
  export const useThemeConfig = () => {
@@ -0,0 +1,69 @@
1
+ import type React from 'react';
2
+ import type { PressableProps, ViewProps } from 'react-native';
3
+
4
+ import type { InteractionPolicy, InteractionPolicyProps } from '../interactionPolicy';
5
+
6
+ export type AccordionMode = 'single' | 'multiple';
7
+
8
+ interface AccordionBaseProps extends Omit<ViewProps, 'children'>, InteractionPolicyProps {
9
+ children?: React.ReactNode;
10
+ disabled?: boolean;
11
+ }
12
+
13
+ export interface AccordionSingleProps extends AccordionBaseProps {
14
+ type?: 'single';
15
+ value?: string;
16
+ defaultValue?: string;
17
+ onValueChange?: (value: string | undefined) => void;
18
+ collapsible?: boolean;
19
+ }
20
+
21
+ export interface AccordionMultipleProps extends AccordionBaseProps {
22
+ type: 'multiple';
23
+ value?: readonly string[];
24
+ defaultValue?: readonly string[];
25
+ onValueChange?: (value: readonly string[]) => void;
26
+ collapsible?: never;
27
+ }
28
+
29
+ export type AccordionProps = AccordionSingleProps | AccordionMultipleProps;
30
+
31
+ export interface AccordionItemProps extends Omit<ViewProps, 'children'>, InteractionPolicyProps {
32
+ value: string;
33
+ children?: React.ReactNode;
34
+ disabled?: boolean;
35
+ }
36
+
37
+ export interface AccordionTriggerProps
38
+ extends
39
+ Omit<
40
+ PressableProps,
41
+ 'accessibilityRole' | 'accessibilityState' | 'children' | 'disabled' | 'onPress'
42
+ >,
43
+ InteractionPolicyProps {
44
+ children?: PressableProps['children'];
45
+ disabled?: boolean;
46
+ }
47
+
48
+ export interface AccordionContentProps extends Omit<ViewProps, 'children'> {
49
+ children?: React.ReactNode;
50
+ forceMount?: boolean;
51
+ }
52
+
53
+ export interface AccordionContextValue {
54
+ disabled: boolean;
55
+ interactionPolicy: InteractionPolicy;
56
+ mode: AccordionMode;
57
+ openValues: readonly string[];
58
+ toggleValue: (value: string) => void;
59
+ }
60
+
61
+ export interface AccordionItemContextValue {
62
+ contentId: string;
63
+ disabled: boolean;
64
+ interactionPolicy: InteractionPolicy;
65
+ open: boolean;
66
+ toggle: () => void;
67
+ triggerId: string;
68
+ value: string;
69
+ }
@@ -0,0 +1,25 @@
1
+ import type React from 'react';
2
+
3
+ import type { InteractionPolicy } from '../interactionPolicy';
4
+
5
+ export type PopoverPlacement =
6
+ 'bottom-start' | 'bottom' | 'bottom-end' | 'top-start' | 'top' | 'top-end';
7
+
8
+ export interface PopoverAnchorRenderProps {
9
+ close: () => void;
10
+ open: boolean;
11
+ toggle: () => void;
12
+ }
13
+
14
+ export interface PopoverProps {
15
+ anchor: (controls: PopoverAnchorRenderProps) => React.ReactNode;
16
+ children?: React.ReactNode;
17
+ closeOnOutsidePress?: boolean;
18
+ defaultOpen?: boolean;
19
+ interactionPolicy?: InteractionPolicy;
20
+ offset?: number;
21
+ onOpenChange?: ((open: boolean) => void) | undefined;
22
+ open?: boolean;
23
+ placement?: PopoverPlacement;
24
+ testID?: string;
25
+ }
@@ -1,7 +1,7 @@
1
1
  import type React from 'react';
2
2
 
3
- import type { InteractionPolicy } from '../../interactionPolicy';
4
- import type { SurfaceStatusColor } from '../../surfaceColor';
3
+ import type { InteractionPolicy } from '../interactionPolicy';
4
+ import type { SurfaceStatusColor } from '../surfaceColor';
5
5
 
6
6
  export type ToastStatus = 'default' | SurfaceStatusColor;
7
7
 
@@ -18,3 +18,13 @@ export interface ToastOptions extends Omit<ToastProps, 'onDismiss'> {
18
18
  duration?: number;
19
19
  id?: string;
20
20
  }
21
+
22
+ export interface ToastProviderProps {
23
+ children: React.ReactNode;
24
+ defaultDuration?: number;
25
+ }
26
+
27
+ export interface ToastController {
28
+ dismissToast: (id: string) => void;
29
+ showToast: (options: ToastOptions) => string;
30
+ }
@@ -1 +0,0 @@
1
- {"version":3,"file":"Toast.d.ts","sourceRoot":"","sources":["../../../src/components/toast/Toast.tsx"],"names":[],"mappings":"AAAA,OAAO,KAAK,MAAM,OAAO,CAAC;AAU1B,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAgC1C,wBAAgB,KAAK,CAAC,EACpB,KAAK,EACL,WAAW,EACX,MAAkB,EAClB,SAAS,EACT,iBAA6B,EAC7B,MAAM,GACP,EAAE,UAAU,qBA0CZ"}