@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.
Files changed (55) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +245 -0
  3. package/default.project.json +6 -0
  4. package/out/index.d.ts +35 -0
  5. package/out/init.luau +57 -0
  6. package/out/logger.d.ts +23 -0
  7. package/out/logger.luau +73 -0
  8. package/out/primitives/Box.d.ts +23 -0
  9. package/out/primitives/Box.luau +103 -0
  10. package/out/primitives/Button.d.ts +62 -0
  11. package/out/primitives/Button.luau +170 -0
  12. package/out/primitives/Image.d.ts +37 -0
  13. package/out/primitives/Image.luau +79 -0
  14. package/out/primitives/InlineText.d.ts +25 -0
  15. package/out/primitives/InlineText.luau +273 -0
  16. package/out/primitives/Input.d.ts +59 -0
  17. package/out/primitives/Input.luau +126 -0
  18. package/out/primitives/MotionBox.d.ts +15 -0
  19. package/out/primitives/MotionBox.luau +69 -0
  20. package/out/primitives/MotionButton.d.ts +15 -0
  21. package/out/primitives/MotionButton.luau +146 -0
  22. package/out/primitives/MotionImage.d.ts +13 -0
  23. package/out/primitives/MotionImage.luau +70 -0
  24. package/out/primitives/MotionText.d.ts +12 -0
  25. package/out/primitives/MotionText.luau +116 -0
  26. package/out/primitives/MotionUIScale.d.ts +9 -0
  27. package/out/primitives/MotionUIScale.luau +48 -0
  28. package/out/primitives/ScrollBox.d.ts +25 -0
  29. package/out/primitives/ScrollBox.luau +69 -0
  30. package/out/primitives/Text.d.ts +50 -0
  31. package/out/primitives/Text.luau +139 -0
  32. package/out/primitives/usePercentageConstraints.d.ts +3 -0
  33. package/out/primitives/usePercentageConstraints.luau +112 -0
  34. package/out/primitives/useVariantResolver.d.ts +13 -0
  35. package/out/primitives/useVariantResolver.luau +260 -0
  36. package/out/styles/CSSTypes.d.ts +96 -0
  37. package/out/styles/ParentSizeContext.d.ts +6 -0
  38. package/out/styles/ParentSizeContext.luau +13 -0
  39. package/out/styles/colorParser.d.ts +28 -0
  40. package/out/styles/colorParser.luau +229 -0
  41. package/out/styles/dimensionParser.d.ts +49 -0
  42. package/out/styles/dimensionParser.luau +205 -0
  43. package/out/styles/gradientParser.d.ts +9 -0
  44. package/out/styles/gradientParser.luau +434 -0
  45. package/out/styles/namedColors.d.ts +7 -0
  46. package/out/styles/namedColors.luau +162 -0
  47. package/out/styles/transitions.d.ts +18 -0
  48. package/out/styles/transitions.luau +19 -0
  49. package/out/styles/webStyle.d.ts +74 -0
  50. package/out/styles/webStyle.luau +973 -0
  51. package/out/types.d.ts +4 -0
  52. package/out/types.luau +3 -0
  53. package/out/utils/parseInlineImages.d.ts +20 -0
  54. package/out/utils/parseInlineImages.luau +93 -0
  55. 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,3 @@
1
+ import React from "@rbxts/react";
2
+ import type { CSSProperties } from "../styles/CSSTypes";
3
+ export declare function usePercentageConstraints(style: CSSProperties | undefined): React.Element | undefined;
@@ -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
+ }