@nrbx/topbar-components 1.0.12 → 1.1.1
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/LICENSE.txt +1 -1
- package/README.md +163 -17
- package/out/components/custom-dropdown.d.ts +19 -0
- package/out/components/custom-dropdown.luau +86 -0
- package/out/components/dropdown.d.ts +20 -0
- package/out/components/dropdown.luau +12 -7
- package/out/components/icon.d.ts +68 -3
- package/out/components/icon.luau +99 -21
- package/out/components/notification.d.ts +10 -0
- package/out/components/notification.luau +52 -0
- package/out/components/overflow.d.ts +10 -0
- package/out/components/overflow.luau +62 -0
- package/out/components/provider.d.ts +16 -1
- package/out/components/provider.luau +17 -4
- package/out/components/stylesheet.d.ts +7 -0
- package/out/components/stylesheet.luau +9 -1
- package/out/components/tooltip.d.ts +14 -0
- package/out/components/tooltip.luau +99 -0
- package/out/hooks/use-animateable-props.d.ts +2 -3
- package/out/hooks/use-animateable-props.luau +11 -13
- package/out/hooks/use-toggle-key.d.ts +6 -0
- package/out/hooks/use-toggle-key.luau +29 -0
- package/out/hooks/use-voicechat-enabled.luau +1 -1
- package/out/index.d.ts +5 -0
- package/out/init.luau +15 -0
- package/out/style.d.ts +117 -35
- package/out/style.luau +47 -0
- package/out/utilities/debug.d.ts +0 -4
- package/out/utilities/debug.luau +0 -6
- package/out/utilities/merge.d.ts +1 -2
- package/out/utilities/merge.luau +33 -29
- package/out/utilities/springs.d.ts +4 -0
- package/out/utilities/springs.luau +1 -1
- package/package.json +11 -5
package/out/components/icon.luau
CHANGED
|
@@ -4,6 +4,7 @@ local deepEquals = TS.import(script, TS.getModule(script, "@rbxts", "object-util
|
|
|
4
4
|
local _pretty_react_hooks = TS.import(script, TS.getModule(script, "@rbxts", "pretty-react-hooks").out)
|
|
5
5
|
local mapBinding = _pretty_react_hooks.mapBinding
|
|
6
6
|
local useAsyncEffect = _pretty_react_hooks.useAsyncEffect
|
|
7
|
+
local useMotion = _pretty_react_hooks.useMotion
|
|
7
8
|
local useMountEffect = _pretty_react_hooks.useMountEffect
|
|
8
9
|
local useUnmountEffect = _pretty_react_hooks.useUnmountEffect
|
|
9
10
|
local useUpdateEffect = _pretty_react_hooks.useUpdateEffect
|
|
@@ -21,9 +22,38 @@ local useStylesheet = _context.useStylesheet
|
|
|
21
22
|
local useAnimateableProps = TS.import(script, script.Parent.Parent, "hooks", "use-animateable-props").useAnimateableProps
|
|
22
23
|
local useGuiInset = TS.import(script, script.Parent.Parent, "hooks", "use-gui-inset").useGuiInset
|
|
23
24
|
local useId = TS.import(script, script.Parent.Parent, "hooks", "use-id").useId
|
|
25
|
+
local useToggleKey = TS.import(script, script.Parent.Parent, "hooks", "use-toggle-key").useToggleKey
|
|
24
26
|
local noop = TS.import(script, script.Parent.Parent, "style").noop
|
|
25
27
|
local stateful = TS.import(script, script.Parent.Parent, "utilities", "resolve-state-dependent").stateful
|
|
28
|
+
local Notification = TS.import(script, script.Parent, "notification").Notification
|
|
29
|
+
--[[
|
|
30
|
+
*
|
|
31
|
+
* Properties accepted by {@link Icon}.
|
|
32
|
+
*
|
|
33
|
+
* Most visual props accept a `StateDependent<T>` — either a single value for
|
|
34
|
+
* both states, or `{ deselected: T; selected: T }` to animate between them.
|
|
35
|
+
|
|
36
|
+
]]
|
|
26
37
|
local ANIMATEABLE = { "backgroundColor", "backgroundTransparency", "imageColor", "imageTransparency" }
|
|
38
|
+
--* Icon select/deselect state.
|
|
39
|
+
--[[
|
|
40
|
+
*
|
|
41
|
+
* A value that can change with icon state.
|
|
42
|
+
* Pass a single value for both states, or `{ deselected, selected }` for
|
|
43
|
+
* separate values that animate on state transitions.
|
|
44
|
+
|
|
45
|
+
]]
|
|
46
|
+
--* Extracts the inner type from a `StateDependent`.
|
|
47
|
+
--* Unique numeric identifier for an icon instance.
|
|
48
|
+
--[[
|
|
49
|
+
*
|
|
50
|
+
* A single icon in the topbar.
|
|
51
|
+
*
|
|
52
|
+
* Supports an image, a text label, state-dependent styling, animated state
|
|
53
|
+
* transitions, hover lift, notification badges, toggle keys, and nested
|
|
54
|
+
* dropdown content.
|
|
55
|
+
|
|
56
|
+
]]
|
|
27
57
|
local function Icon(componentProps)
|
|
28
58
|
local _binding = componentProps
|
|
29
59
|
local children = _binding.children
|
|
@@ -32,6 +62,8 @@ local function Icon(componentProps)
|
|
|
32
62
|
local location = useLocation()
|
|
33
63
|
local id = useId()
|
|
34
64
|
local currentState, setState = useState(componentProps.forcedState or "deselected")
|
|
65
|
+
local hovered, setHovered = useState(false)
|
|
66
|
+
local hoverLift, hoverLiftMotion = useMotion(0)
|
|
35
67
|
local dropdownAnimating, setAnimationState = useState(false)
|
|
36
68
|
local contentSize, setContentSize = useState(Vector2.new(0, 0))
|
|
37
69
|
local dropdownSize, setDropdownSize = useBinding(Vector2.new(0, 0))
|
|
@@ -99,6 +131,21 @@ local function Icon(componentProps)
|
|
|
99
131
|
setState("deselected")
|
|
100
132
|
end
|
|
101
133
|
end, { location.selectedIcons })
|
|
134
|
+
useEffect(function()
|
|
135
|
+
if stylesheet.animation.hoverEnabled then
|
|
136
|
+
hoverLiftMotion:spring(if hovered then 1 else 0, stylesheet.animation.stateSpring)
|
|
137
|
+
end
|
|
138
|
+
end, { hovered, stylesheet.animation.hoverEnabled })
|
|
139
|
+
if componentProps.toggleKey and componentProps.toggleKey ~= Enum.KeyCode.Unknown then
|
|
140
|
+
useToggleKey(componentProps.toggleKey, function()
|
|
141
|
+
if props.static then
|
|
142
|
+
return nil
|
|
143
|
+
end
|
|
144
|
+
if stateful(props.toggleStateOnClick, currentState) then
|
|
145
|
+
setState(if currentState == "deselected" then "selected" else "deselected")
|
|
146
|
+
end
|
|
147
|
+
end)
|
|
148
|
+
end
|
|
102
149
|
local currentImage = stateful(props.imageId, currentState)
|
|
103
150
|
local currentText = stateful(props.text, currentState)
|
|
104
151
|
local previousQueryRef = useRef()
|
|
@@ -136,17 +183,37 @@ local function Icon(componentProps)
|
|
|
136
183
|
end
|
|
137
184
|
end
|
|
138
185
|
local iconHeight = _condition
|
|
139
|
-
local
|
|
186
|
+
local _condition_1 = props.contentPaddingX
|
|
187
|
+
if _condition_1 == nil then
|
|
188
|
+
_condition_1 = sizing.contentPaddingX
|
|
189
|
+
end
|
|
190
|
+
local contentPadX = _condition_1
|
|
191
|
+
local _condition_2 = props.contentPaddingY
|
|
192
|
+
if _condition_2 == nil then
|
|
193
|
+
_condition_2 = sizing.contentPaddingY
|
|
194
|
+
end
|
|
195
|
+
local contentPadY = _condition_2
|
|
196
|
+
local _condition_3 = props.imageToTextSpacing
|
|
197
|
+
if _condition_3 == nil then
|
|
198
|
+
_condition_3 = sizing.imageToTextSpacing
|
|
199
|
+
end
|
|
200
|
+
local imageToTextGap = _condition_3
|
|
201
|
+
local imageSize = iconHeight - contentPadY * 2 + imageSizeOff
|
|
140
202
|
local minLabelWidth = if location.type == "dropdown" then location.desiredIconWidth - sizing.minLabelWidthPadding else inset.Height - sizing.iconHorizontalPadding * 2
|
|
141
203
|
local accumulatedLabelWidth = if currentImage ~= "" and currentImage then textBounds.X else math.max(textBounds.X, minLabelWidth)
|
|
142
|
-
local
|
|
143
|
-
local
|
|
144
|
-
|
|
145
|
-
|
|
204
|
+
local contentWidth = if currentImage ~= "" and currentImage then imageSize + imageToTextGap + textBounds.X else textBounds.X
|
|
205
|
+
local autoWidth = contentWidth + contentPadX * 2
|
|
206
|
+
local _condition_4 = props.iconWidth
|
|
207
|
+
if not (_condition_4 ~= 0 and _condition_4 == _condition_4 and _condition_4) then
|
|
208
|
+
_condition_4 = sizing.iconWidth
|
|
209
|
+
if not (_condition_4 ~= 0 and _condition_4 == _condition_4 and _condition_4) then
|
|
210
|
+
_condition_4 = math.max(iconHeight, autoWidth)
|
|
211
|
+
end
|
|
146
212
|
end
|
|
147
|
-
local
|
|
148
|
-
local
|
|
149
|
-
local
|
|
213
|
+
local iconWidth = _condition_4
|
|
214
|
+
local iconSize = Vector2.new(iconWidth, iconHeight)
|
|
215
|
+
local imagePosY = (iconHeight - imageSize) / 2 + imageSizeOff * -0.5
|
|
216
|
+
local textLabelPos = UDim2.new(0, if currentImage ~= "" and currentImage then contentPadX + imageSize + imageToTextGap else contentPadX, 0.5, 0)
|
|
150
217
|
useEffect(function()
|
|
151
218
|
if props.static then
|
|
152
219
|
return nil
|
|
@@ -184,12 +251,15 @@ local function Icon(componentProps)
|
|
|
184
251
|
}, React.createElement("frame", {
|
|
185
252
|
Size = wrapSize,
|
|
186
253
|
LayoutOrder = stateful(props.layoutOrder, currentState),
|
|
254
|
+
Position = mapBinding(hoverLift, function(t)
|
|
255
|
+
return UDim2.new(0, 0, 0, -t * stylesheet.animation.hoverLift)
|
|
256
|
+
end),
|
|
187
257
|
BackgroundTransparency = 1,
|
|
188
|
-
key = "IconWrapper",
|
|
189
258
|
}, React.createElement("textbutton", {
|
|
190
259
|
Size = UDim2.new(1, 0, 0, iconSize.Y),
|
|
191
260
|
Active = not props.static,
|
|
192
261
|
Selectable = not props.static,
|
|
262
|
+
AutoButtonColor = not props.static,
|
|
193
263
|
Event = {
|
|
194
264
|
MouseButton1Click = function()
|
|
195
265
|
if props.static then
|
|
@@ -219,21 +289,31 @@ local function Icon(componentProps)
|
|
|
219
289
|
end
|
|
220
290
|
props.playSound(soundId)
|
|
221
291
|
end,
|
|
222
|
-
MouseEnter =
|
|
223
|
-
|
|
292
|
+
MouseEnter = function()
|
|
293
|
+
if props.static then
|
|
294
|
+
return nil
|
|
295
|
+
end
|
|
296
|
+
setHovered(true)
|
|
297
|
+
props.hover()
|
|
298
|
+
end,
|
|
299
|
+
MouseLeave = function()
|
|
300
|
+
if props.static then
|
|
301
|
+
return nil
|
|
302
|
+
end
|
|
303
|
+
setHovered(false)
|
|
304
|
+
props.unhover()
|
|
305
|
+
end,
|
|
224
306
|
},
|
|
225
307
|
Text = "",
|
|
226
|
-
BackgroundTransparency = props.backgroundTransparency,
|
|
308
|
+
BackgroundTransparency = stateful(props.backgroundTransparency, currentState),
|
|
227
309
|
BackgroundColor3 = stateful(props.backgroundColor, currentState),
|
|
228
|
-
key = "IconButton",
|
|
229
310
|
}, children, currentImage ~= nil and currentImage ~= "" and (React.createElement("imagelabel", {
|
|
230
|
-
key = "IconImage",
|
|
231
311
|
Size = UDim2.fromOffset(imageSize, imageSize),
|
|
232
|
-
Position = UDim2.fromOffset(
|
|
312
|
+
Position = UDim2.fromOffset(contentPadX, imagePosY),
|
|
233
313
|
Image = currentImage,
|
|
234
314
|
BackgroundTransparency = 1,
|
|
235
|
-
ImageColor3 = props.imageColor,
|
|
236
|
-
ImageTransparency = props.imageTransparency,
|
|
315
|
+
ImageColor3 = stateful(props.imageColor, currentState),
|
|
316
|
+
ImageTransparency = stateful(props.imageTransparency, currentState),
|
|
237
317
|
ImageRectOffset = stateful(props.imageRectOffset, currentState),
|
|
238
318
|
ImageRectSize = stateful(props.imageRectSize, currentState),
|
|
239
319
|
})), currentText ~= nil and currentText ~= "" and (React.createElement("textlabel", {
|
|
@@ -248,22 +328,20 @@ local function Icon(componentProps)
|
|
|
248
328
|
RichText = stateful(props.richText, currentState),
|
|
249
329
|
BackgroundTransparency = 1,
|
|
250
330
|
Text = currentText,
|
|
251
|
-
key = "IconText",
|
|
252
331
|
}, React.createElement("uistroke", {
|
|
253
|
-
key = "UIStroke",
|
|
254
332
|
Thickness = stateful(props.strokeThickness, currentState),
|
|
255
333
|
Color = stateful(props.strokeColor, currentState),
|
|
256
334
|
Transparency = stateful(props.strokeTransparency, currentState),
|
|
257
335
|
}))), React.createElement("uicorner", {
|
|
258
|
-
key = "UICorner",
|
|
259
336
|
CornerRadius = if location.type == "dropdown" then stylesheet.dropdown.iconCornerRadius else stateful(props.cornerRadius, currentState),
|
|
260
337
|
}), props.disabled and (React.createElement("frame", {
|
|
261
|
-
key = "DisabledOverlay",
|
|
262
338
|
Size = UDim2.fromScale(1, 1),
|
|
263
339
|
BackgroundTransparency = sizing.disabledOverlayTransparency,
|
|
264
340
|
BackgroundColor3 = sizing.disabledOverlayColor,
|
|
265
341
|
BorderSizePixel = 0,
|
|
266
342
|
ZIndex = 10,
|
|
343
|
+
})), componentProps.notificationCount ~= nil and (React.createElement(Notification, {
|
|
344
|
+
count = componentProps.notificationCount,
|
|
267
345
|
})))))
|
|
268
346
|
end
|
|
269
347
|
return {
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from '@rbxts/react';
|
|
2
|
+
/**
|
|
3
|
+
* A red notification badge that overlays an icon.
|
|
4
|
+
*
|
|
5
|
+
* Displays a count number. Values over 99 show "99+".
|
|
6
|
+
* Hides automatically when `count` is 0 or less.
|
|
7
|
+
*/
|
|
8
|
+
export declare function Notification({ count }: {
|
|
9
|
+
count: number;
|
|
10
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local React = TS.import(script, TS.getModule(script, "@rbxts", "react"))
|
|
4
|
+
local useStylesheet = TS.import(script, script.Parent.Parent, "context").useStylesheet
|
|
5
|
+
--[[
|
|
6
|
+
*
|
|
7
|
+
* A red notification badge that overlays an icon.
|
|
8
|
+
*
|
|
9
|
+
* Displays a count number. Values over 99 show "99+".
|
|
10
|
+
* Hides automatically when `count` is 0 or less.
|
|
11
|
+
|
|
12
|
+
]]
|
|
13
|
+
local function Notification(_param)
|
|
14
|
+
local count = _param.count
|
|
15
|
+
local stylesheet = useStylesheet().notification
|
|
16
|
+
if count <= 0 then
|
|
17
|
+
return React.createElement(React.Fragment)
|
|
18
|
+
end
|
|
19
|
+
local exceeded99 = count > 99
|
|
20
|
+
local displayText = if exceeded99 then "99+" else tostring(count)
|
|
21
|
+
return React.createElement("frame", {
|
|
22
|
+
Size = stylesheet.size,
|
|
23
|
+
Position = stylesheet.position,
|
|
24
|
+
AnchorPoint = Vector2.new(1, 0),
|
|
25
|
+
ZIndex = 25,
|
|
26
|
+
AutomaticSize = if exceeded99 then Enum.AutomaticSize.X else nil,
|
|
27
|
+
BackgroundColor3 = stylesheet.backgroundColor,
|
|
28
|
+
BackgroundTransparency = stylesheet.backgroundTransparency,
|
|
29
|
+
BorderSizePixel = 0,
|
|
30
|
+
}, React.createElement("uicorner", {
|
|
31
|
+
CornerRadius = stylesheet.cornerRadius,
|
|
32
|
+
}), React.createElement("uistroke", {
|
|
33
|
+
Color = stylesheet.borderColor,
|
|
34
|
+
Transparency = stylesheet.borderTransparency,
|
|
35
|
+
Thickness = 1,
|
|
36
|
+
}), React.createElement("textlabel", {
|
|
37
|
+
Size = UDim2.new(1, 0, 1, 0),
|
|
38
|
+
AnchorPoint = Vector2.new(0.5, 0.5),
|
|
39
|
+
Position = UDim2.new(0.5, 0, 0.515, 0),
|
|
40
|
+
AutomaticSize = if exceeded99 then Enum.AutomaticSize.X else nil,
|
|
41
|
+
BackgroundTransparency = 1,
|
|
42
|
+
FontFace = stylesheet.fontFace,
|
|
43
|
+
Text = displayText,
|
|
44
|
+
TextColor3 = stylesheet.textColor,
|
|
45
|
+
TextSize = stylesheet.textSize,
|
|
46
|
+
TextWrapped = true,
|
|
47
|
+
ZIndex = 26,
|
|
48
|
+
}))
|
|
49
|
+
end
|
|
50
|
+
return {
|
|
51
|
+
Notification = Notification,
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from '@rbxts/react';
|
|
2
|
+
/**
|
|
3
|
+
* Collects child icons and renders a "more" button (⋯) when they overflow
|
|
4
|
+
* the available space. Overflowing icons appear in a dropdown.
|
|
5
|
+
*
|
|
6
|
+
* Simplified from TopbarPlus v3 overflow handler.
|
|
7
|
+
*/
|
|
8
|
+
export declare function Overflow({ children }: {
|
|
9
|
+
children: React.ReactNode;
|
|
10
|
+
}): React.JSX.Element;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local _react = TS.import(script, TS.getModule(script, "@rbxts", "react"))
|
|
4
|
+
local React = _react
|
|
5
|
+
local useCallback = _react.useCallback
|
|
6
|
+
local useMemo = _react.useMemo
|
|
7
|
+
local useState = _react.useState
|
|
8
|
+
local useStylesheet = TS.import(script, script.Parent.Parent, "context").useStylesheet
|
|
9
|
+
local Icon = TS.import(script, script.Parent, "icon").Icon
|
|
10
|
+
--[[
|
|
11
|
+
*
|
|
12
|
+
* Collects child icons and renders a "more" button (⋯) when they overflow
|
|
13
|
+
* the available space. Overflowing icons appear in a dropdown.
|
|
14
|
+
*
|
|
15
|
+
* Simplified from TopbarPlus v3 overflow handler.
|
|
16
|
+
|
|
17
|
+
]]
|
|
18
|
+
local function Overflow(_param)
|
|
19
|
+
local children = _param.children
|
|
20
|
+
local stylesheet = useStylesheet()
|
|
21
|
+
local expanded, setExpanded = useState(false)
|
|
22
|
+
local childArray = useMemo(function()
|
|
23
|
+
local arr = React.Children.toArray(children)
|
|
24
|
+
return arr
|
|
25
|
+
end, { children })
|
|
26
|
+
local overflowCount = #childArray
|
|
27
|
+
local handleToggle = useCallback(function()
|
|
28
|
+
setExpanded(function(prev)
|
|
29
|
+
return not prev
|
|
30
|
+
end)
|
|
31
|
+
end, {})
|
|
32
|
+
if overflowCount == 0 then
|
|
33
|
+
return React.createElement(React.Fragment)
|
|
34
|
+
end
|
|
35
|
+
return React.createElement(Icon, {
|
|
36
|
+
imageId = "rbxassetid://6069276526",
|
|
37
|
+
static = false,
|
|
38
|
+
toggleStateOnClick = true,
|
|
39
|
+
contentPaddingX = 4,
|
|
40
|
+
contentPaddingY = 4,
|
|
41
|
+
}, expanded and (React.createElement("frame", {
|
|
42
|
+
Size = UDim2.new(0, 0, 0, 0),
|
|
43
|
+
Position = stylesheet.customDropdown.position or stylesheet.dropdownTheme.position,
|
|
44
|
+
BackgroundColor3 = stylesheet.dropdownTheme.backgroundColor,
|
|
45
|
+
BackgroundTransparency = stylesheet.dropdownTheme.backgroundTransparency,
|
|
46
|
+
BorderSizePixel = stylesheet.dropdownTheme.borderSize,
|
|
47
|
+
BorderColor3 = stylesheet.dropdownTheme.borderColor,
|
|
48
|
+
ZIndex = 50,
|
|
49
|
+
}, React.createElement("uicorner", {
|
|
50
|
+
CornerRadius = stylesheet.dropdownTheme.cornerRadius,
|
|
51
|
+
}), React.createElement("uistroke", {
|
|
52
|
+
Color = stylesheet.dropdownTheme.borderColor,
|
|
53
|
+
Transparency = stylesheet.dropdownTheme.borderTransparency,
|
|
54
|
+
Thickness = stylesheet.dropdownTheme.borderSize,
|
|
55
|
+
}), React.createElement("uilistlayout", {
|
|
56
|
+
SortOrder = Enum.SortOrder.LayoutOrder,
|
|
57
|
+
Padding = UDim.new(0, 2),
|
|
58
|
+
}), childArray)))
|
|
59
|
+
end
|
|
60
|
+
return {
|
|
61
|
+
Overflow = Overflow,
|
|
62
|
+
}
|
|
@@ -1,11 +1,26 @@
|
|
|
1
1
|
import React from '@rbxts/react';
|
|
2
2
|
export type SelectionMode = 'Single' | 'Multiple';
|
|
3
|
+
/** Docks icons to the left side of the topbar. */
|
|
3
4
|
export declare function LeftDock({ children }: React.PropsWithChildren): React.JSX.Element;
|
|
5
|
+
/** Docks icons to the center of the topbar. */
|
|
4
6
|
export declare function CenterDock({ children }: React.PropsWithChildren): React.JSX.Element;
|
|
7
|
+
/** Docks icons to the right side of the topbar. */
|
|
5
8
|
export declare function RightDock({ children }: React.PropsWithChildren): React.JSX.Element;
|
|
6
9
|
interface ProviderProps extends React.PropsWithChildren {
|
|
10
|
+
/**
|
|
11
|
+
* How icon selection works across the topbar.
|
|
12
|
+
* `Single` deselects all other icons when one is selected;
|
|
13
|
+
* `Multiple` allows any number to be selected at once.
|
|
14
|
+
*/
|
|
7
15
|
selectionMode?: SelectionMode;
|
|
16
|
+
/** When `true`, shows a "Beta" label if voice chat is enabled on the client. */
|
|
8
17
|
gameVoiceChatEnabled?: boolean;
|
|
9
18
|
}
|
|
10
|
-
|
|
19
|
+
/**
|
|
20
|
+
* Root provider for the topbar system.
|
|
21
|
+
*
|
|
22
|
+
* Must wrap all topbar components. Manages global selection state,
|
|
23
|
+
* gui inset tracking, and the topbar frame bounds.
|
|
24
|
+
*/
|
|
25
|
+
export declare function TopbarProvider({ selectionMode, gameVoiceChatEnabled, children }: ProviderProps): React.JSX.Element;
|
|
11
26
|
export {};
|
|
@@ -9,7 +9,12 @@ local useStylesheet = _context.useStylesheet
|
|
|
9
9
|
local useGuiInset = TS.import(script, script.Parent.Parent, "hooks", "use-gui-inset").useGuiInset
|
|
10
10
|
local useVoicechatEnabled = TS.import(script, script.Parent.Parent, "hooks", "use-voicechat-enabled").useVoicechatEnabled
|
|
11
11
|
local debugLog = TS.import(script, script.Parent.Parent, "utilities", "debug").debugLog
|
|
12
|
-
--
|
|
12
|
+
--[[
|
|
13
|
+
*
|
|
14
|
+
* Anchors children to a side of the topbar.
|
|
15
|
+
* Uses a horizontal list layout with icon spacing from the stylesheet.
|
|
16
|
+
|
|
17
|
+
]]
|
|
13
18
|
local function DockFrame(props)
|
|
14
19
|
local stylesheet = useStylesheet().provider
|
|
15
20
|
return React.createElement("frame", {
|
|
@@ -28,6 +33,7 @@ local function DockFrame(props)
|
|
|
28
33
|
VerticalAlignment = Enum.VerticalAlignment.Center,
|
|
29
34
|
}), props.children)
|
|
30
35
|
end
|
|
36
|
+
--* Docks icons to the left side of the topbar.
|
|
31
37
|
local function LeftDock(_param)
|
|
32
38
|
local children = _param.children
|
|
33
39
|
return React.createElement(DockFrame, {
|
|
@@ -36,6 +42,7 @@ local function LeftDock(_param)
|
|
|
36
42
|
children = children,
|
|
37
43
|
})
|
|
38
44
|
end
|
|
45
|
+
--* Docks icons to the center of the topbar.
|
|
39
46
|
local function CenterDock(_param)
|
|
40
47
|
local children = _param.children
|
|
41
48
|
local stylesheet = useStylesheet().provider
|
|
@@ -47,6 +54,7 @@ local function CenterDock(_param)
|
|
|
47
54
|
children = children,
|
|
48
55
|
})
|
|
49
56
|
end
|
|
57
|
+
--* Docks icons to the right side of the topbar.
|
|
50
58
|
local function RightDock(_param)
|
|
51
59
|
local children = _param.children
|
|
52
60
|
local stylesheet = useStylesheet().provider
|
|
@@ -57,7 +65,14 @@ local function RightDock(_param)
|
|
|
57
65
|
children = children,
|
|
58
66
|
})
|
|
59
67
|
end
|
|
60
|
-
--
|
|
68
|
+
--[[
|
|
69
|
+
*
|
|
70
|
+
* Root provider for the topbar system.
|
|
71
|
+
*
|
|
72
|
+
* Must wrap all topbar components. Manages global selection state,
|
|
73
|
+
* gui inset tracking, and the topbar frame bounds.
|
|
74
|
+
|
|
75
|
+
]]
|
|
61
76
|
local function TopbarProvider(_param)
|
|
62
77
|
local selectionMode = _param.selectionMode
|
|
63
78
|
if selectionMode == nil then
|
|
@@ -120,14 +135,12 @@ local function TopbarProvider(_param)
|
|
|
120
135
|
end,
|
|
121
136
|
},
|
|
122
137
|
}, React.createElement("frame", {
|
|
123
|
-
key = "TopbarProvider",
|
|
124
138
|
BackgroundTransparency = stylesheet.backgroundTransparency,
|
|
125
139
|
BackgroundColor3 = stylesheet.backgroundColor,
|
|
126
140
|
Size = UDim2.fromOffset(inset.Width * stylesheet.sizeScale.X, frameHeight * stylesheet.sizeScale.Y),
|
|
127
141
|
AnchorPoint = stylesheet.anchorPoint,
|
|
128
142
|
Position = stylesheet.position,
|
|
129
143
|
}, React.createElement("uipadding", {
|
|
130
|
-
key = "UIPadding",
|
|
131
144
|
PaddingLeft = UDim.new(0, leftPadding),
|
|
132
145
|
PaddingRight = UDim.new(0, stylesheet.paddingRight),
|
|
133
146
|
PaddingTop = UDim.new(0, stylesheet.paddingTop),
|
|
@@ -2,9 +2,16 @@ import React from '@rbxts/react';
|
|
|
2
2
|
import { DefaultStylesheet, type Stylesheet as StylesheetType } from '../style';
|
|
3
3
|
import type { DeepPartial } from '../utilities/types';
|
|
4
4
|
interface Props extends React.PropsWithChildren {
|
|
5
|
+
/** A partial stylesheet that is deep-merged over the defaults. */
|
|
5
6
|
stylesheet: PartialStylesheet;
|
|
6
7
|
}
|
|
7
8
|
type PartialStylesheet = DeepPartial<StylesheetType>;
|
|
8
9
|
export { DefaultStylesheet };
|
|
9
10
|
export type { StylesheetType };
|
|
11
|
+
/**
|
|
12
|
+
* Applies a custom stylesheet to all descendant topbar components.
|
|
13
|
+
*
|
|
14
|
+
* Only the properties you provide will override the defaults — missing
|
|
15
|
+
* keys fall back to {@link DefaultStylesheet}.
|
|
16
|
+
*/
|
|
10
17
|
export declare function Stylesheet({ stylesheet, children }: Props): React.JSX.Element;
|
|
@@ -3,7 +3,15 @@ local TS = _G[script]
|
|
|
3
3
|
local React = TS.import(script, TS.getModule(script, "@rbxts", "react"))
|
|
4
4
|
local StylesheetContext = TS.import(script, script.Parent.Parent, "context").StylesheetContext
|
|
5
5
|
local DefaultStylesheet = TS.import(script, script.Parent.Parent, "style").DefaultStylesheet
|
|
6
|
-
local reconcile = TS.import(script, script.Parent.Parent, "utilities", "merge")
|
|
6
|
+
local reconcile = TS.import(script, script.Parent.Parent, "utilities", "merge").default
|
|
7
|
+
--[[
|
|
8
|
+
*
|
|
9
|
+
* Applies a custom stylesheet to all descendant topbar components.
|
|
10
|
+
*
|
|
11
|
+
* Only the properties you provide will override the defaults — missing
|
|
12
|
+
* keys fall back to {@link DefaultStylesheet}.
|
|
13
|
+
|
|
14
|
+
]]
|
|
7
15
|
local function Stylesheet(_param)
|
|
8
16
|
local stylesheet = _param.stylesheet
|
|
9
17
|
local children = _param.children
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import React from '@rbxts/react';
|
|
2
|
+
type TooltipSide = 'top' | 'bottom' | 'left' | 'right';
|
|
3
|
+
interface TooltipProps {
|
|
4
|
+
/** Text displayed inside the tooltip. */
|
|
5
|
+
content: string;
|
|
6
|
+
/** The element that triggers the tooltip on hover. */
|
|
7
|
+
children: React.ReactNode;
|
|
8
|
+
/** Which side of the child the tooltip appears on. */
|
|
9
|
+
side?: TooltipSide;
|
|
10
|
+
/** Delay in milliseconds before the tooltip becomes visible. */
|
|
11
|
+
delayMs?: number;
|
|
12
|
+
}
|
|
13
|
+
export declare function Tooltip({ content, children, side, delayMs }: TooltipProps): React.JSX.Element;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local _react = TS.import(script, TS.getModule(script, "@rbxts", "react"))
|
|
4
|
+
local React = _react
|
|
5
|
+
local useState = _react.useState
|
|
6
|
+
local useStylesheet = TS.import(script, script.Parent.Parent, "context").useStylesheet
|
|
7
|
+
local positionMap = {
|
|
8
|
+
top = UDim2.new(0.5, 0, 0, -4),
|
|
9
|
+
bottom = UDim2.new(0.5, 0, 1, 4),
|
|
10
|
+
left = UDim2.new(0, -4, 0.5, 0),
|
|
11
|
+
right = UDim2.new(1, 4, 0.5, 0),
|
|
12
|
+
}
|
|
13
|
+
local anchorMap = {
|
|
14
|
+
top = Vector2.new(0.5, 1),
|
|
15
|
+
bottom = Vector2.new(0.5, 0),
|
|
16
|
+
left = Vector2.new(1, 0.5),
|
|
17
|
+
right = Vector2.new(0, 0.5),
|
|
18
|
+
}
|
|
19
|
+
local function Tooltip(_param)
|
|
20
|
+
local content = _param.content
|
|
21
|
+
local children = _param.children
|
|
22
|
+
local side = _param.side
|
|
23
|
+
if side == nil then
|
|
24
|
+
side = "top"
|
|
25
|
+
end
|
|
26
|
+
local delayMs = _param.delayMs
|
|
27
|
+
local stylesheet = useStylesheet().tooltip
|
|
28
|
+
local visible, setVisible = useState(false)
|
|
29
|
+
local showTimer, setShowTimer = useState(nil)
|
|
30
|
+
local _condition = delayMs
|
|
31
|
+
if _condition == nil then
|
|
32
|
+
_condition = stylesheet.delayMs
|
|
33
|
+
end
|
|
34
|
+
local delay = _condition
|
|
35
|
+
local handleMouseEnter = function()
|
|
36
|
+
local timer = task.delay(delay / 1000, function()
|
|
37
|
+
setVisible(true)
|
|
38
|
+
end)
|
|
39
|
+
setShowTimer(timer)
|
|
40
|
+
end
|
|
41
|
+
local handleMouseLeave = function()
|
|
42
|
+
if showTimer then
|
|
43
|
+
task.cancel(showTimer)
|
|
44
|
+
end
|
|
45
|
+
setVisible(false)
|
|
46
|
+
end
|
|
47
|
+
return React.createElement("frame", {
|
|
48
|
+
Size = UDim2.new(0, 0, 0, 0),
|
|
49
|
+
AutomaticSize = Enum.AutomaticSize.XY,
|
|
50
|
+
BackgroundTransparency = 1,
|
|
51
|
+
ClipsDescendants = true,
|
|
52
|
+
}, React.createElement("textbutton", {
|
|
53
|
+
Size = UDim2.new(0, 0, 0, 0),
|
|
54
|
+
AutomaticSize = Enum.AutomaticSize.XY,
|
|
55
|
+
AutoButtonColor = false,
|
|
56
|
+
Text = "",
|
|
57
|
+
BackgroundTransparency = 1,
|
|
58
|
+
BorderSizePixel = 0,
|
|
59
|
+
Event = {
|
|
60
|
+
MouseEnter = handleMouseEnter,
|
|
61
|
+
MouseLeave = handleMouseLeave,
|
|
62
|
+
},
|
|
63
|
+
}, children), visible and (React.createElement("frame", {
|
|
64
|
+
Position = positionMap[side],
|
|
65
|
+
AnchorPoint = anchorMap[side],
|
|
66
|
+
Size = UDim2.new(0, 0, 0, 0),
|
|
67
|
+
AutomaticSize = Enum.AutomaticSize.XY,
|
|
68
|
+
BackgroundColor3 = stylesheet.backgroundColor,
|
|
69
|
+
BackgroundTransparency = stylesheet.backgroundTransparency,
|
|
70
|
+
BorderSizePixel = 0,
|
|
71
|
+
ZIndex = 100,
|
|
72
|
+
ClipsDescendants = true,
|
|
73
|
+
}, React.createElement("uicorner", {
|
|
74
|
+
CornerRadius = stylesheet.cornerRadius,
|
|
75
|
+
}), React.createElement("uistroke", {
|
|
76
|
+
Color = stylesheet.borderColor,
|
|
77
|
+
Transparency = stylesheet.borderTransparency,
|
|
78
|
+
Thickness = stylesheet.borderSize,
|
|
79
|
+
}), React.createElement("uipadding", {
|
|
80
|
+
PaddingTop = UDim.new(0, stylesheet.paddingY),
|
|
81
|
+
PaddingBottom = UDim.new(0, stylesheet.paddingY),
|
|
82
|
+
PaddingLeft = UDim.new(0, stylesheet.paddingX),
|
|
83
|
+
PaddingRight = UDim.new(0, stylesheet.paddingX),
|
|
84
|
+
}), React.createElement("textlabel", {
|
|
85
|
+
Size = UDim2.new(0, 0, 0, 0),
|
|
86
|
+
AutomaticSize = Enum.AutomaticSize.XY,
|
|
87
|
+
BackgroundTransparency = 1,
|
|
88
|
+
BorderSizePixel = 0,
|
|
89
|
+
FontFace = stylesheet.fontFace,
|
|
90
|
+
Text = content,
|
|
91
|
+
TextColor3 = stylesheet.textColor,
|
|
92
|
+
TextSize = stylesheet.textSize,
|
|
93
|
+
TextXAlignment = Enum.TextXAlignment.Center,
|
|
94
|
+
TextYAlignment = Enum.TextYAlignment.Center,
|
|
95
|
+
}))))
|
|
96
|
+
end
|
|
97
|
+
return {
|
|
98
|
+
Tooltip = Tooltip,
|
|
99
|
+
}
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { type Binding } from '@rbxts/react';
|
|
2
|
-
import type { MotionGoal } from '@rbxts/ripple';
|
|
3
2
|
import type { FromStateDependent, IconState, StateDependent } from '../components/icon';
|
|
4
|
-
type Result<T extends Record<string, StateDependent<
|
|
3
|
+
type Result<T extends Record<string, StateDependent<unknown>>, K extends keyof T> = ExcludeMembers<{
|
|
5
4
|
[P in keyof T]: P extends K ? Binding<NonNullable<FromStateDependent<T[P]>>> : undefined;
|
|
6
5
|
}, undefined>;
|
|
7
|
-
export declare function useAnimateableProps<T extends Record<string, StateDependent<
|
|
6
|
+
export declare function useAnimateableProps<T extends Record<string, StateDependent<unknown>>, K extends keyof T>(state: IconState, props: T, ...keys: K[]): Result<T, K>;
|
|
8
7
|
export {};
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
-- Compiled with roblox-ts v3.0.0
|
|
2
2
|
local TS = _G[script]
|
|
3
|
-
local
|
|
3
|
+
local object = TS.import(script, TS.getModule(script, "@rbxts", "object-utils"))
|
|
4
4
|
local _pretty_react_hooks = TS.import(script, TS.getModule(script, "@rbxts", "pretty-react-hooks").out)
|
|
5
5
|
local useMotion = _pretty_react_hooks.useMotion
|
|
6
6
|
local usePrevious = _pretty_react_hooks.usePrevious
|
|
@@ -10,36 +10,34 @@ local springs = TS.import(script, script.Parent.Parent, "utilities", "springs").
|
|
|
10
10
|
local function useAnimateableProps(state, props, ...)
|
|
11
11
|
local keys = { ... }
|
|
12
12
|
local previousProps = usePrevious(props)
|
|
13
|
-
local
|
|
13
|
+
local motionBindings = {}
|
|
14
|
+
local motionControls = {}
|
|
14
15
|
for _, key in keys do
|
|
15
16
|
local binding, motion = useMotion(stateful(props[key], state))
|
|
16
|
-
|
|
17
|
-
table.insert(
|
|
17
|
+
table.insert(motionBindings, binding)
|
|
18
|
+
table.insert(motionControls, motion)
|
|
18
19
|
end
|
|
19
20
|
useEffect(function()
|
|
20
|
-
for
|
|
21
|
+
for i = 0, #keys - 1 do
|
|
22
|
+
local key = keys[i + 1]
|
|
21
23
|
local value = stateful(props[key], state)
|
|
22
24
|
local previousValue = stateful(previousProps, state)
|
|
23
25
|
if value == previousValue then
|
|
24
26
|
continue
|
|
25
27
|
end
|
|
26
|
-
|
|
28
|
+
motionControls[i + 1].spring(value, springs.responsive)
|
|
27
29
|
end
|
|
28
30
|
end, { props })
|
|
29
31
|
-- ▼ ReadonlyArray.map ▼
|
|
30
32
|
local _newValue = table.create(#keys)
|
|
31
|
-
local _callback = function(key)
|
|
32
|
-
|
|
33
|
-
local _exp_1 = motions
|
|
34
|
-
local _keys = keys
|
|
35
|
-
local _key = key
|
|
36
|
-
return { _exp, _exp_1[table.find(_keys, _key) or 0][1] }
|
|
33
|
+
local _callback = function(key, i)
|
|
34
|
+
return { key, motionBindings[i + 1] }
|
|
37
35
|
end
|
|
38
36
|
for _k, _v in keys do
|
|
39
37
|
_newValue[_k] = _callback(_v, _k - 1, keys)
|
|
40
38
|
end
|
|
41
39
|
-- ▲ ReadonlyArray.map ▲
|
|
42
|
-
return
|
|
40
|
+
return object.fromEntries(_newValue)
|
|
43
41
|
end
|
|
44
42
|
return {
|
|
45
43
|
useAnimateableProps = useAnimateableProps,
|