@lattice-ui/system 0.1.1

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 (45) hide show
  1. package/out/density/DensityProvider.d.ts +4 -0
  2. package/out/density/DensityProvider.luau +62 -0
  3. package/out/density/density.d.ts +12 -0
  4. package/out/density/density.luau +99 -0
  5. package/out/density/types.d.ts +12 -0
  6. package/out/density/types.luau +2 -0
  7. package/out/index.d.ts +12 -0
  8. package/out/init.luau +17 -0
  9. package/out/layout/Row.d.ts +3 -0
  10. package/out/layout/Row.luau +13 -0
  11. package/out/layout/Stack.d.ts +3 -0
  12. package/out/layout/Stack.luau +117 -0
  13. package/out/layout/space.d.ts +11 -0
  14. package/out/layout/space.luau +94 -0
  15. package/out/layout/types.d.ts +29 -0
  16. package/out/layout/types.luau +2 -0
  17. package/out/surface/surface.d.ts +10 -0
  18. package/out/surface/surface.luau +48 -0
  19. package/out/surface/surfacePrimitive.d.ts +17 -0
  20. package/out/surface/surfacePrimitive.luau +55 -0
  21. package/out/system/SystemProvider.d.ts +4 -0
  22. package/out/system/SystemProvider.luau +43 -0
  23. package/out/system/baseThemeContext.d.ts +2 -0
  24. package/out/system/baseThemeContext.luau +10 -0
  25. package/out/system/systemThemeContext.d.ts +2 -0
  26. package/out/system/systemThemeContext.luau +10 -0
  27. package/out/system/types.d.ts +26 -0
  28. package/out/system/types.luau +2 -0
  29. package/package.json +20 -0
  30. package/src/density/DensityProvider.tsx +61 -0
  31. package/src/density/density.ts +102 -0
  32. package/src/density/types.ts +15 -0
  33. package/src/index.ts +22 -0
  34. package/src/layout/Row.tsx +7 -0
  35. package/src/layout/Stack.tsx +133 -0
  36. package/src/layout/space.ts +101 -0
  37. package/src/layout/types.ts +34 -0
  38. package/src/surface/surface.ts +45 -0
  39. package/src/surface/surfacePrimitive.tsx +59 -0
  40. package/src/system/SystemProvider.tsx +47 -0
  41. package/src/system/baseThemeContext.ts +5 -0
  42. package/src/system/systemThemeContext.ts +5 -0
  43. package/src/system/types.ts +29 -0
  44. package/tsconfig.json +16 -0
  45. package/tsconfig.typecheck.json +25 -0
