@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 +21 -0
- package/out/Toast/ToastAction.d.ts +3 -0
- package/out/Toast/ToastAction.luau +43 -0
- package/out/Toast/ToastClose.d.ts +3 -0
- package/out/Toast/ToastClose.luau +43 -0
- package/out/Toast/ToastDescription.d.ts +3 -0
- package/out/Toast/ToastDescription.luau +26 -0
- package/out/Toast/ToastProvider.d.ts +4 -0
- package/out/Toast/ToastProvider.luau +111 -0
- package/out/Toast/ToastRoot.d.ts +3 -0
- package/out/Toast/ToastRoot.luau +37 -0
- package/out/Toast/ToastTitle.d.ts +3 -0
- package/out/Toast/ToastTitle.luau +26 -0
- package/out/Toast/ToastViewport.d.ts +3 -0
- package/out/Toast/ToastViewport.luau +95 -0
- package/out/Toast/context.d.ts +3 -0
- package/out/Toast/context.luau +10 -0
- package/out/Toast/queue.d.ts +11 -0
- package/out/Toast/queue.luau +82 -0
- package/out/Toast/types.d.ts +56 -0
- package/out/Toast/types.luau +2 -0
- package/out/index.d.ts +20 -0
- package/out/init.luau +27 -0
- package/package.json +24 -0
- package/src/Toast/ToastAction.tsx +46 -0
- package/src/Toast/ToastClose.tsx +44 -0
- package/src/Toast/ToastDescription.tsx +25 -0
- package/src/Toast/ToastProvider.tsx +85 -0
- package/src/Toast/ToastRoot.tsx +33 -0
- package/src/Toast/ToastTitle.tsx +25 -0
- package/src/Toast/ToastViewport.tsx +72 -0
- package/src/Toast/context.ts +6 -0
- package/src/Toast/queue.ts +78 -0
- package/src/Toast/types.ts +66 -0
- package/src/index.ts +32 -0
- package/tsconfig.json +16 -0
- package/tsconfig.typecheck.json +35 -0
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,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,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,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,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,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,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,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,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
|
+
};
|
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,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
|
+
}
|