@nrbx/topbar-components 1.0.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/LICENSE.txt +20 -0
- package/README.md +116 -0
- package/out/components/dropdown.d.ts +18 -0
- package/out/components/dropdown.luau +193 -0
- package/out/components/icon.d.ts +40 -0
- package/out/components/icon.luau +237 -0
- package/out/components/provider.d.ts +8 -0
- package/out/components/provider.luau +92 -0
- package/out/components/stylesheet.d.ts +10 -0
- package/out/components/stylesheet.luau +17 -0
- package/out/context.d.ts +29 -0
- package/out/context.luau +20 -0
- package/out/hooks/use-animateable-props.d.ts +8 -0
- package/out/hooks/use-animateable-props.luau +46 -0
- package/out/hooks/use-gui-inset.d.ts +1 -0
- package/out/hooks/use-gui-inset.luau +20 -0
- package/out/hooks/use-id.d.ts +1 -0
- package/out/hooks/use-id.luau +13 -0
- package/out/hooks/use-voicechat-enabled.d.ts +1 -0
- package/out/hooks/use-voicechat-enabled.luau +17 -0
- package/out/index.d.ts +4 -0
- package/out/init.luau +16 -0
- package/out/style.d.ts +70 -0
- package/out/style.luau +117 -0
- package/out/utilities/id-gen.d.ts +1 -0
- package/out/utilities/id-gen.luau +12 -0
- package/out/utilities/merge.d.ts +2 -0
- package/out/utilities/merge.luau +29 -0
- package/out/utilities/resolve-state-dependent.d.ts +2 -0
- package/out/utilities/resolve-state-dependent.luau +17 -0
- package/out/utilities/springs.d.ts +23 -0
- package/out/utilities/springs.luau +24 -0
- package/out/utilities/types.d.ts +5 -0
- package/out/utilities/types.luau +2 -0
- package/package.json +47 -0
package/LICENSE.txt
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 wad4444
|
|
4
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
5
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
6
|
+
in the Software without restriction, including without limitation the rights
|
|
7
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
8
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
9
|
+
furnished to do so, subject to the following conditions:
|
|
10
|
+
|
|
11
|
+
The above copyright notice and this permission notice shall be included in all
|
|
12
|
+
copies or substantial portions of the Software.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
16
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
17
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
18
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
19
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
20
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
<div align="center" id="top">
|
|
2
|
+
<img src="https://github.com/nn140/Branding/blob/main/LogoWhite-Full.png?raw=true" alt="NN140.UK logo" width="800"/>
|
|
3
|
+
<img src="https://github.com/nn140/Branding/blob/main/LogoBlack-Full.png?raw=true" alt="NN140.UK logo" width="800"/>
|
|
4
|
+
<br />
|
|
5
|
+
<br />
|
|
6
|
+
<img src="https://img.shields.io/badge/Stripe-Donate%20to%20support%20NN140.UK-1b1b1b?style=for-the-badge&labelColor=6860ff&logo=stripe&logoColor=ffffff&logoSize=auto&link=https%3A%2F%2Fdonate.stripe.com%2F9B6eVdbTd4n1a6H1yXa3u04&link=https%3A%2F%2Fdonate.stripe.com%2F9B6eVdbTd4n1a6H1yXa3u04" alt="Badge">
|
|
7
|
+
<img src="https://img.shields.io/badge/Stripe-Donate%20to%20Support%20NN140.UK%20(RECCURING)-1b1b1b?style=for-the-badge&labelColor=6860ff&logo=stripe&logoColor=ffffff&logoSize=auto&link=https%3A%2F%2Fdonate.stripe.com%2FdRm9ATe1laLpgv5b9xa3u05&link=https%3A%2F%2Fdonate.stripe.com%2FdRm9ATe1laLpgv5b9xa3u05" alt="Badge">
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
<hr />
|
|
11
|
+
|
|
12
|
+
## @nrbx/topbar-components
|
|
13
|
+
|
|
14
|
+
- A Fork of @rbxts/topbar-components
|
|
15
|
+
|
|
16
|
+
**Topbar Components** is a react component package that mimics [*topbar-plus*](https://devforum.roblox.com/t/v3-topbarplus-v300-construct-intuitive-topbar-icons-customise-them-with-themes-dropdowns-captions-labels-and-much-more/1017485) for [Roblox-TS](https://roblox-ts.com), with JSX markup support.
|
|
17
|
+
|
|
18
|
+
## 📦 Installation
|
|
19
|
+
|
|
20
|
+
**@nrbx/topbar-components** is available on NPM and can be installed with the following commands:
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
npm install @nrbx/topbar-components
|
|
24
|
+
yarn add @nrbx/topbar-components
|
|
25
|
+
pnpm add @nrbx/topbar-components
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Then add the following to your Rojo project file, under your `node_modules` configuration.
|
|
29
|
+
|
|
30
|
+
```json
|
|
31
|
+
"node_modules": {
|
|
32
|
+
"$className": "Folder",
|
|
33
|
+
"@rbxts": {
|
|
34
|
+
"$path": "node_modules/@rbxts"
|
|
35
|
+
},
|
|
36
|
+
"@nrbx": {
|
|
37
|
+
"$path": "node_modules/@nrbx"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
And this to your `tsconfig.json`
|
|
43
|
+
|
|
44
|
+
```json
|
|
45
|
+
"typeRoots": ["node_modules/@rbxts", "node_modules/@nrbx"],
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### ⚡ Quick Start
|
|
49
|
+
|
|
50
|
+
Instantiate `<TopbarProvider />` to be a root of your topbar component tree.
|
|
51
|
+
|
|
52
|
+
```jsx
|
|
53
|
+
<TopbarProvider>
|
|
54
|
+
<Icon text="Hello, World!" />
|
|
55
|
+
</TopbarProvider>
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Every `<Icon />` can be in only two states `selected`, and `deselected`.
|
|
59
|
+
You can conditionally apply properties based on icon's current state, by providing a state markup object:
|
|
60
|
+
|
|
61
|
+
```jsx
|
|
62
|
+
<Icon text={{
|
|
63
|
+
selected: "Selected!",
|
|
64
|
+
deselected: "Deselected!",
|
|
65
|
+
}} />
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
You can add a dropdown to an icon by mounting `<Dropdown />` component as it's child:
|
|
69
|
+
Dropdowns & TopbarProvider have a property called `selectionMode`, which lets you specify how many icons can be selected at once.
|
|
70
|
+
|
|
71
|
+
```jsx
|
|
72
|
+
<Icon text="Skins">
|
|
73
|
+
<Dropdown selectionMode="single">
|
|
74
|
+
<Icon text="yellow" selected={() => chooseSkin("yellow")} />
|
|
75
|
+
<Icon text="red" selected={() => chooseSkin("red")} />
|
|
76
|
+
</Dropdown>
|
|
77
|
+
</Icon>
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Dropdowns **can be nested.**
|
|
81
|
+
|
|
82
|
+
### 🎨 Stylesheets
|
|
83
|
+
|
|
84
|
+
You can use stylesheets to override default properties of all components within:
|
|
85
|
+
Stylesheets are partial, and work like patches to already established default properties within the package:
|
|
86
|
+
|
|
87
|
+
```jsx
|
|
88
|
+
<Stylesheet stylesheet={{
|
|
89
|
+
icon: {
|
|
90
|
+
textSize: 25,
|
|
91
|
+
cornerRadius: new UDim(0.5, 0),
|
|
92
|
+
}
|
|
93
|
+
}}>
|
|
94
|
+
<Icon text="Skins">
|
|
95
|
+
<Dropdown selectionMode="single">
|
|
96
|
+
<Icon text="yellow" selected={() => chooseSkin("yellow")} />
|
|
97
|
+
<Icon text="red" selected={() => chooseSkin("red")} />
|
|
98
|
+
</Dropdown>
|
|
99
|
+
</Icon>
|
|
100
|
+
</Stylesheet>
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 📝 License
|
|
104
|
+
|
|
105
|
+
Package is licensed under the MIT License.
|
|
106
|
+
|
|
107
|
+
<hr />
|
|
108
|
+
|
|
109
|
+
<div align="center" id="top">
|
|
110
|
+
<img src="https://img.shields.io/badge/Stripe-Donate%20to%20support%20NN140.UK-1b1b1b?style=for-the-badge&labelColor=6860ff&logo=stripe&logoColor=ffffff&logoSize=auto&link=https%3A%2F%2Fdonate.stripe.com%2F9B6eVdbTd4n1a6H1yXa3u04&link=https%3A%2F%2Fdonate.stripe.com%2F9B6eVdbTd4n1a6H1yXa3u04" alt="Badge">
|
|
111
|
+
<img src="https://img.shields.io/badge/Stripe-Donate%20to%20Support%20NN140.UK%20(RECCURING)-1b1b1b?style=for-the-badge&labelColor=6860ff&logo=stripe&logoColor=ffffff&logoSize=auto&link=https%3A%2F%2Fdonate.stripe.com%2FdRm9ATe1laLpgv5b9xa3u05&link=https%3A%2F%2Fdonate.stripe.com%2FdRm9ATe1laLpgv5b9xa3u05" alt="Badge">
|
|
112
|
+
<br />
|
|
113
|
+
<br />
|
|
114
|
+
<img src="https://github.com/nn140/Branding/blob/main/LogoBlack-Full.png?raw=true" alt="NN140.UK logo" width="800"/>
|
|
115
|
+
<img src="https://github.com/nn140/Branding/blob/main/LogoWhite-Full.png?raw=true" alt="NN140.UK logo" width="800"/>
|
|
116
|
+
</div>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import React from '@rbxts/react';
|
|
2
|
+
import type { SelectionMode } from './provider';
|
|
3
|
+
export interface DropdownProps extends React.PropsWithChildren {
|
|
4
|
+
minWidth?: number;
|
|
5
|
+
maxHeight?: number;
|
|
6
|
+
maxWidth?: number;
|
|
7
|
+
padding?: UDim;
|
|
8
|
+
forceHeight?: number;
|
|
9
|
+
iconCornerRadius?: UDim;
|
|
10
|
+
scrollBarThickness?: number;
|
|
11
|
+
scrollBarTransparency?: number;
|
|
12
|
+
topImage?: string;
|
|
13
|
+
bottomImage?: string;
|
|
14
|
+
midImage?: string;
|
|
15
|
+
scrollBarImageColor?: Color3;
|
|
16
|
+
selectionMode?: SelectionMode;
|
|
17
|
+
}
|
|
18
|
+
export declare function Dropdown(componentProps: DropdownProps): React.JSX.Element;
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v1.0.2
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local _pretty_react_hooks = TS.import(script, TS.getModule(script, "@rbxts", "pretty-react-hooks").out)
|
|
4
|
+
local mapBinding = _pretty_react_hooks.mapBinding
|
|
5
|
+
local useMotion = _pretty_react_hooks.useMotion
|
|
6
|
+
local useMountEffect = _pretty_react_hooks.useMountEffect
|
|
7
|
+
local _react = TS.import(script, TS.getModule(script, "@rbxts", "react"))
|
|
8
|
+
local React = _react
|
|
9
|
+
local useEffect = _react.useEffect
|
|
10
|
+
local useMemo = _react.useMemo
|
|
11
|
+
local useState = _react.useState
|
|
12
|
+
local _context = TS.import(script, script.Parent.Parent, "context")
|
|
13
|
+
local LocationContext = _context.LocationContext
|
|
14
|
+
local useLocation = _context.useLocation
|
|
15
|
+
local useStylesheet = _context.useStylesheet
|
|
16
|
+
local function Dropdown(componentProps)
|
|
17
|
+
local location = useLocation()
|
|
18
|
+
local fullStylesheet = useStylesheet()
|
|
19
|
+
local stylesheet = fullStylesheet.dropdown
|
|
20
|
+
local selectedIcons, setSelectedIcons = useState({})
|
|
21
|
+
local contents, setContents = useState({})
|
|
22
|
+
local _arg0 = location.type == "icon"
|
|
23
|
+
assert(_arg0, "Dropdowns can only be located under icons")
|
|
24
|
+
local transition, transitionMotion = useMotion(if location.isVisible then 1 else 0)
|
|
25
|
+
local _object = table.clone(stylesheet)
|
|
26
|
+
setmetatable(_object, nil)
|
|
27
|
+
for _k, _v in componentProps do
|
|
28
|
+
_object[_k] = _v
|
|
29
|
+
end
|
|
30
|
+
local props = _object
|
|
31
|
+
local isNested = location.isUnderDropdown
|
|
32
|
+
local maxWidth = if isNested then location.width else props.maxWidth
|
|
33
|
+
local minWidth = if isNested then location.width else props.minWidth
|
|
34
|
+
local maxHeight = props.maxHeight
|
|
35
|
+
local contentSize = useMemo(function()
|
|
36
|
+
local y = 0
|
|
37
|
+
local x = minWidth
|
|
38
|
+
for _, size in contents do
|
|
39
|
+
x = math.min(maxWidth, math.max(x, size.X))
|
|
40
|
+
y += size.Y + stylesheet.padding.Offset
|
|
41
|
+
end
|
|
42
|
+
return Vector2.new(x, y)
|
|
43
|
+
end, { contents, maxWidth, minWidth, stylesheet.padding.Offset })
|
|
44
|
+
useEffect(function()
|
|
45
|
+
location.setAnimationState(true)
|
|
46
|
+
transitionMotion:linear(if location.isVisible then 1 else 0, {
|
|
47
|
+
speed = fullStylesheet.animation.dropdownTransitionSpeed,
|
|
48
|
+
})
|
|
49
|
+
end, { location.isVisible })
|
|
50
|
+
useMountEffect(function()
|
|
51
|
+
return transitionMotion:onComplete(function()
|
|
52
|
+
return location.setAnimationState(false)
|
|
53
|
+
end)
|
|
54
|
+
end)
|
|
55
|
+
useEffect(function()
|
|
56
|
+
location.setContentSize(contentSize)
|
|
57
|
+
end, { contentSize, location.setContentSize })
|
|
58
|
+
local scrollingEnabled = not isNested and contentSize.Y > maxHeight
|
|
59
|
+
return React.createElement(LocationContext.Provider, {
|
|
60
|
+
value = {
|
|
61
|
+
type = "dropdown",
|
|
62
|
+
selectedIcons = selectedIcons,
|
|
63
|
+
iconSelected = function(iconId)
|
|
64
|
+
if props.selectionMode == "Single" then
|
|
65
|
+
return setSelectedIcons({ iconId })
|
|
66
|
+
end
|
|
67
|
+
return setSelectedIcons(function(icons)
|
|
68
|
+
local _array = {}
|
|
69
|
+
local _length = #_array
|
|
70
|
+
local _iconsLength = #icons
|
|
71
|
+
table.move(icons, 1, _iconsLength, _length + 1, _array)
|
|
72
|
+
_length += _iconsLength
|
|
73
|
+
_array[_length + 1] = iconId
|
|
74
|
+
return _array
|
|
75
|
+
end)
|
|
76
|
+
end,
|
|
77
|
+
iconDeselected = function(iconId)
|
|
78
|
+
local _condition = props.selectionMode == "Single"
|
|
79
|
+
if _condition then
|
|
80
|
+
local _iconId = iconId
|
|
81
|
+
_condition = table.find(selectedIcons, _iconId) ~= nil
|
|
82
|
+
end
|
|
83
|
+
if _condition then
|
|
84
|
+
return setSelectedIcons({})
|
|
85
|
+
end
|
|
86
|
+
return setSelectedIcons(function(icons)
|
|
87
|
+
-- ▼ ReadonlyArray.filter ▼
|
|
88
|
+
local _newValue = {}
|
|
89
|
+
local _callback = function(T)
|
|
90
|
+
return T ~= iconId
|
|
91
|
+
end
|
|
92
|
+
local _length = 0
|
|
93
|
+
for _k, _v in icons do
|
|
94
|
+
if _callback(_v, _k - 1, icons) == true then
|
|
95
|
+
_length += 1
|
|
96
|
+
_newValue[_length] = _v
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
-- ▲ ReadonlyArray.filter ▲
|
|
100
|
+
return _newValue
|
|
101
|
+
end)
|
|
102
|
+
end,
|
|
103
|
+
registerChild = function(id, size)
|
|
104
|
+
setContents(function(contents)
|
|
105
|
+
local _array = {}
|
|
106
|
+
local _length = #_array
|
|
107
|
+
for _k, _v in contents do
|
|
108
|
+
_length += 1
|
|
109
|
+
_array[_length] = { _k, _v }
|
|
110
|
+
end
|
|
111
|
+
_array[_length + 1] = { id, size }
|
|
112
|
+
local _map = {}
|
|
113
|
+
for _, _v in _array do
|
|
114
|
+
_map[_v[1]] = _v[2]
|
|
115
|
+
end
|
|
116
|
+
return _map
|
|
117
|
+
end)
|
|
118
|
+
end,
|
|
119
|
+
removeChild = function(id)
|
|
120
|
+
setContents(function(contents)
|
|
121
|
+
local _array = {}
|
|
122
|
+
local _length = #_array
|
|
123
|
+
for _k, _v in contents do
|
|
124
|
+
_length += 1
|
|
125
|
+
_array[_length] = { _k, _v }
|
|
126
|
+
end
|
|
127
|
+
-- ▼ ReadonlyArray.filter ▼
|
|
128
|
+
local _newValue = {}
|
|
129
|
+
local _callback = function(T)
|
|
130
|
+
return T[1] ~= id
|
|
131
|
+
end
|
|
132
|
+
local _length_1 = 0
|
|
133
|
+
for _k, _v in _array do
|
|
134
|
+
if _callback(_v, _k - 1, _array) == true then
|
|
135
|
+
_length_1 += 1
|
|
136
|
+
_newValue[_length_1] = _v
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
-- ▲ ReadonlyArray.filter ▲
|
|
140
|
+
local _map = {}
|
|
141
|
+
for _, _v in _newValue do
|
|
142
|
+
_map[_v[1]] = _v[2]
|
|
143
|
+
end
|
|
144
|
+
return _map
|
|
145
|
+
end)
|
|
146
|
+
end,
|
|
147
|
+
desiredIconWidth = if isNested then location.width else contentSize.X,
|
|
148
|
+
},
|
|
149
|
+
}, React.createElement("scrollingframe", {
|
|
150
|
+
ClipsDescendants = true,
|
|
151
|
+
Size = mapBinding(transition, function(t)
|
|
152
|
+
return UDim2.fromOffset(contentSize.X + (if scrollingEnabled then props.scrollBarThickness else 0), t * math.min(contentSize.Y, if isNested then contentSize.Y else maxHeight))
|
|
153
|
+
end),
|
|
154
|
+
BorderSizePixel = fullStylesheet.dropdownTheme.borderSize,
|
|
155
|
+
BorderColor3 = fullStylesheet.dropdownTheme.borderColor,
|
|
156
|
+
BackgroundColor3 = fullStylesheet.dropdownTheme.backgroundColor,
|
|
157
|
+
Position = fullStylesheet.dropdownTheme.position,
|
|
158
|
+
ScrollBarImageColor3 = props.scrollBarImageColor,
|
|
159
|
+
ScrollBarImageTransparency = if scrollingEnabled and location.isVisible then props.scrollBarTransparency else 1,
|
|
160
|
+
ScrollingEnabled = scrollingEnabled,
|
|
161
|
+
AutomaticCanvasSize = Enum.AutomaticSize.None,
|
|
162
|
+
CanvasSize = UDim2.fromOffset(0, contentSize.Y),
|
|
163
|
+
ScrollBarThickness = if scrollingEnabled then props.scrollBarThickness else 0,
|
|
164
|
+
BackgroundTransparency = fullStylesheet.dropdownTheme.backgroundTransparency,
|
|
165
|
+
Change = {
|
|
166
|
+
AbsoluteSize = function(rbx)
|
|
167
|
+
return location.setDropdownSize(rbx.AbsoluteSize)
|
|
168
|
+
end,
|
|
169
|
+
},
|
|
170
|
+
MidImage = props.midImage,
|
|
171
|
+
TopImage = props.topImage,
|
|
172
|
+
BottomImage = props.bottomImage,
|
|
173
|
+
key = "Dropdown",
|
|
174
|
+
}, React.createElement("uicorner", {
|
|
175
|
+
key = "DropdownCorner",
|
|
176
|
+
CornerRadius = fullStylesheet.dropdownTheme.cornerRadius,
|
|
177
|
+
}), React.createElement("uistroke", {
|
|
178
|
+
key = "DropdownStroke",
|
|
179
|
+
Thickness = fullStylesheet.dropdownTheme.borderSize,
|
|
180
|
+
Color = fullStylesheet.dropdownTheme.borderColor,
|
|
181
|
+
Transparency = fullStylesheet.dropdownTheme.borderTransparency,
|
|
182
|
+
}), props.children, isNested and React.createElement("uipadding", {
|
|
183
|
+
key = "UIPadding",
|
|
184
|
+
PaddingTop = stylesheet.padding,
|
|
185
|
+
}), React.createElement("uilistlayout", {
|
|
186
|
+
key = "UIListLayout",
|
|
187
|
+
SortOrder = Enum.SortOrder.LayoutOrder,
|
|
188
|
+
Padding = stylesheet.padding,
|
|
189
|
+
})))
|
|
190
|
+
end
|
|
191
|
+
return {
|
|
192
|
+
Dropdown = Dropdown,
|
|
193
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import React from '@rbxts/react';
|
|
2
|
+
export interface IconProps extends React.PropsWithChildren {
|
|
3
|
+
backgroundTransparency?: StateDependent<number>;
|
|
4
|
+
backgroundColor?: StateDependent<Color3>;
|
|
5
|
+
imageId?: StateDependent<string>;
|
|
6
|
+
imageColor?: StateDependent<Color3>;
|
|
7
|
+
textColor?: StateDependent<Color3>;
|
|
8
|
+
imageTransparency?: StateDependent<number>;
|
|
9
|
+
layoutOrder?: StateDependent<number>;
|
|
10
|
+
text?: StateDependent<string>;
|
|
11
|
+
textSize?: StateDependent<number>;
|
|
12
|
+
imageSizeOffset?: StateDependent<number>;
|
|
13
|
+
imageRectOffset?: StateDependent<Vector2>;
|
|
14
|
+
imageRectSize?: StateDependent<Vector2>;
|
|
15
|
+
defaultState?: IconState;
|
|
16
|
+
fontFace?: StateDependent<Font>;
|
|
17
|
+
forcedState?: IconState;
|
|
18
|
+
leftClickSound?: StateDependent<string>;
|
|
19
|
+
rightClickSound?: StateDependent<string>;
|
|
20
|
+
cornerRadius?: StateDependent<UDim>;
|
|
21
|
+
strokeTransparency?: StateDependent<number>;
|
|
22
|
+
strokeColor?: StateDependent<Color3>;
|
|
23
|
+
strokeThickness?: StateDependent<number>;
|
|
24
|
+
textAlignment?: StateDependent<Enum.TextXAlignment>;
|
|
25
|
+
richText?: StateDependent<boolean>;
|
|
26
|
+
toggleStateOnClick?: boolean;
|
|
27
|
+
selected?: () => void;
|
|
28
|
+
deselected?: () => void;
|
|
29
|
+
hover?: () => void;
|
|
30
|
+
unhover?: () => void;
|
|
31
|
+
stateChanged?: (state: IconState) => void;
|
|
32
|
+
onClick?: () => void;
|
|
33
|
+
onRightClick?: () => void;
|
|
34
|
+
playSound?: (id: string) => void;
|
|
35
|
+
}
|
|
36
|
+
export type IconState = 'selected' | 'deselected';
|
|
37
|
+
export type StateDependent<T> = Record<IconState, T> | T;
|
|
38
|
+
export type FromStateDependent<T> = T extends StateDependent<infer U> ? U : T;
|
|
39
|
+
export type IconId = number;
|
|
40
|
+
export declare function Icon({ children, ...componentProps }: IconProps): React.JSX.Element;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v1.0.2
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local deepEquals = TS.import(script, TS.getModule(script, "@rbxts", "object-utils")).deepEquals
|
|
4
|
+
local _pretty_react_hooks = TS.import(script, TS.getModule(script, "@rbxts", "pretty-react-hooks").out)
|
|
5
|
+
local mapBinding = _pretty_react_hooks.mapBinding
|
|
6
|
+
local useAsyncEffect = _pretty_react_hooks.useAsyncEffect
|
|
7
|
+
local useMountEffect = _pretty_react_hooks.useMountEffect
|
|
8
|
+
local useUnmountEffect = _pretty_react_hooks.useUnmountEffect
|
|
9
|
+
local useUpdateEffect = _pretty_react_hooks.useUpdateEffect
|
|
10
|
+
local _react = TS.import(script, TS.getModule(script, "@rbxts", "react"))
|
|
11
|
+
local React = _react
|
|
12
|
+
local useBinding = _react.useBinding
|
|
13
|
+
local useEffect = _react.useEffect
|
|
14
|
+
local useRef = _react.useRef
|
|
15
|
+
local useState = _react.useState
|
|
16
|
+
local TextService = TS.import(script, TS.getModule(script, "@rbxts", "services")).TextService
|
|
17
|
+
local _context = TS.import(script, script.Parent.Parent, "context")
|
|
18
|
+
local LocationContext = _context.LocationContext
|
|
19
|
+
local useLocation = _context.useLocation
|
|
20
|
+
local useStylesheet = _context.useStylesheet
|
|
21
|
+
local useAnimateableProps = TS.import(script, script.Parent.Parent, "hooks", "use-animateable-props").useAnimateableProps
|
|
22
|
+
local useGuiInset = TS.import(script, script.Parent.Parent, "hooks", "use-gui-inset").useGuiInset
|
|
23
|
+
local useId = TS.import(script, script.Parent.Parent, "hooks", "use-id").useId
|
|
24
|
+
local noop = TS.import(script, script.Parent.Parent, "style").noop
|
|
25
|
+
local stateful = TS.import(script, script.Parent.Parent, "utilities", "resolve-state-dependent").stateful
|
|
26
|
+
local ANIMATEABLE = { "backgroundColor", "backgroundTransparency", "imageColor", "imageTransparency" }
|
|
27
|
+
local function Icon(_param)
|
|
28
|
+
local children = _param.children
|
|
29
|
+
local _extracted = {
|
|
30
|
+
["children"] = true,
|
|
31
|
+
}
|
|
32
|
+
local _rest = {}
|
|
33
|
+
for _k, _v in _param do
|
|
34
|
+
if not _extracted[_k] then
|
|
35
|
+
_rest[_k] = _v
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
local componentProps = _rest
|
|
39
|
+
local inset = useGuiInset()
|
|
40
|
+
local location = useLocation()
|
|
41
|
+
local id = useId()
|
|
42
|
+
local currentState, setState = useState(componentProps.forcedState or "deselected")
|
|
43
|
+
local dropdownAnimating, setAnimationState = useState(false)
|
|
44
|
+
local contentSize, setContentSize = useState(Vector2.new(0, 0))
|
|
45
|
+
local dropdownSize, setDropdownSize = useBinding(Vector2.new(0, 0))
|
|
46
|
+
local textBounds, setTextBounds = useState(Vector2.zero)
|
|
47
|
+
local stylesheet = useStylesheet()
|
|
48
|
+
local _arg0 = location.type ~= "icon"
|
|
49
|
+
assert(_arg0, "Icons cannot be nested")
|
|
50
|
+
local _object = table.clone(stylesheet.icon)
|
|
51
|
+
setmetatable(_object, nil)
|
|
52
|
+
for _k, _v in componentProps do
|
|
53
|
+
_object[_k] = _v
|
|
54
|
+
end
|
|
55
|
+
local animatedProps = useAnimateableProps(currentState, _object, unpack(ANIMATEABLE))
|
|
56
|
+
local _object_1 = table.clone(stylesheet.icon)
|
|
57
|
+
setmetatable(_object_1, nil)
|
|
58
|
+
for _k, _v in componentProps do
|
|
59
|
+
_object_1[_k] = _v
|
|
60
|
+
end
|
|
61
|
+
for _k, _v in animatedProps do
|
|
62
|
+
_object_1[_k] = _v
|
|
63
|
+
end
|
|
64
|
+
local props = _object_1
|
|
65
|
+
useMountEffect(function()
|
|
66
|
+
local _ = props.defaultState and not componentProps.forcedState and setState(props.defaultState)
|
|
67
|
+
end)
|
|
68
|
+
useEffect(function()
|
|
69
|
+
if not componentProps.forcedState then
|
|
70
|
+
return nil
|
|
71
|
+
end
|
|
72
|
+
setState(componentProps.forcedState)
|
|
73
|
+
end, { componentProps.forcedState })
|
|
74
|
+
useUpdateEffect(function()
|
|
75
|
+
props.stateChanged(currentState)
|
|
76
|
+
if currentState == "selected" then
|
|
77
|
+
location.iconSelected(id)
|
|
78
|
+
props.selected()
|
|
79
|
+
else
|
|
80
|
+
location.iconDeselected(id)
|
|
81
|
+
props.deselected()
|
|
82
|
+
end
|
|
83
|
+
end, { currentState })
|
|
84
|
+
useUpdateEffect(function()
|
|
85
|
+
if currentState == "selected" and not (table.find(location.selectedIcons, id) ~= nil) then
|
|
86
|
+
setState("deselected")
|
|
87
|
+
end
|
|
88
|
+
end, { location.selectedIcons })
|
|
89
|
+
local currentImage = stateful(props.imageId, currentState)
|
|
90
|
+
local currentText = stateful(props.text, currentState)
|
|
91
|
+
local previousQueryRef = useRef()
|
|
92
|
+
useAsyncEffect(TS.async(function()
|
|
93
|
+
local currentQuery = {
|
|
94
|
+
Font = stateful(props.fontFace, currentState),
|
|
95
|
+
Size = stateful(props.textSize, currentState),
|
|
96
|
+
Text = currentText,
|
|
97
|
+
}
|
|
98
|
+
local _condition = previousQueryRef.current
|
|
99
|
+
if _condition == nil then
|
|
100
|
+
_condition = {}
|
|
101
|
+
end
|
|
102
|
+
if deepEquals(currentQuery, _condition) then
|
|
103
|
+
return nil
|
|
104
|
+
end
|
|
105
|
+
if not (currentText ~= "" and currentText) then
|
|
106
|
+
return setTextBounds(Vector2.zero)
|
|
107
|
+
end
|
|
108
|
+
local params = Instance.new("GetTextBoundsParams")
|
|
109
|
+
params.Text = currentText
|
|
110
|
+
params.Font = stateful(props.fontFace, currentState)
|
|
111
|
+
params.Size = stateful(props.textSize, currentState)
|
|
112
|
+
params.Width = stylesheet.sizing.textMeasurementWidth
|
|
113
|
+
setTextBounds(TextService:GetTextBoundsAsync(params))
|
|
114
|
+
previousQueryRef.current = currentQuery
|
|
115
|
+
end), { currentText, props.fontFace, props.textSize, currentState })
|
|
116
|
+
local imageSizeOff = stateful(props.imageSizeOffset, currentState)
|
|
117
|
+
local forceHeight = if location.type == "dropdown" then stylesheet.dropdown.forceHeight else nil
|
|
118
|
+
local _condition = stylesheet.sizing.iconHeight
|
|
119
|
+
if _condition == nil then
|
|
120
|
+
_condition = forceHeight
|
|
121
|
+
if _condition == nil then
|
|
122
|
+
_condition = inset.Height - 12
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
local iconHeight = _condition
|
|
126
|
+
local imageSize = iconHeight - stylesheet.sizing.imagePadding * 2 + imageSizeOff
|
|
127
|
+
local minLabelWidth = if location.type == "dropdown" then location.desiredIconWidth - stylesheet.sizing.minLabelWidthPadding else inset.Height - stylesheet.sizing.labelPadding * 2
|
|
128
|
+
local accumulatedLabelWidth = if currentImage ~= "" and currentImage then textBounds.X else math.max(textBounds.X, minLabelWidth)
|
|
129
|
+
local _exp = textBounds.X + stylesheet.sizing.labelPadding * 2
|
|
130
|
+
local _condition_1 = currentImage
|
|
131
|
+
if _condition_1 ~= "" and _condition_1 then
|
|
132
|
+
_condition_1 = textBounds.X ~= 0
|
|
133
|
+
end
|
|
134
|
+
local iconSize = Vector2.new(math.max(iconHeight, _exp + (if _condition_1 ~= "" and _condition_1 then imageSize + stylesheet.sizing.imageToTextSpacing else 0)), iconHeight)
|
|
135
|
+
local imagePos = stylesheet.sizing.imagePadding + imageSizeOff * -0.5
|
|
136
|
+
local textLabelPos = UDim2.new(0, if currentImage ~= "" and currentImage then imageSize + stylesheet.sizing.imagePadding * 2 else stylesheet.sizing.labelPadding, 0.5, 0)
|
|
137
|
+
useEffect(function()
|
|
138
|
+
if location.type ~= "dropdown" then
|
|
139
|
+
return nil
|
|
140
|
+
end
|
|
141
|
+
local includeContents = currentState == "selected" or dropdownAnimating
|
|
142
|
+
local _vector2 = Vector2.new(iconSize.X, iconSize.Y)
|
|
143
|
+
local _vector2_1 = Vector2.new(0, if includeContents then contentSize.Y else 0)
|
|
144
|
+
location.registerChild(id, _vector2 + _vector2_1)
|
|
145
|
+
end, { currentState, contentSize.Y, dropdownAnimating, iconSize })
|
|
146
|
+
useUnmountEffect(function()
|
|
147
|
+
if location.type ~= "dropdown" then
|
|
148
|
+
return nil
|
|
149
|
+
end
|
|
150
|
+
location.removeChild(id)
|
|
151
|
+
end)
|
|
152
|
+
local wrapSize = mapBinding(dropdownSize, function(t)
|
|
153
|
+
return UDim2.fromOffset(if location.type == "dropdown" then location.desiredIconWidth else iconSize.X, iconSize.Y + t.Y)
|
|
154
|
+
end)
|
|
155
|
+
return React.createElement(LocationContext.Provider, {
|
|
156
|
+
value = {
|
|
157
|
+
type = "icon",
|
|
158
|
+
isVisible = currentState == "selected",
|
|
159
|
+
isUnderDropdown = location.type == "dropdown",
|
|
160
|
+
width = if location.type == "dropdown" then location.desiredIconWidth else iconSize.X,
|
|
161
|
+
setDropdownSize = setDropdownSize,
|
|
162
|
+
setContentSize = setContentSize,
|
|
163
|
+
setAnimationState = setAnimationState,
|
|
164
|
+
},
|
|
165
|
+
}, React.createElement("frame", {
|
|
166
|
+
Size = wrapSize,
|
|
167
|
+
LayoutOrder = stateful(props.layoutOrder, currentState),
|
|
168
|
+
BackgroundTransparency = 1,
|
|
169
|
+
key = "IconWrapper",
|
|
170
|
+
}, React.createElement("textbutton", {
|
|
171
|
+
Size = UDim2.new(1, 0, 0, iconSize.Y),
|
|
172
|
+
Event = {
|
|
173
|
+
MouseButton1Click = function()
|
|
174
|
+
if stateful(props.toggleStateOnClick, currentState) then
|
|
175
|
+
setState(if currentState == "deselected" then "selected" else "deselected")
|
|
176
|
+
end
|
|
177
|
+
props.onClick()
|
|
178
|
+
local soundId = stateful(props.leftClickSound, currentState)
|
|
179
|
+
if not (soundId ~= "" and soundId) then
|
|
180
|
+
return nil
|
|
181
|
+
end
|
|
182
|
+
props.playSound(soundId)
|
|
183
|
+
end,
|
|
184
|
+
MouseButton2Click = function()
|
|
185
|
+
if props.onRightClick == noop then
|
|
186
|
+
return nil
|
|
187
|
+
end
|
|
188
|
+
props.onRightClick()
|
|
189
|
+
local soundId = stateful(props.rightClickSound, currentState)
|
|
190
|
+
if not (soundId ~= "" and soundId) then
|
|
191
|
+
return nil
|
|
192
|
+
end
|
|
193
|
+
props.playSound(soundId)
|
|
194
|
+
end,
|
|
195
|
+
MouseEnter = props.hover,
|
|
196
|
+
MouseLeave = props.unhover,
|
|
197
|
+
},
|
|
198
|
+
Text = "",
|
|
199
|
+
BackgroundTransparency = props.backgroundTransparency,
|
|
200
|
+
BackgroundColor3 = stateful(props.backgroundColor, currentState),
|
|
201
|
+
key = "IconButton",
|
|
202
|
+
}, children, currentImage ~= nil and currentImage ~= "" and (React.createElement("imagelabel", {
|
|
203
|
+
key = "IconImage",
|
|
204
|
+
Size = UDim2.fromOffset(imageSize, imageSize),
|
|
205
|
+
Position = UDim2.fromOffset(imagePos, imagePos),
|
|
206
|
+
Image = currentImage,
|
|
207
|
+
BackgroundTransparency = 1,
|
|
208
|
+
ImageColor3 = props.imageColor,
|
|
209
|
+
ImageTransparency = props.imageTransparency,
|
|
210
|
+
ImageRectOffset = stateful(props.imageRectOffset, currentState),
|
|
211
|
+
ImageRectSize = stateful(props.imageRectSize, currentState),
|
|
212
|
+
})), currentText ~= nil and currentText ~= "" and (React.createElement("textlabel", {
|
|
213
|
+
FontFace = stateful(props.fontFace, currentState),
|
|
214
|
+
TextSize = stateful(props.textSize, currentState),
|
|
215
|
+
TextColor3 = stateful(props.textColor, currentState),
|
|
216
|
+
TextWrapped = false,
|
|
217
|
+
AnchorPoint = Vector2.new(0, 0.5),
|
|
218
|
+
Size = UDim2.new(0, accumulatedLabelWidth, stylesheet.sizing.buttonLabelHeightFraction, 0),
|
|
219
|
+
Position = textLabelPos,
|
|
220
|
+
TextXAlignment = stateful(props.textAlignment, currentState),
|
|
221
|
+
RichText = stateful(props.richText, currentState),
|
|
222
|
+
BackgroundTransparency = 1,
|
|
223
|
+
Text = currentText,
|
|
224
|
+
key = "IconText",
|
|
225
|
+
}, React.createElement("uistroke", {
|
|
226
|
+
key = "UIStroke",
|
|
227
|
+
Thickness = stateful(props.strokeThickness, currentState),
|
|
228
|
+
Color = stateful(props.strokeColor, currentState),
|
|
229
|
+
Transparency = stateful(props.strokeTransparency, currentState),
|
|
230
|
+
}))), React.createElement("uicorner", {
|
|
231
|
+
key = "UICorner",
|
|
232
|
+
CornerRadius = if location.type == "dropdown" then stylesheet.dropdown.iconCornerRadius else stateful(props.cornerRadius, currentState),
|
|
233
|
+
}))))
|
|
234
|
+
end
|
|
235
|
+
return {
|
|
236
|
+
Icon = Icon,
|
|
237
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import React from '@rbxts/react';
|
|
2
|
+
export type SelectionMode = 'Single' | 'Multiple';
|
|
3
|
+
interface ProviderProps extends React.PropsWithChildren {
|
|
4
|
+
selectionMode?: SelectionMode;
|
|
5
|
+
gameVoiceChatEnabled?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export declare function TopbarProvider({ selectionMode, gameVoiceChatEnabled, children }: ProviderProps): React.JSX.Element;
|
|
8
|
+
export {};
|