@@ -0,0 +1,43 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _core = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out)
4
+ local React = _core.React
5
+ local useControllableState = _core.useControllableState
6
+ local defaultLightTheme = TS.import(script, TS.getModule(script, "@lattice-ui", "style").out).defaultLightTheme
7
+ local DensityProvider = TS.import(script, script.Parent.Parent, "density", "DensityProvider").DensityProvider
8
+ local SystemBaseThemeContextProvider = TS.import(script, script.Parent, "baseThemeContext").SystemBaseThemeContextProvider
9
+ local useSystemThemeContext = TS.import(script, script.Parent, "systemThemeContext").useSystemThemeContext
10
+ local function SystemProvider(props)
11
+ -- SystemProvider owns raw/base theme state. Density is applied in DensityProvider.
12
+ local _binding = useControllableState({
13
+ value = props.theme,
14
+ defaultValue = props.defaultTheme or defaultLightTheme,
15
+ onChange = props.onThemeChange,
16
+ })
17
+ local baseThemeValue = _binding[1]
18
+ local setBaseThemeValue = _binding[2]
19
+ local setBaseTheme = React.useCallback(function(nextTheme)
20
+ -- Write-path contract: updates should target baseTheme, not resolved theme.
21
+ setBaseThemeValue(nextTheme)
22
+ end, { setBaseThemeValue })
23
+ local baseThemeContextValue = React.useMemo(function()
24
+ return {
25
+ baseTheme = baseThemeValue,
26
+ setBaseTheme = setBaseTheme,
27
+ }
28
+ end, { baseThemeValue, setBaseTheme })
29
+ return React.createElement(SystemBaseThemeContextProvider, {
30
+ value = baseThemeContextValue,
31
+ }, React.createElement(DensityProvider, {
32
+ defaultDensity = props.defaultDensity,
33
+ density = props.density,
34
+ onDensityChange = props.onDensityChange,
35
+ }, props.children))
36
+ end
37
+ local function useSystemTheme()
38
+ return useSystemThemeContext()
39
+ end
40
+ return {
41
+ SystemProvider = SystemProvider,
42
+ useSystemTheme = useSystemTheme,
43
+ }
@@ -0,0 +1,2 @@
1
+ import type { SystemBaseThemeContextValue } from "./types";
2
+ export declare const SystemBaseThemeContextProvider: import("@rbxts/react").Provider<SystemBaseThemeContextValue | undefined>, useSystemBaseThemeContext: () => SystemBaseThemeContextValue;
@@ -0,0 +1,10 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local createStrictContext = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out).createStrictContext
4
+ local _binding = createStrictContext("SystemBaseThemeContext")
5
+ local SystemBaseThemeContextProvider = _binding[1]
6
+ local useSystemBaseThemeContext = _binding[2]
7
+ return {
8
+ SystemBaseThemeContextProvider = SystemBaseThemeContextProvider,
9
+ useSystemBaseThemeContext = useSystemBaseThemeContext,
10
+ }
@@ -0,0 +1,2 @@
1
+ import type { SystemThemeContextValue } from "./types";
2
+ export declare const SystemThemeContextProvider: import("@rbxts/react").Provider<SystemThemeContextValue | undefined>, useSystemThemeContext: () => SystemThemeContextValue;
@@ -0,0 +1,10 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local createStrictContext = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out).createStrictContext
4
+ local _binding = createStrictContext("SystemThemeContext")
5
+ local SystemThemeContextProvider = _binding[1]
6
+ local useSystemThemeContext = _binding[2]
7
+ return {
8
+ SystemThemeContextProvider = SystemThemeContextProvider,
9
+ useSystemThemeContext = useSystemThemeContext,
10
+ }
@@ -0,0 +1,26 @@
1
+ import type { Theme } from "@lattice-ui/style";
2
+ import type React from "@rbxts/react";
3
+ import type { DensityToken } from "../density/types";
4
+ export type SystemProviderProps = {
5
+ theme?: Theme;
6
+ defaultTheme?: Theme;
7
+ onThemeChange?: (nextTheme: Theme) => void;
8
+ density?: DensityToken;
9
+ defaultDensity?: DensityToken;
10
+ onDensityChange?: (next: DensityToken) => void;
11
+ children?: React.ReactNode;
12
+ };
13
+ export type SystemThemeContextValue = {
14
+ /** Density-resolved theme for read usage in system-managed trees. */
15
+ theme: Theme;
16
+ /** Raw theme before density transforms. Writes must target this base theme. */
17
+ baseTheme: Theme;
18
+ density: DensityToken;
19
+ /** Use this to update the raw/base theme source. */
20
+ setBaseTheme: (next: Theme) => void;
21
+ setDensity: (next: DensityToken) => void;
22
+ };
23
+ export type SystemBaseThemeContextValue = {
24
+ baseTheme: Theme;
25
+ setBaseTheme: (nextTheme: Theme) => void;
26
+ };
@@ -0,0 +1,2 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ return nil
package/package.json ADDED
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@lattice-ui/system",
3
+ "version": "0.1.1",
4
+ "private": false,
5
+ "main": "out/init.luau",
6
+ "types": "out/index.d.ts",
7
+ "dependencies": {
8
+ "@lattice-ui/core": "0.1.1",
9
+ "@lattice-ui/style": "0.1.1"
10
+ },
11
+ "peerDependencies": {
12
+ "@rbxts/react": "^17",
13
+ "@rbxts/react-roblox": "^17"
14
+ },
15
+ "scripts": {
16
+ "build": "rbxtsc -p tsconfig.json",
17
+ "typecheck": "tsc -p tsconfig.typecheck.json",
18
+ "watch": "rbxtsc -p tsconfig.json -w"
19
+ }
20
+ }
@@ -0,0 +1,61 @@
1
+ import { createStrictContext, React, useControllableState } from "@lattice-ui/core";
2
+ import { ThemeProvider } from "@lattice-ui/style";
3
+ import { useSystemBaseThemeContext } from "../system/baseThemeContext";
4
+ import { SystemThemeContextProvider } from "../system/systemThemeContext";
5
+ import type { SystemThemeContextValue } from "../system/types";
6
+ import { applyDensity } from "./density";
7
+ import type { DensityContextValue, DensityProviderProps, DensityToken } from "./types";
8
+
9
+ const [DensityContextProvider, useDensityContext] = createStrictContext<DensityContextValue>("DensityProvider");
10
+ const DEFAULT_DENSITY: DensityToken = "comfortable";
11
+
12
+ export function DensityProvider(props: DensityProviderProps) {
13
+ const { baseTheme, setBaseTheme } = useSystemBaseThemeContext();
14
+
15
+ const [densityValue, setDensityValue] = useControllableState<DensityToken>({
16
+ value: props.density,
17
+ defaultValue: props.defaultDensity ?? DEFAULT_DENSITY,
18
+ onChange: props.onDensityChange,
19
+ });
20
+
21
+ // Read-path contract: resolvedTheme is derived from baseTheme + current density.
22
+ const resolvedTheme = React.useMemo(() => applyDensity(baseTheme, densityValue), [baseTheme, densityValue]);
23
+
24
+ const setDensity = React.useCallback(
25
+ (nextDensity: DensityToken) => {
26
+ setDensityValue(nextDensity);
27
+ },
28
+ [setDensityValue],
29
+ );
30
+
31
+ const densityContextValue = React.useMemo<DensityContextValue>(
32
+ () => ({
33
+ density: densityValue,
34
+ setDensity,
35
+ }),
36
+ [densityValue, setDensity],
37
+ );
38
+
39
+ const systemThemeContextValue = React.useMemo<SystemThemeContextValue>(
40
+ () => ({
41
+ theme: resolvedTheme,
42
+ baseTheme,
43
+ density: densityValue,
44
+ setBaseTheme,
45
+ setDensity,
46
+ }),
47
+ [baseTheme, densityValue, resolvedTheme, setBaseTheme, setDensity],
48
+ );
49
+
50
+ return (
51
+ <DensityContextProvider value={densityContextValue}>
52
+ <SystemThemeContextProvider value={systemThemeContextValue}>
53
+ <ThemeProvider theme={resolvedTheme}>{props.children}</ThemeProvider>
54
+ </SystemThemeContextProvider>
55
+ </DensityContextProvider>
56
+ );
57
+ }
58
+
59
+ export function useDensity() {
60
+ return useDensityContext();
61
+ }
@@ -0,0 +1,102 @@
1
+ import type { Theme } from "@lattice-ui/style";
2
+ import type { DensityToken } from "./types";
3
+
4
+ type DensityScale = {
5
+ space: number;
6
+ radius: number;
7
+ typography: number;
8
+ };
9
+
10
+ const DENSITY_SCALES: Record<DensityToken, DensityScale> = {
11
+ compact: {
12
+ space: 0.85,
13
+ radius: 0.9,
14
+ typography: 0.92,
15
+ },
16
+ comfortable: {
17
+ space: 1,
18
+ radius: 1,
19
+ typography: 1,
20
+ },
21
+ spacious: {
22
+ space: 1.15,
23
+ radius: 1.1,
24
+ typography: 1.08,
25
+ },
26
+ };
27
+
28
+ function scaleNonNegative(value: number, factor: number) {
29
+ return math.max(0, math.round(value * factor));
30
+ }
31
+
32
+ function scaleTextSize(value: number, factor: number) {
33
+ return math.max(10, math.round(value * factor));
34
+ }
35
+
36
+ function scaleSpace(theme: Theme, factor: number): Theme["space"] {
37
+ return {
38
+ 0: scaleNonNegative(theme.space[0], factor),
39
+ 2: scaleNonNegative(theme.space[2], factor),
40
+ 4: scaleNonNegative(theme.space[4], factor),
41
+ 6: scaleNonNegative(theme.space[6], factor),
42
+ 8: scaleNonNegative(theme.space[8], factor),
43
+ 10: scaleNonNegative(theme.space[10], factor),
44
+ 12: scaleNonNegative(theme.space[12], factor),
45
+ 14: scaleNonNegative(theme.space[14], factor),
46
+ 16: scaleNonNegative(theme.space[16], factor),
47
+ 20: scaleNonNegative(theme.space[20], factor),
48
+ 24: scaleNonNegative(theme.space[24], factor),
49
+ 32: scaleNonNegative(theme.space[32], factor),
50
+ };
51
+ }
52
+
53
+ function scaleRadius(theme: Theme, factor: number): Theme["radius"] {
54
+ return {
55
+ none: scaleNonNegative(theme.radius.none, factor),
56
+ sm: scaleNonNegative(theme.radius.sm, factor),
57
+ md: scaleNonNegative(theme.radius.md, factor),
58
+ lg: scaleNonNegative(theme.radius.lg, factor),
59
+ xl: scaleNonNegative(theme.radius.xl, factor),
60
+ full: scaleNonNegative(theme.radius.full, factor),
61
+ };
62
+ }
63
+
64
+ function scaleTypography(theme: Theme, factor: number): Theme["typography"] {
65
+ return {
66
+ labelSm: {
67
+ font: theme.typography.labelSm.font,
68
+ textSize: scaleTextSize(theme.typography.labelSm.textSize, factor),
69
+ },
70
+ bodyMd: {
71
+ font: theme.typography.bodyMd.font,
72
+ textSize: scaleTextSize(theme.typography.bodyMd.textSize, factor),
73
+ },
74
+ titleMd: {
75
+ font: theme.typography.titleMd.font,
76
+ textSize: scaleTextSize(theme.typography.titleMd.textSize, factor),
77
+ },
78
+ };
79
+ }
80
+
81
+ /**
82
+ * M1 limitation: density is a pure theme transformer.
83
+ * It does not create layout or child instances.
84
+ */
85
+ export function applyDensity(theme: Theme, token: DensityToken): Theme {
86
+ const scale = DENSITY_SCALES[token];
87
+
88
+ return {
89
+ colors: { ...theme.colors },
90
+ space: scaleSpace(theme, scale.space),
91
+ radius: scaleRadius(theme, scale.radius),
92
+ typography: scaleTypography(theme, scale.typography),
93
+ };
94
+ }
95
+
96
+ /**
97
+ * M1 limitation: this helper only returns a theme transformer.
98
+ * It does not modify instance graphs or perform runtime layout composition.
99
+ */
100
+ export function density(token: DensityToken) {
101
+ return (theme: Theme) => applyDensity(theme, token);
102
+ }
@@ -0,0 +1,15 @@
1
+ import type React from "@rbxts/react";
2
+
3
+ export type DensityToken = "compact" | "comfortable" | "spacious";
4
+
5
+ export type DensityProviderProps = {
6
+ density?: DensityToken;
7
+ defaultDensity?: DensityToken;
8
+ onDensityChange?: (next: DensityToken) => void;
9
+ children?: React.ReactNode;
10
+ };
11
+
12
+ export type DensityContextValue = {
13
+ density: DensityToken;
14
+ setDensity: (next: DensityToken) => void;
15
+ };
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ export { DensityProvider, useDensity } from "./density/DensityProvider";
2
+ export { applyDensity, density } from "./density/density";
3
+ export type { DensityContextValue, DensityProviderProps, DensityToken } from "./density/types";
4
+ export { Row } from "./layout/Row";
5
+ export { Stack } from "./layout/Stack";
6
+ export type {
7
+ LayoutDirection,
8
+ RowProps,
9
+ SpaceToken,
10
+ SpaceValue,
11
+ StackAlign,
12
+ StackAutoSize,
13
+ StackJustify,
14
+ StackPadding,
15
+ StackProps,
16
+ } from "./layout/types";
17
+ export type { SurfaceToken } from "./surface/surface";
18
+ export { surface } from "./surface/surface";
19
+ export type { SurfaceProps } from "./surface/surfacePrimitive";
20
+ export { Surface } from "./surface/surfacePrimitive";
21
+ export { SystemProvider, useSystemTheme } from "./system/SystemProvider";
22
+ export type { SystemProviderProps, SystemThemeContextValue } from "./system/types";
@@ -0,0 +1,7 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import { Stack } from "./Stack";
3
+ import type { RowProps } from "./types";
4
+
5
+ export function Row(props: RowProps) {
6
+ return <Stack {...props} direction="horizontal" />;
7
+ }
@@ -0,0 +1,133 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import { mergeGuiProps, resolveSx, useTheme } from "@lattice-ui/style";
3
+ import { resolvePadding, resolveSpace } from "./space";
4
+ import type { LayoutDirection, StackAlign, StackAutoSize, StackJustify, StackProps } from "./types";
5
+
6
+ type StyleProps = React.Attributes & Record<string, unknown>;
7
+
8
+ function toHorizontalAlignment(value: StackAlign | StackJustify) {
9
+ switch (value) {
10
+ case "center":
11
+ return Enum.HorizontalAlignment.Center;
12
+ case "end":
13
+ return Enum.HorizontalAlignment.Right;
14
+ case "start":
15
+ default:
16
+ return Enum.HorizontalAlignment.Left;
17
+ }
18
+ }
19
+
20
+ function toVerticalAlignment(value: StackAlign | StackJustify) {
21
+ switch (value) {
22
+ case "center":
23
+ return Enum.VerticalAlignment.Center;
24
+ case "end":
25
+ return Enum.VerticalAlignment.Bottom;
26
+ case "start":
27
+ default:
28
+ return Enum.VerticalAlignment.Top;
29
+ }
30
+ }
31
+
32
+ function toAutomaticSize(autoSize: StackAutoSize | undefined, direction: LayoutDirection) {
33
+ if (autoSize === undefined || autoSize === false) {
34
+ return Enum.AutomaticSize.None;
35
+ }
36
+
37
+ if (autoSize === true) {
38
+ return direction === "vertical" ? Enum.AutomaticSize.Y : Enum.AutomaticSize.X;
39
+ }
40
+
41
+ switch (autoSize) {
42
+ case "x":
43
+ return Enum.AutomaticSize.X;
44
+ case "y":
45
+ return Enum.AutomaticSize.Y;
46
+ case "xy":
47
+ return Enum.AutomaticSize.XY;
48
+ default:
49
+ return Enum.AutomaticSize.None;
50
+ }
51
+ }
52
+
53
+ export function Stack(props: StackProps) {
54
+ const direction = props.direction ?? "vertical";
55
+ const gap = props.gap ?? 0;
56
+ const align = props.align ?? "start";
57
+ const justify = props.justify ?? "start";
58
+ const autoSize = props.autoSize;
59
+ const sx = props.sx;
60
+ const children = props.children;
61
+ const asChild = (props as { asChild?: unknown }).asChild;
62
+
63
+ if (asChild !== undefined) {
64
+ error("[Stack] `asChild` is not supported in M3.");
65
+ }
66
+
67
+ const restProps: StyleProps = {};
68
+ for (const [rawKey, value] of pairs(props as Record<string, unknown>)) {
69
+ if (!typeIs(rawKey, "string")) {
70
+ continue;
71
+ }
72
+
73
+ if (
74
+ rawKey === "direction" ||
75
+ rawKey === "gap" ||
76
+ rawKey === "align" ||
77
+ rawKey === "justify" ||
78
+ rawKey === "autoSize" ||
79
+ rawKey === "sx" ||
80
+ rawKey === "asChild" ||
81
+ rawKey === "padding" ||
82
+ rawKey === "paddingX" ||
83
+ rawKey === "paddingY" ||
84
+ rawKey === "paddingTop" ||
85
+ rawKey === "paddingRight" ||
86
+ rawKey === "paddingBottom" ||
87
+ rawKey === "paddingLeft" ||
88
+ rawKey === "children"
89
+ ) {
90
+ continue;
91
+ }
92
+
93
+ restProps[rawKey] = value;
94
+ }
95
+
96
+ const { theme } = useTheme();
97
+ const sxProps = resolveSx(sx, theme);
98
+ const baseProps: Partial<StyleProps> = {
99
+ BackgroundTransparency: 1,
100
+ BorderSizePixel: 0,
101
+ AutomaticSize: toAutomaticSize(autoSize, direction),
102
+ };
103
+ const mergedProps = mergeGuiProps(baseProps, sxProps, restProps);
104
+
105
+ const resolvedGap = resolveSpace(theme, gap);
106
+ const padding = resolvePadding(theme, props);
107
+ const hasPadding = padding.top > 0 || padding.right > 0 || padding.bottom > 0 || padding.left > 0;
108
+
109
+ const vertical = direction === "vertical";
110
+ const horizontalAlignment = vertical ? toHorizontalAlignment(align) : toHorizontalAlignment(justify);
111
+ const verticalAlignment = vertical ? toVerticalAlignment(justify) : toVerticalAlignment(align);
112
+
113
+ return (
114
+ <frame {...(mergedProps as Record<string, unknown>)}>
115
+ <uilistlayout
116
+ FillDirection={vertical ? Enum.FillDirection.Vertical : Enum.FillDirection.Horizontal}
117
+ HorizontalAlignment={horizontalAlignment}
118
+ Padding={new UDim(0, resolvedGap)}
119
+ SortOrder={Enum.SortOrder.LayoutOrder}
120
+ VerticalAlignment={verticalAlignment}
121
+ />
122
+ {hasPadding ? (
123
+ <uipadding
124
+ PaddingBottom={new UDim(0, padding.bottom)}
125
+ PaddingLeft={new UDim(0, padding.left)}
126
+ PaddingRight={new UDim(0, padding.right)}
127
+ PaddingTop={new UDim(0, padding.top)}
128
+ />
129
+ ) : undefined}
130
+ {children}
131
+ </frame>
132
+ );
133
+ }
@@ -0,0 +1,101 @@
1
+ import type { Theme } from "@lattice-ui/style";
2
+ import type { SpaceValue, StackPadding } from "./types";
3
+
4
+ type ResolvedPadding = {
5
+ top: number;
6
+ right: number;
7
+ bottom: number;
8
+ left: number;
9
+ };
10
+
11
+ function spaceTokenValue(theme: Theme, value: number) {
12
+ switch (value) {
13
+ case 0:
14
+ return theme.space[0];
15
+ case 2:
16
+ return theme.space[2];
17
+ case 4:
18
+ return theme.space[4];
19
+ case 6:
20
+ return theme.space[6];
21
+ case 8:
22
+ return theme.space[8];
23
+ case 10:
24
+ return theme.space[10];
25
+ case 12:
26
+ return theme.space[12];
27
+ case 14:
28
+ return theme.space[14];
29
+ case 16:
30
+ return theme.space[16];
31
+ case 20:
32
+ return theme.space[20];
33
+ case 24:
34
+ return theme.space[24];
35
+ case 32:
36
+ return theme.space[32];
37
+ default:
38
+ return undefined;
39
+ }
40
+ }
41
+
42
+ function clampSpace(value: number) {
43
+ return math.max(0, value);
44
+ }
45
+
46
+ export function resolveSpace(theme: Theme, value?: SpaceValue) {
47
+ if (value === undefined) {
48
+ return 0;
49
+ }
50
+
51
+ const tokenValue = spaceTokenValue(theme, value);
52
+ if (tokenValue !== undefined) {
53
+ return clampSpace(tokenValue);
54
+ }
55
+
56
+ return clampSpace(value);
57
+ }
58
+
59
+ export function resolvePadding(theme: Theme, value: StackPadding): ResolvedPadding {
60
+ const base = resolveSpace(theme, value.padding);
61
+
62
+ let top = base;
63
+ let right = base;
64
+ let bottom = base;
65
+ let left = base;
66
+
67
+ if (value.paddingX !== undefined) {
68
+ const axis = resolveSpace(theme, value.paddingX);
69
+ left = axis;
70
+ right = axis;
71
+ }
72
+
73
+ if (value.paddingY !== undefined) {
74
+ const axis = resolveSpace(theme, value.paddingY);
75
+ top = axis;
76
+ bottom = axis;
77
+ }
78
+
79
+ if (value.paddingTop !== undefined) {
80
+ top = resolveSpace(theme, value.paddingTop);
81
+ }
82
+
83
+ if (value.paddingRight !== undefined) {
84
+ right = resolveSpace(theme, value.paddingRight);
85
+ }
86
+
87
+ if (value.paddingBottom !== undefined) {
88
+ bottom = resolveSpace(theme, value.paddingBottom);
89
+ }
90
+
91
+ if (value.paddingLeft !== undefined) {
92
+ left = resolveSpace(theme, value.paddingLeft);
93
+ }
94
+
95
+ return {
96
+ top,
97
+ right,
98
+ bottom,
99
+ left,
100
+ };
101
+ }
@@ -0,0 +1,34 @@
1
+ import type { Sx, Theme } from "@lattice-ui/style";
2
+ import type React from "@rbxts/react";
3
+
4
+ export type LayoutDirection = "vertical" | "horizontal";
5
+ export type StackAlign = "start" | "center" | "end";
6
+ export type StackJustify = "start" | "center" | "end";
7
+ export type StackAutoSize = boolean | "x" | "y" | "xy";
8
+ export type SpaceToken = keyof Theme["space"];
9
+ export type SpaceValue = SpaceToken | number;
10
+
11
+ export type StackPadding = {
12
+ padding?: SpaceValue;
13
+ paddingX?: SpaceValue;
14
+ paddingY?: SpaceValue;
15
+ paddingTop?: SpaceValue;
16
+ paddingRight?: SpaceValue;
17
+ paddingBottom?: SpaceValue;
18
+ paddingLeft?: SpaceValue;
19
+ };
20
+
21
+ type StyleProps = React.Attributes & Record<string, unknown>;
22
+
23
+ export type StackProps = {
24
+ direction?: LayoutDirection;
25
+ gap?: SpaceValue;
26
+ align?: StackAlign;
27
+ justify?: StackJustify;
28
+ autoSize?: StackAutoSize;
29
+ sx?: Sx<StyleProps>;
30
+ children?: React.ReactNode;
31
+ } & StackPadding &
32
+ StyleProps;
33
+
34
+ export type RowProps = Omit<StackProps, "direction">;
@@ -0,0 +1,45 @@
1
+ import type { Sx } from "@lattice-ui/style";
2
+
3
+ type GuiPropRecord = Record<string, unknown>;
4
+
5
+ export type SurfaceToken = "surface" | "elevated" | "sunken" | "overlay";
6
+
7
+ function asSxProps<Props extends GuiPropRecord>(value: GuiPropRecord): Partial<Props> {
8
+ return value as unknown as Partial<Props>;
9
+ }
10
+
11
+ /**
12
+ * Props-only surface helper.
13
+ * Use this when you only want host props (including host border props).
14
+ * It does not create child instances like UICorner/UIStroke/shadow nodes.
15
+ */
16
+ export function surface<Props extends GuiPropRecord>(token: SurfaceToken): Sx<Props> {
17
+ return (theme) => {
18
+ switch (token) {
19
+ case "surface":
20
+ return asSxProps<Props>({
21
+ BackgroundColor3: theme.colors.surface,
22
+ BorderColor3: theme.colors.border,
23
+ BorderSizePixel: 1,
24
+ });
25
+ case "elevated":
26
+ return asSxProps<Props>({
27
+ BackgroundColor3: theme.colors.surfaceElevated,
28
+ BorderColor3: theme.colors.border,
29
+ BorderSizePixel: 1,
30
+ });
31
+ case "sunken":
32
+ return asSxProps<Props>({
33
+ BackgroundColor3: theme.colors.background,
34
+ BorderColor3: theme.colors.border,
35
+ BorderSizePixel: 1,
36
+ });
37
+ case "overlay":
38
+ return asSxProps<Props>({
39
+ BackgroundColor3: theme.colors.overlay,
40
+ BackgroundTransparency: 0.35,
41
+ BorderSizePixel: 0,
42
+ });
43
+ }
44
+ };
45
+ }