@rbxts/react-clean-ui 1.0.36 → 1.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 (91) hide show
  1. package/out/Components/Decorator/Corners.luau +5 -1
  2. package/out/Components/Decorator/Padding.luau +1 -1
  3. package/out/Components/Input/Button.d.ts +9 -0
  4. package/out/Components/Input/Button.luau +155 -46
  5. package/out/Components/Input/Checkbox.luau +22 -6
  6. package/out/Components/Input/Increment.d.ts +11 -0
  7. package/out/Components/Input/Increment.luau +107 -0
  8. package/out/Components/Input/Increment.step.d.ts +1 -0
  9. package/out/Components/Input/Increment.step.luau +25 -0
  10. package/out/Components/Input/Input.d.ts +4 -3
  11. package/out/Components/Input/Input.luau +115 -18
  12. package/out/Components/Input/Input.validation.d.ts +4 -0
  13. package/out/Components/Input/Input.validation.luau +42 -0
  14. package/out/Components/Input/Select.d.ts +4 -3
  15. package/out/Components/Input/Select.luau +23 -18
  16. package/out/Components/Input/Slider.d.ts +1 -1
  17. package/out/Components/Input/Slider.luau +68 -34
  18. package/out/Components/Input/index.d.ts +1 -0
  19. package/out/Components/Input/init.luau +3 -0
  20. package/out/Components/Layout/Accordion.d.ts +4 -4
  21. package/out/Components/Layout/Accordion.luau +75 -23
  22. package/out/Components/Layout/Container.d.ts +4 -2
  23. package/out/Components/Layout/Container.luau +12 -3
  24. package/out/Components/Layout/Draggable.luau +1 -1
  25. package/out/Components/Layout/Fieldset.d.ts +1 -1
  26. package/out/Components/Layout/Fieldset.luau +40 -6
  27. package/out/Components/Layout/FlexItem.d.ts +2 -1
  28. package/out/Components/Layout/FlexItem.luau +1 -0
  29. package/out/Components/Layout/Group.luau +30 -8
  30. package/out/Components/Layout/HStack.luau +1 -0
  31. package/out/Components/Layout/Row.luau +43 -9
  32. package/out/Components/Layout/Scroller.d.ts +1 -0
  33. package/out/Components/Layout/Scroller.luau +20 -5
  34. package/out/Components/Layout/Tabs.d.ts +5 -3
  35. package/out/Components/Layout/Tabs.luau +48 -16
  36. package/out/Components/Navigation/Menu.d.ts +1 -1
  37. package/out/Components/Navigation/Menu.luau +2 -0
  38. package/out/Components/Surface/Box.d.ts +4 -2
  39. package/out/Components/Surface/Box.luau +25 -22
  40. package/out/Components/Surface/Card.d.ts +12 -6
  41. package/out/Components/Surface/Card.luau +456 -26
  42. package/out/Components/Surface/Icon.luau +42 -4
  43. package/out/Components/Typography/Text.d.ts +1 -0
  44. package/out/Components/Typography/Text.luau +119 -10
  45. package/out/Contexts/row.context.d.ts +1 -1
  46. package/out/Contexts/theme.context.d.ts +2 -2
  47. package/out/Helpers/color.helper.d.ts +5 -3
  48. package/out/Helpers/color.helper.luau +62 -34
  49. package/out/Helpers/create-ui-story.d.ts +2 -2
  50. package/out/Helpers/create-ui-story.luau +33 -1
  51. package/out/Helpers/css.helper.d.ts +28 -2
  52. package/out/Helpers/css.helper.luau +208 -0
  53. package/out/Helpers/index.d.ts +1 -0
  54. package/out/Helpers/init.luau +3 -0
  55. package/out/Helpers/size.helper.d.ts +3 -1
  56. package/out/Helpers/size.helper.luau +10 -9
  57. package/out/Helpers/spacing.helper.d.ts +5 -4
  58. package/out/Helpers/spacing.helper.luau +102 -18
  59. package/out/Helpers/typography.helper.d.ts +2 -2
  60. package/out/Interfaces/css.types.d.ts +30 -0
  61. package/out/Interfaces/css.types.luau +21 -0
  62. package/out/Interfaces/element.props.d.ts +61 -0
  63. package/out/Interfaces/element.props.luau +1 -0
  64. package/out/Interfaces/index.d.ts +5 -3
  65. package/out/Interfaces/init.luau +1 -7
  66. package/out/Interfaces/responsive.types.d.ts +26 -0
  67. package/out/Interfaces/semantics.d.ts +3 -0
  68. package/out/Interfaces/semantics.luau +2 -0
  69. package/out/Providers/app.provider.d.ts +2 -2
  70. package/out/Providers/theme.provider.d.ts +2 -2
  71. package/out/Theme/index.d.ts +1 -0
  72. package/out/Theme/theme.factory.d.ts +3 -3
  73. package/out/Theme/theme.style.d.ts +48 -0
  74. package/out/Theme/theme.style.luau +1 -0
  75. package/out/Theme/theme.template.d.ts +34 -26
  76. package/out/Theme/themes/dark.theme.d.ts +1 -1
  77. package/out/Theme/themes/dark.theme.luau +28 -0
  78. package/out/Theme/themes/default.theme.d.ts +2 -2
  79. package/out/Theme/themes/default.theme.luau +11 -1
  80. package/out/Theme/themes/index.d.ts +1 -0
  81. package/out/Theme/themes/init.luau +3 -0
  82. package/out/Theme/themes/sandstone.theme.d.ts +1 -1
  83. package/out/Theme/themes/sandstone.theme.luau +25 -0
  84. package/out/Theme/themes/wooden.theme.d.ts +1 -0
  85. package/out/Theme/themes/wooden.theme.luau +554 -0
  86. package/out/index.d.ts +8 -8
  87. package/package.json +19 -7
  88. package/out/Interfaces/clean.element.props.d.ts +0 -114
  89. /package/out/{Interfaces/userinput.d.ts → Helpers/input.helper.d.ts} +0 -0
  90. /package/out/{Interfaces/userinput.luau → Helpers/input.helper.luau} +0 -0
  91. /package/out/Interfaces/{clean.element.props.luau → responsive.types.luau} +0 -0
