@k9kbdev/roblox-css 0.1.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 +15 -0
- package/README.md +245 -0
- package/default.project.json +6 -0
- package/out/index.d.ts +35 -0
- package/out/init.luau +57 -0
- package/out/logger.d.ts +23 -0
- package/out/logger.luau +73 -0
- package/out/primitives/Box.d.ts +23 -0
- package/out/primitives/Box.luau +103 -0
- package/out/primitives/Button.d.ts +62 -0
- package/out/primitives/Button.luau +170 -0
- package/out/primitives/Image.d.ts +37 -0
- package/out/primitives/Image.luau +79 -0
- package/out/primitives/InlineText.d.ts +25 -0
- package/out/primitives/InlineText.luau +273 -0
- package/out/primitives/Input.d.ts +59 -0
- package/out/primitives/Input.luau +126 -0
- package/out/primitives/MotionBox.d.ts +15 -0
- package/out/primitives/MotionBox.luau +69 -0
- package/out/primitives/MotionButton.d.ts +15 -0
- package/out/primitives/MotionButton.luau +146 -0
- package/out/primitives/MotionImage.d.ts +13 -0
- package/out/primitives/MotionImage.luau +70 -0
- package/out/primitives/MotionText.d.ts +12 -0
- package/out/primitives/MotionText.luau +116 -0
- package/out/primitives/MotionUIScale.d.ts +9 -0
- package/out/primitives/MotionUIScale.luau +48 -0
- package/out/primitives/ScrollBox.d.ts +25 -0
- package/out/primitives/ScrollBox.luau +69 -0
- package/out/primitives/Text.d.ts +50 -0
- package/out/primitives/Text.luau +139 -0
- package/out/primitives/usePercentageConstraints.d.ts +3 -0
- package/out/primitives/usePercentageConstraints.luau +112 -0
- package/out/primitives/useVariantResolver.d.ts +13 -0
- package/out/primitives/useVariantResolver.luau +260 -0
- package/out/styles/CSSTypes.d.ts +96 -0
- package/out/styles/ParentSizeContext.d.ts +6 -0
- package/out/styles/ParentSizeContext.luau +13 -0
- package/out/styles/colorParser.d.ts +28 -0
- package/out/styles/colorParser.luau +229 -0
- package/out/styles/dimensionParser.d.ts +49 -0
- package/out/styles/dimensionParser.luau +205 -0
- package/out/styles/gradientParser.d.ts +9 -0
- package/out/styles/gradientParser.luau +434 -0
- package/out/styles/namedColors.d.ts +7 -0
- package/out/styles/namedColors.luau +162 -0
- package/out/styles/transitions.d.ts +18 -0
- package/out/styles/transitions.luau +19 -0
- package/out/styles/webStyle.d.ts +74 -0
- package/out/styles/webStyle.luau +973 -0
- package/out/types.d.ts +4 -0
- package/out/types.luau +3 -0
- package/out/utils/parseInlineImages.d.ts +20 -0
- package/out/utils/parseInlineImages.luau +93 -0
- package/package.json +56 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
--[[
|
|
4
|
+
*
|
|
5
|
+
* ScrollBox.tsx — A scrollable container component for Roblox.
|
|
6
|
+
*
|
|
7
|
+
* Provides a scrollable <div> equivalent that supports a web-like CSS syntax (`style` prop).
|
|
8
|
+
* It automatically translates CSS properties (e.g., `backgroundColor: "red"`) into
|
|
9
|
+
* Roblox engine equivalents (`BackgroundColor3`) and injects layout constraints
|
|
10
|
+
* like `UICorner`, `UIPadding`, `UIListLayout`, and `UIGridLayout` as child instances.
|
|
11
|
+
*
|
|
12
|
+
* Mirrors the architecture of Box.tsx but renders a native <scrollingframe> instead
|
|
13
|
+
* of a <frame>, with clean defaults for a modern scrolling experience.
|
|
14
|
+
*
|
|
15
|
+
* Usage:
|
|
16
|
+
* <ScrollBox style={{ width: "100%", height: "300px", display: "flex", gap: "8px" }}>
|
|
17
|
+
* <textlabel Text="Item 1" />
|
|
18
|
+
* <textlabel Text="Item 2" />
|
|
19
|
+
* </ScrollBox>
|
|
20
|
+
|
|
21
|
+
]]
|
|
22
|
+
local React = TS.import(script, TS.getModule(script, "@rbxts", "react"))
|
|
23
|
+
local webStyle = TS.import(script, script.Parent.Parent, "styles", "webStyle").webStyle
|
|
24
|
+
local ScrollBox = React.forwardRef(function(props, ref)
|
|
25
|
+
local style = props.style
|
|
26
|
+
local children = props.children
|
|
27
|
+
local defaultProps = {
|
|
28
|
+
BackgroundTransparency = 1,
|
|
29
|
+
BorderSizePixel = 0,
|
|
30
|
+
ScrollBarThickness = 4,
|
|
31
|
+
ScrollBarImageTransparency = 0.5,
|
|
32
|
+
CanvasSize = UDim2.new(0, 0, 0, 0),
|
|
33
|
+
AutomaticCanvasSize = Enum.AutomaticSize.Y,
|
|
34
|
+
}
|
|
35
|
+
local _object = table.clone(props)
|
|
36
|
+
setmetatable(_object, nil)
|
|
37
|
+
local explicitProps = _object
|
|
38
|
+
explicitProps.style = nil
|
|
39
|
+
explicitProps.children = nil
|
|
40
|
+
if style then
|
|
41
|
+
local parsedStyle = webStyle(style)
|
|
42
|
+
local _attributes = {
|
|
43
|
+
ref = ref,
|
|
44
|
+
}
|
|
45
|
+
for _k, _v in defaultProps do
|
|
46
|
+
_attributes[_k] = _v
|
|
47
|
+
end
|
|
48
|
+
for _k, _v in parsedStyle.props do
|
|
49
|
+
_attributes[_k] = _v
|
|
50
|
+
end
|
|
51
|
+
for _k, _v in explicitProps do
|
|
52
|
+
_attributes[_k] = _v
|
|
53
|
+
end
|
|
54
|
+
return React.createElement("scrollingframe", _attributes, parsedStyle.children, children)
|
|
55
|
+
end
|
|
56
|
+
local _attributes = {
|
|
57
|
+
ref = ref,
|
|
58
|
+
}
|
|
59
|
+
for _k, _v in defaultProps do
|
|
60
|
+
_attributes[_k] = _v
|
|
61
|
+
end
|
|
62
|
+
for _k, _v in explicitProps do
|
|
63
|
+
_attributes[_k] = _v
|
|
64
|
+
end
|
|
65
|
+
return React.createElement("scrollingframe", _attributes, children)
|
|
66
|
+
end)
|
|
67
|
+
return {
|
|
68
|
+
ScrollBox = ScrollBox,
|
|
69
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Text.tsx — <span>/<p>/<h1> equivalent for Roblox.
|
|
3
|
+
*
|
|
4
|
+
* Renders a <textlabel> with web-like style props.
|
|
5
|
+
* Extends the core webStyle translation by adding typography-specific mappings.
|
|
6
|
+
* Automatically maps primitive string/number children to the Text property,
|
|
7
|
+
* while still supporting regular React children for nested elements.
|
|
8
|
+
*
|
|
9
|
+
* Typography mapping:
|
|
10
|
+
* CSS Property → Roblox Output
|
|
11
|
+
* ─────────────────────────────────────────────────────────
|
|
12
|
+
* fontSize → TextSize (number)
|
|
13
|
+
* fontFamily → FontFace (Font object, defaults to BuilderSans)
|
|
14
|
+
* fontWeight → FontFace weight (normal/bold/black → Regular/Bold/Heavy)
|
|
15
|
+
* textAlign → TextXAlignment (left/center/right)
|
|
16
|
+
* whiteSpace → TextWrapped (nowrap → false, otherwise true)
|
|
17
|
+
* color → TextColor3 (Color3) + TextTransparency
|
|
18
|
+
*
|
|
19
|
+
* Maps to: HTML <span>/<p> → Roblox <textlabel>
|
|
20
|
+
*
|
|
21
|
+
* Usage:
|
|
22
|
+
* <Text style={{ fontSize: 24, fontWeight: "bold", color: "#FFFFFF" }}>
|
|
23
|
+
* Hello Roblox!
|
|
24
|
+
* </Text>
|
|
25
|
+
*/
|
|
26
|
+
import React from "@rbxts/react";
|
|
27
|
+
import { CSSProperties } from "../styles/CSSTypes";
|
|
28
|
+
import { DeepReadonly } from "../types";
|
|
29
|
+
/**
|
|
30
|
+
* Branded TextProps — the `_textProps` brand ensures strict nominal typing.
|
|
31
|
+
* This prevents accidental bypasses or incorrect prop structures.
|
|
32
|
+
*/
|
|
33
|
+
export type TextProps = React.PropsWithChildren<React.ComponentProps<"textlabel">> & {
|
|
34
|
+
/**
|
|
35
|
+
* Web-like CSS styling object that automatically maps to Roblox properties
|
|
36
|
+
* and injects necessary UI constraints (UICorner, UIPadding, UIListLayout).
|
|
37
|
+
*/
|
|
38
|
+
style?: CSSProperties;
|
|
39
|
+
} & {
|
|
40
|
+
readonly _textProps?: unique symbol;
|
|
41
|
+
};
|
|
42
|
+
/** Helper to construct a branded TextProps. */
|
|
43
|
+
export declare function makeTextProps(props: Omit<TextProps, "_textProps">): DeepReadonly<TextProps>;
|
|
44
|
+
/**
|
|
45
|
+
* Text component — The primary component for rendering typography.
|
|
46
|
+
*
|
|
47
|
+
* Maps to: HTML <span>/<p>/<h1> → Roblox <textlabel>
|
|
48
|
+
* Renders a native <textlabel> while mapping web typography styles to Roblox Font properties.
|
|
49
|
+
*/
|
|
50
|
+
export declare const Text: React.ForwardRefExoticComponent<Omit<TextProps, "ref"> & React.RefAttributes<TextLabel>>;
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
--[[
|
|
4
|
+
*
|
|
5
|
+
* Text.tsx — <span>/<p>/<h1> equivalent for Roblox.
|
|
6
|
+
*
|
|
7
|
+
* Renders a <textlabel> with web-like style props.
|
|
8
|
+
* Extends the core webStyle translation by adding typography-specific mappings.
|
|
9
|
+
* Automatically maps primitive string/number children to the Text property,
|
|
10
|
+
* while still supporting regular React children for nested elements.
|
|
11
|
+
*
|
|
12
|
+
* Typography mapping:
|
|
13
|
+
* CSS Property → Roblox Output
|
|
14
|
+
* ─────────────────────────────────────────────────────────
|
|
15
|
+
* fontSize → TextSize (number)
|
|
16
|
+
* fontFamily → FontFace (Font object, defaults to BuilderSans)
|
|
17
|
+
* fontWeight → FontFace weight (normal/bold/black → Regular/Bold/Heavy)
|
|
18
|
+
* textAlign → TextXAlignment (left/center/right)
|
|
19
|
+
* whiteSpace → TextWrapped (nowrap → false, otherwise true)
|
|
20
|
+
* color → TextColor3 (Color3) + TextTransparency
|
|
21
|
+
*
|
|
22
|
+
* Maps to: HTML <span>/<p> → Roblox <textlabel>
|
|
23
|
+
*
|
|
24
|
+
* Usage:
|
|
25
|
+
* <Text style={{ fontSize: 24, fontWeight: "bold", color: "#FFFFFF" }}>
|
|
26
|
+
* Hello Roblox!
|
|
27
|
+
* </Text>
|
|
28
|
+
|
|
29
|
+
]]
|
|
30
|
+
local _react = TS.import(script, TS.getModule(script, "@rbxts", "react"))
|
|
31
|
+
local React = _react
|
|
32
|
+
local forwardRef = _react.forwardRef
|
|
33
|
+
local webStyle = TS.import(script, script.Parent.Parent, "styles", "webStyle").webStyle
|
|
34
|
+
--[[
|
|
35
|
+
*
|
|
36
|
+
* Branded TextProps — the `_textProps` brand ensures strict nominal typing.
|
|
37
|
+
* This prevents accidental bypasses or incorrect prop structures.
|
|
38
|
+
|
|
39
|
+
]]
|
|
40
|
+
--* Helper to construct a branded TextProps.
|
|
41
|
+
local function makeTextProps(props)
|
|
42
|
+
return props
|
|
43
|
+
end
|
|
44
|
+
--[[
|
|
45
|
+
*
|
|
46
|
+
* Text component — The primary component for rendering typography.
|
|
47
|
+
*
|
|
48
|
+
* Maps to: HTML <span>/<p>/<h1> → Roblox <textlabel>
|
|
49
|
+
* Renders a native <textlabel> while mapping web typography styles to Roblox Font properties.
|
|
50
|
+
|
|
51
|
+
]]
|
|
52
|
+
local Text = forwardRef(function(props, ref)
|
|
53
|
+
-- 1. Extract custom styling props and children
|
|
54
|
+
local style = props.style
|
|
55
|
+
local children = props.children
|
|
56
|
+
local defaultProps = {
|
|
57
|
+
BackgroundTransparency = 1,
|
|
58
|
+
TextWrapped = true,
|
|
59
|
+
Text = "",
|
|
60
|
+
}
|
|
61
|
+
if props.Size == nil and (not style or (style.width == nil and style.height == nil)) then
|
|
62
|
+
defaultProps.AutomaticSize = Enum.AutomaticSize.XY
|
|
63
|
+
end
|
|
64
|
+
local _object = table.clone(props)
|
|
65
|
+
setmetatable(_object, nil)
|
|
66
|
+
local explicitProps = _object
|
|
67
|
+
explicitProps.style = nil
|
|
68
|
+
explicitProps.children = nil
|
|
69
|
+
-- 3. Map primitive children directly to the Text property
|
|
70
|
+
if type(children) == "string" or type(children) == "number" then
|
|
71
|
+
explicitProps.Text = tostring(children)
|
|
72
|
+
end
|
|
73
|
+
local parsedStyleProps = {}
|
|
74
|
+
local parsedStyleChildren = {}
|
|
75
|
+
-- 4. If a style object is provided, compile it and extract typography mappings
|
|
76
|
+
if style then
|
|
77
|
+
local parsed = webStyle(style)
|
|
78
|
+
parsedStyleProps = parsed.props
|
|
79
|
+
parsedStyleChildren = parsed.children
|
|
80
|
+
-- Specialized string manipulation for wordBreak
|
|
81
|
+
if style.wordBreak ~= nil then
|
|
82
|
+
if style.wordBreak == "break-all" then
|
|
83
|
+
local _text = explicitProps.Text
|
|
84
|
+
if type(_text) == "string" then
|
|
85
|
+
explicitProps.Text = table.concat(string.split(explicitProps.Text, ""), "\u{200B}")
|
|
86
|
+
end
|
|
87
|
+
elseif style.wordBreak == "keep-all" then
|
|
88
|
+
local _text = explicitProps.Text
|
|
89
|
+
if type(_text) == "string" then
|
|
90
|
+
explicitProps.Text = table.concat(string.split(explicitProps.Text, " "), "\n")
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
-- textTransform — mutate text content
|
|
95
|
+
if style.textTransform ~= nil and style.textTransform ~= "none" then
|
|
96
|
+
local _text = explicitProps.Text
|
|
97
|
+
if type(_text) == "string" then
|
|
98
|
+
if style.textTransform == "uppercase" then
|
|
99
|
+
explicitProps.Text = string.upper((explicitProps.Text))
|
|
100
|
+
elseif style.textTransform == "lowercase" then
|
|
101
|
+
explicitProps.Text = string.lower((explicitProps.Text))
|
|
102
|
+
elseif style.textTransform == "capitalize" then
|
|
103
|
+
explicitProps.Text = (string.gsub((explicitProps.Text), "%w+", function(word)
|
|
104
|
+
return string.upper(string.sub(word, 1, 1)) .. string.sub(word, 2)
|
|
105
|
+
end))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
-- textDecoration — wrap text in rich text tags (RichText=true is set by webStyle)
|
|
110
|
+
if style.textDecoration ~= nil and style.textDecoration ~= "none" then
|
|
111
|
+
local _text = explicitProps.Text
|
|
112
|
+
if type(_text) == "string" then
|
|
113
|
+
if style.textDecoration == "underline" then
|
|
114
|
+
explicitProps.Text = `<u>{explicitProps.Text}</u>`
|
|
115
|
+
elseif style.textDecoration == "line-through" then
|
|
116
|
+
explicitProps.Text = `<s>{explicitProps.Text}</s>`
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
-- 5. Render the native textlabel with merged properties and children
|
|
122
|
+
local _attributes = {
|
|
123
|
+
ref = ref,
|
|
124
|
+
}
|
|
125
|
+
for _k, _v in defaultProps do
|
|
126
|
+
_attributes[_k] = _v
|
|
127
|
+
end
|
|
128
|
+
for _k, _v in parsedStyleProps do
|
|
129
|
+
_attributes[_k] = _v
|
|
130
|
+
end
|
|
131
|
+
for _k, _v in explicitProps do
|
|
132
|
+
_attributes[_k] = _v
|
|
133
|
+
end
|
|
134
|
+
return React.createElement("textlabel", _attributes, parsedStyleChildren, if type(children) == "string" or type(children) == "number" then nil else children)
|
|
135
|
+
end)
|
|
136
|
+
return {
|
|
137
|
+
makeTextProps = makeTextProps,
|
|
138
|
+
Text = Text,
|
|
139
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
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 useContext = _react.useContext
|
|
6
|
+
local ParentSizeContext = TS.import(script, script.Parent.Parent, "styles", "ParentSizeContext").ParentSizeContext
|
|
7
|
+
local parseDimension = TS.import(script, script.Parent.Parent, "styles", "dimensionParser").parseDimension
|
|
8
|
+
local function usePercentageConstraints(style)
|
|
9
|
+
local parentSize = useContext(ParentSizeContext)
|
|
10
|
+
if not parentSize then
|
|
11
|
+
return nil
|
|
12
|
+
end
|
|
13
|
+
if not style then
|
|
14
|
+
return nil
|
|
15
|
+
end
|
|
16
|
+
-- Extract Scale and Offset components from all four constraint properties.
|
|
17
|
+
-- Scale is used for percentage axes; Offset is the pixel fallback for non-percentage axes.
|
|
18
|
+
local minW = if style.minWidth ~= nil then parseDimension(style.minWidth) else nil
|
|
19
|
+
local minH = if style.minHeight ~= nil then parseDimension(style.minHeight) else nil
|
|
20
|
+
local maxW = if style.maxWidth ~= nil then parseDimension(style.maxWidth) else nil
|
|
21
|
+
local maxH = if style.maxHeight ~= nil then parseDimension(style.maxHeight) else nil
|
|
22
|
+
local _result = minW
|
|
23
|
+
if _result ~= nil then
|
|
24
|
+
_result = _result.Scale
|
|
25
|
+
end
|
|
26
|
+
local _condition = _result
|
|
27
|
+
if _condition == nil then
|
|
28
|
+
_condition = 0
|
|
29
|
+
end
|
|
30
|
+
local minWScale = _condition
|
|
31
|
+
local _result_1 = minH
|
|
32
|
+
if _result_1 ~= nil then
|
|
33
|
+
_result_1 = _result_1.Scale
|
|
34
|
+
end
|
|
35
|
+
local _condition_1 = _result_1
|
|
36
|
+
if _condition_1 == nil then
|
|
37
|
+
_condition_1 = 0
|
|
38
|
+
end
|
|
39
|
+
local minHScale = _condition_1
|
|
40
|
+
local _result_2 = maxW
|
|
41
|
+
if _result_2 ~= nil then
|
|
42
|
+
_result_2 = _result_2.Scale
|
|
43
|
+
end
|
|
44
|
+
local _condition_2 = _result_2
|
|
45
|
+
if _condition_2 == nil then
|
|
46
|
+
_condition_2 = 0
|
|
47
|
+
end
|
|
48
|
+
local maxWScale = _condition_2
|
|
49
|
+
local _result_3 = maxH
|
|
50
|
+
if _result_3 ~= nil then
|
|
51
|
+
_result_3 = _result_3.Scale
|
|
52
|
+
end
|
|
53
|
+
local _condition_3 = _result_3
|
|
54
|
+
if _condition_3 == nil then
|
|
55
|
+
_condition_3 = 0
|
|
56
|
+
end
|
|
57
|
+
local maxHScale = _condition_3
|
|
58
|
+
-- Only activate if at least one constraint has a Scale component
|
|
59
|
+
if minWScale == 0 and minHScale == 0 and maxWScale == 0 and maxHScale == 0 then
|
|
60
|
+
return nil
|
|
61
|
+
end
|
|
62
|
+
-- When this hook activates, it takes EXCLUSIVE ownership of the <uisizeconstraint>.
|
|
63
|
+
-- For axes without a percentage, use the pixel (.Offset) fallback from parseDimension().
|
|
64
|
+
-- This handles mixed cases like { minWidth: "200px", maxWidth: "50%" }.
|
|
65
|
+
local _result_4 = minW
|
|
66
|
+
if _result_4 ~= nil then
|
|
67
|
+
_result_4 = _result_4.Offset
|
|
68
|
+
end
|
|
69
|
+
local _condition_4 = _result_4
|
|
70
|
+
if _condition_4 == nil then
|
|
71
|
+
_condition_4 = 0
|
|
72
|
+
end
|
|
73
|
+
local minWFallback = _condition_4
|
|
74
|
+
local _result_5 = minH
|
|
75
|
+
if _result_5 ~= nil then
|
|
76
|
+
_result_5 = _result_5.Offset
|
|
77
|
+
end
|
|
78
|
+
local _condition_5 = _result_5
|
|
79
|
+
if _condition_5 == nil then
|
|
80
|
+
_condition_5 = 0
|
|
81
|
+
end
|
|
82
|
+
local minHFallback = _condition_5
|
|
83
|
+
local _result_6 = maxW
|
|
84
|
+
if _result_6 ~= nil then
|
|
85
|
+
_result_6 = _result_6.Offset
|
|
86
|
+
end
|
|
87
|
+
local _condition_6 = _result_6
|
|
88
|
+
if _condition_6 == nil then
|
|
89
|
+
_condition_6 = math.huge
|
|
90
|
+
end
|
|
91
|
+
local maxWFallback = _condition_6
|
|
92
|
+
local _result_7 = maxH
|
|
93
|
+
if _result_7 ~= nil then
|
|
94
|
+
_result_7 = _result_7.Offset
|
|
95
|
+
end
|
|
96
|
+
local _condition_7 = _result_7
|
|
97
|
+
if _condition_7 == nil then
|
|
98
|
+
_condition_7 = math.huge
|
|
99
|
+
end
|
|
100
|
+
local maxHFallback = _condition_7
|
|
101
|
+
return React.createElement("uisizeconstraint", {
|
|
102
|
+
MinSize = parentSize:map(function(ps)
|
|
103
|
+
return Vector2.new(if minWScale > 0 then ps.X * minWScale else minWFallback, if minHScale > 0 then ps.Y * minHScale else minHFallback)
|
|
104
|
+
end),
|
|
105
|
+
MaxSize = parentSize:map(function(ps)
|
|
106
|
+
return Vector2.new(if maxWScale > 0 then ps.X * maxWScale else maxWFallback, if maxHScale > 0 then ps.Y * maxHScale else maxHFallback)
|
|
107
|
+
end),
|
|
108
|
+
})
|
|
109
|
+
end
|
|
110
|
+
return {
|
|
111
|
+
usePercentageConstraints = usePercentageConstraints,
|
|
112
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import React from "@rbxts/react";
|
|
2
|
+
import { CSSProperties } from "../styles/webStyle";
|
|
3
|
+
export interface MotionProps {
|
|
4
|
+
initial?: string;
|
|
5
|
+
animate?: string;
|
|
6
|
+
variants?: Record<string, Partial<CSSProperties> & Record<string, unknown>>;
|
|
7
|
+
transition?: TweenInfo;
|
|
8
|
+
}
|
|
9
|
+
export declare function isAnimatable(value: unknown): boolean;
|
|
10
|
+
export declare function useVariantResolver(animate?: string, initial?: string, variants?: Record<string, Partial<CSSProperties> & Record<string, unknown>>, transition?: TweenInfo, parser?: (style: Partial<CSSProperties> & Record<string, unknown>) => Record<string, unknown>): {
|
|
11
|
+
animatedProps: Record<string, React.Binding<unknown>>;
|
|
12
|
+
staticProps: Record<string, unknown>;
|
|
13
|
+
};
|
|
@@ -0,0 +1,260 @@
|
|
|
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 useEffect = _react.useEffect
|
|
6
|
+
local useMemo = _react.useMemo
|
|
7
|
+
local useRef = _react.useRef
|
|
8
|
+
local createMotion = TS.import(script, TS.getModule(script, "@rbxts", "ripple").src).createMotion
|
|
9
|
+
local RunService = TS.import(script, TS.getModule(script, "@rbxts", "services")).RunService
|
|
10
|
+
local webStyle = TS.import(script, script.Parent.Parent, "styles", "webStyle").webStyle
|
|
11
|
+
local function isAnimatable(value)
|
|
12
|
+
local _value = value
|
|
13
|
+
local t = typeof(_value)
|
|
14
|
+
return t == "number" or t == "UDim" or t == "UDim2" or t == "Vector2" or t == "Vector3" or t == "Color3" or t == "CFrame" or t == "Rect"
|
|
15
|
+
end
|
|
16
|
+
local function useVariantResolver(animate, initial, variants, transition, parser)
|
|
17
|
+
if parser == nil then
|
|
18
|
+
parser = function(style)
|
|
19
|
+
local result = webStyle(style).props
|
|
20
|
+
for k, v in pairs(style) do
|
|
21
|
+
if type(k) == "string" and k ~= "_parsed" and result[k] == nil then
|
|
22
|
+
local firstChar = string.sub(k, 1, 1)
|
|
23
|
+
if firstChar >= "A" and firstChar <= "Z" then
|
|
24
|
+
result[k] = v
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
return result
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
-- Parse all variants once to find animatable vs static keys
|
|
32
|
+
local _binding = useMemo(function()
|
|
33
|
+
local anim = {}
|
|
34
|
+
local stat = {}
|
|
35
|
+
if variants then
|
|
36
|
+
for _, variantStyle in pairs(variants) do
|
|
37
|
+
local parsed = parser(variantStyle)
|
|
38
|
+
for key, value in pairs(parsed) do
|
|
39
|
+
if isAnimatable(value) then
|
|
40
|
+
anim[key] = true
|
|
41
|
+
else
|
|
42
|
+
stat[key] = true
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
return {
|
|
48
|
+
animatableKeys = anim,
|
|
49
|
+
staticKeys = stat,
|
|
50
|
+
}
|
|
51
|
+
end, { variants, parser })
|
|
52
|
+
local animatableKeys = _binding.animatableKeys
|
|
53
|
+
local staticKeys = _binding.staticKeys
|
|
54
|
+
-- Maintain independent motions and bindings per property
|
|
55
|
+
-- to avoid Ripple's limitation with mixed type dictionaries.
|
|
56
|
+
local motionState = useRef()
|
|
57
|
+
if not motionState.current then
|
|
58
|
+
motionState.current = {
|
|
59
|
+
bindings = {},
|
|
60
|
+
setBindings = {},
|
|
61
|
+
motions = {},
|
|
62
|
+
loopState = {},
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
-- 1. Initialize bindings for each animatable key
|
|
66
|
+
for key in animatableKeys do
|
|
67
|
+
if not (motionState.current.bindings[key] ~= nil) then
|
|
68
|
+
local initialVal = nil
|
|
69
|
+
-- Seed priority: explicit `initial` prop → "idle" key → current animate target → first found
|
|
70
|
+
local initialKey = initial
|
|
71
|
+
local _condition = initialKey
|
|
72
|
+
if _condition ~= "" and _condition then
|
|
73
|
+
_condition = variants and variants[initialKey] and parser(variants[initialKey])[key] ~= nil
|
|
74
|
+
end
|
|
75
|
+
if _condition ~= "" and _condition then
|
|
76
|
+
initialVal = parser(variants[initialKey])[key]
|
|
77
|
+
elseif variants and variants.idle and parser(variants.idle)[key] ~= nil then
|
|
78
|
+
initialVal = parser(variants.idle)[key]
|
|
79
|
+
else
|
|
80
|
+
local _condition_1 = animate
|
|
81
|
+
if _condition_1 ~= "" and _condition_1 then
|
|
82
|
+
_condition_1 = variants and variants[animate]
|
|
83
|
+
end
|
|
84
|
+
if _condition_1 ~= "" and _condition_1 then
|
|
85
|
+
local parsed = parser(variants[animate])
|
|
86
|
+
initialVal = parsed[key]
|
|
87
|
+
end
|
|
88
|
+
end
|
|
89
|
+
-- If it's missing in the initial variant, grab it from any variant
|
|
90
|
+
-- just to know its type for Ripple.
|
|
91
|
+
if initialVal == nil and variants then
|
|
92
|
+
for _, variantStyle in pairs(variants) do
|
|
93
|
+
local parsed = parser(variantStyle)
|
|
94
|
+
if parsed[key] ~= nil then
|
|
95
|
+
initialVal = parsed[key]
|
|
96
|
+
break
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
if initialVal ~= nil then
|
|
101
|
+
local binding, setBinding = React.createBinding(initialVal)
|
|
102
|
+
local motion = createMotion(initialVal)
|
|
103
|
+
motionState.current.bindings[key] = binding
|
|
104
|
+
motionState.current.setBindings[key] = setBinding
|
|
105
|
+
motionState.current.motions[key] = motion
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
-- Setup Ripple heartbeat update loop
|
|
110
|
+
useEffect(function()
|
|
111
|
+
local state = motionState.current
|
|
112
|
+
local connection = RunService.Heartbeat:Connect(function(dt)
|
|
113
|
+
for key, motion in state.motions do
|
|
114
|
+
local value = motion:step(dt)
|
|
115
|
+
local binding = state.bindings[key]
|
|
116
|
+
if value ~= (binding:getValue()) then
|
|
117
|
+
state.setBindings[key](value)
|
|
118
|
+
end
|
|
119
|
+
-- Custom loop handling for repeating/reversing animations
|
|
120
|
+
local loopData = state.loopState[key]
|
|
121
|
+
if loopData and motion:isComplete() then
|
|
122
|
+
if loopData.isReversing then
|
|
123
|
+
-- We just finished reversing back to initial value.
|
|
124
|
+
loopData.loopsDone += 1
|
|
125
|
+
if loopData.repeatCount == -1 or loopData.loopsDone <= loopData.repeatCount then
|
|
126
|
+
-- Play forward again
|
|
127
|
+
loopData.isReversing = false
|
|
128
|
+
motion:tween(loopData.targetValue, loopData.options)
|
|
129
|
+
else
|
|
130
|
+
state.loopState[key] = nil
|
|
131
|
+
end
|
|
132
|
+
else
|
|
133
|
+
-- We just finished playing forward.
|
|
134
|
+
if loopData.reverses then
|
|
135
|
+
-- Now reverse
|
|
136
|
+
loopData.isReversing = true
|
|
137
|
+
motion:tween(loopData.initialValue, loopData.options)
|
|
138
|
+
else
|
|
139
|
+
-- We don't reverse, so just snap back and play forward
|
|
140
|
+
loopData.loopsDone += 1
|
|
141
|
+
if loopData.repeatCount == -1 or loopData.loopsDone <= loopData.repeatCount then
|
|
142
|
+
motion:set(loopData.initialValue)
|
|
143
|
+
motion:tween(loopData.targetValue, loopData.options)
|
|
144
|
+
else
|
|
145
|
+
state.loopState[key] = nil
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
151
|
+
end)
|
|
152
|
+
return function()
|
|
153
|
+
connection:Disconnect()
|
|
154
|
+
end
|
|
155
|
+
end, {})
|
|
156
|
+
-- Animate target changes
|
|
157
|
+
useEffect(function()
|
|
158
|
+
local _condition = animate
|
|
159
|
+
if _condition ~= "" and _condition then
|
|
160
|
+
_condition = variants and variants[animate]
|
|
161
|
+
end
|
|
162
|
+
if _condition ~= "" and _condition then
|
|
163
|
+
local parsed = parser(variants[animate])
|
|
164
|
+
local options = {
|
|
165
|
+
time = 0.3,
|
|
166
|
+
}
|
|
167
|
+
local repeatCount = 0
|
|
168
|
+
local reverses = false
|
|
169
|
+
if transition then
|
|
170
|
+
local _object = {}
|
|
171
|
+
local _left = "time"
|
|
172
|
+
local _condition_1 = transition.Time
|
|
173
|
+
if _condition_1 == nil then
|
|
174
|
+
_condition_1 = 0.3
|
|
175
|
+
end
|
|
176
|
+
_object[_left] = _condition_1
|
|
177
|
+
_object.style = transition.EasingStyle
|
|
178
|
+
_object.direction = transition.EasingDirection
|
|
179
|
+
_object.delayTime = transition.DelayTime
|
|
180
|
+
options = _object
|
|
181
|
+
local _condition_2 = transition.RepeatCount
|
|
182
|
+
if _condition_2 == nil then
|
|
183
|
+
_condition_2 = 0
|
|
184
|
+
end
|
|
185
|
+
repeatCount = _condition_2
|
|
186
|
+
local _condition_3 = transition.Reverses
|
|
187
|
+
if _condition_3 == nil then
|
|
188
|
+
_condition_3 = false
|
|
189
|
+
end
|
|
190
|
+
reverses = _condition_3
|
|
191
|
+
end
|
|
192
|
+
for key in animatableKeys do
|
|
193
|
+
local targetValue = parsed[key]
|
|
194
|
+
if targetValue ~= nil then
|
|
195
|
+
local motion = motionState.current.motions[key]
|
|
196
|
+
if motion then
|
|
197
|
+
if repeatCount ~= 0 or reverses then
|
|
198
|
+
local existingLoop = motionState.current.loopState[key]
|
|
199
|
+
-- Only reset the loop if we are targeting a new value
|
|
200
|
+
-- or if we aren't currently looping this property
|
|
201
|
+
if not existingLoop or existingLoop.targetValue ~= targetValue then
|
|
202
|
+
local _loopState = motionState.current.loopState
|
|
203
|
+
local _arg1 = {
|
|
204
|
+
initialValue = motionState.current.bindings[key]:getValue(),
|
|
205
|
+
targetValue = targetValue,
|
|
206
|
+
options = options,
|
|
207
|
+
repeatCount = repeatCount,
|
|
208
|
+
reverses = reverses,
|
|
209
|
+
loopsDone = 0,
|
|
210
|
+
isReversing = false,
|
|
211
|
+
}
|
|
212
|
+
_loopState[key] = _arg1
|
|
213
|
+
motion:tween(targetValue, options)
|
|
214
|
+
end
|
|
215
|
+
else
|
|
216
|
+
-- Clear old loop state for this property to prevent conflicts
|
|
217
|
+
motionState.current.loopState[key] = nil
|
|
218
|
+
motion:tween(targetValue, options)
|
|
219
|
+
end
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
end
|
|
224
|
+
end, { animate, variants, transition, animatableKeys, parser })
|
|
225
|
+
-- Create a plain object of the bindings to spread onto the component
|
|
226
|
+
local animatedProps = useMemo(function()
|
|
227
|
+
local props = {}
|
|
228
|
+
for key, binding in motionState.current.bindings do
|
|
229
|
+
if animatableKeys[key] ~= nil then
|
|
230
|
+
props[key] = binding
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
return props
|
|
234
|
+
end, { animatableKeys })
|
|
235
|
+
-- Compute current static props
|
|
236
|
+
local staticProps = useMemo(function()
|
|
237
|
+
local props = {}
|
|
238
|
+
local _condition = animate
|
|
239
|
+
if _condition ~= "" and _condition then
|
|
240
|
+
_condition = variants and variants[animate]
|
|
241
|
+
end
|
|
242
|
+
if _condition ~= "" and _condition then
|
|
243
|
+
local parsed = parser(variants[animate])
|
|
244
|
+
for key in staticKeys do
|
|
245
|
+
if parsed[key] ~= nil then
|
|
246
|
+
props[key] = parsed[key]
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
return props
|
|
251
|
+
end, { animate, variants, staticKeys, parser })
|
|
252
|
+
return {
|
|
253
|
+
animatedProps = animatedProps,
|
|
254
|
+
staticProps = staticProps,
|
|
255
|
+
}
|
|
256
|
+
end
|
|
257
|
+
return {
|
|
258
|
+
isAnimatable = isAnimatable,
|
|
259
|
+
useVariantResolver = useVariantResolver,
|
|
260
|
+
}
|