@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 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 computed = if isUpdater(nextValue) then nextValue(state) else nextValue
17
- if not controlled then
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 = onChange
31
+ local _result = onChangeRef.current
21
32
  if _result ~= nil then
22
33
  _result(computed)
23
34
  end
24
- end, { controlled, onChange, state })
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.1.1",
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
- "watch": "rbxtsc -p tsconfig.json -w",
20
- "typecheck": "tsc -p tsconfig.typecheck.json"
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
@@ -1,6 +0,0 @@
1
- export * from "./context";
2
- export { default as React } from "./react";
3
- export { default as ReactRoblox } from "./reactRoblox";
4
- export * from "./refs";
5
- export * from "./slot";
6
- export * from "./useControllableState";
package/src/react.ts DELETED
@@ -1,3 +0,0 @@
1
- import React = require("@rbxts/react");
2
-
3
- export default React;
@@ -1,3 +0,0 @@
1
- import ReactRoblox = require("@rbxts/react-roblox");
2
-
3
- export default ReactRoblox;
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
- }
@@ -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
- }