@lattice-ui/layer 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/out/dismissable/DismissableLayer.d.ts +3 -0
  2. package/out/dismissable/DismissableLayer.luau +111 -0
  3. package/out/dismissable/events.d.ts +8 -0
  4. package/out/dismissable/events.luau +63 -0
  5. package/out/dismissable/layerStack.d.ts +19 -0
  6. package/out/dismissable/layerStack.luau +141 -0
  7. package/out/dismissable/types.d.ts +16 -0
  8. package/out/dismissable/types.luau +2 -0
  9. package/out/index.d.ts +5 -0
  10. package/out/init.luau +19 -0
  11. package/out/internals/constants.d.ts +3 -0
  12. package/out/internals/constants.luau +9 -0
  13. package/out/internals/env.d.ts +3 -0
  14. package/out/internals/env.luau +12 -0
  15. package/out/portal/Portal.d.ts +2 -0
  16. package/out/portal/Portal.luau +12 -0
  17. package/out/portal/PortalProvider.d.ts +5 -0
  18. package/out/portal/PortalProvider.luau +29 -0
  19. package/out/portal/types.d.ts +14 -0
  20. package/out/portal/types.luau +2 -0
  21. package/out/presence/Presence.d.ts +3 -0
  22. package/out/presence/Presence.luau +88 -0
  23. package/out/presence/types.d.ts +13 -0
  24. package/out/presence/types.luau +2 -0
  25. package/package.json +23 -0
  26. package/src/dismissable/DismissableLayer.tsx +118 -0
  27. package/src/dismissable/events.ts +78 -0
  28. package/src/dismissable/layerStack.ts +147 -0
  29. package/src/dismissable/types.ts +18 -0
  30. package/src/index.ts +5 -0
  31. package/src/internals/constants.ts +3 -0
  32. package/src/internals/env.ts +7 -0
  33. package/src/portal/Portal.tsx +9 -0
  34. package/src/portal/PortalProvider.tsx +20 -0
  35. package/src/portal/types.ts +17 -0
  36. package/src/presence/Presence.tsx +93 -0
  37. package/src/presence/types.ts +16 -0
  38. package/tsconfig.json +16 -0
  39. package/tsconfig.typecheck.json +25 -0
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { DismissableLayerProps } from "./types";
3
+ export declare function DismissableLayer(props: DismissableLayerProps): React.JSX.Element;
@@ -0,0 +1,111 @@
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 DEFAULT_LAYER_IGNORE_GUI_INSET = TS.import(script, script.Parent.Parent, "internals", "constants").DEFAULT_LAYER_IGNORE_GUI_INSET
5
+ local Portal = TS.import(script, script.Parent.Parent, "portal", "Portal").Portal
6
+ local usePortalContext = TS.import(script, script.Parent.Parent, "portal", "PortalProvider").usePortalContext
7
+ local isOutsidePointerEvent = TS.import(script, script.Parent, "events").isOutsidePointerEvent
8
+ local _layerStack = TS.import(script, script.Parent, "layerStack")
9
+ local registerLayer = _layerStack.registerLayer
10
+ local unregisterLayer = _layerStack.unregisterLayer
11
+ local function useLatest(value)
12
+ local ref = React.useRef(value)
13
+ React.useEffect(function()
14
+ ref.current = value
15
+ end, { value })
16
+ return ref
17
+ end
18
+ local function DismissableLayer(props)
19
+ local _condition = props.enabled
20
+ if _condition == nil then
21
+ _condition = true
22
+ end
23
+ local enabled = _condition
24
+ local shouldBlockOutsidePointer = props.modal == true or props.disableOutsidePointerEvents == true
25
+ local layerIgnoresGuiInset = DEFAULT_LAYER_IGNORE_GUI_INSET
26
+ local portalContext = usePortalContext()
27
+ local contentRootRef = React.useRef()
28
+ local stackOrder, setStackOrder = React.useState(0)
29
+ local enabledRef = useLatest(enabled)
30
+ local onDismissRef = useLatest(props.onDismiss)
31
+ local onPointerDownOutsideRef = useLatest(props.onPointerDownOutside)
32
+ local onInteractOutsideRef = useLatest(props.onInteractOutside)
33
+ local onEscapeKeyDownRef = useLatest(props.onEscapeKeyDown)
34
+ local callPointerDownOutside = React.useCallback(function(event)
35
+ local _result = onPointerDownOutsideRef.current
36
+ if _result ~= nil then
37
+ _result(event)
38
+ end
39
+ end, {})
40
+ local callInteractOutside = React.useCallback(function(event)
41
+ local _result = onInteractOutsideRef.current
42
+ if _result ~= nil then
43
+ _result(event)
44
+ end
45
+ end, {})
46
+ local callEscape = React.useCallback(function(event)
47
+ local _result = onEscapeKeyDownRef.current
48
+ if _result ~= nil then
49
+ _result(event)
50
+ end
51
+ end, {})
52
+ local callDismiss = React.useCallback(function()
53
+ local _result = onDismissRef.current
54
+ if _result ~= nil then
55
+ _result()
56
+ end
57
+ end, {})
58
+ React.useEffect(function()
59
+ local registration = registerLayer({
60
+ getEnabled = function()
61
+ return enabledRef.current
62
+ end,
63
+ isPointerOutside = function(inputObject)
64
+ local contentRoot = contentRootRef.current
65
+ if not contentRoot then
66
+ return false
67
+ end
68
+ return isOutsidePointerEvent(inputObject, portalContext.container, contentRoot, {
69
+ layerIgnoresGuiInset = layerIgnoresGuiInset,
70
+ })
71
+ end,
72
+ onPointerDownOutside = callPointerDownOutside,
73
+ onInteractOutside = callInteractOutside,
74
+ onEscapeKeyDown = callEscape,
75
+ onDismiss = callDismiss,
76
+ })
77
+ setStackOrder(registration.mountOrder)
78
+ return function()
79
+ unregisterLayer(registration.id)
80
+ end
81
+ end, { callDismiss, callEscape, callInteractOutside, callPointerDownOutside, enabledRef, layerIgnoresGuiInset, portalContext.container })
82
+ return React.createElement(Portal, nil, React.createElement("screengui", {
83
+ key = `Layer_{stackOrder}`,
84
+ DisplayOrder = portalContext.displayOrderBase + stackOrder,
85
+ IgnoreGuiInset = layerIgnoresGuiInset,
86
+ ResetOnSpawn = false,
87
+ ZIndexBehavior = Enum.ZIndexBehavior.Sibling,
88
+ }, if shouldBlockOutsidePointer then (React.createElement("textbutton", {
89
+ Active = true,
90
+ AutoButtonColor = false,
91
+ BackgroundTransparency = 1,
92
+ BorderSizePixel = 0,
93
+ Modal = true,
94
+ Position = UDim2.fromScale(0, 0),
95
+ Selectable = false,
96
+ Size = UDim2.fromScale(1, 1),
97
+ Text = "",
98
+ TextTransparency = 1,
99
+ ZIndex = 0,
100
+ })) else nil, React.createElement("frame", {
101
+ BackgroundTransparency = 1,
102
+ BorderSizePixel = 0,
103
+ Position = UDim2.fromScale(0, 0),
104
+ Size = UDim2.fromScale(1, 1),
105
+ ref = contentRootRef,
106
+ ZIndex = 1,
107
+ }, props.children)))
108
+ end
109
+ return {
110
+ DismissableLayer = DismissableLayer,
111
+ }
@@ -0,0 +1,8 @@
1
+ import type { LayerInteractEvent } from "./types";
2
+ type OutsidePointerOptions = {
3
+ layerIgnoresGuiInset: boolean;
4
+ };
5
+ export declare function isPointerInput(inputObject: InputObject): boolean;
6
+ export declare function toLayerInteractEvent(originalEvent: InputObject): LayerInteractEvent;
7
+ export declare function isOutsidePointerEvent(inputObject: InputObject, container: BasePlayerGui, contentRoot: GuiObject, options: OutsidePointerOptions): boolean;
8
+ export {};
@@ -0,0 +1,63 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local getGuiInsetTopLeft = TS.import(script, script.Parent.Parent, "internals", "env").getGuiInsetTopLeft
4
+ local function isPointerInput(inputObject)
5
+ return inputObject.UserInputType == Enum.UserInputType.MouseButton1 or inputObject.UserInputType == Enum.UserInputType.Touch
6
+ end
7
+ local function toLayerInteractEvent(originalEvent)
8
+ local event
9
+ event = {
10
+ originalEvent = originalEvent,
11
+ defaultPrevented = false,
12
+ preventDefault = function()
13
+ event.defaultPrevented = true
14
+ end,
15
+ }
16
+ return event
17
+ end
18
+ local function addUniqueSample(samples, sampleKeys, x, y)
19
+ local roundedX = math.round(x)
20
+ local roundedY = math.round(y)
21
+ local key = `{roundedX}:{roundedY}`
22
+ if sampleKeys[key] then
23
+ return nil
24
+ end
25
+ sampleKeys[key] = true
26
+ local _samples = samples
27
+ local _vector2 = Vector2.new(roundedX, roundedY)
28
+ table.insert(_samples, _vector2)
29
+ end
30
+ local function getPointerSamples(pointerPosition, options)
31
+ local insetTopLeft = getGuiInsetTopLeft()
32
+ local samples = {}
33
+ local sampleKeys = {}
34
+ addUniqueSample(samples, sampleKeys, pointerPosition.X, pointerPosition.Y)
35
+ addUniqueSample(samples, sampleKeys, pointerPosition.X + insetTopLeft.X, pointerPosition.Y + insetTopLeft.Y)
36
+ addUniqueSample(samples, sampleKeys, pointerPosition.X - insetTopLeft.X, pointerPosition.Y - insetTopLeft.Y)
37
+ if options.layerIgnoresGuiInset then
38
+ addUniqueSample(samples, sampleKeys, pointerPosition.X, pointerPosition.Y + insetTopLeft.Y)
39
+ addUniqueSample(samples, sampleKeys, pointerPosition.X, pointerPosition.Y - insetTopLeft.Y)
40
+ addUniqueSample(samples, sampleKeys, pointerPosition.X + insetTopLeft.X, pointerPosition.Y)
41
+ addUniqueSample(samples, sampleKeys, pointerPosition.X - insetTopLeft.X, pointerPosition.Y)
42
+ end
43
+ return samples
44
+ end
45
+ local function isOutsidePointerEvent(inputObject, container, contentRoot, options)
46
+ local rawPointerPosition = inputObject.Position
47
+ local pointerPosition = Vector2.new(rawPointerPosition.X, rawPointerPosition.Y)
48
+ local pointerSamples = getPointerSamples(pointerPosition, options)
49
+ for _, sample in pointerSamples do
50
+ local hitGuiObjects = container:GetGuiObjectsAtPosition(sample.X, sample.Y)
51
+ for _1, hitObject in hitGuiObjects do
52
+ if hitObject:IsDescendantOf(contentRoot) then
53
+ return false
54
+ end
55
+ end
56
+ end
57
+ return true
58
+ end
59
+ return {
60
+ isPointerInput = isPointerInput,
61
+ toLayerInteractEvent = toLayerInteractEvent,
62
+ isOutsidePointerEvent = isOutsidePointerEvent,
63
+ }
@@ -0,0 +1,19 @@
1
+ import type { LayerInteractEvent } from "./types";
2
+ type LayerEntry = {
3
+ id: number;
4
+ mountOrder: number;
5
+ getEnabled: () => boolean;
6
+ isPointerOutside: (inputObject: InputObject) => boolean;
7
+ onPointerDownOutside?: (event: LayerInteractEvent) => void;
8
+ onInteractOutside?: (event: LayerInteractEvent) => void;
9
+ onEscapeKeyDown?: (event: LayerInteractEvent) => void;
10
+ onDismiss?: () => void;
11
+ };
12
+ type RegisterLayerParams = Omit<LayerEntry, "id" | "mountOrder">;
13
+ export type LayerRegistration = {
14
+ id: number;
15
+ mountOrder: number;
16
+ };
17
+ export declare function registerLayer(params: RegisterLayerParams): LayerRegistration;
18
+ export declare function unregisterLayer(layerId: number): void;
19
+ export {};
@@ -0,0 +1,141 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _env = TS.import(script, script.Parent.Parent, "internals", "env")
4
+ local GuiService = _env.GuiService
5
+ local UserInputService = _env.UserInputService
6
+ local _events = TS.import(script, script.Parent, "events")
7
+ local isPointerInput = _events.isPointerInput
8
+ local toLayerInteractEvent = _events.toLayerInteractEvent
9
+ local layerEntries = {}
10
+ local nextLayerId = 0
11
+ local nextMountOrder = 0
12
+ local inputConnection
13
+ local function getTopMostEnabledLayer()
14
+ for index = #layerEntries - 1, 0, -1 do
15
+ local entry = layerEntries[index + 1]
16
+ if entry.getEnabled() then
17
+ return entry
18
+ end
19
+ end
20
+ return nil
21
+ end
22
+ local function handleDismissEvent(entry, event)
23
+ if not event.defaultPrevented then
24
+ local _result = entry.onDismiss
25
+ if _result ~= nil then
26
+ _result()
27
+ end
28
+ end
29
+ end
30
+ local function shouldIgnoreEscapeDismiss()
31
+ local focusedTextBox = UserInputService:GetFocusedTextBox()
32
+ if focusedTextBox then
33
+ return true
34
+ end
35
+ local selectedObject = GuiService.SelectedObject
36
+ if selectedObject and selectedObject:IsA("TextBox") then
37
+ return true
38
+ end
39
+ return false
40
+ end
41
+ local function handleInputBegan(inputObject, gameProcessedEvent)
42
+ if gameProcessedEvent then
43
+ return nil
44
+ end
45
+ local topLayer = getTopMostEnabledLayer()
46
+ if not topLayer then
47
+ return nil
48
+ end
49
+ if inputObject.KeyCode == Enum.KeyCode.Escape then
50
+ if shouldIgnoreEscapeDismiss() then
51
+ return nil
52
+ end
53
+ local escapeEvent = toLayerInteractEvent(inputObject)
54
+ local _result = topLayer.onEscapeKeyDown
55
+ if _result ~= nil then
56
+ _result(escapeEvent)
57
+ end
58
+ handleDismissEvent(topLayer, escapeEvent)
59
+ return nil
60
+ end
61
+ if not isPointerInput(inputObject) then
62
+ return nil
63
+ end
64
+ if not topLayer.isPointerOutside(inputObject) then
65
+ return nil
66
+ end
67
+ local outsideEvent = toLayerInteractEvent(inputObject)
68
+ local _result = topLayer.onPointerDownOutside
69
+ if _result ~= nil then
70
+ _result(outsideEvent)
71
+ end
72
+ local _result_1 = topLayer.onInteractOutside
73
+ if _result_1 ~= nil then
74
+ _result_1(outsideEvent)
75
+ end
76
+ handleDismissEvent(topLayer, outsideEvent)
77
+ end
78
+ local function startInputListener()
79
+ if inputConnection then
80
+ return nil
81
+ end
82
+ inputConnection = UserInputService.InputBegan:Connect(function(inputObject, gameProcessedEvent)
83
+ handleInputBegan(inputObject, gameProcessedEvent)
84
+ end)
85
+ end
86
+ local function stopInputListener()
87
+ if not inputConnection then
88
+ return nil
89
+ end
90
+ inputConnection:Disconnect()
91
+ inputConnection = nil
92
+ end
93
+ local function syncInputListener()
94
+ if #layerEntries > 0 then
95
+ startInputListener()
96
+ else
97
+ stopInputListener()
98
+ end
99
+ end
100
+ local function registerLayer(params)
101
+ nextLayerId += 1
102
+ nextMountOrder += 1
103
+ local _object = {
104
+ id = nextLayerId,
105
+ mountOrder = nextMountOrder,
106
+ }
107
+ for _k, _v in params do
108
+ _object[_k] = _v
109
+ end
110
+ local entry = _object
111
+ table.insert(layerEntries, entry)
112
+ syncInputListener()
113
+ return {
114
+ id = entry.id,
115
+ mountOrder = entry.mountOrder,
116
+ }
117
+ end
118
+ local function unregisterLayer(layerId)
119
+ -- ▼ ReadonlyArray.findIndex ▼
120
+ local _callback = function(entry)
121
+ return entry.id == layerId
122
+ end
123
+ local _result = -1
124
+ for _i, _v in layerEntries do
125
+ if _callback(_v, _i - 1, layerEntries) == true then
126
+ _result = _i - 1
127
+ break
128
+ end
129
+ end
130
+ -- ▲ ReadonlyArray.findIndex ▲
131
+ local layerIndex = _result
132
+ if layerIndex == -1 then
133
+ return nil
134
+ end
135
+ table.remove(layerEntries, layerIndex + 1)
136
+ syncInputListener()
137
+ end
138
+ return {
139
+ registerLayer = registerLayer,
140
+ unregisterLayer = unregisterLayer,
141
+ }
@@ -0,0 +1,16 @@
1
+ import type React from "@rbxts/react";
2
+ export type LayerInteractEvent = {
3
+ originalEvent: InputObject;
4
+ defaultPrevented: boolean;
5
+ preventDefault: () => void;
6
+ };
7
+ export type DismissableLayerProps = {
8
+ children?: React.ReactNode;
9
+ enabled?: boolean;
10
+ modal?: boolean;
11
+ disableOutsidePointerEvents?: boolean;
12
+ onPointerDownOutside?: (event: LayerInteractEvent) => void;
13
+ onInteractOutside?: (event: LayerInteractEvent) => void;
14
+ onEscapeKeyDown?: (event: LayerInteractEvent) => void;
15
+ onDismiss?: () => void;
16
+ };
@@ -0,0 +1,2 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ return nil
package/out/index.d.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./dismissable/DismissableLayer";
2
+ export * from "./dismissable/types";
3
+ export * from "./portal/Portal";
4
+ export * from "./portal/PortalProvider";
5
+ export * from "./presence/Presence";
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
+ for _k, _v in TS.import(script, script, "dismissable", "DismissableLayer") or {} do
5
+ exports[_k] = _v
6
+ end
7
+ for _k, _v in TS.import(script, script, "dismissable", "types") or {} do
8
+ exports[_k] = _v
9
+ end
10
+ for _k, _v in TS.import(script, script, "portal", "Portal") or {} do
11
+ exports[_k] = _v
12
+ end
13
+ for _k, _v in TS.import(script, script, "portal", "PortalProvider") or {} do
14
+ exports[_k] = _v
15
+ end
16
+ for _k, _v in TS.import(script, script, "presence", "Presence") or {} do
17
+ exports[_k] = _v
18
+ end
19
+ return exports
@@ -0,0 +1,3 @@
1
+ export declare const DEFAULT_DISPLAY_ORDER_BASE = 1000;
2
+ export declare const DEFAULT_PRESENCE_EXIT_FALLBACK_MS = 500;
3
+ export declare const DEFAULT_LAYER_IGNORE_GUI_INSET = true;
@@ -0,0 +1,9 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local DEFAULT_DISPLAY_ORDER_BASE = 1000
3
+ local DEFAULT_PRESENCE_EXIT_FALLBACK_MS = 500
4
+ local DEFAULT_LAYER_IGNORE_GUI_INSET = true
5
+ return {
6
+ DEFAULT_DISPLAY_ORDER_BASE = DEFAULT_DISPLAY_ORDER_BASE,
7
+ DEFAULT_PRESENCE_EXIT_FALLBACK_MS = DEFAULT_PRESENCE_EXIT_FALLBACK_MS,
8
+ DEFAULT_LAYER_IGNORE_GUI_INSET = DEFAULT_LAYER_IGNORE_GUI_INSET,
9
+ }
@@ -0,0 +1,3 @@
1
+ export declare const UserInputService: UserInputService;
2
+ export declare const GuiService: GuiService;
3
+ export declare function getGuiInsetTopLeft(): Vector2;
@@ -0,0 +1,12 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local UserInputService = game:GetService("UserInputService")
3
+ local GuiService = game:GetService("GuiService")
4
+ local function getGuiInsetTopLeft()
5
+ local topLeftInset = GuiService:GetGuiInset()
6
+ return topLeftInset
7
+ end
8
+ return {
9
+ getGuiInsetTopLeft = getGuiInsetTopLeft,
10
+ UserInputService = UserInputService,
11
+ GuiService = GuiService,
12
+ }
@@ -0,0 +1,2 @@
1
+ import type { PortalProps } from "./types";
2
+ export declare function Portal(props: PortalProps): import("@rbxts/react").ReactPortal;
@@ -0,0 +1,12 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local ReactRoblox = TS.import(script, TS.getModule(script, "@lattice-ui", "core").out).ReactRoblox
4
+ local usePortalContext = TS.import(script, script.Parent, "PortalProvider").usePortalContext
5
+ local function Portal(props)
6
+ local contextValue = usePortalContext()
7
+ local target = props.container or contextValue.container
8
+ return ReactRoblox.createPortal(props.children, target)
9
+ end
10
+ return {
11
+ Portal = Portal,
12
+ }
@@ -0,0 +1,5 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { PortalContextValue, PortalProviderProps } from "./types";
3
+ declare const usePortalContext: () => PortalContextValue;
4
+ export declare function PortalProvider(props: PortalProviderProps): React.JSX.Element;
5
+ export { usePortalContext };
@@ -0,0 +1,29 @@
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 DEFAULT_DISPLAY_ORDER_BASE = TS.import(script, script.Parent.Parent, "internals", "constants").DEFAULT_DISPLAY_ORDER_BASE
7
+ local _binding = createStrictContext("PortalProvider")
8
+ local PortalContextProvider = _binding[1]
9
+ local usePortalContext = _binding[2]
10
+ local function PortalProvider(props)
11
+ local _condition = props.displayOrderBase
12
+ if _condition == nil then
13
+ _condition = DEFAULT_DISPLAY_ORDER_BASE
14
+ end
15
+ local displayOrderBase = _condition
16
+ local contextValue = React.useMemo(function()
17
+ return {
18
+ container = props.container,
19
+ displayOrderBase = displayOrderBase,
20
+ }
21
+ end, { displayOrderBase, props.container })
22
+ return React.createElement(PortalContextProvider, {
23
+ value = contextValue,
24
+ }, props.children)
25
+ end
26
+ return {
27
+ PortalProvider = PortalProvider,
28
+ usePortalContext = usePortalContext,
29
+ }
@@ -0,0 +1,14 @@
1
+ import type React from "@rbxts/react";
2
+ export type PortalContextValue = {
3
+ container: BasePlayerGui;
4
+ displayOrderBase: number;
5
+ };
6
+ export type PortalProviderProps = {
7
+ container: BasePlayerGui;
8
+ displayOrderBase?: number;
9
+ children?: React.ReactNode;
10
+ };
11
+ export type PortalProps = {
12
+ children?: React.ReactNode;
13
+ container?: Instance;
14
+ };
@@ -0,0 +1,2 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ return nil
@@ -0,0 +1,3 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import type { PresenceProps } from "./types";
3
+ export declare function Presence(props: PresenceProps): React.ReactElement<any, string | React.JSXElementConstructor<any>> | undefined;
@@ -0,0 +1,88 @@
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 DEFAULT_PRESENCE_EXIT_FALLBACK_MS = TS.import(script, script.Parent.Parent, "internals", "constants").DEFAULT_PRESENCE_EXIT_FALLBACK_MS
5
+ local function Presence(props)
6
+ local mounted, setMounted = React.useState(props.present)
7
+ local isPresent, setIsPresent = React.useState(props.present)
8
+ local mountedRef = React.useRef(mounted)
9
+ local fallbackTaskRef = React.useRef()
10
+ local onExitCompleteRef = React.useRef(props.onExitComplete)
11
+ React.useEffect(function()
12
+ onExitCompleteRef.current = props.onExitComplete
13
+ end, { props.onExitComplete })
14
+ React.useEffect(function()
15
+ mountedRef.current = mounted
16
+ end, { mounted })
17
+ local completeExit = React.useCallback(function()
18
+ if not mountedRef.current then
19
+ return nil
20
+ end
21
+ local fallbackTask = fallbackTaskRef.current
22
+ if fallbackTask then
23
+ task.cancel(fallbackTask)
24
+ fallbackTaskRef.current = nil
25
+ end
26
+ mountedRef.current = false
27
+ setMounted(false)
28
+ local _result = onExitCompleteRef.current
29
+ if _result ~= nil then
30
+ _result()
31
+ end
32
+ end, {})
33
+ React.useEffect(function()
34
+ if props.present then
35
+ local fallbackTask = fallbackTaskRef.current
36
+ if fallbackTask then
37
+ task.cancel(fallbackTask)
38
+ fallbackTaskRef.current = nil
39
+ end
40
+ if not mountedRef.current then
41
+ mountedRef.current = true
42
+ setMounted(true)
43
+ end
44
+ setIsPresent(true)
45
+ return nil
46
+ end
47
+ if not mountedRef.current then
48
+ return nil
49
+ end
50
+ setIsPresent(false)
51
+ local fallbackTask = fallbackTaskRef.current
52
+ if fallbackTask then
53
+ task.cancel(fallbackTask)
54
+ fallbackTaskRef.current = nil
55
+ end
56
+ local _condition = props.exitFallbackMs
57
+ if _condition == nil then
58
+ _condition = DEFAULT_PRESENCE_EXIT_FALLBACK_MS
59
+ end
60
+ local timeoutMs = _condition
61
+ fallbackTaskRef.current = task.delay(timeoutMs / 1000, function()
62
+ completeExit()
63
+ end)
64
+ end, { completeExit, props.exitFallbackMs, props.present })
65
+ React.useEffect(function()
66
+ return function()
67
+ local fallbackTask = fallbackTaskRef.current
68
+ if fallbackTask then
69
+ task.cancel(fallbackTask)
70
+ fallbackTaskRef.current = nil
71
+ end
72
+ end
73
+ end, {})
74
+ if not mounted then
75
+ return nil
76
+ end
77
+ local render = props.render or props.children
78
+ if not render then
79
+ return nil
80
+ end
81
+ return render({
82
+ isPresent = isPresent,
83
+ onExitComplete = completeExit,
84
+ })
85
+ end
86
+ return {
87
+ Presence = Presence,
88
+ }
@@ -0,0 +1,13 @@
1
+ import type React from "@rbxts/react";
2
+ export type PresenceRenderState = {
3
+ isPresent: boolean;
4
+ onExitComplete: () => void;
5
+ };
6
+ export type PresenceRender = (state: PresenceRenderState) => React.ReactElement | undefined;
7
+ export type PresenceProps = {
8
+ present: boolean;
9
+ exitFallbackMs?: number;
10
+ onExitComplete?: () => void;
11
+ children?: PresenceRender;
12
+ render?: PresenceRender;
13
+ };
@@ -0,0 +1,2 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ return nil
package/package.json ADDED
@@ -0,0 +1,23 @@
1
+ {
2
+ "name": "@lattice-ui/layer",
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
+ }
@@ -0,0 +1,118 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import { DEFAULT_LAYER_IGNORE_GUI_INSET } from "../internals/constants";
3
+ import { Portal } from "../portal/Portal";
4
+ import { usePortalContext } from "../portal/PortalProvider";
5
+ import { isOutsidePointerEvent } from "./events";
6
+ import { registerLayer, unregisterLayer } from "./layerStack";
7
+ import type { DismissableLayerProps, LayerInteractEvent } from "./types";
8
+
9
+ function useLatest<T>(value: T) {
10
+ const ref = React.useRef(value);
11
+ React.useEffect(() => {
12
+ ref.current = value;
13
+ }, [value]);
14
+ return ref;
15
+ }
16
+
17
+ export function DismissableLayer(props: DismissableLayerProps) {
18
+ const enabled = props.enabled ?? true;
19
+ const shouldBlockOutsidePointer = props.modal === true || props.disableOutsidePointerEvents === true;
20
+ const layerIgnoresGuiInset = DEFAULT_LAYER_IGNORE_GUI_INSET;
21
+
22
+ const portalContext = usePortalContext();
23
+ const contentRootRef = React.useRef<Frame>();
24
+ const [stackOrder, setStackOrder] = React.useState(0);
25
+
26
+ const enabledRef = useLatest(enabled);
27
+ const onDismissRef = useLatest(props.onDismiss);
28
+ const onPointerDownOutsideRef = useLatest(props.onPointerDownOutside);
29
+ const onInteractOutsideRef = useLatest(props.onInteractOutside);
30
+ const onEscapeKeyDownRef = useLatest(props.onEscapeKeyDown);
31
+
32
+ const callPointerDownOutside = React.useCallback((event: LayerInteractEvent) => {
33
+ onPointerDownOutsideRef.current?.(event);
34
+ }, []);
35
+
36
+ const callInteractOutside = React.useCallback((event: LayerInteractEvent) => {
37
+ onInteractOutsideRef.current?.(event);
38
+ }, []);
39
+
40
+ const callEscape = React.useCallback((event: LayerInteractEvent) => {
41
+ onEscapeKeyDownRef.current?.(event);
42
+ }, []);
43
+
44
+ const callDismiss = React.useCallback(() => {
45
+ onDismissRef.current?.();
46
+ }, []);
47
+
48
+ React.useEffect(() => {
49
+ const registration = registerLayer({
50
+ getEnabled: () => enabledRef.current,
51
+ isPointerOutside: (inputObject) => {
52
+ const contentRoot = contentRootRef.current;
53
+ if (!contentRoot) {
54
+ return false;
55
+ }
56
+ return isOutsidePointerEvent(inputObject, portalContext.container, contentRoot, {
57
+ layerIgnoresGuiInset,
58
+ });
59
+ },
60
+ onPointerDownOutside: callPointerDownOutside,
61
+ onInteractOutside: callInteractOutside,
62
+ onEscapeKeyDown: callEscape,
63
+ onDismiss: callDismiss,
64
+ });
65
+
66
+ setStackOrder(registration.mountOrder);
67
+
68
+ return () => {
69
+ unregisterLayer(registration.id);
70
+ };
71
+ }, [
72
+ callDismiss,
73
+ callEscape,
74
+ callInteractOutside,
75
+ callPointerDownOutside,
76
+ enabledRef,
77
+ layerIgnoresGuiInset,
78
+ portalContext.container,
79
+ ]);
80
+
81
+ return (
82
+ <Portal>
83
+ <screengui
84
+ key={`Layer_${stackOrder}`}
85
+ DisplayOrder={portalContext.displayOrderBase + stackOrder}
86
+ IgnoreGuiInset={layerIgnoresGuiInset}
87
+ ResetOnSpawn={false}
88
+ ZIndexBehavior={Enum.ZIndexBehavior.Sibling}
89
+ >
90
+ {shouldBlockOutsidePointer ? (
91
+ <textbutton
92
+ Active={true}
93
+ AutoButtonColor={false}
94
+ BackgroundTransparency={1}
95
+ BorderSizePixel={0}
96
+ Modal={true}
97
+ Position={UDim2.fromScale(0, 0)}
98
+ Selectable={false}
99
+ Size={UDim2.fromScale(1, 1)}
100
+ Text=""
101
+ TextTransparency={1}
102
+ ZIndex={0}
103
+ />
104
+ ) : undefined}
105
+ <frame
106
+ BackgroundTransparency={1}
107
+ BorderSizePixel={0}
108
+ Position={UDim2.fromScale(0, 0)}
109
+ Size={UDim2.fromScale(1, 1)}
110
+ ref={contentRootRef}
111
+ ZIndex={1}
112
+ >
113
+ {props.children}
114
+ </frame>
115
+ </screengui>
116
+ </Portal>
117
+ );
118
+ }
@@ -0,0 +1,78 @@
1
+ import { getGuiInsetTopLeft } from "../internals/env";
2
+ import type { LayerInteractEvent } from "./types";
3
+
4
+ type OutsidePointerOptions = {
5
+ layerIgnoresGuiInset: boolean;
6
+ };
7
+
8
+ export function isPointerInput(inputObject: InputObject) {
9
+ return (
10
+ inputObject.UserInputType === Enum.UserInputType.MouseButton1 ||
11
+ inputObject.UserInputType === Enum.UserInputType.Touch
12
+ );
13
+ }
14
+
15
+ export function toLayerInteractEvent(originalEvent: InputObject): LayerInteractEvent {
16
+ const event: LayerInteractEvent = {
17
+ originalEvent,
18
+ defaultPrevented: false,
19
+ preventDefault: () => {
20
+ event.defaultPrevented = true;
21
+ },
22
+ };
23
+ return event;
24
+ }
25
+
26
+ function addUniqueSample(samples: Array<Vector2>, sampleKeys: Record<string, true>, x: number, y: number) {
27
+ const roundedX = math.round(x);
28
+ const roundedY = math.round(y);
29
+ const key = `${roundedX}:${roundedY}`;
30
+ if (sampleKeys[key]) {
31
+ return;
32
+ }
33
+
34
+ sampleKeys[key] = true;
35
+ samples.push(new Vector2(roundedX, roundedY));
36
+ }
37
+
38
+ function getPointerSamples(pointerPosition: Vector2, options: OutsidePointerOptions) {
39
+ const insetTopLeft = getGuiInsetTopLeft();
40
+
41
+ const samples = new Array<Vector2>();
42
+ const sampleKeys: Record<string, true> = {};
43
+
44
+ addUniqueSample(samples, sampleKeys, pointerPosition.X, pointerPosition.Y);
45
+ addUniqueSample(samples, sampleKeys, pointerPosition.X + insetTopLeft.X, pointerPosition.Y + insetTopLeft.Y);
46
+ addUniqueSample(samples, sampleKeys, pointerPosition.X - insetTopLeft.X, pointerPosition.Y - insetTopLeft.Y);
47
+
48
+ if (options.layerIgnoresGuiInset) {
49
+ addUniqueSample(samples, sampleKeys, pointerPosition.X, pointerPosition.Y + insetTopLeft.Y);
50
+ addUniqueSample(samples, sampleKeys, pointerPosition.X, pointerPosition.Y - insetTopLeft.Y);
51
+ addUniqueSample(samples, sampleKeys, pointerPosition.X + insetTopLeft.X, pointerPosition.Y);
52
+ addUniqueSample(samples, sampleKeys, pointerPosition.X - insetTopLeft.X, pointerPosition.Y);
53
+ }
54
+
55
+ return samples;
56
+ }
57
+
58
+ export function isOutsidePointerEvent(
59
+ inputObject: InputObject,
60
+ container: BasePlayerGui,
61
+ contentRoot: GuiObject,
62
+ options: OutsidePointerOptions,
63
+ ) {
64
+ const rawPointerPosition = inputObject.Position;
65
+ const pointerPosition = new Vector2(rawPointerPosition.X, rawPointerPosition.Y);
66
+ const pointerSamples = getPointerSamples(pointerPosition, options);
67
+
68
+ for (const sample of pointerSamples) {
69
+ const hitGuiObjects = container.GetGuiObjectsAtPosition(sample.X, sample.Y);
70
+ for (const hitObject of hitGuiObjects) {
71
+ if (hitObject.IsDescendantOf(contentRoot)) {
72
+ return false;
73
+ }
74
+ }
75
+ }
76
+
77
+ return true;
78
+ }
@@ -0,0 +1,147 @@
1
+ import { GuiService, UserInputService } from "../internals/env";
2
+ import { isPointerInput, toLayerInteractEvent } from "./events";
3
+ import type { LayerInteractEvent } from "./types";
4
+
5
+ type LayerEntry = {
6
+ id: number;
7
+ mountOrder: number;
8
+ getEnabled: () => boolean;
9
+ isPointerOutside: (inputObject: InputObject) => boolean;
10
+ onPointerDownOutside?: (event: LayerInteractEvent) => void;
11
+ onInteractOutside?: (event: LayerInteractEvent) => void;
12
+ onEscapeKeyDown?: (event: LayerInteractEvent) => void;
13
+ onDismiss?: () => void;
14
+ };
15
+
16
+ type RegisterLayerParams = Omit<LayerEntry, "id" | "mountOrder">;
17
+
18
+ export type LayerRegistration = {
19
+ id: number;
20
+ mountOrder: number;
21
+ };
22
+
23
+ const layerEntries = new Array<LayerEntry>();
24
+ let nextLayerId = 0;
25
+ let nextMountOrder = 0;
26
+ let inputConnection: RBXScriptConnection | undefined;
27
+
28
+ function getTopMostEnabledLayer() {
29
+ for (let index = layerEntries.size() - 1; index >= 0; index--) {
30
+ const entry = layerEntries[index];
31
+ if (entry.getEnabled()) {
32
+ return entry;
33
+ }
34
+ }
35
+ return undefined;
36
+ }
37
+
38
+ function handleDismissEvent(entry: LayerEntry, event: LayerInteractEvent) {
39
+ if (!event.defaultPrevented) {
40
+ entry.onDismiss?.();
41
+ }
42
+ }
43
+
44
+ function shouldIgnoreEscapeDismiss() {
45
+ const focusedTextBox = UserInputService.GetFocusedTextBox();
46
+ if (focusedTextBox) {
47
+ return true;
48
+ }
49
+
50
+ const selectedObject = GuiService.SelectedObject;
51
+ if (selectedObject && selectedObject.IsA("TextBox")) {
52
+ return true;
53
+ }
54
+
55
+ return false;
56
+ }
57
+
58
+ function handleInputBegan(inputObject: InputObject, gameProcessedEvent: boolean) {
59
+ if (gameProcessedEvent) {
60
+ return;
61
+ }
62
+
63
+ const topLayer = getTopMostEnabledLayer();
64
+ if (!topLayer) {
65
+ return;
66
+ }
67
+
68
+ if (inputObject.KeyCode === Enum.KeyCode.Escape) {
69
+ if (shouldIgnoreEscapeDismiss()) {
70
+ return;
71
+ }
72
+
73
+ const escapeEvent = toLayerInteractEvent(inputObject);
74
+ topLayer.onEscapeKeyDown?.(escapeEvent);
75
+ handleDismissEvent(topLayer, escapeEvent);
76
+ return;
77
+ }
78
+
79
+ if (!isPointerInput(inputObject)) {
80
+ return;
81
+ }
82
+
83
+ if (!topLayer.isPointerOutside(inputObject)) {
84
+ return;
85
+ }
86
+
87
+ const outsideEvent = toLayerInteractEvent(inputObject);
88
+ topLayer.onPointerDownOutside?.(outsideEvent);
89
+ topLayer.onInteractOutside?.(outsideEvent);
90
+ handleDismissEvent(topLayer, outsideEvent);
91
+ }
92
+
93
+ function startInputListener() {
94
+ if (inputConnection) {
95
+ return;
96
+ }
97
+
98
+ inputConnection = UserInputService.InputBegan.Connect((inputObject, gameProcessedEvent) => {
99
+ handleInputBegan(inputObject, gameProcessedEvent);
100
+ });
101
+ }
102
+
103
+ function stopInputListener() {
104
+ if (!inputConnection) {
105
+ return;
106
+ }
107
+
108
+ inputConnection.Disconnect();
109
+ inputConnection = undefined;
110
+ }
111
+
112
+ function syncInputListener() {
113
+ if (layerEntries.size() > 0) {
114
+ startInputListener();
115
+ } else {
116
+ stopInputListener();
117
+ }
118
+ }
119
+
120
+ export function registerLayer(params: RegisterLayerParams): LayerRegistration {
121
+ nextLayerId += 1;
122
+ nextMountOrder += 1;
123
+
124
+ const entry: LayerEntry = {
125
+ id: nextLayerId,
126
+ mountOrder: nextMountOrder,
127
+ ...params,
128
+ };
129
+
130
+ layerEntries.push(entry);
131
+ syncInputListener();
132
+
133
+ return {
134
+ id: entry.id,
135
+ mountOrder: entry.mountOrder,
136
+ };
137
+ }
138
+
139
+ export function unregisterLayer(layerId: number) {
140
+ const layerIndex = layerEntries.findIndex((entry) => entry.id === layerId);
141
+ if (layerIndex === -1) {
142
+ return;
143
+ }
144
+
145
+ layerEntries.remove(layerIndex);
146
+ syncInputListener();
147
+ }
@@ -0,0 +1,18 @@
1
+ import type React from "@rbxts/react";
2
+
3
+ export type LayerInteractEvent = {
4
+ originalEvent: InputObject;
5
+ defaultPrevented: boolean;
6
+ preventDefault: () => void;
7
+ };
8
+
9
+ export type DismissableLayerProps = {
10
+ children?: React.ReactNode;
11
+ enabled?: boolean;
12
+ modal?: boolean;
13
+ disableOutsidePointerEvents?: boolean;
14
+ onPointerDownOutside?: (event: LayerInteractEvent) => void;
15
+ onInteractOutside?: (event: LayerInteractEvent) => void;
16
+ onEscapeKeyDown?: (event: LayerInteractEvent) => void;
17
+ onDismiss?: () => void;
18
+ };
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./dismissable/DismissableLayer";
2
+ export * from "./dismissable/types";
3
+ export * from "./portal/Portal";
4
+ export * from "./portal/PortalProvider";
5
+ export * from "./presence/Presence";
@@ -0,0 +1,3 @@
1
+ export const DEFAULT_DISPLAY_ORDER_BASE = 1000;
2
+ export const DEFAULT_PRESENCE_EXIT_FALLBACK_MS = 500;
3
+ export const DEFAULT_LAYER_IGNORE_GUI_INSET = true;
@@ -0,0 +1,7 @@
1
+ export const UserInputService = game.GetService("UserInputService");
2
+ export const GuiService = game.GetService("GuiService");
3
+
4
+ export function getGuiInsetTopLeft() {
5
+ const [topLeftInset] = GuiService.GetGuiInset();
6
+ return topLeftInset;
7
+ }
@@ -0,0 +1,9 @@
1
+ import { ReactRoblox } from "@lattice-ui/core";
2
+ import { usePortalContext } from "./PortalProvider";
3
+ import type { PortalProps } from "./types";
4
+
5
+ export function Portal(props: PortalProps) {
6
+ const contextValue = usePortalContext();
7
+ const target = props.container ?? contextValue.container;
8
+ return ReactRoblox.createPortal(props.children, target);
9
+ }
@@ -0,0 +1,20 @@
1
+ import { createStrictContext, React } from "@lattice-ui/core";
2
+ import { DEFAULT_DISPLAY_ORDER_BASE } from "../internals/constants";
3
+ import type { PortalContextValue, PortalProviderProps } from "./types";
4
+
5
+ const [PortalContextProvider, usePortalContext] = createStrictContext<PortalContextValue>("PortalProvider");
6
+
7
+ export function PortalProvider(props: PortalProviderProps) {
8
+ const displayOrderBase = props.displayOrderBase ?? DEFAULT_DISPLAY_ORDER_BASE;
9
+ const contextValue = React.useMemo(
10
+ () => ({
11
+ container: props.container,
12
+ displayOrderBase,
13
+ }),
14
+ [displayOrderBase, props.container],
15
+ );
16
+
17
+ return <PortalContextProvider value={contextValue}>{props.children}</PortalContextProvider>;
18
+ }
19
+
20
+ export { usePortalContext };
@@ -0,0 +1,17 @@
1
+ import type React from "@rbxts/react";
2
+
3
+ export type PortalContextValue = {
4
+ container: BasePlayerGui;
5
+ displayOrderBase: number;
6
+ };
7
+
8
+ export type PortalProviderProps = {
9
+ container: BasePlayerGui;
10
+ displayOrderBase?: number;
11
+ children?: React.ReactNode;
12
+ };
13
+
14
+ export type PortalProps = {
15
+ children?: React.ReactNode;
16
+ container?: Instance;
17
+ };
@@ -0,0 +1,93 @@
1
+ import { React } from "@lattice-ui/core";
2
+ import { DEFAULT_PRESENCE_EXIT_FALLBACK_MS } from "../internals/constants";
3
+ import type { PresenceProps } from "./types";
4
+
5
+ export function Presence(props: PresenceProps) {
6
+ const [mounted, setMounted] = React.useState(props.present);
7
+ const [isPresent, setIsPresent] = React.useState(props.present);
8
+ const mountedRef = React.useRef(mounted);
9
+ const fallbackTaskRef = React.useRef<thread>();
10
+ const onExitCompleteRef = React.useRef(props.onExitComplete);
11
+
12
+ React.useEffect(() => {
13
+ onExitCompleteRef.current = props.onExitComplete;
14
+ }, [props.onExitComplete]);
15
+
16
+ React.useEffect(() => {
17
+ mountedRef.current = mounted;
18
+ }, [mounted]);
19
+
20
+ const completeExit = React.useCallback(() => {
21
+ if (!mountedRef.current) {
22
+ return;
23
+ }
24
+
25
+ const fallbackTask = fallbackTaskRef.current;
26
+ if (fallbackTask) {
27
+ task.cancel(fallbackTask);
28
+ fallbackTaskRef.current = undefined;
29
+ }
30
+
31
+ mountedRef.current = false;
32
+ setMounted(false);
33
+ onExitCompleteRef.current?.();
34
+ }, []);
35
+
36
+ React.useEffect(() => {
37
+ if (props.present) {
38
+ const fallbackTask = fallbackTaskRef.current;
39
+ if (fallbackTask) {
40
+ task.cancel(fallbackTask);
41
+ fallbackTaskRef.current = undefined;
42
+ }
43
+
44
+ if (!mountedRef.current) {
45
+ mountedRef.current = true;
46
+ setMounted(true);
47
+ }
48
+ setIsPresent(true);
49
+ return;
50
+ }
51
+
52
+ if (!mountedRef.current) {
53
+ return;
54
+ }
55
+
56
+ setIsPresent(false);
57
+
58
+ const fallbackTask = fallbackTaskRef.current;
59
+ if (fallbackTask) {
60
+ task.cancel(fallbackTask);
61
+ fallbackTaskRef.current = undefined;
62
+ }
63
+
64
+ const timeoutMs = props.exitFallbackMs ?? DEFAULT_PRESENCE_EXIT_FALLBACK_MS;
65
+ fallbackTaskRef.current = task.delay(timeoutMs / 1000, () => {
66
+ completeExit();
67
+ });
68
+ }, [completeExit, props.exitFallbackMs, props.present]);
69
+
70
+ React.useEffect(() => {
71
+ return () => {
72
+ const fallbackTask = fallbackTaskRef.current;
73
+ if (fallbackTask) {
74
+ task.cancel(fallbackTask);
75
+ fallbackTaskRef.current = undefined;
76
+ }
77
+ };
78
+ }, []);
79
+
80
+ if (!mounted) {
81
+ return undefined;
82
+ }
83
+
84
+ const render = props.render ?? props.children;
85
+ if (!render) {
86
+ return undefined;
87
+ }
88
+
89
+ return render({
90
+ isPresent,
91
+ onExitComplete: completeExit,
92
+ });
93
+ }
@@ -0,0 +1,16 @@
1
+ import type React from "@rbxts/react";
2
+
3
+ export type PresenceRenderState = {
4
+ isPresent: boolean;
5
+ onExitComplete: () => void;
6
+ };
7
+
8
+ export type PresenceRender = (state: PresenceRenderState) => React.ReactElement | undefined;
9
+
10
+ export type PresenceProps = {
11
+ present: boolean;
12
+ exitFallbackMs?: number;
13
+ onExitComplete?: () => void;
14
+ children?: PresenceRender;
15
+ render?: PresenceRender;
16
+ };
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
+ }