@@ -14,30 +14,191 @@ local SpacingHelper = _Helpers.SpacingHelper
14
14
  local CleanThemeContext = TS.import(script, script.Parent.Parent.Parent, "Contexts").CleanThemeContext
15
15
  local Padding = TS.import(script, script.Parent.Parent, "Decorator").Padding
16
16
  local CardContext = React.createContext({})
17
+ -- Resolves a theme-driven CssPosition into raw Position/AnchorPoint offsets.
18
+ -- This is a one-off resolver rather than a SizeHelper addition because
19
+ -- CssPosition's `center` is axis-aware (`"x"` / `"y"`) so it can express
20
+ -- "center horizontally, anchor to the top with an offset" — something
21
+ -- SizeHelper.GetPosition/GetAnchor can't do since they apply `center`
22
+ -- identically to both axes, and that symmetric behavior is relied on
23
+ -- elsewhere in the codebase so isn't something to widen here.
24
+ local function resolveCssPositionOffset(position)
25
+ local centerX = position.center == true or position.center == "x"
26
+ local centerY = position.center == true or position.center == "y"
27
+ local anchorX = if centerX then 0.5 else (if position.left == nil and position.right ~= nil then 1 else 0)
28
+ local anchorY = if centerY then 0.5 else (if position.top == nil and position.bottom ~= nil then 1 else 0)
29
+ local posX = if centerX then UDim.new(0.5, 0) elseif position.left ~= nil then SizeHelper:toUDim(position.left) elseif position.right ~= nil then UDim.new(1 - SizeHelper:toUDim(position.right).Scale, -SizeHelper:toUDim(position.right).Offset) else UDim.new(0, 0)
30
+ local posY = if centerY then UDim.new(0.5, 0) elseif position.top ~= nil then SizeHelper:toUDim(position.top) elseif position.bottom ~= nil then UDim.new(1 - SizeHelper:toUDim(position.bottom).Scale, -SizeHelper:toUDim(position.bottom).Offset) else UDim.new(0, 0)
31
+ -- A theme-driven fixed/relative width (only meaningful in overlay mode,
32
+ -- see CssPosition.width) forces a fixed-width/auto-height Size, taking
33
+ -- over the caller's default Size={fromScale(0,0)}/AutomaticSize={XY} —
34
+ -- otherwise Size/AutomaticSize are left undefined so the caller's own
35
+ -- default applies unchanged. Parsed via CssHelper.parseCssSize (not
36
+ -- SizeHelper.toUDim, used for top/left/right/bottom above) since it's
37
+ -- the one that understands the "<percent>% - <px>px" calc() shape a
38
+ -- width value may use (e.g. "100% - 50px").
39
+ --
40
+ -- When `width` isn't explicit but both `left` and `right` are set, imply
41
+ -- a width from them the same way CSS does for an absolutely-positioned
42
+ -- element: width = 100% - left - right. Explicit `width` always wins if
43
+ -- somehow both are set.
44
+ local impliedWidth = if position.width == nil and position.left ~= nil and position.right ~= nil then (function()
45
+ local leftUDim = SizeHelper:toUDim(position.left)
46
+ local rightUDim = SizeHelper:toUDim(position.right)
47
+ return UDim.new(1 - leftUDim.Scale - rightUDim.Scale, -leftUDim.Offset - rightUDim.Offset)
48
+ end)() else nil
49
+ local sizeOverride = if position.width ~= nil then {
50
+ Size = UDim2.new(CssHelper:parseCssSize(position.width), UDim.new(0, 0)),
51
+ AutomaticSize = Enum.AutomaticSize.Y,
52
+ } elseif impliedWidth ~= nil then {
53
+ Size = UDim2.new(impliedWidth, UDim.new(0, 0)),
54
+ AutomaticSize = Enum.AutomaticSize.Y,
55
+ } else {}
56
+ local _object = {
57
+ Position = UDim2.new(posX, posY),
58
+ AnchorPoint = Vector2.new(anchorX, anchorY),
59
+ ZIndex = position.zIndex,
60
+ }
61
+ for _k, _v in sizeOverride do
62
+ _object[_k] = _v
63
+ end
64
+ return _object
65
+ end
66
+ -- Header/footer overlay mode escapes the card's VStack (a UIListLayout
67
+ -- forcibly repositions every sibling it lays out, so there's no way to keep
68
+ -- a custom Position/AnchorPoint on a child that stays inside it) — Card
69
+ -- pulls an overlay-requesting Header/Footer out of the flow and renders it
70
+ -- as an absolutely-positioned sibling of the Box instead. Overlay mode is
71
+ -- requested via the instance `overlay` prop or `theme.components.card.header
72
+ -- /footer.position.position === "absolute"`; when the instance sets any of
73
+ -- its own Position/AnchorPoint/top/left/right/bottom/center fields, those
74
+ -- win wholesale over the theme's CssPosition (mixing partial fields across
75
+ -- the two differently-shaped position vocabularies isn't attempted), exactly
76
+ -- mirroring the precedence the previous PositionElementProps-based
77
+ -- implementation gave instance props.
78
+ local function resolveOverlayPosition(props, themePosition)
79
+ local _condition = props.overlay
80
+ if _condition == nil then
81
+ local _result = themePosition
82
+ if _result ~= nil then
83
+ _result = _result.position
84
+ end
85
+ _condition = (_result == "absolute")
86
+ end
87
+ local overlay = _condition
88
+ if not overlay then
89
+ return {
90
+ overlay = overlay,
91
+ positionProps = {},
92
+ }
93
+ end
94
+ local hasInstancePosition = props.Position ~= nil or props.AnchorPoint ~= nil or props.top ~= nil or props.left ~= nil or props.right ~= nil or props.bottom ~= nil or props.center ~= nil
95
+ if hasInstancePosition then
96
+ local _object = {
97
+ overlay = overlay,
98
+ }
99
+ local _left = "positionProps"
100
+ local _object_1 = {
101
+ Position = SizeHelper:GetPosition(props),
102
+ AnchorPoint = SizeHelper:GetAnchor(props),
103
+ }
104
+ local _left_1 = "ZIndex"
105
+ local _condition_1 = props.ZIndex
106
+ if _condition_1 == nil then
107
+ local _result = themePosition
108
+ if _result ~= nil then
109
+ _result = _result.zIndex
110
+ end
111
+ _condition_1 = _result
112
+ if _condition_1 == nil then
113
+ _condition_1 = 2
114
+ end
115
+ end
116
+ _object_1[_left_1] = _condition_1
117
+ _object[_left] = _object_1
118
+ return _object
119
+ end
120
+ if themePosition ~= nil then
121
+ local resolved = resolveCssPositionOffset(themePosition)
122
+ local _object = {
123
+ overlay = overlay,
124
+ }
125
+ local _left = "positionProps"
126
+ local _object_1 = {
127
+ Position = resolved.Position,
128
+ AnchorPoint = resolved.AnchorPoint,
129
+ }
130
+ local _left_1 = "ZIndex"
131
+ local _condition_1 = props.ZIndex
132
+ if _condition_1 == nil then
133
+ _condition_1 = resolved.ZIndex
134
+ if _condition_1 == nil then
135
+ _condition_1 = 2
136
+ end
137
+ end
138
+ _object_1[_left_1] = _condition_1
139
+ _object_1.Size = resolved.Size
140
+ _object_1.AutomaticSize = resolved.AutomaticSize
141
+ _object[_left] = _object_1
142
+ return _object
143
+ end
144
+ local _object = {
145
+ overlay = overlay,
146
+ }
147
+ local _left = "positionProps"
148
+ local _object_1 = {}
149
+ local _left_1 = "ZIndex"
150
+ local _condition_1 = props.ZIndex
151
+ if _condition_1 == nil then
152
+ _condition_1 = 2
153
+ end
154
+ _object_1[_left_1] = _condition_1
155
+ _object[_left] = _object_1
156
+ return _object
157
+ end
17
158
  local CardHeader = React.forwardRef(function(props, ref)
18
159
  local theme = React.useContext(CleanThemeContext)
19
160
  local card = React.useContext(CardContext)
20
161
  local intent = ColorHelper:getIntentColors(theme, props.intent or card.intent or "primary", "default", theme.components.card.header.intents)
21
- local padding = SpacingHelper:ResolveNumberPadding(SpacingHelper:GetPadding(theme, "md", theme.components.card.header.spacing))
162
+ local padding = SpacingHelper:GetResolvedPadding(theme, props, theme.components.card.header.spacing, theme.components.card.header.padding, "md")
22
163
  local corners = CssHelper:parseCssSize(theme.components.card.cornerRadius)
164
+ local _binding = resolveOverlayPosition(props, theme.components.card.header.position)
165
+ local overlay = _binding.overlay
166
+ local positionProps = _binding.positionProps
23
167
  local _attributes = {
24
168
  ref = ref,
25
169
  }
26
170
  for _k, _v in props do
27
171
  _attributes[_k] = _v
28
172
  end
29
- _attributes.Size = UDim2.fromScale(0, 0)
30
- _attributes.AutomaticSize = Enum.AutomaticSize.XY
173
+ for _k, _v in positionProps do
174
+ _attributes[_k] = _v
175
+ end
176
+ _attributes.Size = positionProps.Size or UDim2.fromScale(0, 0)
177
+ _attributes.AutomaticSize = positionProps.AutomaticSize or Enum.AutomaticSize.XY
31
178
  _attributes.BackgroundTransparency = intent.backgroundTransparency
32
179
  _attributes.BackgroundColor3 = intent.backgroundColor
33
180
  _attributes.BorderSizePixel = 0
34
- return React.createElement(Container, _attributes, React.createElement(Padding, {
181
+ _attributes.backgroundImage = intent.backgroundImage
182
+ _attributes.Change = if overlay and card.reportOverlayHeaderRect ~= nil then {
183
+ AbsolutePosition = function(instance)
184
+ return card.reportOverlayHeaderRect(instance.AbsolutePosition, instance.AbsoluteSize)
185
+ end,
186
+ AbsoluteSize = function(instance)
187
+ return card.reportOverlayHeaderRect(instance.AbsolutePosition, instance.AbsoluteSize)
188
+ end,
189
+ } else nil
190
+ local _exp = React.createElement(Padding, {
35
191
  resolvedPadding = padding,
36
- }), React.createElement("uistroke", {
37
- Thickness = 1,
38
- Color = intent.borderColor,
39
- BorderStrokePosition = Enum.BorderStrokePosition.Inner,
40
- }), React.createElement("uicorner", {
192
+ })
193
+ local _attributes_1 = {}
194
+ local _condition = theme.components.card.header.borderThickness
195
+ if _condition == nil then
196
+ _condition = theme.components.card.borderThickness
197
+ end
198
+ _attributes_1.Thickness = _condition
199
+ _attributes_1.Color = intent.borderColor
200
+ _attributes_1.BorderStrokePosition = Enum.BorderStrokePosition.Inner
201
+ return React.createElement(Container, _attributes, _exp, React.createElement("uistroke", _attributes_1), React.createElement("uicorner", {
41
202
  TopLeftRadius = corners,
42
203
  TopRightRadius = corners,
43
204
  BottomLeftRadius = UDim.new(0, 0),
@@ -46,7 +207,15 @@ local CardHeader = React.forwardRef(function(props, ref)
46
207
  end)
47
208
  local CardBody = React.forwardRef(function(props, ref)
48
209
  local theme = React.useContext(CleanThemeContext)
49
- local padding = SpacingHelper:ResolveNumberPadding(SpacingHelper:GetPadding(theme, "md", theme.components.card.header.spacing))
210
+ -- Unified 4-tier padding resolution (see SpacingHelper.GetResolvedPadding):
211
+ -- `card.body.spacing` (tier 2) overrides the global spacing map per
212
+ -- scale key, falling back to it for any key it doesn't define, and
213
+ -- `card.body.padding` (tier 3) overrides the scale-based tiers
214
+ -- entirely at the active key. CardBody wraps its children in a
215
+ -- Container, not a Box, so it doesn't get Box's automatic
216
+ -- `box.padding` application for free — this resolves independently
217
+ -- against `theme.components.card.body`'s own tiers.
218
+ local paddingSourceProps = props
50
219
  local _attributes = {
51
220
  ref = ref,
52
221
  Size = SizeHelper:GetSize(props, UDim2.fromScale(0, 0)),
@@ -55,34 +224,62 @@ local CardBody = React.forwardRef(function(props, ref)
55
224
  _attributes[_k] = _v
56
225
  end
57
226
  _attributes.AutomaticSize = Enum.AutomaticSize.XY
58
- return React.createElement(FlexItem, nil, React.createElement(Container, _attributes, React.createElement(Padding, {
59
- resolvedPadding = padding,
60
- }), props.children))
227
+ local _attributes_1 = {}
228
+ local _result = theme.components.card.body
229
+ if _result ~= nil then
230
+ _result = _result.spacing
231
+ end
232
+ local _result_1 = theme.components.card.body
233
+ if _result_1 ~= nil then
234
+ _result_1 = _result_1.padding
235
+ end
236
+ _attributes_1.resolvedPadding = SpacingHelper:GetResolvedPadding(theme, paddingSourceProps, _result, _result_1)
237
+ return React.createElement(FlexItem, nil, React.createElement(Container, _attributes, React.createElement(Padding, _attributes_1), props.children))
61
238
  end)
62
239
  local CardFooter = React.forwardRef(function(props, ref)
63
240
  local theme = React.useContext(CleanThemeContext)
64
241
  local card = React.useContext(CardContext)
65
242
  local intent = ColorHelper:getIntentColors(theme, props.intent or card.intent or "primary", "default", theme.components.card.footer.intents)
66
- local padding = SpacingHelper:ResolveNumberPadding(SpacingHelper:GetPadding(theme, "md", theme.components.card.footer.spacing))
243
+ local padding = SpacingHelper:GetResolvedPadding(theme, props, theme.components.card.footer.spacing, theme.components.card.footer.padding, "md")
67
244
  local corners = CssHelper:parseCssSize(theme.components.card.cornerRadius)
245
+ local _binding = resolveOverlayPosition(props, theme.components.card.footer.position)
246
+ local overlay = _binding.overlay
247
+ local positionProps = _binding.positionProps
68
248
  local _attributes = {
69
249
  ref = ref,
70
250
  }
71
251
  for _k, _v in props do
72
252
  _attributes[_k] = _v
73
253
  end
74
- _attributes.Size = UDim2.fromScale(0, 0)
75
- _attributes.AutomaticSize = Enum.AutomaticSize.XY
254
+ for _k, _v in positionProps do
255
+ _attributes[_k] = _v
256
+ end
257
+ _attributes.Size = positionProps.Size or UDim2.fromScale(0, 0)
258
+ _attributes.AutomaticSize = positionProps.AutomaticSize or Enum.AutomaticSize.XY
76
259
  _attributes.BackgroundTransparency = intent.backgroundTransparency
77
260
  _attributes.BackgroundColor3 = intent.backgroundColor
78
261
  _attributes.BorderSizePixel = 0
79
- return React.createElement(Container, _attributes, React.createElement(Padding, {
262
+ _attributes.backgroundImage = intent.backgroundImage
263
+ _attributes.Change = if overlay and card.reportOverlayFooterRect ~= nil then {
264
+ AbsolutePosition = function(instance)
265
+ return card.reportOverlayFooterRect(instance.AbsolutePosition, instance.AbsoluteSize)
266
+ end,
267
+ AbsoluteSize = function(instance)
268
+ return card.reportOverlayFooterRect(instance.AbsolutePosition, instance.AbsoluteSize)
269
+ end,
270
+ } else nil
271
+ local _exp = React.createElement(Padding, {
80
272
  resolvedPadding = padding,
81
- }), React.createElement("uistroke", {
82
- Thickness = 1,
83
- Color = intent.borderColor,
84
- BorderStrokePosition = Enum.BorderStrokePosition.Inner,
85
- }), React.createElement("uicorner", {
273
+ })
274
+ local _attributes_1 = {}
275
+ local _condition = theme.components.card.footer.borderThickness
276
+ if _condition == nil then
277
+ _condition = theme.components.card.borderThickness
278
+ end
279
+ _attributes_1.Thickness = _condition
280
+ _attributes_1.Color = intent.borderColor
281
+ _attributes_1.BorderStrokePosition = Enum.BorderStrokePosition.Inner
282
+ return React.createElement(Container, _attributes, _exp, React.createElement("uistroke", _attributes_1), React.createElement("uicorner", {
86
283
  TopLeftRadius = UDim.new(0, 0),
87
284
  TopRightRadius = UDim.new(0, 0),
88
285
  BottomLeftRadius = corners,
@@ -91,13 +288,214 @@ local CardFooter = React.forwardRef(function(props, ref)
91
288
  end)
92
289
  local Card = React.forwardRef(function(props, ref)
93
290
  local theme = React.useContext(CleanThemeContext)
291
+ -- Overlay clearance: an overlay header (see resolveOverlayPosition)
292
+ -- is pulled out of the VStack entirely, so nothing reserves space
293
+ -- for it — its rendered box typically extends downward past its own
294
+ -- anchor point and overlaps into the top of the Box. These two
295
+ -- pieces of state capture the header's reported rect (via
296
+ -- CardContext.reportOverlayHeaderRect, set on CardHeader below) and
297
+ -- the overlay wrapper frame's own AbsolutePosition, so the amount of
298
+ -- that overlap can be measured and reserved as extra top padding on
299
+ -- the Box (see headerClearance/boxProps below). Both are undefined
300
+ -- until their first Change event fires after mount, which is the
301
+ -- same measure-then-settle characteristic Accordion.tsx accepts for
302
+ -- its own AbsoluteSize-driven measurements.
303
+ local headerRect, setHeaderRect = React.useState()
304
+ local footerRect, setFooterRect = React.useState()
305
+ local wrapperAbsoluteY, setWrapperAbsoluteY = React.useState()
306
+ local wrapperAbsoluteSize, setWrapperAbsoluteSize = React.useState()
307
+ local overlayWrapperRef = React.useRef()
308
+ local reportOverlayHeaderRect = React.useCallback(function(position, size)
309
+ setHeaderRect({
310
+ position = position,
311
+ size = size,
312
+ })
313
+ end, {})
314
+ local reportOverlayFooterRect = React.useCallback(function(position, size)
315
+ setFooterRect({
316
+ position = position,
317
+ size = size,
318
+ })
319
+ end, {})
94
320
  local contextValue = React.useMemo(function()
95
321
  return {
96
322
  intent = props.intent,
323
+ reportOverlayHeaderRect = reportOverlayHeaderRect,
324
+ reportOverlayFooterRect = reportOverlayFooterRect,
97
325
  }
98
- end, { props.intent })
326
+ end, { props.intent, reportOverlayHeaderRect, reportOverlayFooterRect })
99
327
  local intent = ColorHelper:getIntentColors(theme, props.intent or "primary", "default", theme.components.card.header.intents)
100
- local _attributes = table.clone(props)
328
+ -- Pull out a Header/Footer that wants overlay placement so it can be
329
+ -- rendered outside the VStack below (see resolveOverlayPosition above
330
+ -- for why it can't stay inside the VStack and still be positioned).
331
+ -- Everything else renders in place, in its original order.
332
+ local _binding = React.useMemo(function()
333
+ local flow = {}
334
+ local header
335
+ local footer
336
+ React.Children.forEach(props.children, function(child)
337
+ if child == nil then
338
+ return nil
339
+ end
340
+ if React.isValidElement(child) and child.type == CardHeader and header == nil then
341
+ local headerElement = child
342
+ local _condition = headerElement.props.overlay
343
+ if _condition == nil then
344
+ local _result = theme.components.card.header.position
345
+ if _result ~= nil then
346
+ _result = _result.position
347
+ end
348
+ _condition = (_result == "absolute")
349
+ end
350
+ if _condition then
351
+ header = headerElement
352
+ return nil
353
+ end
354
+ end
355
+ if React.isValidElement(child) and child.type == CardFooter and footer == nil then
356
+ local footerElement = child
357
+ local _condition = footerElement.props.overlay
358
+ if _condition == nil then
359
+ local _result = theme.components.card.footer.position
360
+ if _result ~= nil then
361
+ _result = _result.position
362
+ end
363
+ _condition = (_result == "absolute")
364
+ end
365
+ if _condition then
366
+ footer = footerElement
367
+ return nil
368
+ end
369
+ end
370
+ local _child = child
371
+ table.insert(flow, _child)
372
+ end)
373
+ return {
374
+ flowChildren = flow,
375
+ overlayHeader = header,
376
+ overlayFooter = footer,
377
+ }
378
+ end, { props.children, theme })
379
+ local flowChildren = _binding.flowChildren
380
+ local overlayHeader = _binding.overlayHeader
381
+ local overlayFooter = _binding.overlayFooter
382
+ local hasOverlay = overlayHeader ~= nil or overlayFooter ~= nil
383
+ React.useEffect(function()
384
+ if not hasOverlay then
385
+ return nil
386
+ end
387
+ local frame = overlayWrapperRef.current
388
+ if not frame then
389
+ return nil
390
+ end
391
+ -- Some renderers (e.g. the browser-based Loom scene preview) don't
392
+ -- back this ref with an instance that supports property-changed
393
+ -- signals, so guard the subscription instead of throwing and
394
+ -- losing overlay clearance tracking.
395
+ local positionConn
396
+ local sizeConn
397
+ pcall(function()
398
+ positionConn = frame:GetPropertyChangedSignal("AbsolutePosition"):Connect(function()
399
+ return setWrapperAbsoluteY(frame.AbsolutePosition.Y)
400
+ end)
401
+ end)
402
+ pcall(function()
403
+ sizeConn = frame:GetPropertyChangedSignal("AbsoluteSize"):Connect(function()
404
+ return setWrapperAbsoluteSize(frame.AbsoluteSize)
405
+ end)
406
+ end)
407
+ return function()
408
+ local _result = positionConn
409
+ if _result ~= nil then
410
+ _result:Disconnect()
411
+ end
412
+ local _result_1 = sizeConn
413
+ if _result_1 ~= nil then
414
+ _result_1:Disconnect()
415
+ end
416
+ end
417
+ end, { hasOverlay })
418
+ -- How far the overlay header's bottom edge extends past the
419
+ -- wrapper's (i.e. the Box's) top edge — 0 when there's no overlap
420
+ -- (e.g. a small/no theme offset) or nothing has been measured yet.
421
+ local headerClearance = if overlayHeader ~= nil and headerRect ~= nil and wrapperAbsoluteY ~= nil then math.max(0, (headerRect.position.Y + headerRect.size.Y) - wrapperAbsoluteY) else 0
422
+ -- Same idea, mirrored for the footer: the wrapper's bottom edge
423
+ -- (its top plus its own height) minus the footer's reported top
424
+ -- edge (AbsolutePosition is always the top-left corner regardless
425
+ -- of AnchorPoint) is how far the footer's top edge sits above the
426
+ -- Box's bottom edge, i.e. how much it overlaps upward into it.
427
+ local wrapperBottomY = if wrapperAbsoluteY ~= nil and wrapperAbsoluteSize ~= nil then wrapperAbsoluteY + wrapperAbsoluteSize.Y else nil
428
+ local footerClearance = if overlayFooter ~= nil and footerRect ~= nil and wrapperBottomY ~= nil then math.max(0, wrapperBottomY - footerRect.position.Y) else 0
429
+ -- The Box below now sits inside an extra positioning frame (see
430
+ -- boxWithOverlay), so its own Position/AnchorPoint/ZIndex/LayoutOrder
431
+ -- are cleared here to avoid double-applying them — the wrapper frame
432
+ -- owns those instead.
433
+ local _result
434
+ if hasOverlay then
435
+ local _object = table.clone(props)
436
+ setmetatable(_object, nil)
437
+ _object.Position = nil
438
+ _object.AnchorPoint = nil
439
+ _object.top = nil
440
+ _object.left = nil
441
+ _object.right = nil
442
+ _object.bottom = nil
443
+ _object.ZIndex = nil
444
+ _object.LayoutOrder = nil
445
+ _result = _object
446
+ else
447
+ _result = props
448
+ end
449
+ local boxProps = _result
450
+ -- When there's no footer at all, footerClearance is always 0 (there's
451
+ -- nothing to measure an overlap against), so fall back to the Box's
452
+ -- own explicit theme.components.box.padding bottom inset — if the
453
+ -- theme sets one — as a floor instead. This keeps content clear of a
454
+ -- themed Box's decorative border image (e.g. wooden's wood-frame art,
455
+ -- which needs a real bottom inset) even when there's no footer to
456
+ -- reserve that space via measured overlap. Themes that don't set an
457
+ -- explicit box padding (GetExplicitPadding returns undefined) resolve
458
+ -- this floor to 0, so behavior there is unchanged. When a footer IS
459
+ -- present, footerClearance (its real measured overlap) is used as-is
460
+ -- exactly as before — this floor never applies then, since the
461
+ -- footer's own overlap already reserves whatever space it needs.
462
+ local _result_1
463
+ if overlayFooter == nil then
464
+ local _result_2 = SpacingHelper:GetExplicitPadding(theme.components.box.padding, theme.default.spacing)
465
+ if _result_2 ~= nil then
466
+ _result_2 = _result_2.bottom
467
+ end
468
+ local _condition = _result_2
469
+ if _condition == nil then
470
+ _condition = 0
471
+ end
472
+ _result_1 = _condition
473
+ else
474
+ _result_1 = 0
475
+ end
476
+ local explicitBoxBottomFloor = _result_1
477
+ -- When an overlay header and/or footer measurably overlaps into the
478
+ -- Box (headerClearance/footerClearance > 0), or the footer-absent
479
+ -- floor above is nonzero, reserve that much extra top/bottom padding
480
+ -- via resolvedPadding — this takes over Box's own outer padding,
481
+ -- which is otherwise 0/0/0/0 since Card always forces spacing="None"
482
+ -- on Box below.
483
+ local _result_2
484
+ if headerClearance > 0 or footerClearance > 0 or explicitBoxBottomFloor > 0 then
485
+ local _object = table.clone(boxProps)
486
+ setmetatable(_object, nil)
487
+ _object.resolvedPadding = {
488
+ top = headerClearance,
489
+ bottom = math.max(footerClearance, explicitBoxBottomFloor),
490
+ left = 0,
491
+ right = 0,
492
+ }
493
+ _result_2 = _object
494
+ else
495
+ _result_2 = boxProps
496
+ end
497
+ local boxPropsWithOverlayClearance = _result_2
498
+ local _attributes = table.clone(boxPropsWithOverlayClearance)
101
499
  setmetatable(_attributes, nil)
102
500
  _attributes.ref = ref
103
501
  _attributes.center = nil
@@ -105,7 +503,39 @@ local Card = React.forwardRef(function(props, ref)
105
503
  _attributes["border-color"] = intent.borderColor
106
504
  local cardBox = (React.createElement(Box, _attributes, React.createElement(VStack, {
107
505
  spacing = "None",
108
- }, props.children)))
506
+ }, flowChildren)))
507
+ -- ClipsDescendants is off so an overlay header/footer can protrude
508
+ -- past the Box's own edges (e.g. a plaque mounted above the top
509
+ -- border). This frame takes over the Position/AnchorPoint the Box
510
+ -- would otherwise have used, and auto-sizes to hug the Box (plus
511
+ -- whatever the overlay siblings protrude by).
512
+ --
513
+ -- `center` is stripped before deriving Position/AnchorPoint here,
514
+ -- the same way `cardBox`'s Box always gets `center={undefined}`:
515
+ -- when `center` is set, centering is handled by the outer
516
+ -- `isCentered` wrapper below (a UIListLayout that centers this
517
+ -- whole frame), not by this frame's own Position/AnchorPoint. If
518
+ -- both applied `center` at once, the UIListLayout would place the
519
+ -- frame using an (0,0)-anchor assumption while the frame itself
520
+ -- reported a (0.5,0.5) AnchorPoint, offsetting it by half its own
521
+ -- size and letting its (ClipsDescendants=false) contents spill out.
522
+ -- Explicit top/left/right/bottom/Position/AnchorPoint overrides are
523
+ -- unaffected and still apply directly to this frame.
524
+ local _object = table.clone(props)
525
+ setmetatable(_object, nil)
526
+ _object.center = nil
527
+ local wrapperPositionProps = _object
528
+ local boxWithOverlay = if hasOverlay then (React.createElement("frame", {
529
+ ref = overlayWrapperRef,
530
+ BackgroundTransparency = 1,
531
+ ClipsDescendants = false,
532
+ Size = SizeHelper:GetSize(props, UDim2.fromScale(1, 1)),
533
+ AutomaticSize = SizeHelper:GetAutoSize(props),
534
+ Position = SizeHelper:GetPosition(wrapperPositionProps),
535
+ AnchorPoint = SizeHelper:GetAnchor(wrapperPositionProps),
536
+ ZIndex = props.ZIndex,
537
+ LayoutOrder = props.LayoutOrder,
538
+ }, cardBox, overlayHeader, overlayFooter)) else cardBox
109
539
  local isCentered = props.center == true and props.Position == nil and props.AnchorPoint == nil
110
540
  return React.createElement(CardContext.Provider, {
111
541
  value = contextValue,
@@ -117,7 +547,7 @@ local Card = React.forwardRef(function(props, ref)
117
547
  FillDirection = Enum.FillDirection.Horizontal,
118
548
  HorizontalAlignment = Enum.HorizontalAlignment.Center,
119
549
  VerticalAlignment = Enum.VerticalAlignment.Center,
120
- }), cardBox)) else cardBox)
550
+ }), boxWithOverlay)) else boxWithOverlay)
121
551
  end)
122
552
  Card.Header = CardHeader
123
553
  Card.Footer = CardFooter
@@ -51,14 +51,52 @@ function Icon(props)
51
51
  setmetatable(_attributes, nil)
52
52
  return React.createElement(SpinningIcon, _attributes)
53
53
  end
54
- return React.createElement("imagelabel", {
54
+ local _attributes = {
55
+ AnchorPoint = props.AnchorPoint,
55
56
  Rotation = props.Rotation,
56
57
  Size = props.Size or UDim2.fromOffset(size, size),
58
+ Position = props.Position,
57
59
  LayoutOrder = props.LayoutOrder,
58
60
  Image = if iconId == nil then nil else `rbxassetid://{iconId}`,
59
- BackgroundTransparency = 1,
60
- ImageColor3 = props.color or theme.colors.intents.primary.default.textColor,
61
- })
61
+ }
62
+ local _condition = props.BackgroundTransparency
63
+ if _condition == nil then
64
+ _condition = 1
65
+ end
66
+ _attributes.BackgroundTransparency = _condition
67
+ _attributes.ImageColor3 = props.color or theme.colors.intents.primary.default.textColor
68
+ _attributes.ZIndex = props.ZIndex
69
+ _attributes.Visible = props.Visible
70
+ _attributes.Active = props.Active
71
+ _attributes.Archivable = props.Archivable
72
+ _attributes.BackgroundColor3 = props.BackgroundColor3
73
+ _attributes.BorderColor3 = props.BorderColor3
74
+ _attributes.BorderMode = props.BorderMode
75
+ _attributes.BorderSizePixel = props.BorderSizePixel
76
+ _attributes.ClipsDescendants = props.ClipsDescendants
77
+ _attributes.Selectable = props.Selectable
78
+ _attributes.SelectionImageObject = props.SelectionImageObject
79
+ _attributes.SizeConstraint = props.SizeConstraint
80
+ _attributes.Tag = props.Tag
81
+ _attributes.AutoLocalize = props.AutoLocalize
82
+ _attributes.RootLocalizationTable = props.RootLocalizationTable
83
+ _attributes.NextSelectionDown = props.NextSelectionDown
84
+ _attributes.NextSelectionLeft = props.NextSelectionLeft
85
+ _attributes.NextSelectionRight = props.NextSelectionRight
86
+ _attributes.NextSelectionUp = props.NextSelectionUp
87
+ _attributes.SelectionGroup = props.SelectionGroup
88
+ _attributes.SelectionOrder = props.SelectionOrder
89
+ _attributes.ImageRectOffset = props.ImageRectOffset
90
+ _attributes.ImageRectSize = props.ImageRectSize
91
+ _attributes.ImageTransparency = props.ImageTransparency
92
+ _attributes.ResampleMode = props.ResampleMode
93
+ _attributes.ScaleType = props.ScaleType
94
+ _attributes.SliceCenter = props.SliceCenter
95
+ _attributes.SliceScale = props.SliceScale
96
+ _attributes.TileSize = props.TileSize
97
+ _attributes.Change = props.Change
98
+ _attributes.Event = props.Event
99
+ return React.createElement("imagelabel", _attributes)
62
100
  end
63
101
  return {
64
102
  Icon = Icon,
@@ -8,6 +8,7 @@ interface TextProps extends React.InstanceProps<TextLabel> {
8
8
  weight?: Enum.FontWeight | "bold";
9
9
  align?: "Left" | "Right" | "Center";
10
10
  TextWrap?: boolean;
11
+ letterSpacing?: number;
11
12
  }
12
13
  export declare const Text: React.ForwardRefExoticComponent<Omit<TextProps, "ref"> & React.RefAttributes<TextLabel>>;
13
14
  export {};