@impulse-ui-native/toast 6.0.0 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -101,7 +101,7 @@ The timer starts after entry animation finishes, pauses when the app leaves the
101
101
 
102
102
  onOpen and onOpenFinished run during entry; onClose runs on dismissal, and onCloseFinished lets OverlayHost remove the entry after exit. Automatic dismissal calls the existing OverlayStore.close(id). Duration, foreground/background timer handling, and animation are component-owned. Unmounting cancels animation, timer and app-state subscriptions.
103
103
 
104
- Toasts fade and translate by the theme edge offset. Top toasts sit 8 px below the top safe-area inset, using the toast topOffset token; bottom and side gaps use the 16 px edgeOffset token. Placements and IDs should remain stable for an entry's lifetime. Root is designed for imperative OverlayStore registration; use the returned controller to dismiss it.
104
+ Toasts fade and translate by the theme edge offset. Top toasts sit 8 px below the top safe-area inset, using the toast topOffset token; bottom and side gaps use the 16 px edgeOffset token. The newest toast is full size at the screen edge. Older toasts stack downward at both placements by the existing `gap` token and shrink by `stackScaleStep` (4%) per level. Changes to stack depth animate position and scale together over 220 ms, including moving forward when newer entries are removed. `stackLimit` defaults to five visual levels: scales are 100%, 96%, 92%, 88%, and 84%. All further toasts share the fifth level's scale and offset, behind newer entries; the limit does not discard or queue toasts. Component identity is the registered `Component`, including wrappers. Placements and IDs should remain stable for an entry's lifetime. Root is designed for imperative OverlayStore registration; use the returned controller to dismiss it.
105
105
 
106
106
  ## Theme
107
107
 
@@ -1,14 +1,21 @@
1
1
  import { jsx } from "react/jsx-runtime";
2
- import { memo, useMemo } from "react";
2
+ import { memo, useEffect, useMemo } from "react";
3
3
  import { StyleSheet } from "react-native";
4
- import Animated, { useAnimatedStyle } from "react-native-reanimated";
4
+ import Animated, {
5
+ cancelAnimation,
6
+ useAnimatedStyle,
7
+ useSharedValue,
8
+ withTiming
9
+ } from "react-native-reanimated";
5
10
  import { useSafeAreaInsets } from "react-native-safe-area-context";
11
+ import { useOverlayLayer } from "@impulse-ui-native/overlay";
6
12
  import { useComponentsTokens, useThemedStyles } from "@impulse-ui-native/theme";
7
- import { DefaultDuration } from "../constants/toast.constants";
13
+ import { DefaultDuration, StackDuration } from "../constants/toast.constants";
8
14
  import { ToastContext } from "../contexts/toast.context";
9
15
  import { useToastLifecycle } from "../hooks/use-toast-lifecycle.hook";
