@lattice-ui/style 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.
- package/out/index.d.ts +10 -0
- package/out/init.luau +19 -0
- package/out/primitives/Box.d.ts +10 -0
- package/out/primitives/Box.luau +38 -0
- package/out/primitives/Text.d.ts +10 -0
- package/out/primitives/Text.luau +38 -0
- package/out/recipe/createRecipe.d.ts +16 -0
- package/out/recipe/createRecipe.luau +63 -0
- package/out/sx/mergeGuiProps.d.ts +3 -0
- package/out/sx/mergeGuiProps.luau +105 -0
- package/out/sx/sx.d.ts +6 -0
- package/out/sx/sx.luau +31 -0
- package/out/theme/ThemeProvider.d.ts +5 -0
- package/out/theme/ThemeProvider.luau +46 -0
- package/out/theme/tokens.d.ts +4 -0
- package/out/theme/tokens.luau +133 -0
- package/out/theme/types.d.ts +67 -0
- package/out/theme/types.luau +2 -0
- package/package.json +23 -0
- package/src/index.ts +20 -0
- package/src/primitives/Box.tsx +44 -0
- package/src/primitives/Text.tsx +44 -0
- package/src/recipe/createRecipe.ts +85 -0
- package/src/sx/mergeGuiProps.ts +87 -0
- package/src/sx/sx.ts +35 -0
- package/src/theme/ThemeProvider.tsx +41 -0
- package/src/theme/tokens.ts +112 -0
- package/src/theme/types.ts +76 -0
- package/tsconfig.json +16 -0
- package/tsconfig.typecheck.json +25 -0
package/out/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { Box } from "./primitives/Box";
|
|
2
|
+
export { Text } from "./primitives/Text";
|
|
3
|
+
export type { RecipeConfig, RecipeSelection, RecipeVariants } from "./recipe/createRecipe";
|
|
4
|
+
export { createRecipe } from "./recipe/createRecipe";
|
|
5
|
+
export { mergeGuiProps } from "./sx/mergeGuiProps";
|
|
6
|
+
export type { Sx } from "./sx/sx";
|
|
7
|
+
export { mergeSx, resolveSx } from "./sx/sx";
|
|
8
|
+
export { ThemeProvider, useTheme, useThemeValue } from "./theme/ThemeProvider";
|
|
9
|
+
export { createTheme, defaultDarkTheme, defaultLightTheme } from "./theme/tokens";
|
|
10
|
+
export type { PartialTheme, Theme, ThemeColors, ThemeContextValue, ThemeProviderProps, ThemeRadius, ThemeSpace, ThemeTypography, ThemeTypographyStyle, } from "./theme/types";
|
package/out/init.luau
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local exports = {}
|
|
4
|
+
exports.Box = TS.import(script, script, "primitives", "Box").Box
|
|
5
|
+
exports.Text = TS.import(script, script, "primitives", "Text").Text
|
|
6
|
+
exports.createRecipe = TS.import(script, script, "recipe", "createRecipe").createRecipe
|
|
7
|
+
exports.mergeGuiProps = TS.import(script, script, "sx", "mergeGuiProps").mergeGuiProps
|
|
8
|
+
local _sx = TS.import(script, script, "sx", "sx")
|
|
9
|
+
exports.mergeSx = _sx.mergeSx
|
|
10
|
+
exports.resolveSx = _sx.resolveSx
|
|
11
|
+
local _ThemeProvider = TS.import(script, script, "theme", "ThemeProvider")
|
|
12
|
+
exports.ThemeProvider = _ThemeProvider.ThemeProvider
|
|
13
|
+
exports.useTheme = _ThemeProvider.useTheme
|
|
14
|
+
exports.useThemeValue = _ThemeProvider.useThemeValue
|
|
15
|
+
local _tokens = TS.import(script, script, "theme", "tokens")
|
|
16
|
+
exports.createTheme = _tokens.createTheme
|
|
17
|
+
exports.defaultDarkTheme = _tokens.defaultDarkTheme
|
|
18
|
+
exports.defaultLightTheme = _tokens.defaultLightTheme
|
|
19
|
+
return exports
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { React } from "@lattice-ui/core";
|
|
2
|
+
import type { Sx } from "../sx/sx";
|
|
3
|
+
type StyleProps = React.Attributes & Record<string, unknown>;
|
|
4
|
+
export type BoxProps = {
|
|
5
|
+
asChild?: boolean;
|
|
6
|
+
sx?: Sx<StyleProps>;
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
} & StyleProps;
|
|
9
|
+
export declare function Box(props: BoxProps): React.JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
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 Slot = _core.Slot
|
|
6
|
+
local mergeGuiProps = TS.import(script, script.Parent.Parent, "sx", "mergeGuiProps").mergeGuiProps
|
|
7
|
+
local resolveSx = TS.import(script, script.Parent.Parent, "sx", "sx").resolveSx
|
|
8
|
+
local useTheme = TS.import(script, script.Parent.Parent, "theme", "ThemeProvider").useTheme
|
|
9
|
+
local function Box(props)
|
|
10
|
+
local asChild = props.asChild
|
|
11
|
+
local sx = props.sx
|
|
12
|
+
local children = props.children
|
|
13
|
+
local restProps = {}
|
|
14
|
+
for rawKey, value in pairs(props) do
|
|
15
|
+
if not (type(rawKey) == "string") then
|
|
16
|
+
continue
|
|
17
|
+
end
|
|
18
|
+
if rawKey == "asChild" or rawKey == "sx" or rawKey == "children" then
|
|
19
|
+
continue
|
|
20
|
+
end
|
|
21
|
+
restProps[rawKey] = value
|
|
22
|
+
end
|
|
23
|
+
local _binding = useTheme()
|
|
24
|
+
local theme = _binding.theme
|
|
25
|
+
local mergedProps = mergeGuiProps(restProps, resolveSx(sx, theme))
|
|
26
|
+
if asChild then
|
|
27
|
+
if not React.isValidElement(children) then
|
|
28
|
+
error("[Box] `asChild` requires a single child element.")
|
|
29
|
+
end
|
|
30
|
+
local _attributes = table.clone(mergedProps)
|
|
31
|
+
setmetatable(_attributes, nil)
|
|
32
|
+
return React.createElement(Slot, _attributes, children)
|
|
33
|
+
end
|
|
34
|
+
return React.createElement("frame", mergedProps, children)
|
|
35
|
+
end
|
|
36
|
+
return {
|
|
37
|
+
Box = Box,
|
|
38
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { React } from "@lattice-ui/core";
|
|
2
|
+
import type { Sx } from "../sx/sx";
|
|
3
|
+
type StyleProps = React.Attributes & Record<string, unknown>;
|
|
4
|
+
export type TextProps = {
|
|
5
|
+
asChild?: boolean;
|
|
6
|
+
sx?: Sx<StyleProps>;
|
|
7
|
+
children?: React.ReactNode;
|
|
8
|
+
} & StyleProps;
|
|
9
|
+
export declare function Text(props: TextProps): React.JSX.Element;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
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 Slot = _core.Slot
|
|
6
|
+
local mergeGuiProps = TS.import(script, script.Parent.Parent, "sx", "mergeGuiProps").mergeGuiProps
|
|
7
|
+
local resolveSx = TS.import(script, script.Parent.Parent, "sx", "sx").resolveSx
|
|
8
|
+
local useTheme = TS.import(script, script.Parent.Parent, "theme", "ThemeProvider").useTheme
|
|
9
|
+
local function Text(props)
|
|
10
|
+
local asChild = props.asChild
|
|
11
|
+
local sx = props.sx
|
|
12
|
+
local children = props.children
|
|
13
|
+
local restProps = {}
|
|
14
|
+
for rawKey, value in pairs(props) do
|
|
15
|
+
if not (type(rawKey) == "string") then
|
|
16
|
+
continue
|
|
17
|
+
end
|
|
18
|
+
if rawKey == "asChild" or rawKey == "sx" or rawKey == "children" then
|
|
19
|
+
continue
|
|
20
|
+
end
|
|
21
|
+
restProps[rawKey] = value
|
|
22
|
+
end
|
|
23
|
+
local _binding = useTheme()
|
|
24
|
+
local theme = _binding.theme
|
|
25
|
+
local mergedProps = mergeGuiProps(restProps, resolveSx(sx, theme))
|
|
26
|
+
if asChild then
|
|
27
|
+
if not React.isValidElement(children) then
|
|
28
|
+
error("[Text] `asChild` requires a single child element.")
|
|
29
|
+
end
|
|
30
|
+
local _attributes = table.clone(mergedProps)
|
|
31
|
+
setmetatable(_attributes, nil)
|
|
32
|
+
return React.createElement(Slot, _attributes, children)
|
|
33
|
+
end
|
|
34
|
+
return React.createElement("textlabel", mergedProps, children)
|
|
35
|
+
end
|
|
36
|
+
return {
|
|
37
|
+
Text = Text,
|
|
38
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { Sx } from "../sx/sx";
|
|
2
|
+
import type { Theme } from "../theme/types";
|
|
3
|
+
type GuiPropRecord = Record<string, unknown>;
|
|
4
|
+
export type RecipeVariants<Props extends GuiPropRecord> = Record<string, Record<string, Sx<Props>>>;
|
|
5
|
+
export type RecipeSelection<Variants extends RecipeVariants<GuiPropRecord>> = Partial<Record<keyof Variants & string, string>>;
|
|
6
|
+
export type RecipeConfig<Props extends GuiPropRecord, Variants extends RecipeVariants<Props>> = {
|
|
7
|
+
base?: Sx<Props>;
|
|
8
|
+
variants?: Variants;
|
|
9
|
+
defaultVariants?: RecipeSelection<Variants>;
|
|
10
|
+
compoundVariants?: Array<{
|
|
11
|
+
variants: RecipeSelection<Variants>;
|
|
12
|
+
sx: Sx<Props>;
|
|
13
|
+
}>;
|
|
14
|
+
};
|
|
15
|
+
export declare function createRecipe<Props extends GuiPropRecord, Variants extends RecipeVariants<Props>>(config: RecipeConfig<Props, Variants>): (selection: RecipeSelection<Variants> | undefined, theme: Theme) => Partial<Props>;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local mergeGuiProps = TS.import(script, script.Parent.Parent, "sx", "mergeGuiProps").mergeGuiProps
|
|
4
|
+
local resolveSx = TS.import(script, script.Parent.Parent, "sx", "sx").resolveSx
|
|
5
|
+
local function isCompoundMatch(candidate, resolvedSelection)
|
|
6
|
+
local candidateRecord = candidate
|
|
7
|
+
local resolvedRecord = resolvedSelection
|
|
8
|
+
for rawVariantName, rawExpectedValue in pairs(candidateRecord) do
|
|
9
|
+
if not (type(rawVariantName) == "string") or not (type(rawExpectedValue) == "string") then
|
|
10
|
+
continue
|
|
11
|
+
end
|
|
12
|
+
local actualValue = resolvedRecord[rawVariantName]
|
|
13
|
+
if actualValue ~= rawExpectedValue then
|
|
14
|
+
return false
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
return true
|
|
18
|
+
end
|
|
19
|
+
local function createRecipe(config)
|
|
20
|
+
return function(selection, theme)
|
|
21
|
+
local _condition = config.defaultVariants
|
|
22
|
+
if _condition == nil then
|
|
23
|
+
_condition = {}
|
|
24
|
+
end
|
|
25
|
+
local _object = table.clone(_condition)
|
|
26
|
+
setmetatable(_object, nil)
|
|
27
|
+
local _condition_1 = selection
|
|
28
|
+
if _condition_1 == nil then
|
|
29
|
+
_condition_1 = {}
|
|
30
|
+
end
|
|
31
|
+
for _k, _v in _condition_1 do
|
|
32
|
+
_object[_k] = _v
|
|
33
|
+
end
|
|
34
|
+
local resolvedSelection = _object
|
|
35
|
+
local merged = resolveSx(config.base, theme)
|
|
36
|
+
local variants = config.variants
|
|
37
|
+
if variants then
|
|
38
|
+
local variantsRecord = variants
|
|
39
|
+
local resolvedRecord = resolvedSelection
|
|
40
|
+
for rawVariantName, rawVariantMap in pairs(variantsRecord) do
|
|
41
|
+
if not (type(rawVariantName) == "string") or not (type(rawVariantMap) == "table") then
|
|
42
|
+
continue
|
|
43
|
+
end
|
|
44
|
+
local selectedValue = resolvedRecord[rawVariantName]
|
|
45
|
+
if selectedValue == nil then
|
|
46
|
+
continue
|
|
47
|
+
end
|
|
48
|
+
local sx = rawVariantMap[selectedValue]
|
|
49
|
+
merged = mergeGuiProps(merged, resolveSx(sx, theme))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
for _, compound in config.compoundVariants or {} do
|
|
53
|
+
if not isCompoundMatch(compound.variants, resolvedSelection) then
|
|
54
|
+
continue
|
|
55
|
+
end
|
|
56
|
+
merged = mergeGuiProps(merged, resolveSx(compound.sx, theme))
|
|
57
|
+
end
|
|
58
|
+
return merged
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
return {
|
|
62
|
+
createRecipe = createRecipe,
|
|
63
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local function isRecord(value)
|
|
3
|
+
local _value = value
|
|
4
|
+
return type(_value) == "table"
|
|
5
|
+
end
|
|
6
|
+
local function isFn(value)
|
|
7
|
+
local _value = value
|
|
8
|
+
return type(_value) == "function"
|
|
9
|
+
end
|
|
10
|
+
local function toHandlerTable(value)
|
|
11
|
+
if not isRecord(value) then
|
|
12
|
+
return nil
|
|
13
|
+
end
|
|
14
|
+
local out = {}
|
|
15
|
+
for rawKey, candidate in pairs(value) do
|
|
16
|
+
if not (type(rawKey) == "string") or not isFn(candidate) then
|
|
17
|
+
continue
|
|
18
|
+
end
|
|
19
|
+
out[rawKey] = candidate
|
|
20
|
+
end
|
|
21
|
+
return if (next(out)) ~= nil then out else nil
|
|
22
|
+
end
|
|
23
|
+
local function mergeHandlerTables(tables)
|
|
24
|
+
local out = {}
|
|
25
|
+
for _, handlerTable in tables do
|
|
26
|
+
if not handlerTable then
|
|
27
|
+
continue
|
|
28
|
+
end
|
|
29
|
+
for rawKey, candidate in pairs(handlerTable) do
|
|
30
|
+
if not (type(rawKey) == "string") or not isFn(candidate) then
|
|
31
|
+
continue
|
|
32
|
+
end
|
|
33
|
+
local previous = out[rawKey]
|
|
34
|
+
out[rawKey] = if previous ~= nil then function(...)
|
|
35
|
+
local args = { ... }
|
|
36
|
+
previous(unpack(args))
|
|
37
|
+
candidate(unpack(args))
|
|
38
|
+
end else candidate
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
return if (next(out)) ~= nil then out else nil
|
|
42
|
+
end
|
|
43
|
+
local function mergeGuiProps(base, variant, user)
|
|
44
|
+
local _condition = base
|
|
45
|
+
if _condition == nil then
|
|
46
|
+
_condition = {}
|
|
47
|
+
end
|
|
48
|
+
local _object = table.clone(_condition)
|
|
49
|
+
setmetatable(_object, nil)
|
|
50
|
+
local _condition_1 = variant
|
|
51
|
+
if _condition_1 == nil then
|
|
52
|
+
_condition_1 = {}
|
|
53
|
+
end
|
|
54
|
+
for _k, _v in _condition_1 do
|
|
55
|
+
_object[_k] = _v
|
|
56
|
+
end
|
|
57
|
+
local _condition_2 = user
|
|
58
|
+
if _condition_2 == nil then
|
|
59
|
+
_condition_2 = {}
|
|
60
|
+
end
|
|
61
|
+
for _k, _v in _condition_2 do
|
|
62
|
+
_object[_k] = _v
|
|
63
|
+
end
|
|
64
|
+
local merged = _object
|
|
65
|
+
local _result = base
|
|
66
|
+
if _result ~= nil then
|
|
67
|
+
_result = _result.Event
|
|
68
|
+
end
|
|
69
|
+
local _exp = toHandlerTable(_result)
|
|
70
|
+
local _result_1 = variant
|
|
71
|
+
if _result_1 ~= nil then
|
|
72
|
+
_result_1 = _result_1.Event
|
|
73
|
+
end
|
|
74
|
+
local _exp_1 = toHandlerTable(_result_1)
|
|
75
|
+
local _result_2 = user
|
|
76
|
+
if _result_2 ~= nil then
|
|
77
|
+
_result_2 = _result_2.Event
|
|
78
|
+
end
|
|
79
|
+
local mergedEvent = mergeHandlerTables({ _exp, _exp_1, toHandlerTable(_result_2) })
|
|
80
|
+
if mergedEvent then
|
|
81
|
+
merged.Event = mergedEvent
|
|
82
|
+
end
|
|
83
|
+
local _result_3 = base
|
|
84
|
+
if _result_3 ~= nil then
|
|
85
|
+
_result_3 = _result_3.Change
|
|
86
|
+
end
|
|
87
|
+
local _exp_2 = toHandlerTable(_result_3)
|
|
88
|
+
local _result_4 = variant
|
|
89
|
+
if _result_4 ~= nil then
|
|
90
|
+
_result_4 = _result_4.Change
|
|
91
|
+
end
|
|
92
|
+
local _exp_3 = toHandlerTable(_result_4)
|
|
93
|
+
local _result_5 = user
|
|
94
|
+
if _result_5 ~= nil then
|
|
95
|
+
_result_5 = _result_5.Change
|
|
96
|
+
end
|
|
97
|
+
local mergedChange = mergeHandlerTables({ _exp_2, _exp_3, toHandlerTable(_result_5) })
|
|
98
|
+
if mergedChange then
|
|
99
|
+
merged.Change = mergedChange
|
|
100
|
+
end
|
|
101
|
+
return merged
|
|
102
|
+
end
|
|
103
|
+
return {
|
|
104
|
+
mergeGuiProps = mergeGuiProps,
|
|
105
|
+
}
|
package/out/sx/sx.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Theme } from "../theme/types";
|
|
2
|
+
type GuiPropRecord = Record<string, unknown>;
|
|
3
|
+
export type Sx<Props extends GuiPropRecord> = Partial<Props> | ((theme: Theme) => Partial<Props>) | undefined;
|
|
4
|
+
export declare function resolveSx<Props extends GuiPropRecord>(sx: Sx<Props>, theme: Theme): Partial<Props>;
|
|
5
|
+
export declare function mergeSx<Props extends GuiPropRecord>(...sxList: Array<Sx<Props>>): Sx<Props>;
|
|
6
|
+
export {};
|
package/out/sx/sx.luau
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local TS = _G[script]
|
|
3
|
+
local mergeGuiProps = TS.import(script, script.Parent, "mergeGuiProps").mergeGuiProps
|
|
4
|
+
local function isSxResolver(sx)
|
|
5
|
+
local _sx = sx
|
|
6
|
+
return type(_sx) == "function"
|
|
7
|
+
end
|
|
8
|
+
local function resolveSx(sx, theme)
|
|
9
|
+
if not sx then
|
|
10
|
+
return {}
|
|
11
|
+
end
|
|
12
|
+
if isSxResolver(sx) then
|
|
13
|
+
return sx(theme)
|
|
14
|
+
end
|
|
15
|
+
return sx
|
|
16
|
+
end
|
|
17
|
+
local function mergeSx(...)
|
|
18
|
+
local sxList = { ... }
|
|
19
|
+
return function(theme)
|
|
20
|
+
local merged = {}
|
|
21
|
+
for _, sx in sxList do
|
|
22
|
+
local resolved = resolveSx(sx, theme)
|
|
23
|
+
merged = mergeGuiProps(merged, resolved)
|
|
24
|
+
end
|
|
25
|
+
return merged
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
return {
|
|
29
|
+
resolveSx = resolveSx,
|
|
30
|
+
mergeSx = mergeSx,
|
|
31
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { React } from "@lattice-ui/core";
|
|
2
|
+
import type { Theme, ThemeContextValue, ThemeProviderProps } from "./types";
|
|
3
|
+
export declare function ThemeProvider(props: ThemeProviderProps): React.JSX.Element;
|
|
4
|
+
export declare function useTheme(): ThemeContextValue;
|
|
5
|
+
export declare function useThemeValue<T>(selector: (theme: Theme) => T): T;
|
|
@@ -0,0 +1,46 @@
|
|
|
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 createStrictContext = _core.createStrictContext
|
|
5
|
+
local React = _core.React
|
|
6
|
+
local defaultLightTheme = TS.import(script, script.Parent, "tokens").defaultLightTheme
|
|
7
|
+
local _binding = createStrictContext("ThemeProvider")
|
|
8
|
+
local ThemeContextProvider = _binding[1]
|
|
9
|
+
local useThemeContext = _binding[2]
|
|
10
|
+
local function ThemeProvider(props)
|
|
11
|
+
local internalTheme, setInternalTheme = React.useState(props.defaultTheme or defaultLightTheme)
|
|
12
|
+
local controlled = props.theme ~= nil
|
|
13
|
+
local resolvedTheme = props.theme or internalTheme
|
|
14
|
+
local setTheme = React.useCallback(function(nextTheme)
|
|
15
|
+
if not controlled then
|
|
16
|
+
setInternalTheme(nextTheme)
|
|
17
|
+
end
|
|
18
|
+
local _result = props.onThemeChange
|
|
19
|
+
if _result ~= nil then
|
|
20
|
+
_result(nextTheme)
|
|
21
|
+
end
|
|
22
|
+
end, { controlled, props.onThemeChange })
|
|
23
|
+
local contextValue = React.useMemo(function()
|
|
24
|
+
return {
|
|
25
|
+
theme = resolvedTheme,
|
|
26
|
+
setTheme = setTheme,
|
|
27
|
+
}
|
|
28
|
+
end, { resolvedTheme, setTheme })
|
|
29
|
+
return React.createElement(ThemeContextProvider, {
|
|
30
|
+
value = contextValue,
|
|
31
|
+
}, props.children)
|
|
32
|
+
end
|
|
33
|
+
local function useTheme()
|
|
34
|
+
return useThemeContext()
|
|
35
|
+
end
|
|
36
|
+
local function useThemeValue(selector)
|
|
37
|
+
local context = useThemeContext()
|
|
38
|
+
return React.useMemo(function()
|
|
39
|
+
return selector(context.theme)
|
|
40
|
+
end, { context.theme, selector })
|
|
41
|
+
end
|
|
42
|
+
return {
|
|
43
|
+
ThemeProvider = ThemeProvider,
|
|
44
|
+
useTheme = useTheme,
|
|
45
|
+
useThemeValue = useThemeValue,
|
|
46
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
-- Compiled with roblox-ts v3.0.0
|
|
2
|
+
local defaultSpace = {
|
|
3
|
+
[0] = 0,
|
|
4
|
+
[2] = 2,
|
|
5
|
+
[4] = 4,
|
|
6
|
+
[6] = 6,
|
|
7
|
+
[8] = 8,
|
|
8
|
+
[10] = 10,
|
|
9
|
+
[12] = 12,
|
|
10
|
+
[14] = 14,
|
|
11
|
+
[16] = 16,
|
|
12
|
+
[20] = 20,
|
|
13
|
+
[24] = 24,
|
|
14
|
+
[32] = 32,
|
|
15
|
+
}
|
|
16
|
+
local defaultRadius = {
|
|
17
|
+
none = 0,
|
|
18
|
+
sm = 4,
|
|
19
|
+
md = 8,
|
|
20
|
+
lg = 12,
|
|
21
|
+
xl = 16,
|
|
22
|
+
full = 999,
|
|
23
|
+
}
|
|
24
|
+
local defaultTypography = {
|
|
25
|
+
labelSm = {
|
|
26
|
+
font = Enum.Font.Gotham,
|
|
27
|
+
textSize = 14,
|
|
28
|
+
},
|
|
29
|
+
bodyMd = {
|
|
30
|
+
font = Enum.Font.Gotham,
|
|
31
|
+
textSize = 16,
|
|
32
|
+
},
|
|
33
|
+
titleMd = {
|
|
34
|
+
font = Enum.Font.GothamBold,
|
|
35
|
+
textSize = 22,
|
|
36
|
+
},
|
|
37
|
+
}
|
|
38
|
+
local defaultLightTheme = {
|
|
39
|
+
colors = {
|
|
40
|
+
background = Color3.fromRGB(246, 249, 252),
|
|
41
|
+
surface = Color3.fromRGB(233, 239, 246),
|
|
42
|
+
surfaceElevated = Color3.fromRGB(255, 255, 255),
|
|
43
|
+
border = Color3.fromRGB(193, 202, 214),
|
|
44
|
+
textPrimary = Color3.fromRGB(27, 33, 44),
|
|
45
|
+
textSecondary = Color3.fromRGB(79, 90, 107),
|
|
46
|
+
accent = Color3.fromRGB(46, 114, 216),
|
|
47
|
+
accentContrast = Color3.fromRGB(239, 245, 252),
|
|
48
|
+
danger = Color3.fromRGB(167, 56, 64),
|
|
49
|
+
dangerContrast = Color3.fromRGB(254, 236, 238),
|
|
50
|
+
overlay = Color3.fromRGB(12, 16, 23),
|
|
51
|
+
},
|
|
52
|
+
space = defaultSpace,
|
|
53
|
+
radius = defaultRadius,
|
|
54
|
+
typography = defaultTypography,
|
|
55
|
+
}
|
|
56
|
+
local defaultDarkTheme = {
|
|
57
|
+
colors = {
|
|
58
|
+
background = Color3.fromRGB(18, 21, 26),
|
|
59
|
+
surface = Color3.fromRGB(32, 37, 46),
|
|
60
|
+
surfaceElevated = Color3.fromRGB(40, 47, 60),
|
|
61
|
+
border = Color3.fromRGB(72, 80, 98),
|
|
62
|
+
textPrimary = Color3.fromRGB(233, 239, 246),
|
|
63
|
+
textSecondary = Color3.fromRGB(176, 186, 201),
|
|
64
|
+
accent = Color3.fromRGB(43, 105, 196),
|
|
65
|
+
accentContrast = Color3.fromRGB(240, 244, 250),
|
|
66
|
+
danger = Color3.fromRGB(129, 57, 63),
|
|
67
|
+
dangerContrast = Color3.fromRGB(245, 223, 226),
|
|
68
|
+
overlay = Color3.fromRGB(8, 10, 14),
|
|
69
|
+
},
|
|
70
|
+
space = defaultSpace,
|
|
71
|
+
radius = defaultRadius,
|
|
72
|
+
typography = defaultTypography,
|
|
73
|
+
}
|
|
74
|
+
local function mergeTheme(baseTheme, partialTheme)
|
|
75
|
+
if not partialTheme then
|
|
76
|
+
local _object = {}
|
|
77
|
+
local _left = "colors"
|
|
78
|
+
local _object_1 = table.clone(baseTheme.colors)
|
|
79
|
+
setmetatable(_object_1, nil)
|
|
80
|
+
_object[_left] = _object_1
|
|
81
|
+
local _left_1 = "space"
|
|
82
|
+
local _object_2 = table.clone(baseTheme.space)
|
|
83
|
+
setmetatable(_object_2, nil)
|
|
84
|
+
_object[_left_1] = _object_2
|
|
85
|
+
local _left_2 = "radius"
|
|
86
|
+
local _object_3 = table.clone(baseTheme.radius)
|
|
87
|
+
setmetatable(_object_3, nil)
|
|
88
|
+
_object[_left_2] = _object_3
|
|
89
|
+
local _left_3 = "typography"
|
|
90
|
+
local _object_4 = table.clone(baseTheme.typography)
|
|
91
|
+
setmetatable(_object_4, nil)
|
|
92
|
+
_object[_left_3] = _object_4
|
|
93
|
+
return _object
|
|
94
|
+
end
|
|
95
|
+
local _object = {}
|
|
96
|
+
local _left = "colors"
|
|
97
|
+
local _object_1 = table.clone(baseTheme.colors)
|
|
98
|
+
setmetatable(_object_1, nil)
|
|
99
|
+
for _k, _v in (partialTheme.colors or {}) do
|
|
100
|
+
_object_1[_k] = _v
|
|
101
|
+
end
|
|
102
|
+
_object[_left] = _object_1
|
|
103
|
+
local _left_1 = "space"
|
|
104
|
+
local _object_2 = table.clone(baseTheme.space)
|
|
105
|
+
setmetatable(_object_2, nil)
|
|
106
|
+
for _k, _v in (partialTheme.space or {}) do
|
|
107
|
+
_object_2[_k] = _v
|
|
108
|
+
end
|
|
109
|
+
_object[_left_1] = _object_2
|
|
110
|
+
local _left_2 = "radius"
|
|
111
|
+
local _object_3 = table.clone(baseTheme.radius)
|
|
112
|
+
setmetatable(_object_3, nil)
|
|
113
|
+
for _k, _v in (partialTheme.radius or {}) do
|
|
114
|
+
_object_3[_k] = _v
|
|
115
|
+
end
|
|
116
|
+
_object[_left_2] = _object_3
|
|
117
|
+
local _left_3 = "typography"
|
|
118
|
+
local _object_4 = table.clone(baseTheme.typography)
|
|
119
|
+
setmetatable(_object_4, nil)
|
|
120
|
+
for _k, _v in (partialTheme.typography or {}) do
|
|
121
|
+
_object_4[_k] = _v
|
|
122
|
+
end
|
|
123
|
+
_object[_left_3] = _object_4
|
|
124
|
+
return _object
|
|
125
|
+
end
|
|
126
|
+
local function createTheme(partialTheme)
|
|
127
|
+
return mergeTheme(defaultLightTheme, partialTheme)
|
|
128
|
+
end
|
|
129
|
+
return {
|
|
130
|
+
createTheme = createTheme,
|
|
131
|
+
defaultLightTheme = defaultLightTheme,
|
|
132
|
+
defaultDarkTheme = defaultDarkTheme,
|
|
133
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import type React from "@rbxts/react";
|
|
2
|
+
export type ThemeColors = {
|
|
3
|
+
background: Color3;
|
|
4
|
+
surface: Color3;
|
|
5
|
+
surfaceElevated: Color3;
|
|
6
|
+
border: Color3;
|
|
7
|
+
textPrimary: Color3;
|
|
8
|
+
textSecondary: Color3;
|
|
9
|
+
accent: Color3;
|
|
10
|
+
accentContrast: Color3;
|
|
11
|
+
danger: Color3;
|
|
12
|
+
dangerContrast: Color3;
|
|
13
|
+
overlay: Color3;
|
|
14
|
+
};
|
|
15
|
+
export type ThemeSpace = {
|
|
16
|
+
0: number;
|
|
17
|
+
2: number;
|
|
18
|
+
4: number;
|
|
19
|
+
6: number;
|
|
20
|
+
8: number;
|
|
21
|
+
10: number;
|
|
22
|
+
12: number;
|
|
23
|
+
14: number;
|
|
24
|
+
16: number;
|
|
25
|
+
20: number;
|
|
26
|
+
24: number;
|
|
27
|
+
32: number;
|
|
28
|
+
};
|
|
29
|
+
export type ThemeRadius = {
|
|
30
|
+
none: number;
|
|
31
|
+
sm: number;
|
|
32
|
+
md: number;
|
|
33
|
+
lg: number;
|
|
34
|
+
xl: number;
|
|
35
|
+
full: number;
|
|
36
|
+
};
|
|
37
|
+
export type ThemeTypographyStyle = {
|
|
38
|
+
font: Enum.Font;
|
|
39
|
+
textSize: number;
|
|
40
|
+
};
|
|
41
|
+
export type ThemeTypography = {
|
|
42
|
+
labelSm: ThemeTypographyStyle;
|
|
43
|
+
bodyMd: ThemeTypographyStyle;
|
|
44
|
+
titleMd: ThemeTypographyStyle;
|
|
45
|
+
};
|
|
46
|
+
export type Theme = {
|
|
47
|
+
colors: ThemeColors;
|
|
48
|
+
space: ThemeSpace;
|
|
49
|
+
radius: ThemeRadius;
|
|
50
|
+
typography: ThemeTypography;
|
|
51
|
+
};
|
|
52
|
+
export type PartialTheme = {
|
|
53
|
+
colors?: Partial<ThemeColors>;
|
|
54
|
+
space?: Partial<ThemeSpace>;
|
|
55
|
+
radius?: Partial<ThemeRadius>;
|
|
56
|
+
typography?: Partial<ThemeTypography>;
|
|
57
|
+
};
|
|
58
|
+
export type ThemeContextValue = {
|
|
59
|
+
theme: Theme;
|
|
60
|
+
setTheme: (nextTheme: Theme) => void;
|
|
61
|
+
};
|
|
62
|
+
export type ThemeProviderProps = {
|
|
63
|
+
theme?: Theme;
|
|
64
|
+
defaultTheme?: Theme;
|
|
65
|
+
onThemeChange?: (nextTheme: Theme) => void;
|
|
66
|
+
children?: React.ReactNode;
|
|
67
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@lattice-ui/style",
|
|
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
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@rbxts/react": "17.3.7-ts.1",
|
|
12
|
+
"@rbxts/react-roblox": "17.3.7-ts.1"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"@rbxts/react": "^17",
|
|
16
|
+
"@rbxts/react-roblox": "^17"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "rbxtsc -p tsconfig.json",
|
|
20
|
+
"watch": "rbxtsc -p tsconfig.json -w",
|
|
21
|
+
"typecheck": "tsc -p tsconfig.typecheck.json"
|
|
22
|
+
}
|
|
23
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { Box } from "./primitives/Box";
|
|
2
|
+
export { Text } from "./primitives/Text";
|
|
3
|
+
export type { RecipeConfig, RecipeSelection, RecipeVariants } from "./recipe/createRecipe";
|
|
4
|
+
export { createRecipe } from "./recipe/createRecipe";
|
|
5
|
+
export { mergeGuiProps } from "./sx/mergeGuiProps";
|
|
6
|
+
export type { Sx } from "./sx/sx";
|
|
7
|
+
export { mergeSx, resolveSx } from "./sx/sx";
|
|
8
|
+
export { ThemeProvider, useTheme, useThemeValue } from "./theme/ThemeProvider";
|
|
9
|
+
export { createTheme, defaultDarkTheme, defaultLightTheme } from "./theme/tokens";
|
|
10
|
+
export type {
|
|
11
|
+
PartialTheme,
|
|
12
|
+
Theme,
|
|
13
|
+
ThemeColors,
|
|
14
|
+
ThemeContextValue,
|
|
15
|
+
ThemeProviderProps,
|
|
16
|
+
ThemeRadius,
|
|
17
|
+
ThemeSpace,
|
|
18
|
+
ThemeTypography,
|
|
19
|
+
ThemeTypographyStyle,
|
|
20
|
+
} from "./theme/types";
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { React, Slot } from "@lattice-ui/core";
|
|
2
|
+
import { mergeGuiProps } from "../sx/mergeGuiProps";
|
|
3
|
+
import type { Sx } from "../sx/sx";
|
|
4
|
+
import { resolveSx } from "../sx/sx";
|
|
5
|
+
import { useTheme } from "../theme/ThemeProvider";
|
|
6
|
+
|
|
7
|
+
type StyleProps = React.Attributes & Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
export type BoxProps = {
|
|
10
|
+
asChild?: boolean;
|
|
11
|
+
sx?: Sx<StyleProps>;
|
|
12
|
+
children?: React.ReactNode;
|
|
13
|
+
} & StyleProps;
|
|
14
|
+
|
|
15
|
+
export function Box(props: BoxProps) {
|
|
16
|
+
const asChild = props.asChild;
|
|
17
|
+
const sx = props.sx;
|
|
18
|
+
const children = props.children;
|
|
19
|
+
const restProps: StyleProps = {};
|
|
20
|
+
for (const [rawKey, value] of pairs(props as Record<string, unknown>)) {
|
|
21
|
+
if (!typeIs(rawKey, "string")) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (rawKey === "asChild" || rawKey === "sx" || rawKey === "children") {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
restProps[rawKey] = value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { theme } = useTheme();
|
|
33
|
+
const mergedProps = mergeGuiProps(restProps, resolveSx(sx, theme));
|
|
34
|
+
|
|
35
|
+
if (asChild) {
|
|
36
|
+
if (!React.isValidElement(children)) {
|
|
37
|
+
error("[Box] `asChild` requires a single child element.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return <Slot {...mergedProps}>{children}</Slot>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return React.createElement("frame", mergedProps as never, children);
|
|
44
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { React, Slot } from "@lattice-ui/core";
|
|
2
|
+
import { mergeGuiProps } from "../sx/mergeGuiProps";
|
|
3
|
+
import type { Sx } from "../sx/sx";
|
|
4
|
+
import { resolveSx } from "../sx/sx";
|
|
5
|
+
import { useTheme } from "../theme/ThemeProvider";
|
|
6
|
+
|
|
7
|
+
type StyleProps = React.Attributes & Record<string, unknown>;
|
|
8
|
+
|
|
9
|
+
export type TextProps = {
|
|
10
|
+
asChild?: boolean;
|
|
11
|
+
sx?: Sx<StyleProps>;
|
|
12
|
+
children?: React.ReactNode;
|
|
13
|
+
} & StyleProps;
|
|
14
|
+
|
|
15
|
+
export function Text(props: TextProps) {
|
|
16
|
+
const asChild = props.asChild;
|
|
17
|
+
const sx = props.sx;
|
|
18
|
+
const children = props.children;
|
|
19
|
+
const restProps: StyleProps = {};
|
|
20
|
+
for (const [rawKey, value] of pairs(props as Record<string, unknown>)) {
|
|
21
|
+
if (!typeIs(rawKey, "string")) {
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (rawKey === "asChild" || rawKey === "sx" || rawKey === "children") {
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
restProps[rawKey] = value;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const { theme } = useTheme();
|
|
33
|
+
const mergedProps = mergeGuiProps(restProps, resolveSx(sx, theme));
|
|
34
|
+
|
|
35
|
+
if (asChild) {
|
|
36
|
+
if (!React.isValidElement(children)) {
|
|
37
|
+
error("[Text] `asChild` requires a single child element.");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return <Slot {...mergedProps}>{children}</Slot>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return React.createElement("textlabel", mergedProps as never, children);
|
|
44
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { mergeGuiProps } from "../sx/mergeGuiProps";
|
|
2
|
+
import type { Sx } from "../sx/sx";
|
|
3
|
+
import { resolveSx } from "../sx/sx";
|
|
4
|
+
import type { Theme } from "../theme/types";
|
|
5
|
+
|
|
6
|
+
type GuiPropRecord = Record<string, unknown>;
|
|
7
|
+
|
|
8
|
+
export type RecipeVariants<Props extends GuiPropRecord> = Record<string, Record<string, Sx<Props>>>;
|
|
9
|
+
export type RecipeSelection<Variants extends RecipeVariants<GuiPropRecord>> = Partial<
|
|
10
|
+
Record<keyof Variants & string, string>
|
|
11
|
+
>;
|
|
12
|
+
|
|
13
|
+
export type RecipeConfig<Props extends GuiPropRecord, Variants extends RecipeVariants<Props>> = {
|
|
14
|
+
base?: Sx<Props>;
|
|
15
|
+
variants?: Variants;
|
|
16
|
+
defaultVariants?: RecipeSelection<Variants>;
|
|
17
|
+
compoundVariants?: Array<{
|
|
18
|
+
variants: RecipeSelection<Variants>;
|
|
19
|
+
sx: Sx<Props>;
|
|
20
|
+
}>;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
function isCompoundMatch<Variants extends RecipeVariants<GuiPropRecord>>(
|
|
24
|
+
candidate: RecipeSelection<Variants>,
|
|
25
|
+
resolvedSelection: RecipeSelection<Variants>,
|
|
26
|
+
) {
|
|
27
|
+
const candidateRecord = candidate as Record<string, string | undefined>;
|
|
28
|
+
const resolvedRecord = resolvedSelection as Record<string, string | undefined>;
|
|
29
|
+
|
|
30
|
+
for (const [rawVariantName, rawExpectedValue] of pairs(candidateRecord)) {
|
|
31
|
+
if (!typeIs(rawVariantName, "string") || !typeIs(rawExpectedValue, "string")) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const actualValue = resolvedRecord[rawVariantName];
|
|
36
|
+
if (actualValue !== rawExpectedValue) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function createRecipe<Props extends GuiPropRecord, Variants extends RecipeVariants<Props>>(
|
|
45
|
+
config: RecipeConfig<Props, Variants>,
|
|
46
|
+
) {
|
|
47
|
+
return (selection: RecipeSelection<Variants> | undefined, theme: Theme): Partial<Props> => {
|
|
48
|
+
const resolvedSelection = {
|
|
49
|
+
...(config.defaultVariants ?? {}),
|
|
50
|
+
...(selection ?? {}),
|
|
51
|
+
} as RecipeSelection<Variants>;
|
|
52
|
+
|
|
53
|
+
let merged = resolveSx(config.base, theme);
|
|
54
|
+
|
|
55
|
+
const variants = config.variants;
|
|
56
|
+
if (variants) {
|
|
57
|
+
const variantsRecord = variants as Record<string, Record<string, Sx<Props>>>;
|
|
58
|
+
const resolvedRecord = resolvedSelection as Record<string, string | undefined>;
|
|
59
|
+
|
|
60
|
+
for (const [rawVariantName, rawVariantMap] of pairs(variantsRecord)) {
|
|
61
|
+
if (!typeIs(rawVariantName, "string") || !typeIs(rawVariantMap, "table")) {
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const selectedValue = resolvedRecord[rawVariantName];
|
|
66
|
+
if (selectedValue === undefined) {
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const sx = rawVariantMap[selectedValue];
|
|
71
|
+
merged = mergeGuiProps(merged, resolveSx(sx, theme));
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (const compound of config.compoundVariants ?? []) {
|
|
76
|
+
if (!isCompoundMatch(compound.variants, resolvedSelection)) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
merged = mergeGuiProps(merged, resolveSx(compound.sx, theme));
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return merged;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
type GuiHandler = (...args: unknown[]) => void;
|
|
2
|
+
type GuiHandlerTable = Partial<Record<string, GuiHandler>>;
|
|
3
|
+
type GuiPropRecord = Record<string, unknown>;
|
|
4
|
+
|
|
5
|
+
function isRecord(value: unknown): value is GuiPropRecord {
|
|
6
|
+
return typeIs(value, "table");
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function isFn(value: unknown): value is GuiHandler {
|
|
10
|
+
return typeIs(value, "function");
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function toHandlerTable(value: unknown): GuiHandlerTable | undefined {
|
|
14
|
+
if (!isRecord(value)) {
|
|
15
|
+
return undefined;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const out: GuiHandlerTable = {};
|
|
19
|
+
for (const [rawKey, candidate] of pairs(value)) {
|
|
20
|
+
if (!typeIs(rawKey, "string") || !isFn(candidate)) {
|
|
21
|
+
continue;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
out[rawKey] = candidate;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return next(out)[0] !== undefined ? out : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function mergeHandlerTables(tables: Array<GuiHandlerTable | undefined>): GuiHandlerTable | undefined {
|
|
31
|
+
const out: GuiHandlerTable = {};
|
|
32
|
+
|
|
33
|
+
for (const handlerTable of tables) {
|
|
34
|
+
if (!handlerTable) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
for (const [rawKey, candidate] of pairs(handlerTable)) {
|
|
39
|
+
if (!typeIs(rawKey, "string") || !isFn(candidate)) {
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const previous = out[rawKey];
|
|
44
|
+
out[rawKey] =
|
|
45
|
+
previous !== undefined
|
|
46
|
+
? (...args: unknown[]) => {
|
|
47
|
+
previous(...args);
|
|
48
|
+
candidate(...args);
|
|
49
|
+
}
|
|
50
|
+
: candidate;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return next(out)[0] !== undefined ? out : undefined;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function mergeGuiProps<Props extends GuiPropRecord>(
|
|
58
|
+
base?: Partial<Props>,
|
|
59
|
+
variant?: Partial<Props>,
|
|
60
|
+
user?: Partial<Props>,
|
|
61
|
+
): Partial<Props> {
|
|
62
|
+
const merged = {
|
|
63
|
+
...(base ?? {}),
|
|
64
|
+
...(variant ?? {}),
|
|
65
|
+
...(user ?? {}),
|
|
66
|
+
} as Partial<Props>;
|
|
67
|
+
|
|
68
|
+
const mergedEvent = mergeHandlerTables([
|
|
69
|
+
toHandlerTable(base?.Event),
|
|
70
|
+
toHandlerTable(variant?.Event),
|
|
71
|
+
toHandlerTable(user?.Event),
|
|
72
|
+
]);
|
|
73
|
+
if (mergedEvent) {
|
|
74
|
+
(merged as GuiPropRecord).Event = mergedEvent;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const mergedChange = mergeHandlerTables([
|
|
78
|
+
toHandlerTable(base?.Change),
|
|
79
|
+
toHandlerTable(variant?.Change),
|
|
80
|
+
toHandlerTable(user?.Change),
|
|
81
|
+
]);
|
|
82
|
+
if (mergedChange) {
|
|
83
|
+
(merged as GuiPropRecord).Change = mergedChange;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return merged;
|
|
87
|
+
}
|
package/src/sx/sx.ts
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Theme } from "../theme/types";
|
|
2
|
+
import { mergeGuiProps } from "./mergeGuiProps";
|
|
3
|
+
|
|
4
|
+
type GuiPropRecord = Record<string, unknown>;
|
|
5
|
+
|
|
6
|
+
export type Sx<Props extends GuiPropRecord> = Partial<Props> | ((theme: Theme) => Partial<Props>) | undefined;
|
|
7
|
+
|
|
8
|
+
function isSxResolver<Props extends GuiPropRecord>(sx: Sx<Props>): sx is (theme: Theme) => Partial<Props> {
|
|
9
|
+
return typeIs(sx, "function");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function resolveSx<Props extends GuiPropRecord>(sx: Sx<Props>, theme: Theme): Partial<Props> {
|
|
13
|
+
if (!sx) {
|
|
14
|
+
return {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
if (isSxResolver(sx)) {
|
|
18
|
+
return sx(theme);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return sx;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function mergeSx<Props extends GuiPropRecord>(...sxList: Array<Sx<Props>>): Sx<Props> {
|
|
25
|
+
return (theme) => {
|
|
26
|
+
let merged: Partial<Props> = {};
|
|
27
|
+
|
|
28
|
+
for (const sx of sxList) {
|
|
29
|
+
const resolved = resolveSx(sx, theme);
|
|
30
|
+
merged = mergeGuiProps(merged, resolved);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return merged;
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { createStrictContext, React } from "@lattice-ui/core";
|
|
2
|
+
import { defaultLightTheme } from "./tokens";
|
|
3
|
+
import type { Theme, ThemeContextValue, ThemeProviderProps } from "./types";
|
|
4
|
+
|
|
5
|
+
const [ThemeContextProvider, useThemeContext] = createStrictContext<ThemeContextValue>("ThemeProvider");
|
|
6
|
+
|
|
7
|
+
export function ThemeProvider(props: ThemeProviderProps) {
|
|
8
|
+
const [internalTheme, setInternalTheme] = React.useState(props.defaultTheme ?? defaultLightTheme);
|
|
9
|
+
const controlled = props.theme !== undefined;
|
|
10
|
+
const resolvedTheme = props.theme ?? internalTheme;
|
|
11
|
+
|
|
12
|
+
const setTheme = React.useCallback(
|
|
13
|
+
(nextTheme: Theme) => {
|
|
14
|
+
if (!controlled) {
|
|
15
|
+
setInternalTheme(nextTheme);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
props.onThemeChange?.(nextTheme);
|
|
19
|
+
},
|
|
20
|
+
[controlled, props.onThemeChange],
|
|
21
|
+
);
|
|
22
|
+
|
|
23
|
+
const contextValue = React.useMemo(
|
|
24
|
+
() => ({
|
|
25
|
+
theme: resolvedTheme,
|
|
26
|
+
setTheme,
|
|
27
|
+
}),
|
|
28
|
+
[resolvedTheme, setTheme],
|
|
29
|
+
);
|
|
30
|
+
|
|
31
|
+
return <ThemeContextProvider value={contextValue}>{props.children}</ThemeContextProvider>;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function useTheme() {
|
|
35
|
+
return useThemeContext();
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function useThemeValue<T>(selector: (theme: Theme) => T): T {
|
|
39
|
+
const context = useThemeContext();
|
|
40
|
+
return React.useMemo(() => selector(context.theme), [context.theme, selector]);
|
|
41
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import type { PartialTheme, Theme } from "./types";
|
|
2
|
+
|
|
3
|
+
const defaultSpace = {
|
|
4
|
+
0: 0,
|
|
5
|
+
2: 2,
|
|
6
|
+
4: 4,
|
|
7
|
+
6: 6,
|
|
8
|
+
8: 8,
|
|
9
|
+
10: 10,
|
|
10
|
+
12: 12,
|
|
11
|
+
14: 14,
|
|
12
|
+
16: 16,
|
|
13
|
+
20: 20,
|
|
14
|
+
24: 24,
|
|
15
|
+
32: 32,
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
const defaultRadius = {
|
|
19
|
+
none: 0,
|
|
20
|
+
sm: 4,
|
|
21
|
+
md: 8,
|
|
22
|
+
lg: 12,
|
|
23
|
+
xl: 16,
|
|
24
|
+
full: 999,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const defaultTypography = {
|
|
28
|
+
labelSm: {
|
|
29
|
+
font: Enum.Font.Gotham,
|
|
30
|
+
textSize: 14,
|
|
31
|
+
},
|
|
32
|
+
bodyMd: {
|
|
33
|
+
font: Enum.Font.Gotham,
|
|
34
|
+
textSize: 16,
|
|
35
|
+
},
|
|
36
|
+
titleMd: {
|
|
37
|
+
font: Enum.Font.GothamBold,
|
|
38
|
+
textSize: 22,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export const defaultLightTheme: Theme = {
|
|
43
|
+
colors: {
|
|
44
|
+
background: Color3.fromRGB(246, 249, 252),
|
|
45
|
+
surface: Color3.fromRGB(233, 239, 246),
|
|
46
|
+
surfaceElevated: Color3.fromRGB(255, 255, 255),
|
|
47
|
+
border: Color3.fromRGB(193, 202, 214),
|
|
48
|
+
textPrimary: Color3.fromRGB(27, 33, 44),
|
|
49
|
+
textSecondary: Color3.fromRGB(79, 90, 107),
|
|
50
|
+
accent: Color3.fromRGB(46, 114, 216),
|
|
51
|
+
accentContrast: Color3.fromRGB(239, 245, 252),
|
|
52
|
+
danger: Color3.fromRGB(167, 56, 64),
|
|
53
|
+
dangerContrast: Color3.fromRGB(254, 236, 238),
|
|
54
|
+
overlay: Color3.fromRGB(12, 16, 23),
|
|
55
|
+
},
|
|
56
|
+
space: defaultSpace,
|
|
57
|
+
radius: defaultRadius,
|
|
58
|
+
typography: defaultTypography,
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
export const defaultDarkTheme: Theme = {
|
|
62
|
+
colors: {
|
|
63
|
+
background: Color3.fromRGB(18, 21, 26),
|
|
64
|
+
surface: Color3.fromRGB(32, 37, 46),
|
|
65
|
+
surfaceElevated: Color3.fromRGB(40, 47, 60),
|
|
66
|
+
border: Color3.fromRGB(72, 80, 98),
|
|
67
|
+
textPrimary: Color3.fromRGB(233, 239, 246),
|
|
68
|
+
textSecondary: Color3.fromRGB(176, 186, 201),
|
|
69
|
+
accent: Color3.fromRGB(43, 105, 196),
|
|
70
|
+
accentContrast: Color3.fromRGB(240, 244, 250),
|
|
71
|
+
danger: Color3.fromRGB(129, 57, 63),
|
|
72
|
+
dangerContrast: Color3.fromRGB(245, 223, 226),
|
|
73
|
+
overlay: Color3.fromRGB(8, 10, 14),
|
|
74
|
+
},
|
|
75
|
+
space: defaultSpace,
|
|
76
|
+
radius: defaultRadius,
|
|
77
|
+
typography: defaultTypography,
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
function mergeTheme(baseTheme: Theme, partialTheme?: PartialTheme): Theme {
|
|
81
|
+
if (!partialTheme) {
|
|
82
|
+
return {
|
|
83
|
+
colors: { ...baseTheme.colors },
|
|
84
|
+
space: { ...baseTheme.space },
|
|
85
|
+
radius: { ...baseTheme.radius },
|
|
86
|
+
typography: { ...baseTheme.typography },
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
colors: {
|
|
92
|
+
...baseTheme.colors,
|
|
93
|
+
...(partialTheme.colors ?? {}),
|
|
94
|
+
},
|
|
95
|
+
space: {
|
|
96
|
+
...baseTheme.space,
|
|
97
|
+
...(partialTheme.space ?? {}),
|
|
98
|
+
},
|
|
99
|
+
radius: {
|
|
100
|
+
...baseTheme.radius,
|
|
101
|
+
...(partialTheme.radius ?? {}),
|
|
102
|
+
},
|
|
103
|
+
typography: {
|
|
104
|
+
...baseTheme.typography,
|
|
105
|
+
...(partialTheme.typography ?? {}),
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function createTheme(partialTheme?: PartialTheme) {
|
|
111
|
+
return mergeTheme(defaultLightTheme, partialTheme);
|
|
112
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type React from "@rbxts/react";
|
|
2
|
+
|
|
3
|
+
export type ThemeColors = {
|
|
4
|
+
background: Color3;
|
|
5
|
+
surface: Color3;
|
|
6
|
+
surfaceElevated: Color3;
|
|
7
|
+
border: Color3;
|
|
8
|
+
textPrimary: Color3;
|
|
9
|
+
textSecondary: Color3;
|
|
10
|
+
accent: Color3;
|
|
11
|
+
accentContrast: Color3;
|
|
12
|
+
danger: Color3;
|
|
13
|
+
dangerContrast: Color3;
|
|
14
|
+
overlay: Color3;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type ThemeSpace = {
|
|
18
|
+
0: number;
|
|
19
|
+
2: number;
|
|
20
|
+
4: number;
|
|
21
|
+
6: number;
|
|
22
|
+
8: number;
|
|
23
|
+
10: number;
|
|
24
|
+
12: number;
|
|
25
|
+
14: number;
|
|
26
|
+
16: number;
|
|
27
|
+
20: number;
|
|
28
|
+
24: number;
|
|
29
|
+
32: number;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export type ThemeRadius = {
|
|
33
|
+
none: number;
|
|
34
|
+
sm: number;
|
|
35
|
+
md: number;
|
|
36
|
+
lg: number;
|
|
37
|
+
xl: number;
|
|
38
|
+
full: number;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export type ThemeTypographyStyle = {
|
|
42
|
+
font: Enum.Font;
|
|
43
|
+
textSize: number;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type ThemeTypography = {
|
|
47
|
+
labelSm: ThemeTypographyStyle;
|
|
48
|
+
bodyMd: ThemeTypographyStyle;
|
|
49
|
+
titleMd: ThemeTypographyStyle;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export type Theme = {
|
|
53
|
+
colors: ThemeColors;
|
|
54
|
+
space: ThemeSpace;
|
|
55
|
+
radius: ThemeRadius;
|
|
56
|
+
typography: ThemeTypography;
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export type PartialTheme = {
|
|
60
|
+
colors?: Partial<ThemeColors>;
|
|
61
|
+
space?: Partial<ThemeSpace>;
|
|
62
|
+
radius?: Partial<ThemeRadius>;
|
|
63
|
+
typography?: Partial<ThemeTypography>;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
export type ThemeContextValue = {
|
|
67
|
+
theme: Theme;
|
|
68
|
+
setTheme: (nextTheme: Theme) => void;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export type ThemeProviderProps = {
|
|
72
|
+
theme?: Theme;
|
|
73
|
+
defaultTheme?: Theme;
|
|
74
|
+
onThemeChange?: (nextTheme: Theme) => void;
|
|
75
|
+
children?: React.ReactNode;
|
|
76
|
+
};
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"rootDir": "src",
|
|
5
|
+
"outDir": "out",
|
|
6
|
+
"declaration": true,
|
|
7
|
+
"typeRoots": [
|
|
8
|
+
"./node_modules/@rbxts",
|
|
9
|
+
"../../node_modules/@rbxts",
|
|
10
|
+
"./node_modules/@lattice-ui",
|
|
11
|
+
"../../node_modules/@lattice-ui"
|
|
12
|
+
],
|
|
13
|
+
"types": ["types", "compiler-types"]
|
|
14
|
+
},
|
|
15
|
+
"include": ["src"]
|
|
16
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "./tsconfig.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"noEmit": true,
|
|
5
|
+
"baseUrl": "..",
|
|
6
|
+
"rootDir": "..",
|
|
7
|
+
"paths": {
|
|
8
|
+
"@lattice-ui/checkbox": ["checkbox/src/index.ts"],
|
|
9
|
+
"@lattice-ui/core": ["core/src/index.ts"],
|
|
10
|
+
"@lattice-ui/dialog": ["dialog/src/index.ts"],
|
|
11
|
+
"@lattice-ui/focus": ["focus/src/index.ts"],
|
|
12
|
+
"@lattice-ui/layer": ["layer/src/index.ts"],
|
|
13
|
+
"@lattice-ui/menu": ["menu/src/index.ts"],
|
|
14
|
+
"@lattice-ui/popover": ["popover/src/index.ts"],
|
|
15
|
+
"@lattice-ui/popper": ["popper/src/index.ts"],
|
|
16
|
+
"@lattice-ui/radio-group": ["radio-group/src/index.ts"],
|
|
17
|
+
"@lattice-ui/style": ["style/src/index.ts"],
|
|
18
|
+
"@lattice-ui/switch": ["switch/src/index.ts"],
|
|
19
|
+
"@lattice-ui/system": ["system/src/index.ts"],
|
|
20
|
+
"@lattice-ui/tabs": ["tabs/src/index.ts"],
|
|
21
|
+
"@lattice-ui/toggle-group": ["toggle-group/src/index.ts"],
|
|
22
|
+
"@lattice-ui/tooltip": ["tooltip/src/index.ts"]
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|