@lattice-ui/avatar 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,16 @@
1
+ # @lattice-ui/avatar
2
+
3
+ Headless avatar primitives for Roblox UI with delayed fallback rendering.
4
+
5
+ ## Exports
6
+
7
+ - `Avatar`
8
+ - `Avatar.Root`
9
+ - `Avatar.Image`
10
+ - `Avatar.Fallback`
11
+
12
+ ## Notes
13
+
14
+ - `delayMs` controls fallback reveal timing (default: `250`).
15
+ - Missing image sources immediately show fallback content.
16
+ - Fallback visibility logic is exported for unit testing.
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { AvatarFallbackProps } from "./types";
3
+ export declare function AvatarFallback(props: AvatarFallbackProps): React.JSX.Element;
@@ -0,0 +1,34 @@
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 useAvatarContext = TS.import(script, script.Parent, "context").useAvatarContext
7
+ local resolveAvatarFallbackVisible = TS.import(script, script.Parent, "state").resolveAvatarFallbackVisible
8
+ local function AvatarFallback(props)
9
+ local avatarContext = useAvatarContext()
10
+ local visible = resolveAvatarFallbackVisible(avatarContext.status, avatarContext.delayElapsed)
11
+ if props.asChild then
12
+ local child = props.children
13
+ if not child then
14
+ error("[AvatarFallback] `asChild` requires a child element.")
15
+ end
16
+ return React.createElement(Slot, {
17
+ Visible = visible,
18
+ }, child)
19
+ end
20
+ return React.createElement("textlabel", {
21
+ BackgroundColor3 = Color3.fromRGB(65, 72, 89),
22
+ BorderSizePixel = 0,
23
+ Size = UDim2.fromOffset(40, 40),
24
+ Text = "AB",
25
+ TextColor3 = Color3.fromRGB(235, 240, 248),
26
+ TextSize = 14,
27
+ Visible = visible,
28
+ }, React.createElement("uicorner", {
29
+ CornerRadius = UDim.new(1, 0),
30
+ }), props.children)
31
+ end
32
+ return {
33
+ AvatarFallback = AvatarFallback,
34
+ }
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { AvatarImageProps } from "./types";
3
+ export declare function AvatarImage(props: AvatarImageProps): React.JSX.Element;
@@ -0,0 +1,81 @@
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 useAvatarContext = TS.import(script, script.Parent, "context").useAvatarContext
7
+ local function toImageLabel(instance)
8
+ if not instance or not instance:IsA("ImageLabel") then
9
+ return nil
10
+ end
11
+ return instance
12
+ end
13
+ local function AvatarImage(props)
14
+ local avatarContext = useAvatarContext()
15
+ local _condition = props.src
16
+ if _condition == nil then
17
+ _condition = avatarContext.src
18
+ end
19
+ local source = _condition
20
+ local imageRef = React.useRef()
21
+ local setImageRef = React.useCallback(function(instance)
22
+ imageRef.current = toImageLabel(instance)
23
+ end, {})
24
+ React.useEffect(function()
25
+ if source == nil or #source == 0 then
26
+ avatarContext.setStatus("error")
27
+ return nil
28
+ end
29
+ avatarContext.setStatus("loading")
30
+ end, { avatarContext, source })
31
+ React.useEffect(function()
32
+ local image = imageRef.current
33
+ if not image then
34
+ return nil
35
+ end
36
+ if image.IsLoaded then
37
+ avatarContext.setStatus("loaded")
38
+ end
39
+ local connection = image:GetPropertyChangedSignal("IsLoaded"):Connect(function()
40
+ if image.IsLoaded then
41
+ avatarContext.setStatus("loaded")
42
+ end
43
+ end)
44
+ return function()
45
+ connection:Disconnect()
46
+ end
47
+ end, { avatarContext, source })
48
+ if props.asChild then
49
+ local child = props.children
50
+ if not child then
51
+ error("[AvatarImage] `asChild` requires a child element.")
52
+ end
53
+ local _attributes = {}
54
+ local _condition_1 = source
55
+ if _condition_1 == nil then
56
+ _condition_1 = ""
57
+ end
58
+ _attributes.Image = _condition_1
59
+ _attributes.Visible = avatarContext.status == "loaded"
60
+ _attributes.ref = setImageRef
61
+ return React.createElement(Slot, _attributes, child)
62
+ end
63
+ local _attributes = {
64
+ BackgroundTransparency = 1,
65
+ BorderSizePixel = 0,
66
+ }
67
+ local _condition_1 = source
68
+ if _condition_1 == nil then
69
+ _condition_1 = ""
70
+ end
71
+ _attributes.Image = _condition_1
72
+ _attributes.Size = UDim2.fromOffset(40, 40)
73
+ _attributes.Visible = avatarContext.status == "loaded"
74
+ _attributes.ref = setImageRef
75
+ return React.createElement("imagelabel", _attributes, React.createElement("uicorner", {
76
+ CornerRadius = UDim.new(1, 0),
77
+ }))
78
+ end
79
+ return {
80
+ AvatarImage = AvatarImage,
81
+ }
@@ -0,0 +1,4 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { AvatarProps } from "./types";
3
+ export declare function AvatarRoot(props: AvatarProps): React.JSX.Element;
4
+ export { AvatarRoot as Avatar };
@@ -0,0 +1,48 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local React = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out).React
4
+ local AvatarContextProvider = TS.import(script, script.Parent, "context").AvatarContextProvider
5
+ local function AvatarRoot(props)
6
+ local _condition = props.delayMs
7
+ if _condition == nil then
8
+ _condition = 250
9
+ end
10
+ local delayMs = math.max(0, _condition)
11
+ local hasSource = props.src ~= nil and #props.src > 0
12
+ local status, setStatus = React.useState(if hasSource then "loading" else "error")
13
+ local delayElapsed, setDelayElapsed = React.useState(not hasSource)
14
+ local sequenceRef = React.useRef(0)
15
+ React.useEffect(function()
16
+ sequenceRef.current += 1
17
+ local sequence = sequenceRef.current
18
+ if props.src == nil or #props.src == 0 then
19
+ setStatus("error")
20
+ setDelayElapsed(true)
21
+ return nil
22
+ end
23
+ setStatus("loading")
24
+ setDelayElapsed(false)
25
+ local delaySeconds = delayMs / 1000
26
+ task.delay(delaySeconds, function()
27
+ if sequenceRef.current ~= sequence then
28
+ return nil
29
+ end
30
+ setDelayElapsed(true)
31
+ end)
32
+ end, { delayMs, props.src })
33
+ local contextValue = React.useMemo(function()
34
+ return {
35
+ src = props.src,
36
+ status = status,
37
+ setStatus = setStatus,
38
+ delayElapsed = delayElapsed,
39
+ }
40
+ end, { delayElapsed, props.src, status })
41
+ return React.createElement(AvatarContextProvider, {
42
+ value = contextValue,
43
+ }, props.children)
44
+ end
45
+ return {
46
+ AvatarRoot = AvatarRoot,
47
+ Avatar = AvatarRoot,
48
+ }
@@ -0,0 +1,3 @@
1
+ import type { AvatarContextValue } from "./types";
2
+ declare const AvatarContextProvider: import("@rbxts/react").Provider<AvatarContextValue | undefined>, useAvatarContext: () => AvatarContextValue;
3
+ export { AvatarContextProvider, useAvatarContext };
@@ -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("Avatar")
5
+ local AvatarContextProvider = _binding[1]
6
+ local useAvatarContext = _binding[2]
7
+ return {
8
+ AvatarContextProvider = AvatarContextProvider,
9
+ useAvatarContext = useAvatarContext,
10
+ }
@@ -0,0 +1,2 @@
1
+ export type AvatarStatus = "idle" | "loading" | "loaded" | "error";
2
+ export declare function resolveAvatarFallbackVisible(status: AvatarStatus, delayElapsed: boolean): boolean;
@@ -0,0 +1,13 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local function resolveAvatarFallbackVisible(status, delayElapsed)
3
+ if status == "loaded" then
4
+ return false
5
+ end
6
+ if status == "error" then
7
+ return true
8
+ end
9
+ return delayElapsed
10
+ end
11
+ return {
12
+ resolveAvatarFallbackVisible = resolveAvatarFallbackVisible,
13
+ }
@@ -0,0 +1,22 @@
1
+ import type React from "@rbxts/react";
2
+ import type { AvatarStatus } from "./state";
3
+ export type AvatarContextValue = {
4
+ src?: string;
5
+ status: AvatarStatus;
6
+ setStatus: (status: AvatarStatus) => void;
7
+ delayElapsed: boolean;
8
+ };
9
+ export type AvatarProps = {
10
+ src?: string;
11
+ delayMs?: number;
12
+ children?: React.ReactNode;
13
+ };
14
+ export type AvatarImageProps = {
15
+ asChild?: boolean;
16
+ src?: string;
17
+ children?: React.ReactElement;
18
+ };
19
+ export type AvatarFallbackProps = {
20
+ asChild?: boolean;
21
+ children?: React.ReactElement;
22
+ };
@@ -0,0 +1,2 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ return nil
package/out/index.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { AvatarFallback } from "./Avatar/AvatarFallback";
2
+ import { AvatarImage } from "./Avatar/AvatarImage";
3
+ import { AvatarRoot } from "./Avatar/AvatarRoot";
4
+ export declare const Avatar: {
5
+ readonly Root: typeof AvatarRoot;
6
+ readonly Image: typeof AvatarImage;
7
+ readonly Fallback: typeof AvatarFallback;
8
+ };
9
+ export type { AvatarStatus } from "./Avatar/state";
10
+ export { resolveAvatarFallbackVisible } from "./Avatar/state";
11
+ export type { AvatarContextValue, AvatarFallbackProps, AvatarImageProps, AvatarProps } from "./Avatar/types";
package/out/init.luau ADDED
@@ -0,0 +1,14 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local exports = {}
4
+ local AvatarFallback = TS.import(script, script, "Avatar", "AvatarFallback").AvatarFallback
5
+ local AvatarImage = TS.import(script, script, "Avatar", "AvatarImage").AvatarImage
6
+ local AvatarRoot = TS.import(script, script, "Avatar", "AvatarRoot").AvatarRoot
7
+ local Avatar = {
8
+ Root = AvatarRoot,
9
+ Image = AvatarImage,
10
+ Fallback = AvatarFallback,
11
+ }
12
+ exports.resolveAvatarFallbackVisible = TS.import(script, script, "Avatar", "state").resolveAvatarFallbackVisible
13
+ exports.Avatar = Avatar
14
+ return exports
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@lattice-ui/avatar",
3
+ "version": "0.3.0",
4
+ "private": false,
5
+ "main": "out/init.luau",
6
+ "types": "out/index.d.ts",
7
+ "dependencies": {
8
+ "@lattice-ui/core": "0.3.0"
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
+ "typecheck": "tsc -p tsconfig.typecheck.json",
21
+ "watch": "rbxtsc -p tsconfig.json -w"
22
+ }
23
+ }
@@ -0,0 +1,33 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { useAvatarContext } from "./context";
3
+ import { resolveAvatarFallbackVisible } from "./state";
4
+ import type { AvatarFallbackProps } from "./types";
5
+
6
+ export function AvatarFallback(props: AvatarFallbackProps) {
7
+ const avatarContext = useAvatarContext();
8
+ const visible = resolveAvatarFallbackVisible(avatarContext.status, avatarContext.delayElapsed);
9
+
10
+ if (props.asChild) {
11
+ const child = props.children;
12
+ if (!child) {
13
+ error("[AvatarFallback] `asChild` requires a child element.");
14
+ }
15
+
16
+ return <Slot Visible={visible}>{child}</Slot>;
17
+ }
18
+
19
+ return (
20
+ <textlabel
21
+ BackgroundColor3={Color3.fromRGB(65, 72, 89)}
22
+ BorderSizePixel={0}
23
+ Size={UDim2.fromOffset(40, 40)}
24
+ Text="AB"
25
+ TextColor3={Color3.fromRGB(235, 240, 248)}
26
+ TextSize={14}
27
+ Visible={visible}
28
+ >
29
+ <uicorner CornerRadius={new UDim(1, 0)} />
30
+ {props.children}
31
+ </textlabel>
32
+ );
33
+ }
@@ -0,0 +1,78 @@
1
+ import { React, Slot } from "@lattice-ui/core";
2
+ import { useAvatarContext } from "./context";
3
+ import type { AvatarImageProps } from "./types";
4
+
5
+ function toImageLabel(instance: Instance | undefined) {
6
+ if (!instance || !instance.IsA("ImageLabel")) {
7
+ return undefined;
8
+ }
9
+
10
+ return instance;
11
+ }
12
+
13
+ export function AvatarImage(props: AvatarImageProps) {
14
+ const avatarContext = useAvatarContext();
15
+ const source = props.src ?? avatarContext.src;
16
+
17
+ const imageRef = React.useRef<ImageLabel>();
18
+
19
+ const setImageRef = React.useCallback((instance: Instance | undefined) => {
20
+ imageRef.current = toImageLabel(instance);
21
+ }, []);
22
+
23
+ React.useEffect(() => {
24
+ if (source === undefined || source.size() === 0) {
25
+ avatarContext.setStatus("error");
26
+ return;
27
+ }
28
+
29
+ avatarContext.setStatus("loading");
30
+ }, [avatarContext, source]);
31
+
32
+ React.useEffect(() => {
33
+ const image = imageRef.current;
34
+ if (!image) {
35
+ return;
36
+ }
37
+
38
+ if (image.IsLoaded) {
39
+ avatarContext.setStatus("loaded");
40
+ }
41
+
42
+ const connection = image.GetPropertyChangedSignal("IsLoaded").Connect(() => {
43
+ if (image.IsLoaded) {
44
+ avatarContext.setStatus("loaded");
45
+ }
46
+ });
47
+
48
+ return () => {
49
+ connection.Disconnect();
50
+ };
51
+ }, [avatarContext, source]);
52
+
53
+ if (props.asChild) {
54
+ const child = props.children;
55
+ if (!child) {
56
+ error("[AvatarImage] `asChild` requires a child element.");
57
+ }
58
+
59
+ return (
60
+ <Slot Image={source ?? ""} Visible={avatarContext.status === "loaded"} ref={setImageRef}>
61
+ {child}
62
+ </Slot>
63
+ );
64
+ }
65
+
66
+ return (
67
+ <imagelabel
68
+ BackgroundTransparency={1}
69
+ BorderSizePixel={0}
70
+ Image={source ?? ""}
71
+ Size={UDim2.fromOffset(40, 40)}
72
+ Visible={avatarContext.status === "loaded"}
73
+ ref={setImageRef}
74
+ >
75
+ <uicorner CornerRadius={new UDim(1, 0)} />
76
+ </imagelabel>
77
+ );
78
+ }
@@ -0,0 +1,50 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import { AvatarContextProvider } from "./context";
3
+ import type { AvatarStatus } from "./state";
4
+ import type { AvatarProps } from "./types";
5
+
6
+ export function AvatarRoot(props: AvatarProps) {
7
+ const delayMs = math.max(0, props.delayMs ?? 250);
8
+ const hasSource = props.src !== undefined && props.src.size() > 0;
9
+
10
+ const [status, setStatus] = React.useState<AvatarStatus>(hasSource ? "loading" : "error");
11
+ const [delayElapsed, setDelayElapsed] = React.useState(!hasSource);
12
+ const sequenceRef = React.useRef(0);
13
+
14
+ React.useEffect(() => {
15
+ sequenceRef.current += 1;
16
+ const sequence = sequenceRef.current;
17
+
18
+ if (props.src === undefined || props.src.size() === 0) {
19
+ setStatus("error");
20
+ setDelayElapsed(true);
21
+ return;
22
+ }
23
+
24
+ setStatus("loading");
25
+ setDelayElapsed(false);
26
+
27
+ const delaySeconds = delayMs / 1000;
28
+ task.delay(delaySeconds, () => {
29
+ if (sequenceRef.current !== sequence) {
30
+ return;
31
+ }
32
+
33
+ setDelayElapsed(true);
34
+ });
35
+ }, [delayMs, props.src]);
36
+
37
+ const contextValue = React.useMemo(
38
+ () => ({
39
+ src: props.src,
40
+ status,
41
+ setStatus,
42
+ delayElapsed,
43
+ }),
44
+ [delayElapsed, props.src, status],
45
+ );
46
+
47
+ return <AvatarContextProvider value={contextValue}>{props.children}</AvatarContextProvider>;
48
+ }
49
+
50
+ export { AvatarRoot as Avatar };
@@ -0,0 +1,6 @@
1
+ import { createStrictContext } from "@lattice-ui/core";
2
+ import type { AvatarContextValue } from "./types";
3
+
4
+ const [AvatarContextProvider, useAvatarContext] = createStrictContext<AvatarContextValue>("Avatar");
5
+
6
+ export { AvatarContextProvider, useAvatarContext };
@@ -0,0 +1,13 @@
1
+ export type AvatarStatus = "idle" | "loading" | "loaded" | "error";
2
+
3
+ export function resolveAvatarFallbackVisible(status: AvatarStatus, delayElapsed: boolean) {
4
+ if (status === "loaded") {
5
+ return false;
6
+ }
7
+
8
+ if (status === "error") {
9
+ return true;
10
+ }
11
+
12
+ return delayElapsed;
13
+ }
@@ -0,0 +1,26 @@
1
+ import type React from "@rbxts/react";
2
+ import type { AvatarStatus } from "./state";
3
+
4
+ export type AvatarContextValue = {
5
+ src?: string;
6
+ status: AvatarStatus;
7
+ setStatus: (status: AvatarStatus) => void;
8
+ delayElapsed: boolean;
9
+ };
10
+
11
+ export type AvatarProps = {
12
+ src?: string;
13
+ delayMs?: number;
14
+ children?: React.ReactNode;
15
+ };
16
+
17
+ export type AvatarImageProps = {
18
+ asChild?: boolean;
19
+ src?: string;
20
+ children?: React.ReactElement;
21
+ };
22
+
23
+ export type AvatarFallbackProps = {
24
+ asChild?: boolean;
25
+ children?: React.ReactElement;
26
+ };
package/src/index.ts ADDED
@@ -0,0 +1,13 @@
1
+ import { AvatarFallback } from "./Avatar/AvatarFallback";
2
+ import { AvatarImage } from "./Avatar/AvatarImage";
3
+ import { AvatarRoot } from "./Avatar/AvatarRoot";
4
+
5
+ export const Avatar = {
6
+ Root: AvatarRoot,
7
+ Image: AvatarImage,
8
+ Fallback: AvatarFallback,
9
+ } as const;
10
+
11
+ export type { AvatarStatus } from "./Avatar/state";
12
+ export { resolveAvatarFallbackVisible } from "./Avatar/state";
13
+ export type { AvatarContextValue, AvatarFallbackProps, AvatarImageProps, AvatarProps } from "./Avatar/types";
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,35 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "compilerOptions": {
4
+ "noEmit": true,
5
+ "baseUrl": "..",
6
+ "rootDir": "..",
7
+ "paths": {
8
+ "@lattice-ui/accordion": ["accordion/src/index.ts"],
9
+ "@lattice-ui/avatar": ["avatar/src/index.ts"],
10
+ "@lattice-ui/checkbox": ["checkbox/src/index.ts"],
11
+ "@lattice-ui/combobox": ["combobox/src/index.ts"],
12
+ "@lattice-ui/core": ["core/src/index.ts"],
13
+ "@lattice-ui/dialog": ["dialog/src/index.ts"],
14
+ "@lattice-ui/focus": ["focus/src/index.ts"],
15
+ "@lattice-ui/layer": ["layer/src/index.ts"],
16
+ "@lattice-ui/menu": ["menu/src/index.ts"],
17
+ "@lattice-ui/popover": ["popover/src/index.ts"],
18
+ "@lattice-ui/popper": ["popper/src/index.ts"],
19
+ "@lattice-ui/progress": ["progress/src/index.ts"],
20
+ "@lattice-ui/radio-group": ["radio-group/src/index.ts"],
21
+ "@lattice-ui/scroll-area": ["scroll-area/src/index.ts"],
22
+ "@lattice-ui/select": ["select/src/index.ts"],
23
+ "@lattice-ui/slider": ["slider/src/index.ts"],
24
+ "@lattice-ui/style": ["style/src/index.ts"],
25
+ "@lattice-ui/switch": ["switch/src/index.ts"],
26
+ "@lattice-ui/system": ["system/src/index.ts"],
27
+ "@lattice-ui/tabs": ["tabs/src/index.ts"],
28
+ "@lattice-ui/text-field": ["text-field/src/index.ts"],
29
+ "@lattice-ui/textarea": ["textarea/src/index.ts"],
30
+ "@lattice-ui/toast": ["toast/src/index.ts"],
31
+ "@lattice-ui/toggle-group": ["toggle-group/src/index.ts"],
32
+ "@lattice-ui/tooltip": ["tooltip/src/index.ts"]
33
+ }
34
+ }
35
+ }