@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,96 @@
1
+ /**
2
+ * CSSTypes.d.ts — Strongly-typed style prop interface.
3
+ *
4
+ * This is the contract that every wrapper component's `style` prop uses.
5
+ * Define all supported CSS properties here with their accepted value types.
6
+ * Properties not listed here are intentionally unsupported.
7
+ *
8
+ * Reference: docs/FEATURE_MAPPING.md
9
+ */
10
+
11
+ export interface CSSProperties {
12
+ // --- Box Model & Sizing (FEATURE_MAPPING §2) ---
13
+ width?: string | number;
14
+ height?: string | number;
15
+ minWidth?: string | number;
16
+ maxWidth?: string | number;
17
+ minHeight?: string | number;
18
+ maxHeight?: string | number;
19
+
20
+ // --- Spacing ---
21
+ padding?: string | number;
22
+ paddingTop?: string | number;
23
+ paddingRight?: string | number;
24
+ paddingBottom?: string | number;
25
+ paddingLeft?: string | number;
26
+
27
+ // --- Positioning ---
28
+ position?: "absolute" | "relative";
29
+ top?: string | number;
30
+ right?: string | number;
31
+ bottom?: string | number;
32
+ left?: string | number;
33
+ zIndex?: number;
34
+ transformOrigin?: string;
35
+
36
+ // --- Layout ---
37
+ layoutOrder?: number;
38
+ rotation?: number;
39
+ autoSize?: "none" | "x" | "y" | "xy";
40
+
41
+ // --- Flexbox Layout (FEATURE_MAPPING §3) ---
42
+ display?: "flex" | "grid" | "none";
43
+ flexDirection?: "row" | "column";
44
+ flexWrap?: "nowrap" | "wrap";
45
+ justifyContent?: "flex-start" | "center" | "flex-end" | "space-between" | "space-around";
46
+ alignItems?: "flex-start" | "center" | "flex-end" | "stretch";
47
+ gap?: string | number;
48
+ rowGap?: string | number;
49
+ columnGap?: string | number;
50
+
51
+ // --- Flex Item (per-child, injects UIFlexItem) ---
52
+ flexGrow?: number;
53
+ flexShrink?: number;
54
+ flex?: number | "auto" | "none";
55
+ alignSelf?: "auto" | "flex-start" | "flex-end" | "center" | "stretch";
56
+
57
+ // --- Grid Layout (Emulating UIGridLayout) ---
58
+ gridTemplateColumns?: string | number;
59
+ gridTemplateRows?: string | number;
60
+
61
+ // --- Aesthetics (FEATURE_MAPPING §4) ---
62
+ backgroundColor?: string;
63
+ background?: string;
64
+ backgroundImage?: string;
65
+ color?: string;
66
+ opacity?: number;
67
+ borderRadius?: string | number;
68
+ border?: string;
69
+ boxShadow?: "none" | "sm" | "md" | "lg" | "xl" | "2xl";
70
+ overflow?: "hidden" | "visible";
71
+ objectFit?: "cover" | "contain" | "fill";
72
+ visibility?: "visible" | "hidden";
73
+
74
+ // --- Constraints ---
75
+ aspectRatio?: number;
76
+
77
+ // --- Typography (FEATURE_MAPPING §5) ---
78
+ fontSize?: number;
79
+ fontFamily?: string;
80
+ fontWeight?: "normal" | "bold" | "black";
81
+ fontStyle?: "normal" | "italic";
82
+ textAlign?: "left" | "center" | "right";
83
+ textVerticalAlign?: "top" | "center" | "bottom";
84
+ whiteSpace?: "normal" | "nowrap" | "pre-wrap" | "pre-line";
85
+ wordBreak?: "normal" | "break-word" | "break-all" | "keep-all";
86
+ lineHeight?: number;
87
+ textOverflow?: "ellipsis";
88
+ textDecoration?: "none" | "underline" | "line-through";
89
+ textTransform?: "none" | "uppercase" | "lowercase" | "capitalize";
90
+ richText?: boolean;
91
+ textStroke?: string;
92
+ userSelect?: "none" | "auto" | "text";
93
+
94
+ // --- Interactivity (FEATURE_MAPPING §6) ---
95
+ pointerEvents?: "none" | "auto";
96
+ }
@@ -0,0 +1,6 @@
1
+ import React from "@rbxts/react";
2
+ /**
3
+ * Carries the nearest ancestor <Box>'s AbsoluteSize as a binding,
4
+ * enabling children to reactively resolve percentage-based size constraints.
5
+ */
6
+ export declare const ParentSizeContext: React.Context<React.Binding<Vector2> | undefined>;
@@ -0,0 +1,13 @@
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
+ --[[
5
+ *
6
+ * Carries the nearest ancestor <Box>'s AbsoluteSize as a binding,
7
+ * enabling children to reactively resolve percentage-based size constraints.
8
+
9
+ ]]
10
+ local ParentSizeContext = React.createContext(nil)
11
+ return {
12
+ ParentSizeContext = ParentSizeContext,
13
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * colorParser.ts — Converts CSS color strings to Roblox Color3.
3
+ *
4
+ * Supported formats:
5
+ * - Hex: "#ff0000", "#f00"
6
+ * - RGB: "rgb(255, 0, 0)"
7
+ * - RGBA: "rgba(255, 0, 0, 0.5)"
8
+ * - HSL: "hsl(0, 100%, 50%)"
9
+ * - HSLA: "hsla(0, 100%, 50%, 0.5)"
10
+ * - Named: "red", "blue", "white", "transparent"
11
+ * - Pass-through: Color3 instances returned as-is
12
+ *
13
+ * Returns a ParsedColor with `transparent` flag for "transparent" keyword.
14
+ */
15
+ import { DeepReadonly } from "../types";
16
+ /**
17
+ * Branded ParsedColor — the `_parsed` brand ensures only values produced by
18
+ * parseColor() are assignable. A plain `{ color: Color3; transparency: number }`
19
+ * will NOT satisfy this type, catching accidental bypasses at compile time.
20
+ */
21
+ export type ParsedColor = {
22
+ readonly color: Color3;
23
+ readonly transparent: boolean;
24
+ readonly transparency: number;
25
+ } & {
26
+ readonly _parsed: unique symbol;
27
+ };
28
+ export declare function parseColor(_input: string | Color3): DeepReadonly<ParsedColor>;
@@ -0,0 +1,229 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ --[[
4
+ *
5
+ * colorParser.ts — Converts CSS color strings to Roblox Color3.
6
+ *
7
+ * Supported formats:
8
+ * - Hex: "#ff0000", "#f00"
9
+ * - RGB: "rgb(255, 0, 0)"
10
+ * - RGBA: "rgba(255, 0, 0, 0.5)"
11
+ * - HSL: "hsl(0, 100%, 50%)"
12
+ * - HSLA: "hsla(0, 100%, 50%, 0.5)"
13
+ * - Named: "red", "blue", "white", "transparent"
14
+ * - Pass-through: Color3 instances returned as-is
15
+ *
16
+ * Returns a ParsedColor with `transparent` flag for "transparent" keyword.
17
+
18
+ ]]
19
+ local NAMED_COLORS = TS.import(script, script.Parent, "namedColors").NAMED_COLORS
20
+ local createLogger = TS.import(script, script.Parent.Parent, "logger").createLogger
21
+ local log = createLogger("colorParser")
22
+ --[[
23
+ *
24
+ * Branded ParsedColor — the `_parsed` brand ensures only values produced by
25
+ * parseColor() are assignable. A plain `{ color: Color3; transparency: number }`
26
+ * will NOT satisfy this type, catching accidental bypasses at compile time.
27
+
28
+ ]]
29
+ --* Helper to construct a branded ParsedColor inside this module.
30
+ local function makeParsedColor(color, transparency)
31
+ return {
32
+ color = color,
33
+ transparent = transparency == 1,
34
+ transparency = transparency,
35
+ }
36
+ end
37
+ local function parseColor(_input)
38
+ -- 1. If input is already Color3, return it
39
+ local __input = _input
40
+ if typeof(__input) == "Color3" then
41
+ return makeParsedColor(_input, 0)
42
+ end
43
+ -- if not Color3, it must be a string
44
+ local input = _input
45
+ -- 2. If string "transparent", return white + transparency: 1
46
+ if input == "transparent" then
47
+ return makeParsedColor(Color3.new(1, 1, 1), 1)
48
+ end
49
+ -- 3. If starts with "#" or is a valid hex without "#", parse hex (handle 3-char and 6-char)
50
+ local hasHash = string.sub(input, 1, 1) == "#"
51
+ local possibleHex = if hasHash then string.sub(input, 2) else input
52
+ if (#possibleHex == 3 or #possibleHex == 6) and tonumber(possibleHex, 16) ~= nil then
53
+ local r = 0
54
+ local g = 0
55
+ local b = 0
56
+ if #possibleHex == 3 then
57
+ -- RGB → double each digit: "f" → "ff"
58
+ local _condition = tonumber(string.rep(string.sub(possibleHex, 1, 1), 2), 16)
59
+ if _condition == nil then
60
+ _condition = 0
61
+ end
62
+ r = _condition
63
+ local _condition_1 = tonumber(string.rep(string.sub(possibleHex, 2, 2), 2), 16)
64
+ if _condition_1 == nil then
65
+ _condition_1 = 0
66
+ end
67
+ g = _condition_1
68
+ local _condition_2 = tonumber(string.rep(string.sub(possibleHex, 3, 3), 2), 16)
69
+ if _condition_2 == nil then
70
+ _condition_2 = 0
71
+ end
72
+ b = _condition_2
73
+ else
74
+ -- RRGGBB
75
+ local _condition = tonumber(string.sub(possibleHex, 1, 2), 16)
76
+ if _condition == nil then
77
+ _condition = 0
78
+ end
79
+ r = _condition
80
+ local _condition_1 = tonumber(string.sub(possibleHex, 3, 4), 16)
81
+ if _condition_1 == nil then
82
+ _condition_1 = 0
83
+ end
84
+ g = _condition_1
85
+ local _condition_2 = tonumber(string.sub(possibleHex, 5, 6), 16)
86
+ if _condition_2 == nil then
87
+ _condition_2 = 0
88
+ end
89
+ b = _condition_2
90
+ end
91
+ return makeParsedColor(Color3.fromRGB(r, g, b), 0)
92
+ end
93
+ -- 4. If starts with "rgb(" or "rgba(", extract numbers
94
+ if string.sub(input, 1, 4) == "rgb(" then
95
+ local parts = string.split(input, ",")
96
+ local _condition = tonumber(string.sub(parts[1], 5))
97
+ if _condition == nil then
98
+ _condition = 0
99
+ end
100
+ local r = _condition
101
+ local _condition_1 = tonumber(parts[2])
102
+ if _condition_1 == nil then
103
+ _condition_1 = 0
104
+ end
105
+ local g = _condition_1
106
+ local _condition_2 = tonumber(string.sub(parts[3], 1, -2))
107
+ if _condition_2 == nil then
108
+ _condition_2 = 0
109
+ end
110
+ local b = _condition_2
111
+ return makeParsedColor(Color3.fromRGB(r, g, b), 0)
112
+ elseif string.sub(input, 1, 5) == "rgba(" then
113
+ local parts = string.split(input, ",")
114
+ local _condition = tonumber(string.sub(parts[1], 6))
115
+ if _condition == nil then
116
+ _condition = 0
117
+ end
118
+ local r = _condition
119
+ local _condition_1 = tonumber(parts[2])
120
+ if _condition_1 == nil then
121
+ _condition_1 = 0
122
+ end
123
+ local g = _condition_1
124
+ local _condition_2 = tonumber(parts[3])
125
+ if _condition_2 == nil then
126
+ _condition_2 = 0
127
+ end
128
+ local b = _condition_2
129
+ local _condition_3 = tonumber(string.sub(parts[4], 1, -2))
130
+ if _condition_3 == nil then
131
+ _condition_3 = 1
132
+ end
133
+ local alpha = _condition_3
134
+ return makeParsedColor(Color3.fromRGB(r, g, b), 1 - alpha)
135
+ end
136
+ -- 4.5. HSL / HSLA support
137
+ if string.sub(input, 1, 4) == "hsl(" or string.sub(input, 1, 5) == "hsla(" then
138
+ local isHsla = string.sub(input, 1, 5) == "hsla("
139
+ local parts = string.split(input, ",")
140
+ -- Extract hue from first part: "hsl(120" or "hsla(120"
141
+ local hStr = if isHsla then string.sub(parts[1], 6) else string.sub(parts[1], 5)
142
+ local _condition = tonumber(hStr)
143
+ if _condition == nil then
144
+ _condition = 0
145
+ end
146
+ local h = (_condition % 360 + 360) % 360
147
+ -- Extract saturation — strip non-numeric chars (%, spaces) via Lua pattern
148
+ local _condition_1 = parts[2]
149
+ if _condition_1 == nil then
150
+ _condition_1 = ""
151
+ end
152
+ local sMatch = string.match(_condition_1, "([%d%.]+)")
153
+ local _condition_2 = tonumber(sMatch)
154
+ if _condition_2 == nil then
155
+ _condition_2 = 0
156
+ end
157
+ local s = math.clamp(_condition_2 / 100, 0, 1)
158
+ -- Extract lightness — strip non-numeric chars (%, ), spaces)
159
+ local _condition_3 = parts[3]
160
+ if _condition_3 == nil then
161
+ _condition_3 = ""
162
+ end
163
+ local lMatch = string.match(_condition_3, "([%d%.]+)")
164
+ local _condition_4 = tonumber(lMatch)
165
+ if _condition_4 == nil then
166
+ _condition_4 = 0
167
+ end
168
+ local l = math.clamp(_condition_4 / 100, 0, 1)
169
+ -- Extract alpha (hsla only) — strip closing paren
170
+ local _result
171
+ if isHsla and #parts >= 4 then
172
+ local _condition_5 = tonumber(string.sub(parts[4], 1, -2))
173
+ if _condition_5 == nil then
174
+ _condition_5 = 1
175
+ end
176
+ _result = _condition_5
177
+ else
178
+ _result = 1
179
+ end
180
+ local alpha = _result
181
+ -- HSL to RGB conversion (standard algorithm)
182
+ local c = (1 - math.abs(2 * l - 1)) * s
183
+ local x = c * (1 - math.abs(((h / 60) % 2) - 1))
184
+ local m = l - c / 2
185
+ local r1 = 0
186
+ local g1 = 0
187
+ local b1 = 0
188
+ if h < 60 then
189
+ r1 = c
190
+ g1 = x
191
+ b1 = 0
192
+ elseif h < 120 then
193
+ r1 = x
194
+ g1 = c
195
+ b1 = 0
196
+ elseif h < 180 then
197
+ r1 = 0
198
+ g1 = c
199
+ b1 = x
200
+ elseif h < 240 then
201
+ r1 = 0
202
+ g1 = x
203
+ b1 = c
204
+ elseif h < 300 then
205
+ r1 = x
206
+ g1 = 0
207
+ b1 = c
208
+ else
209
+ r1 = c
210
+ g1 = 0
211
+ b1 = x
212
+ end
213
+ return makeParsedColor(Color3.new(math.clamp(r1 + m, 0, 1), math.clamp(g1 + m, 0, 1), math.clamp(b1 + m, 0, 1)), 1 - alpha)
214
+ end
215
+ -- 5. Lookup in NAMED_COLORS table (case-insensitive)
216
+ local _arg0 = string.lower(input)
217
+ local rgb = NAMED_COLORS[_arg0]
218
+ if rgb then
219
+ return makeParsedColor(Color3.fromRGB(rgb[1], rgb[2], rgb[3]), 0)
220
+ end
221
+ -- 6. Fallback: warn + return white
222
+ log:warn("UNKNOWN_COLOR", "Unknown color string", {
223
+ input = input,
224
+ })
225
+ return makeParsedColor(Color3.new(1, 1, 1), 0)
226
+ end
227
+ return {
228
+ parseColor = parseColor,
229
+ }
@@ -0,0 +1,49 @@
1
+ import { DeepReadonly } from "../types";
2
+ /**
3
+ * dimensionParser.ts — Converts CSS dimension strings to Roblox UDim.
4
+ *
5
+ * Supported formats:
6
+ * - Pixels: "100px" or 100 (number) → UDim(0, 100)
7
+ * - Percent: "50%" → UDim(0.5, 0)
8
+ * - Viewport: "100vw" / "100vh" → UDim(1, 0)
9
+ * - Calc: "calc(100% - 10px)" → UDim(1.0, -10)
10
+ * - Auto: "auto" → undefined
11
+ *
12
+ * Also exports parsePadding() for CSS padding shorthand.
13
+ *
14
+ */
15
+ /**
16
+ * Branded ParsedDimension — the `_parsed` brand ensures only values produced by
17
+ * parseDimension() are assignable. A raw UDim will NOT satisfy this type,
18
+ * catching accidental bypasses at compile time.
19
+ */
20
+ export type ParsedDimension = UDim & {
21
+ readonly _parsed: unique symbol;
22
+ };
23
+ export declare function parseDimension(_input: string | number): ParsedDimension | undefined;
24
+ /**
25
+ * Branded PaddingValues — the `_parsed` brand ensures only values produced by
26
+ * parsePadding() are assignable. A plain object with top, right, bottom, left
27
+ * will NOT satisfy this type, catching accidental bypasses at compile time.
28
+ */
29
+ export type PaddingValues = {
30
+ readonly top: UDim;
31
+ readonly right: UDim;
32
+ readonly bottom: UDim;
33
+ readonly left: UDim;
34
+ } & {
35
+ readonly _parsed: unique symbol;
36
+ };
37
+ /**
38
+ * Converts a CSS padding shorthand string or number into UDim values for all 4 sides.
39
+ *
40
+ * Supported formats (similar to CSS):
41
+ * - 1 value: "10px" → top/right/bottom/left
42
+ * - 2 values: "10px 20%" → top/bottom, right/left
43
+ * - 3 values: "10px 20% 10px" → top, right/left, bottom
44
+ * - 4 values: "10px 20% 10px 5%" → top, right, bottom, left
45
+ *
46
+ * Supports any dimension format supported by parseDimension (px, %, vw, vh).
47
+ * Number inputs are treated as pixels applied to all sides.
48
+ */
49
+ export declare function parsePadding(_input: string | number): DeepReadonly<PaddingValues>;
@@ -0,0 +1,205 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local createLogger = TS.import(script, script.Parent.Parent, "logger").createLogger
4
+ local log = createLogger("dimensionParser")
5
+ --[[
6
+ *
7
+ * dimensionParser.ts — Converts CSS dimension strings to Roblox UDim.
8
+ *
9
+ * Supported formats:
10
+ * - Pixels: "100px" or 100 (number) → UDim(0, 100)
11
+ * - Percent: "50%" → UDim(0.5, 0)
12
+ * - Viewport: "100vw" / "100vh" → UDim(1, 0)
13
+ * - Calc: "calc(100% - 10px)" → UDim(1.0, -10)
14
+ * - Auto: "auto" → undefined
15
+ *
16
+ * Also exports parsePadding() for CSS padding shorthand.
17
+ *
18
+
19
+ ]]
20
+ --[[
21
+ *
22
+ * Branded ParsedDimension — the `_parsed` brand ensures only values produced by
23
+ * parseDimension() are assignable. A raw UDim will NOT satisfy this type,
24
+ * catching accidental bypasses at compile time.
25
+
26
+ ]]
27
+ --* Helper to construct a branded ParsedDimension inside this module.
28
+ local function makeParsedDimension(scale, offset)
29
+ return UDim.new(scale, offset)
30
+ end
31
+ local parseDimension
32
+ local function parseCalc(inner)
33
+ local parts = string.split(inner, " - ")
34
+ local isSubtraction = true
35
+ if #parts ~= 2 then
36
+ parts = string.split(inner, " + ")
37
+ isSubtraction = false
38
+ end
39
+ if #parts ~= 2 then
40
+ log:warn("PARSE_CALC_FAILED", "Malformed calc expression", {
41
+ inner = inner,
42
+ })
43
+ return nil
44
+ end
45
+ local term1 = parseDimension(parts[1])
46
+ local term2 = parseDimension(parts[2])
47
+ if term1 == nil or term2 == nil then
48
+ log:warn("PARSE_CALC_FAILED", "Failed to parse calc terms", {
49
+ inner = inner,
50
+ })
51
+ return nil
52
+ end
53
+ local scale2 = term2.Scale
54
+ local offset2 = term2.Offset
55
+ if isSubtraction then
56
+ scale2 = -scale2
57
+ offset2 = -offset2
58
+ end
59
+ return makeParsedDimension(term1.Scale + scale2, term1.Offset + offset2)
60
+ end
61
+ function parseDimension(_input)
62
+ -- 1. If number, treat as pixels → UDim(0, input)
63
+ local __input = _input
64
+ if type(__input) == "number" then
65
+ return makeParsedDimension(0, _input)
66
+ end
67
+ -- 1.5. If calc(...) expression, delegate to parseCalc
68
+ local __input_1 = _input
69
+ local _condition = type(__input_1) == "string"
70
+ if _condition then
71
+ _condition = string.sub(_input, 1, 5) == "calc("
72
+ end
73
+ if _condition then
74
+ local inner = string.sub(_input, 6, -2)
75
+ local result = parseCalc(inner)
76
+ if result ~= nil then
77
+ return result
78
+ end
79
+ -- Malformed calc — fall through to the warn at the bottom
80
+ end
81
+ -- 2. If string ending in "px", extract number → UDim(0, n)
82
+ if string.sub(_input, -2) == "px" then
83
+ local n = tonumber(string.sub(_input, 1, -3))
84
+ if n ~= nil then
85
+ return makeParsedDimension(0, n)
86
+ end
87
+ end
88
+ -- 3. If string ending in "%", extract number / 100 → UDim(n/100, 0)
89
+ if string.sub(_input, -1) == "%" then
90
+ local n = tonumber(string.sub(_input, 1, -2))
91
+ if n ~= nil then
92
+ return makeParsedDimension(n / 100, 0)
93
+ end
94
+ end
95
+ -- 4. If "100vw" or "100vh", → UDim(1, 0)
96
+ local last2Chars = string.sub(_input, -2)
97
+ if last2Chars == "vh" or last2Chars == "vw" then
98
+ local n = tonumber(string.sub(_input, 1, -3))
99
+ if n ~= nil then
100
+ return makeParsedDimension(n / 100, 0)
101
+ end
102
+ end
103
+ -- 5. If "auto" → undefined
104
+ if _input == "auto" then
105
+ return nil
106
+ end
107
+ -- 6. If raw number string (e.g. "0")
108
+ local __input_2 = _input
109
+ if type(__input_2) == "string" then
110
+ local rawNumber = tonumber(_input)
111
+ if rawNumber ~= nil then
112
+ return makeParsedDimension(0, rawNumber)
113
+ end
114
+ end
115
+ -- Fallback: warn + return undefined (callers must handle this)
116
+ log:warn("PARSE_DIMENSION_FAILED", "Could not parse dimension", {
117
+ input = _input,
118
+ })
119
+ return nil
120
+ end
121
+ --[[
122
+ *
123
+ * Branded PaddingValues — the `_parsed` brand ensures only values produced by
124
+ * parsePadding() are assignable. A plain object with top, right, bottom, left
125
+ * will NOT satisfy this type, catching accidental bypasses at compile time.
126
+
127
+ ]]
128
+ --* Helper to construct a branded PaddingValues inside this module.
129
+ local function makePaddingValues(top, right, bottom, left)
130
+ return {
131
+ top = top,
132
+ right = right,
133
+ bottom = bottom,
134
+ left = left,
135
+ }
136
+ end
137
+ local ZERO_UDIM = UDim.new(0, 0)
138
+ --[[
139
+ *
140
+ * Converts a CSS padding shorthand string or number into UDim values for all 4 sides.
141
+ *
142
+ * Supported formats (similar to CSS):
143
+ * - 1 value: "10px" → top/right/bottom/left
144
+ * - 2 values: "10px 20%" → top/bottom, right/left
145
+ * - 3 values: "10px 20% 10px" → top, right/left, bottom
146
+ * - 4 values: "10px 20% 10px 5%" → top, right, bottom, left
147
+ *
148
+ * Supports any dimension format supported by parseDimension (px, %, vw, vh).
149
+ * Number inputs are treated as pixels applied to all sides.
150
+
151
+ ]]
152
+ local function parsePadding(_input)
153
+ -- 1. If number, treat as pixels on all sides
154
+ local __input = _input
155
+ if type(__input) == "number" then
156
+ local dim = parseDimension(_input)
157
+ return makePaddingValues(dim, dim, dim, dim)
158
+ end
159
+ -- 2. If string, split on whitespace and parse each token through parseDimension
160
+ local splitInput = string.split(_input, " ")
161
+ local size = #splitInput
162
+ if size == 1 then
163
+ -- - 1 value: all sides
164
+ local a = parseDimension(splitInput[1])
165
+ if a ~= nil then
166
+ return makePaddingValues(a, a, a, a)
167
+ end
168
+ end
169
+ if size == 2 then
170
+ -- - 2 values: vertical | horizontal
171
+ local a = parseDimension(splitInput[1])
172
+ local b = parseDimension(splitInput[2])
173
+ if a ~= nil and b ~= nil then
174
+ return makePaddingValues(a, b, a, b)
175
+ end
176
+ end
177
+ -- - 3 values: top | horizontal | bottom
178
+ if size == 3 then
179
+ local a = parseDimension(splitInput[1])
180
+ local b = parseDimension(splitInput[2])
181
+ local c = parseDimension(splitInput[3])
182
+ if a ~= nil and b ~= nil and c ~= nil then
183
+ return makePaddingValues(a, b, c, b)
184
+ end
185
+ end
186
+ -- - 4 values: top | right | bottom | left
187
+ if size == 4 then
188
+ local a = parseDimension(splitInput[1])
189
+ local b = parseDimension(splitInput[2])
190
+ local c = parseDimension(splitInput[3])
191
+ local d = parseDimension(splitInput[4])
192
+ if a ~= nil and b ~= nil and c ~= nil and d ~= nil then
193
+ return makePaddingValues(a, b, c, d)
194
+ end
195
+ end
196
+ -- Fallback warn + return zero UDim padding
197
+ log:warn("PARSE_PADDING_FAILED", "Could not parse padding values", {
198
+ input = _input,
199
+ })
200
+ return makePaddingValues(ZERO_UDIM, ZERO_UDIM, ZERO_UDIM, ZERO_UDIM)
201
+ end
202
+ return {
203
+ parseDimension = parseDimension,
204
+ parsePadding = parsePadding,
205
+ }
@@ -0,0 +1,9 @@
1
+ export type ParsedGradient = {
2
+ readonly colorSequence: ColorSequence;
3
+ readonly transparencySequence: NumberSequence | undefined;
4
+ readonly rotation: number;
5
+ } & {
6
+ readonly _parsed: unique symbol;
7
+ };
8
+ export declare function isGradientString(input: string): boolean;
9
+ export declare function parseGradient(input: string, elementWidth?: number, elementHeight?: number): ParsedGradient | undefined;