@lattice-ui/toast 0.3.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 ADDED
@@ -0,0 +1,21 @@
1
+ # @lattice-ui/toast
2
+
3
+ Headless toast primitives for Roblox UI with queue-based visibility and automatic dismissal.
4
+
5
+ ## Exports
6
+
7
+ - `Toast`
8
+ - `Toast.Provider`
9
+ - `Toast.Viewport`
10
+ - `Toast.Root`
11
+ - `Toast.Title`
12
+ - `Toast.Description`
13
+ - `Toast.Action`
14
+ - `Toast.Close`
15
+ - `useToast`
16
+
17
+ ## Notes
18
+
19
+ - Imperative API is provided through `useToast()`.
20
+ - Default toast timing is `durationMs=4000` with `maxVisible=3`.
21
+ - Queue helpers are exported for deterministic unit testing.
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { ToastActionProps } from "./types";
3
+ export declare function ToastAction(props: ToastActionProps): React.JSX.Element;
@@ -0,0 +1,43 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local Slot = _core.Slot
6
+ local function ToastAction(props)
7
+ local handleActivated = React.useCallback(function()
8
+ local _result = props.onAction
9
+ if _result ~= nil then
10
+ _result()
11
+ end
12
+ end, { props })
13
+ if props.asChild then
14
+ local child = props.children
15
+ if not child then
16
+ error("[ToastAction] `asChild` requires a child element.")
17
+ end
18
+ return React.createElement(Slot, {
19
+ Active = true,
20
+ Event = {
21
+ Activated = handleActivated,
22
+ },
23
+ Selectable = true,
24
+ }, child)
25
+ end
26
+ return React.createElement("textbutton", {
27
+ Active = true,
28
+ AutoButtonColor = false,
29
+ BackgroundColor3 = Color3.fromRGB(58, 66, 84),
30
+ BorderSizePixel = 0,
31
+ Event = {
32
+ Activated = handleActivated,
33
+ },
34
+ Selectable = true,
35
+ Size = UDim2.fromOffset(88, 26),
36
+ Text = "Action",
37
+ TextColor3 = Color3.fromRGB(235, 240, 248),
38
+ TextSize = 13,
39
+ }, props.children)
40
+ end
41
+ return {
42
+ ToastAction = ToastAction,
43
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { ToastCloseProps } from "./types";
3
+ export declare function ToastClose(props: ToastCloseProps): React.JSX.Element;
@@ -0,0 +1,43 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local Slot = _core.Slot
6
+ local function ToastClose(props)
7
+ local handleActivated = React.useCallback(function()
8
+ local _result = props.onClose
9
+ if _result ~= nil then
10
+ _result()
11
+ end
12
+ end, { props })
13
+ if props.asChild then
14
+ local child = props.children
15
+ if not child then
16
+ error("[ToastClose] `asChild` requires a child element.")
17
+ end
18
+ return React.createElement(Slot, {
19
+ Active = true,
20
+ Event = {
21
+ Activated = handleActivated,
22
+ },
23
+ Selectable = true,
24
+ }, child)
25
+ end
26
+ return React.createElement("textbutton", {
27
+ Active = true,
28
+ AutoButtonColor = false,
29
+ BackgroundColor3 = Color3.fromRGB(58, 66, 84),
30
+ BorderSizePixel = 0,
31
+ Event = {
32
+ Activated = handleActivated,
33
+ },
34
+ Selectable = true,
35
+ Size = UDim2.fromOffset(26, 26),
36
+ Text = "X",
37
+ TextColor3 = Color3.fromRGB(235, 240, 248),
38
+ TextSize = 13,
39
+ })
40
+ end
41
+ return {
42
+ ToastClose = ToastClose,
43
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { ToastDescriptionProps } from "./types";
3
+ export declare function ToastDescription(props: ToastDescriptionProps): React.JSX.Element;
@@ -0,0 +1,26 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local Slot = _core.Slot
6
+ local function ToastDescription(props)
7
+ if props.asChild then
8
+ local child = props.children
9
+ if not child then
10
+ error("[ToastDescription] `asChild` requires a child element.")
11
+ end
12
+ return React.createElement(Slot, nil, child)
13
+ end
14
+ return React.createElement("textlabel", {
15
+ BackgroundTransparency = 1,
16
+ BorderSizePixel = 0,
17
+ Size = UDim2.fromOffset(300, 18),
18
+ Text = "Description",
19
+ TextColor3 = Color3.fromRGB(172, 180, 196),
20
+ TextSize = 13,
21
+ TextXAlignment = Enum.TextXAlignment.Left,
22
+ })
23
+ end
24
+ return {
25
+ ToastDescription = ToastDescription,
26
+ }
@@ -0,0 +1,4 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { ToastApi, ToastProviderProps } from "./types";
3
+ export declare function ToastProvider(props: ToastProviderProps): React.JSX.Element;
4
+ export declare function useToast(): ToastApi;
@@ -0,0 +1,111 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local React = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out).React
4
+ local _context = TS.import(script, script.Parent, "context")
5
+ local ToastContextProvider = _context.ToastContextProvider
6
+ local useToastContext = _context.useToastContext
7
+ local _queue = TS.import(script, script.Parent, "queue")
8
+ local enqueueToast = _queue.enqueueToast
9
+ local getVisibleToasts = _queue.getVisibleToasts
10
+ local pruneExpiredToasts = _queue.pruneExpiredToasts
11
+ local RunService = game:GetService("RunService")
12
+ local function nowMs()
13
+ return math.floor(os.clock() * 1000)
14
+ end
15
+ local function ToastProvider(props)
16
+ local _condition = props.defaultDurationMs
17
+ if _condition == nil then
18
+ _condition = 4000
19
+ end
20
+ local defaultDurationMs = math.max(0, _condition)
21
+ local _condition_1 = props.maxVisible
22
+ if _condition_1 == nil then
23
+ _condition_1 = 3
24
+ end
25
+ local maxVisible = math.max(1, _condition_1)
26
+ local toasts, setToasts = React.useState({})
27
+ local idSequenceRef = React.useRef(0)
28
+ local enqueue = React.useCallback(function(options)
29
+ idSequenceRef.current += 1
30
+ local _condition_2 = options.id
31
+ if _condition_2 == nil then
32
+ _condition_2 = `toast-{idSequenceRef.current}`
33
+ end
34
+ local id = _condition_2
35
+ setToasts(function(currentQueue)
36
+ return enqueueToast(currentQueue, {
37
+ id = id,
38
+ title = options.title,
39
+ description = options.description,
40
+ durationMs = options.durationMs,
41
+ createdAtMs = nowMs(),
42
+ })
43
+ end)
44
+ return id
45
+ end, {})
46
+ local remove = React.useCallback(function(id)
47
+ setToasts(function(currentQueue)
48
+ -- ▼ ReadonlyArray.filter ▼
49
+ local _newValue = {}
50
+ local _callback = function(toast)
51
+ return toast.id ~= id
52
+ end
53
+ local _length = 0
54
+ for _k, _v in currentQueue do
55
+ if _callback(_v, _k - 1, currentQueue) == true then
56
+ _length += 1
57
+ _newValue[_length] = _v
58
+ end
59
+ end
60
+ -- ▲ ReadonlyArray.filter ▲
61
+ return _newValue
62
+ end)
63
+ end, {})
64
+ local clear = React.useCallback(function()
65
+ setToasts({})
66
+ end, {})
67
+ React.useEffect(function()
68
+ if #toasts == 0 then
69
+ return nil
70
+ end
71
+ local connection = RunService.Heartbeat:Connect(function()
72
+ setToasts(function(currentQueue)
73
+ return pruneExpiredToasts(currentQueue, nowMs(), maxVisible, defaultDurationMs)
74
+ end)
75
+ end)
76
+ return function()
77
+ connection:Disconnect()
78
+ end
79
+ end, { defaultDurationMs, maxVisible, #toasts })
80
+ local visibleToasts = React.useMemo(function()
81
+ return getVisibleToasts(toasts, maxVisible)
82
+ end, { maxVisible, toasts })
83
+ local contextValue = React.useMemo(function()
84
+ return {
85
+ toasts = toasts,
86
+ visibleToasts = visibleToasts,
87
+ defaultDurationMs = defaultDurationMs,
88
+ maxVisible = maxVisible,
89
+ enqueue = enqueue,
90
+ remove = remove,
91
+ clear = clear,
92
+ }
93
+ end, { clear, defaultDurationMs, enqueue, maxVisible, remove, toasts, visibleToasts })
94
+ return React.createElement(ToastContextProvider, {
95
+ value = contextValue,
96
+ }, props.children)
97
+ end
98
+ local function useToast()
99
+ local toastContext = useToastContext()
100
+ return {
101
+ toasts = toastContext.toasts,
102
+ visibleToasts = toastContext.visibleToasts,
103
+ enqueue = toastContext.enqueue,
104
+ remove = toastContext.remove,
105
+ clear = toastContext.clear,
106
+ }
107
+ end
108
+ return {
109
+ ToastProvider = ToastProvider,
110
+ useToast = useToast,
111
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { ToastRootProps } from "./types";
3
+ export declare function ToastRoot(props: ToastRootProps): React.JSX.Element;
@@ -0,0 +1,37 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local Slot = _core.Slot
6
+ local function ToastRoot(props)
7
+ local _condition = props.visible
8
+ if _condition == nil then
9
+ _condition = true
10
+ end
11
+ local visible = _condition
12
+ if props.asChild then
13
+ local child = props.children
14
+ if not React.isValidElement(child) then
15
+ error("[ToastRoot] `asChild` requires a child element.")
16
+ end
17
+ return React.createElement(Slot, {
18
+ Visible = visible,
19
+ }, child)
20
+ end
21
+ return React.createElement("frame", {
22
+ BackgroundColor3 = Color3.fromRGB(38, 45, 59),
23
+ BorderSizePixel = 0,
24
+ Size = UDim2.fromOffset(320, 72),
25
+ Visible = visible,
26
+ }, React.createElement("uicorner", {
27
+ CornerRadius = UDim.new(0, 10),
28
+ }), React.createElement("uipadding", {
29
+ PaddingBottom = UDim.new(0, 8),
30
+ PaddingLeft = UDim.new(0, 10),
31
+ PaddingRight = UDim.new(0, 10),
32
+ PaddingTop = UDim.new(0, 8),
33
+ }), props.children)
34
+ end
35
+ return {
36
+ ToastRoot = ToastRoot,
37
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { ToastTitleProps } from "./types";
3
+ export declare function ToastTitle(props: ToastTitleProps): React.JSX.Element;
@@ -0,0 +1,26 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local Slot = _core.Slot
6
+ local function ToastTitle(props)
7
+ if props.asChild then
8
+ local child = props.children
9
+ if not child then
10
+ error("[ToastTitle] `asChild` requires a child element.")
11
+ end
12
+ return React.createElement(Slot, nil, child)
13
+ end
14
+ return React.createElement("textlabel", {
15
+ BackgroundTransparency = 1,
16
+ BorderSizePixel = 0,
17
+ Size = UDim2.fromOffset(300, 20),
18
+ Text = "Toast",
19
+ TextColor3 = Color3.fromRGB(235, 240, 248),
20
+ TextSize = 14,
21
+ TextXAlignment = Enum.TextXAlignment.Left,
22
+ })
23
+ end
24
+ return {
25
+ ToastTitle = ToastTitle,
26
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { ToastViewportProps } from "./types";
3
+ export declare function ToastViewport(props: ToastViewportProps): React.JSX.Element;
@@ -0,0 +1,95 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local Slot = _core.Slot
6
+ local useToastContext = TS.import(script, script.Parent, "context").useToastContext
7
+ local ToastClose = TS.import(script, script.Parent, "ToastClose").ToastClose
8
+ local ToastDescription = TS.import(script, script.Parent, "ToastDescription").ToastDescription
9
+ local ToastRoot = TS.import(script, script.Parent, "ToastRoot").ToastRoot
10
+ local ToastTitle = TS.import(script, script.Parent, "ToastTitle").ToastTitle
11
+ local function ToastViewport(props)
12
+ local toastContext = useToastContext()
13
+ if props.asChild then
14
+ local child = props.children
15
+ if not React.isValidElement(child) then
16
+ error("[ToastViewport] `asChild` requires a child element.")
17
+ end
18
+ return React.createElement(Slot, nil, child)
19
+ end
20
+ local _exp = React.createElement("uilistlayout", {
21
+ FillDirection = Enum.FillDirection.Vertical,
22
+ Padding = UDim.new(0, 8),
23
+ SortOrder = Enum.SortOrder.LayoutOrder,
24
+ })
25
+ local _exp_1 = toastContext.visibleToasts
26
+ -- ▼ ReadonlyArray.map ▼
27
+ local _newValue = table.create(#_exp_1)
28
+ local _callback = function(toast)
29
+ local _attributes = {
30
+ BackgroundTransparency = 1,
31
+ BorderSizePixel = 0,
32
+ Size = UDim2.fromOffset(264, 20),
33
+ }
34
+ local _condition = toast.title
35
+ if _condition == nil then
36
+ _condition = "Notification"
37
+ end
38
+ _attributes.Text = _condition
39
+ _attributes.TextColor3 = Color3.fromRGB(235, 240, 248)
40
+ _attributes.TextSize = 14
41
+ _attributes.TextXAlignment = Enum.TextXAlignment.Left
42
+ local _exp_2 = React.createElement("frame", {
43
+ BackgroundTransparency = 1,
44
+ BorderSizePixel = 0,
45
+ Size = UDim2.fromOffset(300, 22),
46
+ }, React.createElement(ToastTitle, {
47
+ asChild = true,
48
+ }, React.createElement("textlabel", _attributes)), React.createElement(ToastClose, {
49
+ asChild = true,
50
+ onClose = function()
51
+ return toastContext.remove(toast.id)
52
+ end,
53
+ }, React.createElement("textbutton", {
54
+ AutoButtonColor = false,
55
+ BackgroundTransparency = 1,
56
+ BorderSizePixel = 0,
57
+ Position = UDim2.fromOffset(274, 0),
58
+ Size = UDim2.fromOffset(24, 20),
59
+ Text = "X",
60
+ TextColor3 = Color3.fromRGB(172, 180, 196),
61
+ TextSize = 12,
62
+ })))
63
+ local _attributes_1 = {
64
+ BackgroundTransparency = 1,
65
+ BorderSizePixel = 0,
66
+ Position = UDim2.fromOffset(0, 24),
67
+ Size = UDim2.fromOffset(300, 18),
68
+ }
69
+ local _condition_1 = toast.description
70
+ if _condition_1 == nil then
71
+ _condition_1 = ""
72
+ end
73
+ _attributes_1.Text = _condition_1
74
+ _attributes_1.TextColor3 = Color3.fromRGB(172, 180, 196)
75
+ _attributes_1.TextSize = 13
76
+ _attributes_1.TextXAlignment = Enum.TextXAlignment.Left
77
+ return React.createElement(ToastRoot, {
78
+ key = toast.id,
79
+ }, _exp_2, React.createElement(ToastDescription, {
80
+ asChild = true,
81
+ }, React.createElement("textlabel", _attributes_1)))
82
+ end
83
+ for _k, _v in _exp_1 do
84
+ _newValue[_k] = _callback(_v, _k - 1, _exp_1)
85
+ end
86
+ -- ▲ ReadonlyArray.map ▲
87
+ return React.createElement("frame", {
88
+ BackgroundTransparency = 1,
89
+ BorderSizePixel = 0,
90
+ Size = UDim2.fromOffset(340, 320),
91
+ }, _exp, _newValue, props.children)
92
+ end
93
+ return {
94
+ ToastViewport = ToastViewport,
95
+ }
@@ -0,0 +1,3 @@
1
+ import type { ToastContextValue } from "./types";
2
+ declare const ToastContextProvider: import("@rbxts/react").Provider<ToastContextValue | undefined>, useToastContext: () => ToastContextValue;
3
+ export { ToastContextProvider, useToastContext };
@@ -0,0 +1,10 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local createStrictContext = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out).createStrictContext
4
+ local _binding = createStrictContext("Toast")
5
+ local ToastContextProvider = _binding[1]
6
+ local useToastContext = _binding[2]
7
+ return {
8
+ ToastContextProvider = ToastContextProvider,
9
+ useToastContext = useToastContext,
10
+ }
@@ -0,0 +1,11 @@
1
+ export type ToastRecord = {
2
+ id: string;
3
+ title?: string;
4
+ description?: string;
5
+ durationMs?: number;
6
+ createdAtMs: number;
7
+ };
8
+ export declare function enqueueToast(queue: Array<ToastRecord>, toast: ToastRecord): ToastRecord[];
9
+ export declare function dequeueToast(queue: Array<ToastRecord>): ToastRecord[];
10
+ export declare function getVisibleToasts(queue: Array<ToastRecord>, maxVisible: number): ToastRecord[];
11
+ export declare function pruneExpiredToasts(queue: Array<ToastRecord>, nowMs: number, maxVisible: number, defaultDurationMs: number): ToastRecord[];
@@ -0,0 +1,82 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local function enqueueToast(queue, toast)
3
+ local _array = {}
4
+ local _length = #_array
5
+ local _queueLength = #queue
6
+ table.move(queue, 1, _queueLength, _length + 1, _array)
7
+ _length += _queueLength
8
+ _array[_length + 1] = toast
9
+ return _array
10
+ end
11
+ local function dequeueToast(queue)
12
+ if #queue == 0 then
13
+ return queue
14
+ end
15
+ local nextQueue = {}
16
+ for index = 1, #queue - 1 do
17
+ local item = queue[index + 1]
18
+ if item then
19
+ table.insert(nextQueue, item)
20
+ end
21
+ end
22
+ return nextQueue
23
+ end
24
+ local function getVisibleToasts(queue, maxVisible)
25
+ local limit = math.max(1, maxVisible)
26
+ local visible = {}
27
+ do
28
+ local index = 0
29
+ local _shouldIncrement = false
30
+ while true do
31
+ if _shouldIncrement then
32
+ index += 1
33
+ else
34
+ _shouldIncrement = true
35
+ end
36
+ if not (index < #queue and index < limit) then
37
+ break
38
+ end
39
+ local item = queue[index + 1]
40
+ if item then
41
+ table.insert(visible, item)
42
+ end
43
+ end
44
+ end
45
+ return visible
46
+ end
47
+ local function pruneExpiredToasts(queue, nowMs, maxVisible, defaultDurationMs)
48
+ local visibleCount = math.max(1, maxVisible)
49
+ local changed = false
50
+ local nextQueue = {}
51
+ for index = 0, #queue - 1 do
52
+ local toast = queue[index + 1]
53
+ if not toast then
54
+ continue
55
+ end
56
+ if index + 1 > visibleCount then
57
+ table.insert(nextQueue, toast)
58
+ continue
59
+ end
60
+ local _condition = toast.durationMs
61
+ if _condition == nil then
62
+ _condition = defaultDurationMs
63
+ end
64
+ local duration = _condition
65
+ if duration <= 0 then
66
+ table.insert(nextQueue, toast)
67
+ continue
68
+ end
69
+ if nowMs - toast.createdAtMs >= duration then
70
+ changed = true
71
+ continue
72
+ end
73
+ table.insert(nextQueue, toast)
74
+ end
75
+ return if changed then nextQueue else queue
76
+ end
77
+ return {
78
+ enqueueToast = enqueueToast,
79
+ dequeueToast = dequeueToast,
80
+ getVisibleToasts = getVisibleToasts,
81
+ pruneExpiredToasts = pruneExpiredToasts,
82
+ }
@@ -0,0 +1,56 @@
1
+ import type React from "@rbxts/react";
2
+ import type { ToastRecord } from "./queue";
3
+ export type ToastOptions = {
4
+ id?: string;
5
+ title?: string;
6
+ description?: string;
7
+ durationMs?: number;
8
+ };
9
+ export type ToastContextValue = {
10
+ toasts: Array<ToastRecord>;
11
+ visibleToasts: Array<ToastRecord>;
12
+ defaultDurationMs: number;
13
+ maxVisible: number;
14
+ enqueue: (options: ToastOptions) => string;
15
+ remove: (id: string) => void;
16
+ clear: () => void;
17
+ };
18
+ export type ToastApi = {
19
+ toasts: Array<ToastRecord>;
20
+ visibleToasts: Array<ToastRecord>;
21
+ enqueue: (options: ToastOptions) => string;
22
+ remove: (id: string) => void;
23
+ clear: () => void;
24
+ };
25
+ export type ToastProviderProps = {
26
+ defaultDurationMs?: number;
27
+ maxVisible?: number;
28
+ children?: React.ReactNode;
29
+ };
30
+ export type ToastViewportProps = {
31
+ asChild?: boolean;
32
+ children?: React.ReactNode;
33
+ };
34
+ export type ToastRootProps = {
35
+ asChild?: boolean;
36
+ visible?: boolean;
37
+ children?: React.ReactNode;
38
+ };
39
+ export type ToastTitleProps = {
40
+ asChild?: boolean;
41
+ children?: React.ReactElement;
42
+ };
43
+ export type ToastDescriptionProps = {
44
+ asChild?: boolean;
45
+ children?: React.ReactElement;
46
+ };
47
+ export type ToastActionProps = {
48
+ asChild?: boolean;
49
+ onAction?: () => void;
50
+ children?: React.ReactElement;
51
+ };
52
+ export type ToastCloseProps = {
53
+ asChild?: boolean;
54
+ onClose?: () => void;
55
+ children?: React.ReactElement;
56
+ };
@@ -0,0 +1,2 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ return nil
package/out/index.d.ts ADDED
@@ -0,0 +1,20 @@
1
+ import { ToastAction } from "./Toast/ToastAction";
2
+ import { ToastClose } from "./Toast/ToastClose";
3
+ import { ToastDescription } from "./Toast/ToastDescription";
4
+ import { ToastProvider } from "./Toast/ToastProvider";
5
+ import { ToastRoot } from "./Toast/ToastRoot";
6
+ import { ToastTitle } from "./Toast/ToastTitle";
7
+ import { ToastViewport } from "./Toast/ToastViewport";
8
+ export declare const Toast: {
9
+ readonly Provider: typeof ToastProvider;
10
+ readonly Viewport: typeof ToastViewport;
11
+ readonly Root: typeof ToastRoot;
12
+ readonly Title: typeof ToastTitle;
13
+ readonly Description: typeof ToastDescription;
14
+ readonly Action: typeof ToastAction;
15
+ readonly Close: typeof ToastClose;
16
+ };
17
+ export type { ToastRecord } from "./Toast/queue";
18
+ export { dequeueToast, enqueueToast, getVisibleToasts, pruneExpiredToasts } from "./Toast/queue";
19
+ export { useToast } from "./Toast/ToastProvider";
20
+ export type { ToastActionProps, ToastCloseProps, ToastContextValue, ToastDescriptionProps, ToastOptions, ToastProviderProps, ToastRootProps, ToastTitleProps, ToastViewportProps, } from "./Toast/types";
package/out/init.luau ADDED
@@ -0,0 +1,27 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local exports = {}
4
+ local ToastAction = TS.import(script, script, "Toast", "ToastAction").ToastAction
5
+ local ToastClose = TS.import(script, script, "Toast", "ToastClose").ToastClose
6
+ local ToastDescription = TS.import(script, script, "Toast", "ToastDescription").ToastDescription
7
+ local ToastProvider = TS.import(script, script, "Toast", "ToastProvider").ToastProvider
8
+ local ToastRoot = TS.import(script, script, "Toast", "ToastRoot").ToastRoot
9
+ local ToastTitle = TS.import(script, script, "Toast", "ToastTitle").ToastTitle
10
+ local ToastViewport = TS.import(script, script, "Toast", "ToastViewport").ToastViewport
11
+ local Toast = {
12
+ Provider = ToastProvider,
13
+ Viewport = ToastViewport,
14
+ Root = ToastRoot,
15
+ Title = ToastTitle,
16
+ Description = ToastDescription,
17
+ Action = ToastAction,
18
+ Close = ToastClose,
19
+ }
20
+ local _queue = TS.import(script, script, "Toast", "queue")
21
+ exports.dequeueToast = _queue.dequeueToast
22
+ exports.enqueueToast = _queue.enqueueToast
23
+ exports.getVisibleToasts = _queue.getVisibleToasts
24
+ exports.pruneExpiredToasts = _queue.pruneExpiredToasts
25
+ exports.useToast = TS.import(script, script, "Toast", "ToastProvider").useToast
26
+ exports.Toast = Toast
27
+ return exports
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "@lattice-ui/toast",
3
+ "version": "0.3.0",
4
+ "private": false,
5
+ "main": "out/init.luau",
6
+ "types": "out/index.d.ts",
7
+ "dependencies": {
8
+ "@lattice-ui/layer": "0.3.0",
9
+ "@lattice-ui/core": "0.3.0"
10
+ },
11
+ "devDependencies": {
12
+ "@rbxts/react": "17.3.7-ts.1",
13
+ "@rbxts/react-roblox": "17.3.7-ts.1"
14
+ },
15
+ "peerDependencies": {
16
+ "@rbxts/react": "^17",
17
+ "@rbxts/react-roblox": "^17"
18
+ },
19
+ "scripts": {
20
+ "build": "rbxtsc -p tsconfig.json",
21
+ "typecheck": "tsc -p tsconfig.typecheck.json",
22
+ "watch": "rbxtsc -p tsconfig.json -w"
23
+ }
24
+ }
@@ -0,0 +1,46 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import type { ToastActionProps } from "./types";
3
+
4
+ export function ToastAction(props: ToastActionProps) {
5
+ const handleActivated = React.useCallback(() => {
6
+ props.onAction?.();
7
+ }, [props]);
8
+
9
+ if (props.asChild) {
10
+ const child = props.children;
11
+ if (!child) {
12
+ error("[ToastAction] `asChild` requires a child element.");
13
+ }
14
+
15
+ return (
16
+ <Slot
17
+ Active
18
+ Event={{
19
+ Activated: handleActivated,
20
+ }}
21
+ Selectable
22
+ >
23
+ {child}
24
+ </Slot>
25
+ );
26
+ }
27
+
28
+ return (
29
+ <textbutton
30
+ Active
31
+ AutoButtonColor={false}
32
+ BackgroundColor3={Color3.fromRGB(58, 66, 84)}
33
+ BorderSizePixel={0}
34
+ Event={{
35
+ Activated: handleActivated,
36
+ }}
37
+ Selectable
38
+ Size={UDim2.fromOffset(88, 26)}
39
+ Text="Action"
40
+ TextColor3={Color3.fromRGB(235, 240, 248)}
41
+ TextSize={13}
42
+ >
43
+ {props.children}
44
+ </textbutton>
45
+ );
46
+ }
@@ -0,0 +1,44 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import type { ToastCloseProps } from "./types";
3
+
4
+ export function ToastClose(props: ToastCloseProps) {
5
+ const handleActivated = React.useCallback(() => {
6
+ props.onClose?.();
7
+ }, [props]);
8
+
9
+ if (props.asChild) {
10
+ const child = props.children;
11
+ if (!child) {
12
+ error("[ToastClose] `asChild` requires a child element.");
13
+ }
14
+
15
+ return (
16
+ <Slot
17
+ Active
18
+ Event={{
19
+ Activated: handleActivated,
20
+ }}
21
+ Selectable
22
+ >
23
+ {child}
24
+ </Slot>
25
+ );
26
+ }
27
+
28
+ return (
29
+ <textbutton
30
+ Active
31
+ AutoButtonColor={false}
32
+ BackgroundColor3={Color3.fromRGB(58, 66, 84)}
33
+ BorderSizePixel={0}
34
+ Event={{
35
+ Activated: handleActivated,
36
+ }}
37
+ Selectable
38
+ Size={UDim2.fromOffset(26, 26)}
39
+ Text="X"
40
+ TextColor3={Color3.fromRGB(235, 240, 248)}
41
+ TextSize={13}
42
+ />
43
+ );
44
+ }
@@ -0,0 +1,25 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import type { ToastDescriptionProps } from "./types";
3
+
4
+ export function ToastDescription(props: ToastDescriptionProps) {
5
+ if (props.asChild) {
6
+ const child = props.children;
7
+ if (!child) {
8
+ error("[ToastDescription] `asChild` requires a child element.");
9
+ }
10
+
11
+ return <Slot>{child}</Slot>;
12
+ }
13
+
14
+ return (
15
+ <textlabel
16
+ BackgroundTransparency={1}
17
+ BorderSizePixel={0}
18
+ Size={UDim2.fromOffset(300, 18)}
19
+ Text="Description"
20
+ TextColor3={Color3.fromRGB(172, 180, 196)}
21
+ TextSize={13}
22
+ TextXAlignment={Enum.TextXAlignment.Left}
23
+ />
24
+ );
25
+ }
@@ -0,0 +1,85 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import { ToastContextProvider, useToastContext } from "./context";
3
+ import { enqueueToast, getVisibleToasts, pruneExpiredToasts, type ToastRecord } from "./queue";
4
+ import type { ToastApi, ToastOptions, ToastProviderProps } from "./types";
5
+
6
+ const RunService = game.GetService("RunService");
7
+
8
+ function nowMs() {
9
+ return math.floor(os.clock() * 1000);
10
+ }
11
+
12
+ export function ToastProvider(props: ToastProviderProps) {
13
+ const defaultDurationMs = math.max(0, props.defaultDurationMs ?? 4000);
14
+ const maxVisible = math.max(1, props.maxVisible ?? 3);
15
+
16
+ const [toasts, setToasts] = React.useState<Array<ToastRecord>>([]);
17
+ const idSequenceRef = React.useRef(0);
18
+
19
+ const enqueue = React.useCallback((options: ToastOptions) => {
20
+ idSequenceRef.current += 1;
21
+ const id = options.id ?? `toast-${idSequenceRef.current}`;
22
+
23
+ setToasts((currentQueue) =>
24
+ enqueueToast(currentQueue, {
25
+ id,
26
+ title: options.title,
27
+ description: options.description,
28
+ durationMs: options.durationMs,
29
+ createdAtMs: nowMs(),
30
+ }),
31
+ );
32
+
33
+ return id;
34
+ }, []);
35
+
36
+ const remove = React.useCallback((id: string) => {
37
+ setToasts((currentQueue) => currentQueue.filter((toast) => toast.id !== id));
38
+ }, []);
39
+
40
+ const clear = React.useCallback(() => {
41
+ setToasts([]);
42
+ }, []);
43
+
44
+ React.useEffect(() => {
45
+ if (toasts.size() === 0) {
46
+ return;
47
+ }
48
+
49
+ const connection = RunService.Heartbeat.Connect(() => {
50
+ setToasts((currentQueue) => pruneExpiredToasts(currentQueue, nowMs(), maxVisible, defaultDurationMs));
51
+ });
52
+
53
+ return () => {
54
+ connection.Disconnect();
55
+ };
56
+ }, [defaultDurationMs, maxVisible, toasts.size()]);
57
+
58
+ const visibleToasts = React.useMemo(() => getVisibleToasts(toasts, maxVisible), [maxVisible, toasts]);
59
+
60
+ const contextValue = React.useMemo(
61
+ () => ({
62
+ toasts,
63
+ visibleToasts,
64
+ defaultDurationMs,
65
+ maxVisible,
66
+ enqueue,
67
+ remove,
68
+ clear,
69
+ }),
70
+ [clear, defaultDurationMs, enqueue, maxVisible, remove, toasts, visibleToasts],
71
+ );
72
+
73
+ return <ToastContextProvider value={contextValue}>{props.children}</ToastContextProvider>;
74
+ }
75
+
76
+ export function useToast(): ToastApi {
77
+ const toastContext = useToastContext();
78
+ return {
79
+ toasts: toastContext.toasts,
80
+ visibleToasts: toastContext.visibleToasts,
81
+ enqueue: toastContext.enqueue,
82
+ remove: toastContext.remove,
83
+ clear: toastContext.clear,
84
+ };
85
+ }
@@ -0,0 +1,33 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import type { ToastRootProps } from "./types";
3
+
4
+ export function ToastRoot(props: ToastRootProps) {
5
+ const visible = props.visible ?? true;
6
+
7
+ if (props.asChild) {
8
+ const child = props.children;
9
+ if (!React.isValidElement(child)) {
10
+ error("[ToastRoot] `asChild` requires a child element.");
11
+ }
12
+
13
+ return <Slot Visible={visible}>{child}</Slot>;
14
+ }
15
+
16
+ return (
17
+ <frame
18
+ BackgroundColor3={Color3.fromRGB(38, 45, 59)}
19
+ BorderSizePixel={0}
20
+ Size={UDim2.fromOffset(320, 72)}
21
+ Visible={visible}
22
+ >
23
+ <uicorner CornerRadius={new UDim(0, 10)} />
24
+ <uipadding
25
+ PaddingBottom={new UDim(0, 8)}
26
+ PaddingLeft={new UDim(0, 10)}
27
+ PaddingRight={new UDim(0, 10)}
28
+ PaddingTop={new UDim(0, 8)}
29
+ />
30
+ {props.children}
31
+ </frame>
32
+ );
33
+ }
@@ -0,0 +1,25 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import type { ToastTitleProps } from "./types";
3
+
4
+ export function ToastTitle(props: ToastTitleProps) {
5
+ if (props.asChild) {
6
+ const child = props.children;
7
+ if (!child) {
8
+ error("[ToastTitle] `asChild` requires a child element.");
9
+ }
10
+
11
+ return <Slot>{child}</Slot>;
12
+ }
13
+
14
+ return (
15
+ <textlabel
16
+ BackgroundTransparency={1}
17
+ BorderSizePixel={0}
18
+ Size={UDim2.fromOffset(300, 20)}
19
+ Text="Toast"
20
+ TextColor3={Color3.fromRGB(235, 240, 248)}
21
+ TextSize={14}
22
+ TextXAlignment={Enum.TextXAlignment.Left}
23
+ />
24
+ );
25
+ }
@@ -0,0 +1,72 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { useToastContext } from "./context";
3
+ import { ToastClose } from "./ToastClose";
4
+ import { ToastDescription } from "./ToastDescription";
5
+ import { ToastRoot } from "./ToastRoot";
6
+ import { ToastTitle } from "./ToastTitle";
7
+ import type { ToastViewportProps } from "./types";
8
+
9
+ export function ToastViewport(props: ToastViewportProps) {
10
+ const toastContext = useToastContext();
11
+
12
+ if (props.asChild) {
13
+ const child = props.children;
14
+ if (!React.isValidElement(child)) {
15
+ error("[ToastViewport] `asChild` requires a child element.");
16
+ }
17
+
18
+ return <Slot>{child}</Slot>;
19
+ }
20
+
21
+ return (
22
+ <frame BackgroundTransparency={1} BorderSizePixel={0} Size={UDim2.fromOffset(340, 320)}>
23
+ <uilistlayout
24
+ FillDirection={Enum.FillDirection.Vertical}
25
+ Padding={new UDim(0, 8)}
26
+ SortOrder={Enum.SortOrder.LayoutOrder}
27
+ />
28
+ {toastContext.visibleToasts.map((toast) => (
29
+ <ToastRoot key={toast.id}>
30
+ <frame BackgroundTransparency={1} BorderSizePixel={0} Size={UDim2.fromOffset(300, 22)}>
31
+ <ToastTitle asChild>
32
+ <textlabel
33
+ BackgroundTransparency={1}
34
+ BorderSizePixel={0}
35
+ Size={UDim2.fromOffset(264, 20)}
36
+ Text={toast.title ?? "Notification"}
37
+ TextColor3={Color3.fromRGB(235, 240, 248)}
38
+ TextSize={14}
39
+ TextXAlignment={Enum.TextXAlignment.Left}
40
+ />
41
+ </ToastTitle>
42
+ <ToastClose asChild onClose={() => toastContext.remove(toast.id)}>
43
+ <textbutton
44
+ AutoButtonColor={false}
45
+ BackgroundTransparency={1}
46
+ BorderSizePixel={0}
47
+ Position={UDim2.fromOffset(274, 0)}
48
+ Size={UDim2.fromOffset(24, 20)}
49
+ Text="X"
50
+ TextColor3={Color3.fromRGB(172, 180, 196)}
51
+ TextSize={12}
52
+ />
53
+ </ToastClose>
54
+ </frame>
55
+ <ToastDescription asChild>
56
+ <textlabel
57
+ BackgroundTransparency={1}
58
+ BorderSizePixel={0}
59
+ Position={UDim2.fromOffset(0, 24)}
60
+ Size={UDim2.fromOffset(300, 18)}
61
+ Text={toast.description ?? ""}
62
+ TextColor3={Color3.fromRGB(172, 180, 196)}
63
+ TextSize={13}
64
+ TextXAlignment={Enum.TextXAlignment.Left}
65
+ />
66
+ </ToastDescription>
67
+ </ToastRoot>
68
+ ))}
69
+ {props.children}
70
+ </frame>
71
+ );
72
+ }
@@ -0,0 +1,6 @@
1
+ import { createStrictContext } from "@lattice-ui/core";
2
+ import type { ToastContextValue } from "./types";
3
+
4
+ const [ToastContextProvider, useToastContext] = createStrictContext<ToastContextValue>("Toast");
5
+
6
+ export { ToastContextProvider, useToastContext };
@@ -0,0 +1,78 @@
1
+ export type ToastRecord = {
2
+ id: string;
3
+ title?: string;
4
+ description?: string;
5
+ durationMs?: number;
6
+ createdAtMs: number;
7
+ };
8
+
9
+ export function enqueueToast(queue: Array<ToastRecord>, toast: ToastRecord) {
10
+ return [...queue, toast];
11
+ }
12
+
13
+ export function dequeueToast(queue: Array<ToastRecord>) {
14
+ if (queue.size() === 0) {
15
+ return queue;
16
+ }
17
+
18
+ const nextQueue: Array<ToastRecord> = [];
19
+ for (let index = 1; index < queue.size(); index++) {
20
+ const item = queue[index];
21
+ if (item) {
22
+ nextQueue.push(item);
23
+ }
24
+ }
25
+
26
+ return nextQueue;
27
+ }
28
+
29
+ export function getVisibleToasts(queue: Array<ToastRecord>, maxVisible: number) {
30
+ const limit = math.max(1, maxVisible);
31
+ const visible: Array<ToastRecord> = [];
32
+ for (let index = 0; index < queue.size() && index < limit; index++) {
33
+ const item = queue[index];
34
+ if (item) {
35
+ visible.push(item);
36
+ }
37
+ }
38
+
39
+ return visible;
40
+ }
41
+
42
+ export function pruneExpiredToasts(
43
+ queue: Array<ToastRecord>,
44
+ nowMs: number,
45
+ maxVisible: number,
46
+ defaultDurationMs: number,
47
+ ) {
48
+ const visibleCount = math.max(1, maxVisible);
49
+ let changed = false;
50
+
51
+ const nextQueue: Array<ToastRecord> = [];
52
+ for (let index = 0; index < queue.size(); index++) {
53
+ const toast = queue[index];
54
+ if (!toast) {
55
+ continue;
56
+ }
57
+
58
+ if (index + 1 > visibleCount) {
59
+ nextQueue.push(toast);
60
+ continue;
61
+ }
62
+
63
+ const duration = toast.durationMs ?? defaultDurationMs;
64
+ if (duration <= 0) {
65
+ nextQueue.push(toast);
66
+ continue;
67
+ }
68
+
69
+ if (nowMs - toast.createdAtMs >= duration) {
70
+ changed = true;
71
+ continue;
72
+ }
73
+
74
+ nextQueue.push(toast);
75
+ }
76
+
77
+ return changed ? nextQueue : queue;
78
+ }
@@ -0,0 +1,66 @@
1
+ import type React from "@rbxts/react";
2
+ import type { ToastRecord } from "./queue";
3
+
4
+ export type ToastOptions = {
5
+ id?: string;
6
+ title?: string;
7
+ description?: string;
8
+ durationMs?: number;
9
+ };
10
+
11
+ export type ToastContextValue = {
12
+ toasts: Array<ToastRecord>;
13
+ visibleToasts: Array<ToastRecord>;
14
+ defaultDurationMs: number;
15
+ maxVisible: number;
16
+ enqueue: (options: ToastOptions) => string;
17
+ remove: (id: string) => void;
18
+ clear: () => void;
19
+ };
20
+
21
+ export type ToastApi = {
22
+ toasts: Array<ToastRecord>;
23
+ visibleToasts: Array<ToastRecord>;
24
+ enqueue: (options: ToastOptions) => string;
25
+ remove: (id: string) => void;
26
+ clear: () => void;
27
+ };
28
+
29
+ export type ToastProviderProps = {
30
+ defaultDurationMs?: number;
31
+ maxVisible?: number;
32
+ children?: React.ReactNode;
33
+ };
34
+
35
+ export type ToastViewportProps = {
36
+ asChild?: boolean;
37
+ children?: React.ReactNode;
38
+ };
39
+
40
+ export type ToastRootProps = {
41
+ asChild?: boolean;
42
+ visible?: boolean;
43
+ children?: React.ReactNode;
44
+ };
45
+
46
+ export type ToastTitleProps = {
47
+ asChild?: boolean;
48
+ children?: React.ReactElement;
49
+ };
50
+
51
+ export type ToastDescriptionProps = {
52
+ asChild?: boolean;
53
+ children?: React.ReactElement;
54
+ };
55
+
56
+ export type ToastActionProps = {
57
+ asChild?: boolean;
58
+ onAction?: () => void;
59
+ children?: React.ReactElement;
60
+ };
61
+
62
+ export type ToastCloseProps = {
63
+ asChild?: boolean;
64
+ onClose?: () => void;
65
+ children?: React.ReactElement;
66
+ };
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { ToastAction } from "./Toast/ToastAction";
2
+ import { ToastClose } from "./Toast/ToastClose";
3
+ import { ToastDescription } from "./Toast/ToastDescription";
4
+ import { ToastProvider } from "./Toast/ToastProvider";
5
+ import { ToastRoot } from "./Toast/ToastRoot";
6
+ import { ToastTitle } from "./Toast/ToastTitle";
7
+ import { ToastViewport } from "./Toast/ToastViewport";
8
+
9
+ export const Toast = {
10
+ Provider: ToastProvider,
11
+ Viewport: ToastViewport,
12
+ Root: ToastRoot,
13
+ Title: ToastTitle,
14
+ Description: ToastDescription,
15
+ Action: ToastAction,
16
+ Close: ToastClose,
17
+ } as const;
18
+
19
+ export type { ToastRecord } from "./Toast/queue";
20
+ export { dequeueToast, enqueueToast, getVisibleToasts, pruneExpiredToasts } from "./Toast/queue";
21
+ export { useToast } from "./Toast/ToastProvider";
22
+ export type {
23
+ ToastActionProps,
24
+ ToastCloseProps,
25
+ ToastContextValue,
26
+ ToastDescriptionProps,
27
+ ToastOptions,
28
+ ToastProviderProps,
29
+ ToastRootProps,
30
+ ToastTitleProps,
31
+ ToastViewportProps,
32
+ } from "./Toast/types";
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../../tsconfig.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "src",
5
+ "outDir": "out",
6
+ "declaration": true,
7
+ "typeRoots": [
8
+ "./node_modules/@rbxts",
9
+ "../../node_modules/@rbxts",
10
+ "./node_modules/@lattice-ui",
11
+ "../../node_modules/@lattice-ui"
12
+ ],
13
+ "types": ["types", "compiler-types"]
14
+ },
15
+ "include": ["src"]
16
+ }
@@ -0,0 +1,35 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "baseUrl": "..",
6
+ "rootDir": "..",
7
+ "paths": {
8
+ "@lattice-ui/accordion": ["accordion/src/index.ts"],
9
+ "@lattice-ui/avatar": ["avatar/src/index.ts"],
10
+ "@lattice-ui/checkbox": ["checkbox/src/index.ts"],
11
+ "@lattice-ui/combobox": ["combobox/src/index.ts"],
12
+ "@lattice-ui/core": ["core/src/index.ts"],
13
+ "@lattice-ui/dialog": ["dialog/src/index.ts"],
14
+ "@lattice-ui/focus": ["focus/src/index.ts"],
15
+ "@lattice-ui/layer": ["layer/src/index.ts"],
16
+ "@lattice-ui/menu": ["menu/src/index.ts"],
17
+ "@lattice-ui/popover": ["popover/src/index.ts"],
18
+ "@lattice-ui/popper": ["popper/src/index.ts"],
19
+ "@lattice-ui/progress": ["progress/src/index.ts"],
20
+ "@lattice-ui/radio-group": ["radio-group/src/index.ts"],
21
+ "@lattice-ui/scroll-area": ["scroll-area/src/index.ts"],
22
+ "@lattice-ui/select": ["select/src/index.ts"],
23
+ "@lattice-ui/slider": ["slider/src/index.ts"],
24
+ "@lattice-ui/style": ["style/src/index.ts"],
25
+ "@lattice-ui/switch": ["switch/src/index.ts"],
26
+ "@lattice-ui/system": ["system/src/index.ts"],
27
+ "@lattice-ui/tabs": ["tabs/src/index.ts"],
28
+ "@lattice-ui/text-field": ["text-field/src/index.ts"],
29
+ "@lattice-ui/textarea": ["textarea/src/index.ts"],
30
+ "@lattice-ui/toast": ["toast/src/index.ts"],
31
+ "@lattice-ui/toggle-group": ["toggle-group/src/index.ts"],
32
+ "@lattice-ui/tooltip": ["tooltip/src/index.ts"]
33
+ }
34
+ }
35
+ }