10
16
  const ToastRoot = memo(function ToastRoot2(props) {
11
17
  const {
18
+ id,
12
19
  children,
13
20
  placement = "bottom",
14
21
  tone = "success",
@@ -18,15 +25,22 @@ const ToastRoot = memo(function ToastRoot2(props) {
18
25
  const insets = useSafeAreaInsets();
19
26
  const tokens = useComponentsTokens().toast;
20
27
  const lifecycle = useToastLifecycle(props, duration);
28
+ const toastLayer = useOverlayLayer(id, void 0, "newest-first");
21
29
  const styles = useThemedStyles(themedStyles);
22
30
  const { progress } = lifecycle;
23
31
  const edgeOffset = tokens.edgeOffset;
32
+ const stackDepth = Math.min(
33
+ Math.max(0, toastLayer),
34
+ Math.max(0, tokens.stackLimit - 1)
35
+ );
36
+ const animatedDepth = useSharedValue(stackDepth);
24
37
  const animatedStyle = useAnimatedStyle(() => ({
25
38
  opacity: progress.value,
26
39
  transform: [
27
40
  {
28
- translateY: (1 - progress.value) * (placement === "top" ? -edgeOffset : edgeOffset)
29
- }
41
+ translateY: (1 - progress.value) * (placement === "top" ? -edgeOffset : edgeOffset) + animatedDepth.value * tokens.gap
42
+ },
43
+ { scale: Math.max(0, 1 - animatedDepth.value * tokens.stackScaleStep) }
30
44
  ]
31
45
  }));
32
46
  const context = useMemo(
@@ -44,12 +58,17 @@ const ToastRoot = memo(function ToastRoot2(props) {
44
58
  left: insets.left + tokens.edgeOffset,
45
59
  right: insets.right + tokens.edgeOffset,
46
60
  [placement]: insets[placement] + (placement === "top" ? tokens.topOffset : tokens.edgeOffset),
61
+ transformOrigin: "center top",
47
62
  zIndex: tokens.zIndexBase + layer
48
63
  },
49
64
  animatedStyle
50
65
  ],
51
66
  [styles.container, insets, tokens, placement, layer, animatedStyle]
52
67
  );
68
+ useEffect(() => {
69
+ animatedDepth.value = withTiming(stackDepth, { duration: StackDuration });
70
+ return () => cancelAnimation(animatedDepth);
71
+ }, [animatedDepth, stackDepth]);
53
72
  if (lifecycle.finished) {
54
73
  return null;
55
74
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/components/toast-root.tsx"],"sourcesContent":["import { memo, useMemo } from \"react\";\nimport { StyleSheet } from \"react-native\";\nimport Animated, { useAnimatedStyle } from \"react-native-reanimated\";\nimport { useSafeAreaInsets } from \"react-native-safe-area-context\";\n\nimport type { AppTheme } from \"@impulse-ui-native/theme\";\nimport { useComponentsTokens, useThemedStyles } from \"@impulse-ui-native/theme\";\n\nimport type { ToastRootProps } from \"../types\";\nimport { DefaultDuration } from \"../constants/toast.constants\";\nimport { ToastContext } from \"../contexts/toast.context\";\nimport { useToastLifecycle } from \"../hooks/use-toast-lifecycle.hook\";\n\nexport const ToastRoot = memo(function ToastRoot(props: ToastRootProps) {\n const {\n children,\n placement = \"bottom\",\n tone = \"success\",\n duration = DefaultDuration,\n layer = 0,\n } = props;\n\n const insets = useSafeAreaInsets();\n\n const tokens = useComponentsTokens().toast;\n\n const lifecycle = useToastLifecycle(props, duration);\n const styles = useThemedStyles(themedStyles);\n const { progress } = lifecycle;\n const edgeOffset = tokens.edgeOffset;\n const animatedStyle = useAnimatedStyle(() => ({\n opacity: progress.value,\n transform: [\n {\n translateY:\n (1 - progress.value) *\n (placement === \"top\" ? -edgeOffset : edgeOffset),\n },\n ],\n }));\n\n const context = useMemo(\n () => ({\n tone,\n interactive: lifecycle.interactive,\n close: lifecycle.close,\n }),\n [tone, lifecycle.interactive, lifecycle.close],\n );\n const containerStyle = useMemo(\n () => [\n styles.container,\n {\n left: insets.left + tokens.edgeOffset,\n right: insets.right + tokens.edgeOffset,\n [placement]:\n insets[placement] +\n (placement === \"top\" ? tokens.topOffset : tokens.edgeOffset),\n zIndex: tokens.zIndexBase + layer,\n },\n animatedStyle,\n ],\n [styles.container, insets, tokens, placement, layer, animatedStyle],\n );\n\n if (lifecycle.finished) {\n return null;\n }\n\n return (\n <ToastContext.Provider value={context}>\n <Animated.View\n pointerEvents={lifecycle.interactive ? \"auto\" : \"none\"}\n style={containerStyle}\n >\n {children}\n </Animated.View>\n </ToastContext.Provider>\n );\n});\n\nfunction themedStyles(theme: AppTheme) {\n const tokens = theme.components.toast;\n\n return StyleSheet.create({\n container: {\n position: \"absolute\",\n flexDirection: \"row\",\n alignItems: \"center\",\n gap: tokens.gap,\n padding: tokens.padding,\n borderRadius: tokens.borderRadius,\n borderWidth: tokens.borderWidth,\n borderColor: tokens.borderColor,\n backgroundColor: tokens.backgroundColor,\n },\n });\n}\n"],"mappings":"AAuEM;AAvEN,SAAS,MAAM,eAAe;AAC9B,SAAS,kBAAkB;AAC3B,OAAO,YAAY,wBAAwB;AAC3C,SAAS,yBAAyB;AAGlC,SAAS,qBAAqB,uBAAuB;AAGrD,SAAS,uBAAuB;AAChC,SAAS,oBAAoB;AAC7B,SAAS,yBAAyB;AAE3B,MAAM,YAAY,KAAK,SAASA,WAAU,OAAuB;AACtE,QAAM;AAAA,IACJ;AAAA,IACA,YAAY;AAAA,IACZ,OAAO;AAAA,IACP,WAAW;AAAA,IACX,QAAQ;AAAA,EACV,IAAI;AAEJ,QAAM,SAAS,kBAAkB;AAEjC,QAAM,SAAS,oBAAoB,EAAE;AAErC,QAAM,YAAY,kBAAkB,OAAO,QAAQ;AACnD,QAAM,SAAS,gBAAgB,YAAY;AAC3C,QAAM,EAAE,SAAS,IAAI;AACrB,QAAM,aAAa,OAAO;AAC1B,QAAM,gBAAgB,iBAAiB,OAAO;AAAA,IAC5C,SAAS,SAAS;AAAA,IAClB,WAAW;AAAA,MACT;AAAA,QACE,aACG,IAAI,SAAS,UACb,cAAc,QAAQ,CAAC,aAAa;AAAA,MACzC;AAAA,IACF;AAAA,EACF,EAAE;AAEF,QAAM,UAAU;AAAA,IACd,OAAO;AAAA,MACL;AAAA,MACA,aAAa,UAAU;AAAA,MACvB,OAAO,UAAU;AAAA,IACnB;AAAA,IACA,CAAC,MAAM,UAAU,aAAa,UAAU,KAAK;AAAA,EAC/C;AACA,QAAM,iBAAiB;AAAA,IACrB,MAAM;AAAA,MACJ,OAAO;AAAA,MACP;AAAA,QACE,MAAM,OAAO,OAAO,OAAO;AAAA,QAC3B,OAAO,OAAO,QAAQ,OAAO;AAAA,QAC7B,CAAC,SAAS,GACR,OAAO,SAAS,KACf,cAAc,QAAQ,OAAO,YAAY,OAAO;AAAA,QACnD,QAAQ,OAAO,aAAa;AAAA,MAC9B;AAAA,MACA;AAAA,IACF;AAAA,IACA,CAAC,OAAO,WAAW,QAAQ,QAAQ,WAAW,OAAO,aAAa;AAAA,EACpE;AAEA,MAAI,UAAU,UAAU;AACtB,WAAO;AAAA,EACT;AAEA,SACE,oBAAC,aAAa,UAAb,EAAsB,OAAO,SAC5B;AAAA,IAAC,SAAS;AAAA,IAAT;AAAA,MACC,eAAe,UAAU,cAAc,SAAS;AAAA,MAChD,OAAO;AAAA,MAEN;AAAA;AAAA,EACH,GACF;AAEJ,CAAC;AAED,SAAS,aAAa,OAAiB;AACrC,QAAM,SAAS,MAAM,WAAW;AAEhC,SAAO,WAAW,OAAO;AAAA,IACvB,WAAW;AAAA,MACT,UAAU;AAAA,MACV,eAAe;AAAA,MACf,YAAY;AAAA,MACZ,KAAK,OAAO;AAAA,MACZ,SAAS,OAAO;AAAA,MAChB,cAAc,OAAO;AAAA,MACrB,aAAa,OAAO;AAAA,MACpB,aAAa,OAAO;AAAA,MACpB,iBAAiB,OAAO;AAAA,IAC1B;AAAA,EACF,CAAC;AACH;","names":["ToastRoot"]}
1
+ {"version":3,"sources":["../../src/components/toast-root.tsx"],"sourcesContent":["import { memo, useEffect, useMemo } from \"react\";\nimport { StyleSheet } from \"react-native\";\nimport Animated, {\n cancelAnimation,\n useAnimatedStyle,\n useSharedValue,\n withTiming,\n} from \"react-native-reanimated\";\nimport { useSafeAreaInsets } from \"react-native-safe-area-context\";\n\nimport type { AppTheme } from \"@impulse-ui-native/theme\";\nimport { useOverlayLayer } from \"@impulse-ui-native/overlay\";\nimport { useComponentsTokens, useThemedStyles } from \"@impulse-ui-native/theme\";\n\nimport type { ToastRootProps } from \"../types\";\nimport { DefaultDuration, StackDuration } from \"../constants/toast.constants\";\nimport { ToastContext } from \"../contexts/toast.context\";\nimport { useToastLifecycle } from \"../hooks/use-toast-lifecycle.hook\";\n\nexport const ToastRoot = memo(function ToastRoot(props: ToastRootProps) {\n const {\n id,\n children,\n placement = \"bottom\",\n tone = \"success\",\n duration = DefaultDuration,\n layer = 0,\n } = props;\n\n const insets = useSafeAreaInsets();\n\n const tokens = useComponentsTokens().toast;\n\n const lifecycle = useToastLifecycle(props, duration);\n const toastLayer = useOverlayLayer(id, undefined, \"newest-first\");\n const styles = useThemedStyles(themedStyles);\n const { progress } = lifecycle;\n const edgeOffset = tokens.edgeOffset;\n const stackDepth = Math.min(\n Math.max(0, toastLayer),\n Math.max(0, tokens.stackLimit - 1),\n );\n const animatedDepth = useSharedValue(stackDepth);\n const animatedStyle = useAnimatedStyle(() => ({\n opacity: progress.value,\n transform: [\n {\n translateY:\n (1 - progress.value) *\n (placement === \"top\" ? -edgeOffset : edgeOffset) +\n animatedDepth.value * tokens.gap,\n },\n { scale: Math.max(0, 1 - animatedDepth.value * tokens.stackScaleStep) },\n ],\n }));\n\n const context = useMemo(\n () => ({\n tone,\n interactive: lifecycle.interactive,\n close: lifecycle.close,\n }),\n [tone, lifecycle.interactive, lifecycle.close],\n );\n const containerStyle = useMemo(\n () => [\n styles.container,\n {\n left: insets.left + tokens.edgeOffset,\n right: insets.right + tokens.edgeOffset,\n [placement]:\n insets[placement] +\n (placement === \"top\" ? tokens.topOffset : tokens.edgeOffset),\n transformOrigin: \"center top\",\n zIndex: tokens.zIndexBase + layer,\n },\n animatedStyle,\n ],\n [styles.container, insets, tokens, placement, layer, animatedStyle],\n );\n\n useEffect(() => {\n animatedDepth.value = withTiming(stackDepth, { duration: StackDuration });\n\n return () => cancelAnimation(animatedDepth);\n }, [animatedDepth, stackDepth]);\n\n if (lifecycle.finished) {\n return null;\n }\n\n return (\n <ToastContext.Provider value={context}>\n <Animated.View\n pointerEvents={lifecycle.interactive ? \"auto\" : \"none\"}\n style={containerStyle}\n >\n {children}\n </Animated.View>\n </ToastContext.Provider>\n );\n});\n\nfunction themedStyles(theme: AppTheme) {\n const tokens = theme.components.toast;\n\n return StyleSheet.create({\n container: {\n position: \"absolute\",\n flexDirection: \"row\",\n alignItems: \"center\",\n gap: tokens.gap,\n padding: tokens.padding,\n borderRadius: tokens.borderRadius,\n borderWidth: tokens.borderWidth,\n borderColor: tokens.borderColor,\n backgroundColor: tokens.backgroundColor,\n },\n });\n}\n"],"mappings":"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","names":["ToastRoot"]}
@@ -1,5 +1,6 @@
1
1
  declare const DefaultDuration = 4000;
2
2
  declare const EnterDuration = 180;
3
3
  declare const ExitDuration = 150;
4
+ declare const StackDuration = 220;
4
5
 
5
- export { DefaultDuration, EnterDuration, ExitDuration };
6
+ export { DefaultDuration, EnterDuration, ExitDuration, StackDuration };
@@ -1,9 +1,11 @@
1
1
  const DefaultDuration = 4e3;
2
2
  const EnterDuration = 180;
3
3
  const ExitDuration = 150;
4
+ const StackDuration = 220;
4
5
  export {
5
6
  DefaultDuration,
6
7
  EnterDuration,
7
- ExitDuration
8
+ ExitDuration,
9
+ StackDuration
8
10
  };
9
11
  //# sourceMappingURL=toast.constants.js.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/constants/toast.constants.ts"],"sourcesContent":["export const DefaultDuration = 4000;\nexport const EnterDuration = 180;\nexport const ExitDuration = 150;\n"],"mappings":"AAAO,MAAM,kBAAkB;AACxB,MAAM,gBAAgB;AACtB,MAAM,eAAe;","names":[]}
1
+ {"version":3,"sources":["../../src/constants/toast.constants.ts"],"sourcesContent":["export const DefaultDuration = 4000;\nexport const EnterDuration = 180;\nexport const ExitDuration = 150;\nexport const StackDuration = 220;\n"],"mappings":"AAAO,MAAM,kBAAkB;AACxB,MAAM,gBAAgB;AACtB,MAAM,eAAe;AACrB,MAAM,gBAAgB;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impulse-ui-native/toast",
3
- "version": "6.0.0",
3
+ "version": "6.1.0",
4
4
  "description": "Compound toast notifications for ImpulseUI Native.",
5
5
  "keywords": [
6
6
  "react-native",
@@ -27,11 +27,11 @@
27
27
  ],
28
28
  "sideEffects": false,
29
29
  "dependencies": {
30
- "@impulse-ui-native/theme": "6.0.0",
31
- "@impulse-ui-native/overlay": "6.0.0",
32
- "@impulse-ui-native/primitives": "6.0.0",
33
- "@impulse-ui-native/core": "6.0.0",
34
- "@impulse-ui-native/icon": "6.0.0"
30
+ "@impulse-ui-native/theme": "6.1.0",
31
+ "@impulse-ui-native/primitives": "6.1.0",
32
+ "@impulse-ui-native/overlay": "6.1.0",
33
+ "@impulse-ui-native/core": "6.1.0",
34
+ "@impulse-ui-native/icon": "6.1.0"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@types/react": "~19.2.14",
@@ -44,8 +44,8 @@
44
44
  "react-native-safe-area-context": "~5.7.0",
45
45
  "typescript": "~6.0.3",
46
46
  "react-native-svg": "15.15.4",
47
- "@impulse-ui-native/prettier-config": "1.0.0",
48
47
  "@impulse-ui-native/eslint-config": "1.0.0",
48
+ "@impulse-ui-native/prettier-config": "1.0.0",
49
49
  "@impulse-ui-native/tsconfig": "1.0.0"
50
50
  },
51
51
  "peerDependencies": {