@startupjs-ui/toast 0.1.3

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 ADDED
@@ -0,0 +1,20 @@
1
+ # Change Log
2
+
3
+ All notable changes to this project will be documented in this file.
4
+ See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
+
6
+ ## [0.1.3](https://github.com/startupjs/startupjs-ui/compare/v0.1.2...v0.1.3) (2025-12-29)
7
+
8
+ **Note:** Version bump only for package @startupjs-ui/toast
9
+
10
+
11
+
12
+
13
+
14
+ ## [0.1.2](https://github.com/startupjs/startupjs-ui/compare/v0.1.1...v0.1.2) (2025-12-29)
15
+
16
+
17
+ ### Features
18
+
19
+ * add mdx and docs packages. Refactor docs to get rid of any @startupjs/ui usage and use startupjs-ui instead ([703c926](https://github.com/startupjs/startupjs-ui/commit/703c92636efb0421ffd11783f692fc892b74018f))
20
+ * **toast:** refactor toast, ToastProvider, Toast components ([f56f8be](https://github.com/startupjs/startupjs-ui/commit/f56f8be5c63dfb7c44855b7c35758be9a5c49387))
@@ -0,0 +1,76 @@
1
+ import { Fragment } from 'react'
2
+ import { pug } from 'startupjs'
3
+ import Button from '@startupjs-ui/button'
4
+ import Br from '@startupjs-ui/br'
5
+ import Div from '@startupjs-ui/div'
6
+ import Toast from './ToastView'
7
+ import { toast } from './index'
8
+
9
+ const BASE_TOAST_PROPS = {
10
+ show: true,
11
+ topPosition: 0,
12
+ height: 72,
13
+ type: 'info',
14
+ title: 'Info',
15
+ text: 'Note archived',
16
+ actionLabel: 'View',
17
+ onAction: () => {}
18
+ }
19
+
20
+ export function ToastProviderSandbox () {
21
+ function onPressInfo () {
22
+ toast({ text: 'Note archived' })
23
+ }
24
+
25
+ function onPressSuccess () {
26
+ toast({ type: 'success', title: 'Success', text: 'Profile saved' })
27
+ }
28
+
29
+ function onPressAlert () {
30
+ toast({ type: 'error', title: 'Error', text: 'Something went wrong', alert: true })
31
+ }
32
+
33
+ return pug`
34
+ Fragment
35
+ Button(onPress=onPressInfo) Show info toast
36
+ Br
37
+ Button(onPress=onPressSuccess) Show success toast
38
+ Br
39
+ Button(onPress=onPressAlert) Show alert toast
40
+ `
41
+ }
42
+
43
+ export function ToastFunctionSandbox ({
44
+ alert,
45
+ icon,
46
+ text,
47
+ type,
48
+ title,
49
+ actionLabel
50
+ }) {
51
+ function onPress () {
52
+ toast({
53
+ alert,
54
+ icon,
55
+ text,
56
+ type,
57
+ title,
58
+ actionLabel
59
+ })
60
+ }
61
+
62
+ return pug`
63
+ Fragment
64
+ Button(onPress=onPress) Show toast
65
+ `
66
+ }
67
+
68
+ export function ToastComponentSandbox (props) {
69
+ const onLayout = () => {}
70
+ const mergedProps = { ...BASE_TOAST_PROPS, ...props }
71
+
72
+ return pug`
73
+ Div(style={ position: 'relative', minHeight: 120 })
74
+ Toast(...mergedProps onLayout=onLayout)
75
+ `
76
+ }
package/README.mdx ADDED
@@ -0,0 +1,79 @@
1
+ import { pug } from 'startupjs'
2
+ import { Sandbox } from '@startupjs-ui/docs'
3
+ import Button from '@startupjs-ui/button'
4
+ import { toast } from './index'
5
+ import { _PropsJsonSchema as ToastOptionsPropsJsonSchema } from './toast'
6
+ import { _PropsJsonSchema as ToastPropsJsonSchema } from './ToastView'
7
+ import { _PropsJsonSchema as ToastProviderPropsJsonSchema } from './ToastProvider'
8
+ import {
9
+ ToastProviderSandbox,
10
+ ToastFunctionSandbox,
11
+ ToastComponentSandbox
12
+ } from './README.helpers'
13
+ import './index.mdx.cssx.styl'
14
+
15
+ # Toast
16
+
17
+ Toast provides brief non-blocking notifications that inform users of a process that an app has performed or will perform.
18
+
19
+ ```js
20
+ import { ToastProvider, toast } from 'startupjs-ui'
21
+ ```
22
+
23
+ ## Installation
24
+
25
+ Render `ToastProvider` once inside `Portal.Provider` before your page context (for example, `Stack`).
26
+
27
+ ```jsx
28
+ import { Portal, ToastProvider, DialogsProvider } from 'startupjs-ui'
29
+ import { Stack } from 'expo-router'
30
+
31
+ export default function RootLayout () {
32
+ return (
33
+ <>
34
+ <Portal.Provider>
35
+ <ToastProvider />
36
+ <Stack />
37
+ </Portal.Provider>
38
+ <DialogsProvider />
39
+ </>
40
+ )
41
+ }
42
+ ```
43
+
44
+ ## Simple example
45
+
46
+ ```jsx example
47
+ function onPress () {
48
+ toast({ text: 'Note archived' })
49
+ }
50
+
51
+ return pug`
52
+ Button(onPress=onPress) Show toast
53
+ `
54
+ ```
55
+
56
+ ## Sandbox
57
+
58
+ ### toast
59
+
60
+ <Sandbox
61
+ Component={ToastFunctionSandbox}
62
+ props={{ title: 'Info', text: 'Note archived', type: 'info' }}
63
+ propsJsonSchema={ToastOptionsPropsJsonSchema}
64
+ />
65
+
66
+ ### ToastProvider
67
+
68
+ <Sandbox
69
+ Component={ToastProviderSandbox}
70
+ propsJsonSchema={ToastProviderPropsJsonSchema}
71
+ />
72
+
73
+ ### Toast (internal component, usually you won't want to use it directly)
74
+
75
+ <Sandbox
76
+ Component={ToastComponentSandbox}
77
+ props={{ title: 'Info', text: 'Note archived', type: 'info' }}
78
+ propsJsonSchema={ToastPropsJsonSchema}
79
+ />
@@ -0,0 +1,24 @@
1
+ import { type ReactNode } from 'react'
2
+ import { pug, observer } from 'startupjs'
3
+ import Portal from '@startupjs-ui/portal'
4
+ import { $toasts } from './helpers'
5
+ import Toast, { type ToastProps } from './ToastView'
6
+
7
+ export const _PropsJsonSchema = {/* ToastProviderProps */}
8
+
9
+ // eslint-disable-next-line @typescript-eslint/no-empty-interface
10
+ export interface ToastProviderProps {}
11
+
12
+ function ToastProvider (): ReactNode {
13
+ const toasts = $toasts.get() as ToastProps[] | undefined
14
+
15
+ if (!toasts?.length) return null
16
+
17
+ return pug`
18
+ Portal
19
+ each toast in toasts
20
+ Toast(...toast key=toast.key)
21
+ `
22
+ }
23
+
24
+ export default observer(ToastProvider) as any
package/ToastView.tsx ADDED
@@ -0,0 +1,138 @@
1
+ import { useCallback, useEffect, useState, type ReactNode } from 'react'
2
+ import { Animated } from 'react-native'
3
+ import { pug, observer } from 'startupjs'
4
+ import Button from '@startupjs-ui/button'
5
+ import Div from '@startupjs-ui/div'
6
+ import Icon, { type IconProps } from '@startupjs-ui/icon'
7
+ import Span from '@startupjs-ui/span'
8
+ import { themed } from '@startupjs-ui/core'
9
+ import { faExclamationCircle } from '@fortawesome/free-solid-svg-icons/faExclamationCircle'
10
+ import { faTimes } from '@fortawesome/free-solid-svg-icons/faTimes'
11
+ import { faCheckCircle } from '@fortawesome/free-solid-svg-icons/faCheckCircle'
12
+ import { faExclamationTriangle } from '@fortawesome/free-solid-svg-icons/faExclamationTriangle'
13
+ import { faInfoCircle } from '@fortawesome/free-solid-svg-icons/faInfoCircle'
14
+ import './index.cssx.styl'
15
+
16
+ const DURATION_OPEN = 300
17
+ const DURATION_CLOSE = 150
18
+
19
+ const ICONS = {
20
+ info: faInfoCircle,
21
+ error: faExclamationCircle,
22
+ warning: faExclamationTriangle,
23
+ success: faCheckCircle
24
+ }
25
+
26
+ const TITLES = {
27
+ info: 'Info',
28
+ error: 'Error',
29
+ warning: 'Warning',
30
+ success: 'Success'
31
+ }
32
+
33
+ export const _PropsJsonSchema = {/* ToastProps */}
34
+
35
+ export interface ToastProps {
36
+ /** Visual style variant @default 'info' */
37
+ type?: 'info' | 'error' | 'warning' | 'success'
38
+ /** Y offset used to stack multiple toasts */
39
+ topPosition: number
40
+ /** Current toast height for stacking calculations */
41
+ height?: number
42
+ /** Controls whether the toast is visible */
43
+ show: boolean
44
+ /** Custom icon shown next to the title */
45
+ icon?: IconProps['icon']
46
+ /** Body text displayed under the title */
47
+ text?: string
48
+ /** Title text displayed in the header */
49
+ title?: string
50
+ /** Action button label displayed below the text @default 'View' */
51
+ actionLabel?: string
52
+ /** Action button press handler */
53
+ onAction?: () => void
54
+ /** Called after the toast is closed and removed */
55
+ onClose?: () => void
56
+ /** Layout callback used to measure toast height */
57
+ onLayout: (layout: { height: number }) => void
58
+ }
59
+
60
+ function Toast ({
61
+ type = 'info',
62
+ topPosition,
63
+ height,
64
+ show,
65
+ icon,
66
+ text,
67
+ title,
68
+ actionLabel = 'View',
69
+ onAction,
70
+ onClose,
71
+ onLayout
72
+ }: ToastProps): ReactNode {
73
+ const [showAnimation] = useState(() => new Animated.Value(0))
74
+ const [topAnimation] = useState(() => new Animated.Value(topPosition))
75
+
76
+ const onShow = useCallback(() => {
77
+ Animated
78
+ .timing(showAnimation, { toValue: 1, duration: DURATION_OPEN, useNativeDriver: false })
79
+ .start()
80
+ }, [showAnimation])
81
+
82
+ const onHide = useCallback(() => {
83
+ Animated
84
+ .timing(showAnimation, { toValue: 0, duration: DURATION_CLOSE, useNativeDriver: false })
85
+ .start(onClose)
86
+ }, [onClose, showAnimation])
87
+
88
+ useEffect(() => {
89
+ Animated
90
+ .timing(topAnimation, { toValue: topPosition, duration: DURATION_OPEN, useNativeDriver: false })
91
+ .start()
92
+ }, [topPosition, topAnimation])
93
+
94
+ useEffect(() => {
95
+ if (show && height) onShow()
96
+ if (!show) onHide()
97
+ }, [height, onHide, onShow, show])
98
+
99
+ return pug`
100
+ Animated.View.root(
101
+ style={
102
+ opacity: showAnimation,
103
+ right: showAnimation.interpolate({
104
+ inputRange: [0, 1],
105
+ outputRange: [-48, 0]
106
+ }),
107
+ top: topAnimation
108
+ }
109
+ onLayout=e=> onLayout(e.nativeEvent.layout)
110
+ )
111
+ Div.toast(styleName=[type])
112
+ Div.header(vAlign='center' row)
113
+ Div(vAlign='center' row)
114
+ Icon.icon(
115
+ icon=icon ? icon : ICONS[type]
116
+ styleName=[type]
117
+ )
118
+ Span.title(styleName=[type])
119
+ = title ? title : TITLES[type]
120
+ Div(onPress=onHide)
121
+ Icon(icon=faTimes)
122
+
123
+ if text
124
+ Span.text= text
125
+
126
+ if onAction
127
+ Div.actions(row)
128
+ Button(
129
+ size='s'
130
+ onPress=() => {
131
+ onAction()
132
+ onHide()
133
+ }
134
+ )= actionLabel
135
+ `
136
+ }
137
+
138
+ export default observer(themed('Toast', Toast))
package/helpers.ts ADDED
@@ -0,0 +1,3 @@
1
+ import { $ } from 'startupjs'
2
+
3
+ export const $toasts = $()
@@ -0,0 +1,68 @@
1
+ .root
2
+ position absolute
3
+ width 100%
4
+
5
+ +from($UI.media.mobile)
6
+ max-width 40u
7
+ min-width 30u
8
+ padding-right 2u
9
+ padding-top 2u
10
+
11
+ .toast
12
+ padding 2u
13
+ justify-content center
14
+ border-radius .5u
15
+ background-color var(--color-bg-main-strong)
16
+ border-top-width 4px
17
+ border-top-style solid
18
+ shadow(2)
19
+
20
+ &.info
21
+ border-top-color var(--color-border-primary)
22
+
23
+ &.error
24
+ border-top-color var(--color-border-error)
25
+
26
+ &.warning
27
+ border-top-color var(--color-border-warning)
28
+
29
+ &.success
30
+ border-top-color var(--color-border-success)
31
+
32
+ .header
33
+ justify-content space-between
34
+
35
+ .text
36
+ margin-top 1u
37
+
38
+ .title
39
+ margin-left 1u
40
+
41
+ &.info
42
+ color var(--color-text-primary)
43
+
44
+ &.error
45
+ color var(--color-text-error)
46
+
47
+ &.warning
48
+ color var(--color-text-warning)
49
+
50
+ &.success
51
+ color var(--color-text-success)
52
+
53
+ .icon
54
+ &.info
55
+ color var(--color-text-primary)
56
+
57
+ &.error
58
+ color var(--color-text-error)
59
+
60
+ &.warning
61
+ color var(--color-text-warning)
62
+
63
+ &.success
64
+ color var(--color-text-success)
65
+
66
+ .actions
67
+ margin-top 1u
68
+ justify-content flex-end
package/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ /* eslint-disable */
2
+ // DO NOT MODIFY THIS FILE - IT IS AUTOMATICALLY GENERATED ON COMMITS.
3
+
4
+ export { default as Toast, type ToastProps } from './ToastView';
5
+ export { default as ToastProvider, type ToastProviderProps } from './ToastProvider';
6
+ export { default as toast, type ToastOptions } from './toast';
@@ -0,0 +1,2 @@
1
+ .table
2
+ background-color white
package/index.tsx ADDED
@@ -0,0 +1,3 @@
1
+ export { default as Toast, type ToastProps } from './ToastView'
2
+ export { default as ToastProvider, type ToastProviderProps } from './ToastProvider'
3
+ export { default as toast, type ToastOptions } from './toast'
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@startupjs-ui/toast",
3
+ "version": "0.1.3",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "main": "index.tsx",
8
+ "types": "index.d.ts",
9
+ "type": "module",
10
+ "dependencies": {
11
+ "@startupjs-ui/button": "^0.1.3",
12
+ "@startupjs-ui/core": "^0.1.3",
13
+ "@startupjs-ui/div": "^0.1.3",
14
+ "@startupjs-ui/icon": "^0.1.3",
15
+ "@startupjs-ui/portal": "^0.1.3",
16
+ "@startupjs-ui/span": "^0.1.3"
17
+ },
18
+ "peerDependencies": {
19
+ "react": "*",
20
+ "react-native": "*",
21
+ "startupjs": "*"
22
+ },
23
+ "gitHead": "fd964ebc3892d3dd0a6c85438c0af619cc50c3f0"
24
+ }
package/toast.ts ADDED
@@ -0,0 +1,127 @@
1
+ import { $ } from 'startupjs'
2
+ import { $toasts } from './helpers'
3
+ import { type ToastProps } from './ToastView'
4
+
5
+ const MAX_SHOW_LENGTH = 3
6
+
7
+ export const _PropsJsonSchema = {/* ToastOptions */}
8
+
9
+ export interface ToastOptions {
10
+ /** Prevents auto-close after 5 seconds */
11
+ alert?: boolean
12
+ /** Custom icon shown next to the title */
13
+ icon?: ToastProps['icon']
14
+ /** Body text displayed under the title */
15
+ text?: string
16
+ /** Visual style variant @default 'info' */
17
+ type?: ToastProps['type']
18
+ /** Title text displayed in the header */
19
+ title?: string
20
+ /** Action button label displayed below the text @default 'View' */
21
+ actionLabel?: string
22
+ /** Action button press handler */
23
+ onAction?: () => void
24
+ /** Called after the toast is closed and removed */
25
+ onClose?: () => void
26
+ }
27
+
28
+ type ToastStackItem = ToastProps & {
29
+ key: string
30
+ alert?: boolean
31
+ }
32
+
33
+ // NOTE
34
+ // Is this the best way to update position of toasts?
35
+ // Is there a better way to do this?
36
+ // We want to remove unnecessary props from toast
37
+ // component that are added by these calculations.
38
+ const updateMatrixPositions = () => {
39
+ const toasts = $toasts.get() as ToastStackItem[]
40
+
41
+ const updateToasts = toasts.map((toast, index) => {
42
+ const prevToast = toasts[index - 1]
43
+
44
+ if (prevToast) {
45
+ const prevHeight = prevToast.height ?? 0
46
+ toast.topPosition = prevToast.topPosition + prevHeight
47
+ } else {
48
+ toast.topPosition = 0
49
+ }
50
+
51
+ return toast
52
+ })
53
+
54
+ $toasts.set(updateToasts)
55
+ }
56
+
57
+ export default function toast ({
58
+ alert,
59
+ icon,
60
+ text,
61
+ type,
62
+ title,
63
+ actionLabel,
64
+ onAction,
65
+ onClose
66
+ }: ToastOptions): void {
67
+ const toastId = $.id() as string
68
+
69
+ if ($toasts.get()?.length === MAX_SHOW_LENGTH) {
70
+ $toasts[MAX_SHOW_LENGTH - 1].show.set(false)
71
+ }
72
+
73
+ if (!alert) {
74
+ setTimeout(() => {
75
+ const index = getValidIndex()
76
+ if (index !== -1) $toasts[index].show.set(false)
77
+ }, 5000)
78
+ }
79
+
80
+ function onRemove () {
81
+ const index = getValidIndex()
82
+ if (index === -1) return
83
+
84
+ $toasts[index].del()
85
+ updateMatrixPositions()
86
+ onClose && onClose()
87
+ }
88
+
89
+ // toastId ensures that the correct index is found at the current moment
90
+ function getValidIndex () {
91
+ const toasts = $toasts.get() as ToastStackItem[]
92
+ return toasts.findIndex(toast => toast.key === toastId)
93
+ }
94
+
95
+ // NOTE
96
+ // Think about using context instead of model
97
+ // We can provide registerToast function in context
98
+ // Which will be better? model or context?
99
+ function onLayout (layout: { height: number }) {
100
+ $toasts[getValidIndex()].height.set(layout.height)
101
+ updateMatrixPositions()
102
+ }
103
+
104
+ const newToast: ToastStackItem = {
105
+ key: toastId,
106
+ show: true,
107
+ topPosition: 0,
108
+ alert,
109
+ icon,
110
+ type,
111
+ text,
112
+ title,
113
+ actionLabel,
114
+ onAction,
115
+ onClose: onRemove,
116
+ onLayout
117
+ }
118
+
119
+ // TODO: The current implementation modifies the toast data directly, which violates the immutability principle of the model.
120
+ // This works only because the data is local, but it's a hacky solution.
121
+ // We should implement an .unshift() method on the Signal to handle this correctly in the future.
122
+ // For now, this serves as a quick fix, but we need to address this properly to ensure data immutability.
123
+
124
+ const toasts = $toasts.get() || []
125
+ toasts.unshift(newToast)
126
+ $toasts.set(toasts)
127
+ }