@lattice-ui/core 0.1.1 → 0.3.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/LICENSE +7 -0
- package/out/slot.luau +24 -0
- package/out/useControllableState.luau +15 -4
- package/package.json +8 -4
- package/scripts/ensure-hoisted-links.mjs +0 -35
- package/src/context.ts +0 -15
- package/src/index.ts +0 -6
- package/src/react.ts +0 -3
- package/src/reactRoblox.ts +0 -3
- package/src/refs.ts +0 -28
- package/src/slot.tsx +0 -116
- package/src/useControllableState.ts +0 -28
- package/tsconfig.json +0 -11
- package/tsconfig.typecheck.json +0 -25
package/LICENSE
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Copyright 2026 astra-void
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
|
4
|
+
|
|
5
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
|
6
|
+
|
|
7
|
+
THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
package/out/slot.luau
CHANGED
|
@@ -77,6 +77,27 @@ local function mergeHandlerTable(a, b)
|
|
|
77
77
|
end
|
|
78
78
|
return out
|
|
79
79
|
end
|
|
80
|
+
local function moveHandlersToReactKeyedProps(props, key)
|
|
81
|
+
local handlers = toHandlerTable(props[key])
|
|
82
|
+
if not handlers then
|
|
83
|
+
props[key] = nil
|
|
84
|
+
return nil
|
|
85
|
+
end
|
|
86
|
+
local reactRuntime = React
|
|
87
|
+
local source = if key == "Event" then reactRuntime.Event else reactRuntime.Change
|
|
88
|
+
local dynamicProps = props
|
|
89
|
+
for rawKey, candidate in pairs(handlers) do
|
|
90
|
+
if not (type(rawKey) == "string") or not isFn(candidate) then
|
|
91
|
+
continue
|
|
92
|
+
end
|
|
93
|
+
local reactKey = source[rawKey]
|
|
94
|
+
if reactKey == nil then
|
|
95
|
+
continue
|
|
96
|
+
end
|
|
97
|
+
dynamicProps[reactKey] = candidate
|
|
98
|
+
end
|
|
99
|
+
props[key] = nil
|
|
100
|
+
end
|
|
80
101
|
local Slot = React.forwardRef(function(props, forwardedRef)
|
|
81
102
|
local child = props.children
|
|
82
103
|
local childProps = toSlotPropBag(child.props)
|
|
@@ -103,6 +124,9 @@ local Slot = React.forwardRef(function(props, forwardedRef)
|
|
|
103
124
|
local childRef = toForwardedRef(childProps.ref)
|
|
104
125
|
local mergedRef = composeRefs(childRef, forwardedRef, slotRef)
|
|
105
126
|
mergedProps.ref = mergedRef
|
|
127
|
+
-- cloneElement bypasses @rbxts/react createElement Event/Change normalization.
|
|
128
|
+
moveHandlersToReactKeyedProps(mergedProps, "Event")
|
|
129
|
+
moveHandlersToReactKeyedProps(mergedProps, "Change")
|
|
106
130
|
return React.cloneElement(child, mergedProps)
|
|
107
131
|
end)
|
|
108
132
|
Slot.displayName = "Slot"
|
|
@@ -12,16 +12,27 @@ local function useControllableState(_param)
|
|
|
12
12
|
local inner, setInner = React.useState(defaultValue)
|
|
13
13
|
local controlled = value ~= nil
|
|
14
14
|
local state = if value ~= nil then value else inner
|
|
15
|
+
local stateRef = React.useRef(state)
|
|
16
|
+
local controlledRef = React.useRef(controlled)
|
|
17
|
+
local onChangeRef = React.useRef(onChange)
|
|
18
|
+
stateRef.current = state
|
|
19
|
+
controlledRef.current = controlled
|
|
20
|
+
onChangeRef.current = onChange
|
|
15
21
|
local setState = React.useCallback(function(nextValue)
|
|
16
|
-
local
|
|
17
|
-
if
|
|
22
|
+
local current = stateRef.current
|
|
23
|
+
local computed = if isUpdater(nextValue) then nextValue(current) else nextValue
|
|
24
|
+
if computed == current then
|
|
25
|
+
return nil
|
|
26
|
+
end
|
|
27
|
+
stateRef.current = computed
|
|
28
|
+
if not controlledRef.current then
|
|
18
29
|
setInner(computed)
|
|
19
30
|
end
|
|
20
|
-
local _result =
|
|
31
|
+
local _result = onChangeRef.current
|
|
21
32
|
if _result ~= nil then
|
|
22
33
|
_result(computed)
|
|
23
34
|
end
|
|
24
|
-
end, {
|
|
35
|
+
end, {})
|
|
25
36
|
return { state, setState }
|
|
26
37
|
end
|
|
27
38
|
return {
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lattice-ui/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"main": "out/init.luau",
|
|
6
6
|
"types": "out/index.d.ts",
|
|
7
|
+
"files": [
|
|
8
|
+
"out",
|
|
9
|
+
"README.md"
|
|
10
|
+
],
|
|
7
11
|
"devDependencies": {
|
|
8
12
|
"@rbxts/react": "17.3.7-ts.1",
|
|
9
13
|
"@rbxts/react-roblox": "17.3.7-ts.1"
|
|
@@ -13,10 +17,10 @@
|
|
|
13
17
|
"@rbxts/react-roblox": "^17"
|
|
14
18
|
},
|
|
15
19
|
"scripts": {
|
|
16
|
-
"prebuild": "node ./scripts/ensure-hoisted-links.mjs",
|
|
17
20
|
"build": "rbxtsc -p tsconfig.json",
|
|
21
|
+
"prebuild": "node ./scripts/ensure-hoisted-links.mjs",
|
|
18
22
|
"prewatch": "node ./scripts/ensure-hoisted-links.mjs",
|
|
19
|
-
"
|
|
20
|
-
"
|
|
23
|
+
"typecheck": "tsc -p tsconfig.typecheck.json",
|
|
24
|
+
"watch": "rbxtsc -p tsconfig.json -w"
|
|
21
25
|
}
|
|
22
26
|
}
|
|
@@ -1,35 +0,0 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath } from "node:url";
|
|
4
|
-
|
|
5
|
-
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
-
const packageDir = path.resolve(scriptDir, "..");
|
|
7
|
-
const packageNodeModulesDir = path.join(packageDir, "node_modules");
|
|
8
|
-
const rootNodeModulesDir = path.resolve(packageDir, "../../node_modules");
|
|
9
|
-
const scopedDirs = ["@rbxts", "@rbxts-js"];
|
|
10
|
-
const symlinkType = process.platform === "win32" ? "junction" : "dir";
|
|
11
|
-
|
|
12
|
-
fs.mkdirSync(packageNodeModulesDir, { recursive: true });
|
|
13
|
-
|
|
14
|
-
for (const scopedDir of scopedDirs) {
|
|
15
|
-
const targetPath = path.join(rootNodeModulesDir, scopedDir);
|
|
16
|
-
const linkPath = path.join(packageNodeModulesDir, scopedDir);
|
|
17
|
-
|
|
18
|
-
if (!fs.existsSync(targetPath)) {
|
|
19
|
-
continue;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
let shouldRelink = true;
|
|
23
|
-
if (fs.existsSync(linkPath)) {
|
|
24
|
-
const current = fs.realpathSync(linkPath);
|
|
25
|
-
const expected = fs.realpathSync(targetPath);
|
|
26
|
-
shouldRelink = current !== expected;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (!shouldRelink) {
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
fs.rmSync(linkPath, { recursive: true, force: true });
|
|
34
|
-
fs.symlinkSync(targetPath, linkPath, symlinkType);
|
|
35
|
-
}
|
package/src/context.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import React from "@rbxts/react";
|
|
2
|
-
|
|
3
|
-
export function createStrictContext<T>(name: string) {
|
|
4
|
-
const Ctx = React.createContext<T | undefined>(undefined);
|
|
5
|
-
|
|
6
|
-
function useCtx(): T {
|
|
7
|
-
const v = React.useContext(Ctx);
|
|
8
|
-
if (v === undefined) {
|
|
9
|
-
error(`[${name}] context is undefined. Wrap components with <${name}.Provider>.`);
|
|
10
|
-
}
|
|
11
|
-
return v;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
return [Ctx.Provider, useCtx] as const;
|
|
15
|
-
}
|
package/src/index.ts
DELETED
package/src/react.ts
DELETED
package/src/reactRoblox.ts
DELETED
package/src/refs.ts
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import type React from "@rbxts/react";
|
|
2
|
-
|
|
3
|
-
type AnyRef<T> = React.Ref<T> | React.ForwardedRef<T>;
|
|
4
|
-
type RefCallback<T> = (value: T | undefined) => void;
|
|
5
|
-
|
|
6
|
-
function isRefCallback<T>(ref: AnyRef<T> | undefined): ref is RefCallback<T> {
|
|
7
|
-
return typeIs(ref, "function");
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function isMutableRefObject<T>(ref: AnyRef<T> | undefined): ref is React.MutableRefObject<T | undefined> {
|
|
11
|
-
return typeIs(ref, "table") && "current" in ref;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function setRef<T>(ref: AnyRef<T> | undefined, value: T | undefined) {
|
|
15
|
-
if (isRefCallback(ref)) {
|
|
16
|
-
ref(value);
|
|
17
|
-
return;
|
|
18
|
-
}
|
|
19
|
-
if (isMutableRefObject(ref)) {
|
|
20
|
-
ref.current = value;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export function composeRefs<T>(...refs: Array<AnyRef<T> | undefined>) {
|
|
25
|
-
return (node: T | undefined) => {
|
|
26
|
-
for (const ref of refs) if (ref) setRef(ref, node);
|
|
27
|
-
};
|
|
28
|
-
}
|
package/src/slot.tsx
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
import React from "@rbxts/react";
|
|
2
|
-
import { composeRefs } from "./refs";
|
|
3
|
-
|
|
4
|
-
type Fn = (...args: unknown[]) => void;
|
|
5
|
-
type HandlerTable = Partial<Record<string, Fn>>;
|
|
6
|
-
type SlotRef = React.ForwardedRef<Instance>;
|
|
7
|
-
type SlotPropBag = React.Attributes & Record<string, unknown>;
|
|
8
|
-
type InstanceRefCallback = (instance: Instance | undefined) => void;
|
|
9
|
-
|
|
10
|
-
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
11
|
-
return typeIs(value, "table");
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
function toSlotPropBag(value: unknown): SlotPropBag {
|
|
15
|
-
return isRecord(value) ? (value as SlotPropBag) : {};
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function isFn(value: unknown): value is Fn {
|
|
19
|
-
return typeIs(value, "function");
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function toHandlerTable(value: unknown): HandlerTable | undefined {
|
|
23
|
-
if (!isRecord(value)) {
|
|
24
|
-
return undefined;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const out: HandlerTable = {};
|
|
28
|
-
for (const [rawKey, candidate] of pairs(value)) {
|
|
29
|
-
if (!typeIs(rawKey, "string")) {
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (isFn(candidate)) {
|
|
34
|
-
out[rawKey] = candidate;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return next(out)[0] !== undefined ? out : undefined;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function toForwardedRef(value: unknown): SlotRef | undefined {
|
|
42
|
-
if (value === undefined) {
|
|
43
|
-
return undefined;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (isInstanceRefCallback(value)) {
|
|
47
|
-
return value;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (isInstanceMutableRefObject(value)) {
|
|
51
|
-
return value;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return undefined;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
function isInstanceRefCallback(value: unknown): value is InstanceRefCallback {
|
|
58
|
-
return typeIs(value, "function");
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function isInstanceMutableRefObject(value: unknown): value is React.MutableRefObject<Instance | undefined> {
|
|
62
|
-
return typeIs(value, "table") && "current" in value;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
function mergeHandlerTable(a?: HandlerTable, b?: HandlerTable) {
|
|
66
|
-
if (!a) return b;
|
|
67
|
-
if (!b) return a;
|
|
68
|
-
const out: HandlerTable = { ...a };
|
|
69
|
-
for (const [rawKey, candidate] of pairs(b)) {
|
|
70
|
-
if (!typeIs(rawKey, "string") || !isFn(candidate)) {
|
|
71
|
-
continue;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const af = a[rawKey];
|
|
75
|
-
const bf = candidate;
|
|
76
|
-
out[rawKey] =
|
|
77
|
-
af && bf
|
|
78
|
-
? (...args) => {
|
|
79
|
-
bf(...args);
|
|
80
|
-
af(...args);
|
|
81
|
-
}
|
|
82
|
-
: (bf ?? af)!;
|
|
83
|
-
}
|
|
84
|
-
return out;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
export type SlotProps = {
|
|
88
|
-
children: React.ReactElement<SlotPropBag>;
|
|
89
|
-
ref?: SlotRef;
|
|
90
|
-
} & SlotPropBag;
|
|
91
|
-
|
|
92
|
-
export const Slot = React.forwardRef<Instance, SlotProps>((props, forwardedRef) => {
|
|
93
|
-
const child = props.children;
|
|
94
|
-
const childProps = toSlotPropBag((child as { props?: unknown }).props);
|
|
95
|
-
|
|
96
|
-
const mergedProps: SlotPropBag = { ...props, ...childProps };
|
|
97
|
-
mergedProps.children = childProps.children;
|
|
98
|
-
|
|
99
|
-
const slotEvent = toHandlerTable(props.Event);
|
|
100
|
-
const childEvent = toHandlerTable(childProps.Event);
|
|
101
|
-
const slotChange = toHandlerTable(props.Change);
|
|
102
|
-
const childChange = toHandlerTable(childProps.Change);
|
|
103
|
-
|
|
104
|
-
const Event = mergeHandlerTable(slotEvent, childEvent);
|
|
105
|
-
const Change = mergeHandlerTable(slotChange, childChange);
|
|
106
|
-
if (Event) mergedProps.Event = Event;
|
|
107
|
-
if (Change) mergedProps.Change = Change;
|
|
108
|
-
|
|
109
|
-
const slotRef = toForwardedRef(props.ref);
|
|
110
|
-
const childRef = toForwardedRef(childProps.ref);
|
|
111
|
-
const mergedRef = composeRefs(childRef, forwardedRef, slotRef);
|
|
112
|
-
mergedProps.ref = mergedRef;
|
|
113
|
-
|
|
114
|
-
return React.cloneElement(child, mergedProps);
|
|
115
|
-
});
|
|
116
|
-
Slot.displayName = "Slot";
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import React from "@rbxts/react";
|
|
2
|
-
|
|
3
|
-
type Props<T> = {
|
|
4
|
-
value?: T;
|
|
5
|
-
defaultValue: T;
|
|
6
|
-
onChange?: (next: T) => void;
|
|
7
|
-
};
|
|
8
|
-
|
|
9
|
-
function isUpdater<T>(value: T | ((prev: T) => T)): value is (prev: T) => T {
|
|
10
|
-
return typeIs(value, "function");
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export function useControllableState<T>({ value, defaultValue, onChange }: Props<T>) {
|
|
14
|
-
const [inner, setInner] = React.useState(defaultValue);
|
|
15
|
-
const controlled = value !== undefined;
|
|
16
|
-
const state = value !== undefined ? value : inner;
|
|
17
|
-
|
|
18
|
-
const setState = React.useCallback(
|
|
19
|
-
(nextValue: T | ((prev: T) => T)) => {
|
|
20
|
-
const computed = isUpdater(nextValue) ? nextValue(state) : nextValue;
|
|
21
|
-
if (!controlled) setInner(computed);
|
|
22
|
-
onChange?.(computed);
|
|
23
|
-
},
|
|
24
|
-
[controlled, onChange, state],
|
|
25
|
-
);
|
|
26
|
-
|
|
27
|
-
return [state, setState] as const;
|
|
28
|
-
}
|
package/tsconfig.json
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"extends": "../../tsconfig.base.json",
|
|
3
|
-
"compilerOptions": {
|
|
4
|
-
"rootDir": "src",
|
|
5
|
-
"outDir": "out",
|
|
6
|
-
"declaration": true,
|
|
7
|
-
"typeRoots": ["./node_modules/@rbxts", "../../node_modules/@rbxts"],
|
|
8
|
-
"types": ["types", "compiler-types"]
|
|
9
|
-
},
|
|
10
|
-
"include": ["src"]
|
|
11
|
-
}
|
package/tsconfig.typecheck.json
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
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
|
-
}
|