@trioforge/anvil 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Trio Forge Studio
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,112 @@
1
- # Temporary Holding Version
1
+ # @trioforge/anvil
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Headless UI primitives for Roblox games, built on [Vide](https://centau.github.io/vide/). Anvil owns behaviour: who gets gamepad selection, which modal is on top, how a press, a hold or a repeat fires, and when the Close button may close a screen. Your game owns every pixel and every sound.
4
+
5
+ Anvil started as the interface runtime of Fragile Delivery. Version 0.1 covers modals, selection and presses. Components such as sliders, tabs and dialogs come next.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ pnpm add @trioforge/anvil @rbxts/vide @rbxts/charm @rbxts/vide-charm @rbxts/services
11
+ ```
12
+
13
+ roblox-ts only looks for packages in the scopes your project lists, so add the `@trioforge` scope in two places.
14
+
15
+ 1. In `tsconfig.json`, add it to `typeRoots`:
16
+
17
+ ```json
18
+ "typeRoots": ["node_modules/@rbxts", "node_modules/@trioforge"]
19
+ ```
20
+
21
+ 2. In your Rojo project, map it beside `@rbxts` under `rbxts_include/node_modules`:
22
+
23
+ ```json
24
+ "node_modules": {
25
+ "$className": "Folder",
26
+ "@rbxts": { "$path": "node_modules/@rbxts" },
27
+ "@trioforge": { "$path": "node_modules/@trioforge" }
28
+ }
29
+ ```
30
+
31
+ ## Connect your game
32
+
33
+ Anvil reads input and gives feedback only through an adapter that you install once, on the client, before any Anvil UI renders. Then start the menu runtime.
34
+
35
+ ```ts
36
+ import { atom } from "@rbxts/charm";
37
+ import { UserInputService } from "@rbxts/services";
38
+ import Vide from "@rbxts/vide";
39
+ import { installAnvil, Modal, startMenuRuntime, usePress, type AnvilAdapter, type InputDevice } from "@trioforge/anvil";
40
+
41
+ const device = atom<InputDevice>("Keyboard");
42
+ UserInputService.LastInputTypeChanged.Connect((input) => {
43
+ if (input === Enum.UserInputType.Touch) device("Touch");
44
+ else if (input === Enum.UserInputType.Gamepad1) device("Gamepad");
45
+ else if (input === Enum.UserInputType.Keyboard || input === Enum.UserInputType.MouseMovement) device("Keyboard");
46
+ });
47
+
48
+ function onButtonB(signal: RBXScriptSignal<(input: InputObject) => void>, listener: () => void) {
49
+ const connection = signal.Connect((input) => {
50
+ if (input.KeyCode === Enum.KeyCode.ButtonB) listener();
51
+ });
52
+ return () => connection.Disconnect();
53
+ }
54
+
55
+ const sounds = new Map<string, Sound>();
56
+
57
+ const adapter: AnvilAdapter = {
58
+ device,
59
+ close: {
60
+ held: () =>
61
+ UserInputService.GetConnectedGamepads().some((gamepad) =>
62
+ UserInputService.IsGamepadButtonDown(gamepad, Enum.KeyCode.ButtonB),
63
+ ),
64
+ onPressed: (listener) => onButtonB(UserInputService.InputBegan, listener),
65
+ onReleased: (listener) => onButtonB(UserInputService.InputEnded, listener),
66
+ },
67
+ claimMenuInput: () => () => {},
68
+ feedback: (kind) => sounds.get(kind)?.Play(),
69
+ };
70
+
71
+ installAnvil(adapter);
72
+ startMenuRuntime();
73
+ ```
74
+
75
+ `installAnvil` accepts the same adapter again and does nothing. It refuses a different adapter. Anvil reads the adapter when the first control renders or the runtime starts, and raises an error if you never installed one.
76
+
77
+ | Member | What Anvil does with it |
78
+ | --- | --- |
79
+ | `device` | Reads the current input device. Selection only moves while it is `"Gamepad"`, and highlights ignore `"Touch"`. It must be a Charm atom or computed, because Anvil reads it inside Charm effects. |
80
+ | `close.held` | Asks whether the close input is down. A screen closes only on a fresh press, so a press that started before the screen opened does nothing. |
81
+ | `close.onPressed` | Subscribes to close presses. The top native modal's `onClose` runs on each armed press. |
82
+ | `close.onReleased` | Subscribes to anything that may have lifted the close input. Anvil reads `close.held` again each time, so extra calls are harmless. |
83
+ | `claimMenuInput` | Runs while a native modal is on top and no text box has focus. Return a function that releases the claim, for example to stop gameplay actions from reading ButtonB. |
84
+ | `feedback` | Receives `"Focus"`, `"Press"` or `"Refused"` for each press that is not `silent`. Play your sounds here. |
85
+
86
+ ## Build a modal
87
+
88
+ ```tsx
89
+ function BuyButton(props: { onBuy: () => void }) {
90
+ const press = usePress({ onPress: props.onBuy, selectFirst: true });
91
+ return (
92
+ <textbutton
93
+ action={press.action}
94
+ Text="Buy"
95
+ Size={() => (press.pressed() ? UDim2.fromOffset(112, 38) : UDim2.fromOffset(120, 40))}
96
+ BackgroundColor3={() => (press.highlighted() ? Color3.fromRGB(255, 214, 102) : Color3.fromRGB(240, 240, 240))}
97
+ />
98
+ );
99
+ }
100
+
101
+ export function Shop(props: { onBuy: () => void; onClose: () => void }) {
102
+ return <Modal name="Shop" onClose={props.onClose}>{() => <BuyButton onBuy={props.onBuy} />}</Modal>;
103
+ }
104
+ ```
105
+
106
+ - `usePress` returns `highlighted`, `pressed` and `progress` getters, and an `action` that you attach to your own `GuiButton`. Pass `hold` for a hold-to-confirm button, or `repeating` for a button that repeats while held.
107
+ - `Modal` takes its children as a function, so the controls inside know which modal owns them.
108
+ - The topmost modal, ranked by its `ScreenGui`'s `DisplayOrder`, owns gamepad selection and blocks presses underneath. Use `modalOpen()` to pause gameplay while any modal is open.
109
+
110
+ ## License
111
+
112
+ MIT
@@ -0,0 +1,16 @@
1
+ import type { Getter } from "@rbxts/charm";
2
+ export type InputDevice = "Gamepad" | "Touch" | "Keyboard";
3
+ export type PressFeedback = "Focus" | "Press" | "Refused";
4
+ export interface CloseInput {
5
+ readonly held: () => boolean;
6
+ readonly onPressed: (listener: () => void) => () => void;
7
+ readonly onReleased: (listener: () => void) => () => void;
8
+ }
9
+ export interface AnvilAdapter {
10
+ readonly device: Getter<InputDevice>;
11
+ readonly close: CloseInput;
12
+ readonly claimMenuInput: () => () => void;
13
+ readonly feedback: (kind: PressFeedback) => void;
14
+ }
15
+ export declare function installAnvil(adapter: AnvilAdapter): void;
16
+ export declare function adapter(): AnvilAdapter;
@@ -0,0 +1,19 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local installed
3
+ local function installAnvil(adapter)
4
+ if installed == adapter then
5
+ return nil
6
+ end
7
+ local _arg0 = installed == nil
8
+ assert(_arg0, "Anvil already has a different adapter")
9
+ installed = adapter
10
+ end
11
+ local function adapter()
12
+ local _installed = installed
13
+ assert(_installed, "Anvil has no adapter. Call installAnvil before rendering Anvil UI.")
14
+ return installed
15
+ end
16
+ return {
17
+ installAnvil = installAnvil,
18
+ adapter = adapter,
19
+ }
@@ -0,0 +1,3 @@
1
+ export type FrameListener = (now: number, delta: number) => void;
2
+ export declare const heartbeat: (listener: FrameListener) => (() => void);
3
+ export declare const preRender: (listener: FrameListener) => (() => void);
@@ -0,0 +1,56 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local RunService = TS.import(script, TS.getModule(script, "@rbxts", "services")).RunService
4
+ local function loop(signal)
5
+ local listeners = {}
6
+ local order
7
+ local connection
8
+ return function(listener)
9
+ local _listener = listener
10
+ listeners[_listener] = true
11
+ order = nil
12
+ if connection == nil then
13
+ connection = signal:Connect(function(delta)
14
+ if order == nil then
15
+ local _array = {}
16
+ local _length = #_array
17
+ for _v in listeners do
18
+ _length += 1
19
+ _array[_length] = _v
20
+ end
21
+ order = _array
22
+ end
23
+ local now = os.clock()
24
+ for _, callback in order do
25
+ if listeners[callback] ~= nil then
26
+ callback(now, delta)
27
+ end
28
+ end
29
+ end)
30
+ end
31
+ return function()
32
+ local _listener_1 = listener
33
+ -- ▼ Set.delete ▼
34
+ local _valueExisted = listeners[_listener_1] ~= nil
35
+ listeners[_listener_1] = nil
36
+ -- ▲ Set.delete ▲
37
+ if not _valueExisted then
38
+ return nil
39
+ end
40
+ order = nil
41
+ if next(listeners) == nil then
42
+ local _result = connection
43
+ if _result ~= nil then
44
+ _result:Disconnect()
45
+ end
46
+ connection = nil
47
+ end
48
+ end
49
+ end
50
+ end
51
+ local heartbeat = loop(RunService.Heartbeat)
52
+ local preRender = loop(RunService.PreRender)
53
+ return {
54
+ heartbeat = heartbeat,
55
+ preRender = preRender,
56
+ }
package/out/index.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ export { installAnvil, type AnvilAdapter, type CloseInput, type InputDevice, type PressFeedback } from "./adapter";
2
+ export { Modal, modalOpen, useModalActive, useTextEditing, type ModalProps } from "./modal";
3
+ export { closeFallbackContext, startMenuRuntime, useSelection } from "./runtime";
4
+ export { usePress, type PressOptions, type PressTiming } from "./use-press";
5
+ export { heartbeat, preRender, type FrameListener } from "./frame-loop";
6
+ export type { Children, ChildFactory, Getter, Placement, Value } from "./props";
package/out/init.luau ADDED
@@ -0,0 +1,18 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local exports = {}
4
+ exports.installAnvil = TS.import(script, script, "adapter").installAnvil
5
+ local _modal = TS.import(script, script, "modal")
6
+ exports.Modal = _modal.Modal
7
+ exports.modalOpen = _modal.modalOpen
8
+ exports.useModalActive = _modal.useModalActive
9
+ exports.useTextEditing = _modal.useTextEditing
10
+ local _runtime = TS.import(script, script, "runtime")
11
+ exports.closeFallbackContext = _runtime.closeFallbackContext
12
+ exports.startMenuRuntime = _runtime.startMenuRuntime
13
+ exports.useSelection = _runtime.useSelection
14
+ exports.usePress = TS.import(script, script, "use-press").usePress
15
+ local _frame_loop = TS.import(script, script, "frame-loop")
16
+ exports.heartbeat = _frame_loop.heartbeat
17
+ exports.preRender = _frame_loop.preRender
18
+ return exports
@@ -0,0 +1,12 @@
1
+ export interface ModalEntry<Id> {
2
+ readonly id: Id;
3
+ readonly order: number | undefined;
4
+ readonly serial: number;
5
+ }
6
+ export interface ModalState<Id> {
7
+ readonly entries: readonly ModalEntry<Id>[];
8
+ }
9
+ export declare function enter<Id>(state: ModalState<Id>, entry: ModalEntry<Id>): ModalState<Id>;
10
+ export declare function leave<Id>(state: ModalState<Id>, id: Id): ModalState<Id>;
11
+ export declare function top<Id>(state: ModalState<Id>): ModalEntry<Id> | undefined;
12
+ export declare function hasModal<Id>(state: ModalState<Id>): boolean;
@@ -0,0 +1,133 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local function enter(state, entry)
3
+ local _exp = state.entries
4
+ -- ▼ ReadonlyArray.find ▼
5
+ local _callback = function(value)
6
+ return value.id == entry.id
7
+ end
8
+ local _result
9
+ for _i, _v in _exp do
10
+ if _callback(_v, _i - 1, _exp) == true then
11
+ _result = _v
12
+ break
13
+ end
14
+ end
15
+ -- ▲ ReadonlyArray.find ▲
16
+ local existing = _result
17
+ local _result_1 = existing
18
+ if _result_1 ~= nil then
19
+ _result_1 = _result_1.order
20
+ end
21
+ local _condition = _result_1 == entry.order
22
+ if _condition then
23
+ _condition = existing ~= nil
24
+ end
25
+ if _condition then
26
+ return state
27
+ end
28
+ local _object = {}
29
+ local _left = "entries"
30
+ local _result_2
31
+ if existing then
32
+ local _exp_1 = state.entries
33
+ -- ▼ ReadonlyArray.map ▼
34
+ local _newValue = table.create(#_exp_1)
35
+ local _callback_1 = function(value)
36
+ local _result_3
37
+ if value.id == entry.id then
38
+ local _object_1 = table.clone(value)
39
+ setmetatable(_object_1, nil)
40
+ _object_1.order = entry.order
41
+ _result_3 = _object_1
42
+ else
43
+ _result_3 = value
44
+ end
45
+ return _result_3
46
+ end
47
+ for _k, _v in _exp_1 do
48
+ _newValue[_k] = _callback_1(_v, _k - 1, _exp_1)
49
+ end
50
+ -- ▲ ReadonlyArray.map ▲
51
+ _result_2 = _newValue
52
+ else
53
+ local _array = {}
54
+ local _length = #_array
55
+ local _array_1 = state.entries
56
+ local _Length = #_array_1
57
+ table.move(_array_1, 1, _Length, _length + 1, _array)
58
+ _length += _Length
59
+ _array[_length + 1] = entry
60
+ _result_2 = _array
61
+ end
62
+ _object[_left] = _result_2
63
+ return _object
64
+ end
65
+ local function leave(state, id)
66
+ local _exp = state.entries
67
+ -- ▼ ReadonlyArray.some ▼
68
+ local _result = false
69
+ local _callback = function(value)
70
+ return value.id == id
71
+ end
72
+ for _k, _v in _exp do
73
+ if _callback(_v, _k - 1, _exp) then
74
+ _result = true
75
+ break
76
+ end
77
+ end
78
+ -- ▲ ReadonlyArray.some ▲
79
+ if not _result then
80
+ return state
81
+ end
82
+ local _object = {}
83
+ local _left = "entries"
84
+ local _exp_1 = state.entries
85
+ -- ▼ ReadonlyArray.filter ▼
86
+ local _newValue = {}
87
+ local _callback_1 = function(value)
88
+ return value.id ~= id
89
+ end
90
+ local _length = 0
91
+ for _k, _v in _exp_1 do
92
+ if _callback_1(_v, _k - 1, _exp_1) == true then
93
+ _length += 1
94
+ _newValue[_length] = _v
95
+ end
96
+ end
97
+ -- ▲ ReadonlyArray.filter ▲
98
+ _object[_left] = _newValue
99
+ return _object
100
+ end
101
+ local function top(state)
102
+ local result
103
+ for _, entry in state.entries do
104
+ if entry.order == nil then
105
+ return nil
106
+ end
107
+ local _condition = not result
108
+ if not _condition then
109
+ local _exp = entry.order
110
+ local _condition_1 = result.order
111
+ if _condition_1 == nil then
112
+ _condition_1 = 0
113
+ end
114
+ _condition = _exp > _condition_1
115
+ if not _condition then
116
+ _condition = (entry.order == result.order and entry.serial > result.serial)
117
+ end
118
+ end
119
+ if _condition then
120
+ result = entry
121
+ end
122
+ end
123
+ return result
124
+ end
125
+ local function hasModal(state)
126
+ return not (#state.entries == 0)
127
+ end
128
+ return {
129
+ enter = enter,
130
+ leave = leave,
131
+ top = top,
132
+ hasModal = hasModal,
133
+ }
package/out/modal.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ import Vide from "@rbxts/vide";
2
+ import type { ChildFactory, Placement, Value } from "./props";
3
+ import { modalOpen } from "./runtime";
4
+ export { modalOpen };
5
+ export interface ModalProps extends Placement {
6
+ selection?: "Native" | "Pointer";
7
+ onClose?: () => void;
8
+ size?: Value<UDim2>;
9
+ children: ChildFactory;
10
+ }
11
+ export declare function Modal(props: ModalProps): Vide.Node;
12
+ export declare function useModalActive(): () => boolean;
13
+ export declare function useTextEditing(): () => boolean;
package/out/modal.luau ADDED
@@ -0,0 +1,58 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local TS = _G[script]
3
+ local _vide = TS.import(script, TS.getModule(script, "@rbxts", "vide").src)
4
+ local Vide = _vide
5
+ local cleanup = _vide.cleanup
6
+ local useAtom = TS.import(script, TS.getModule(script, "@rbxts", "vide-charm").src).useAtom
7
+ local _runtime = TS.import(script, script.Parent, "runtime")
8
+ local active = _runtime.active
9
+ local attachRoot = _runtime.attachRoot
10
+ local closeRoot = _runtime.closeRoot
11
+ local modalOpen = _runtime.modalOpen
12
+ local openRoot = _runtime.openRoot
13
+ local ownerContext = _runtime.ownerContext
14
+ local textEditing = _runtime.textEditing
15
+ local function Modal(props)
16
+ local id = openRoot(props.selection or "Native", props.onClose)
17
+ cleanup(function()
18
+ return closeRoot(id)
19
+ end)
20
+ return ownerContext(id, function()
21
+ local _attributes = {}
22
+ local _condition = props.name
23
+ if _condition == nil then
24
+ _condition = "Modal"
25
+ end
26
+ _attributes.Name = _condition
27
+ _attributes.Size = props.size or UDim2.fromScale(1, 1)
28
+ _attributes.Position = props.position
29
+ _attributes.AnchorPoint = props.anchorPoint
30
+ _attributes.LayoutOrder = props.layoutOrder
31
+ _attributes.ZIndex = props.zIndex
32
+ _attributes.BackgroundTransparency = 1
33
+ _attributes.SelectionGroup = true
34
+ _attributes.SelectionBehaviorUp = Enum.SelectionBehavior.Stop
35
+ _attributes.SelectionBehaviorDown = Enum.SelectionBehavior.Stop
36
+ _attributes.SelectionBehaviorLeft = Enum.SelectionBehavior.Stop
37
+ _attributes.SelectionBehaviorRight = Enum.SelectionBehavior.Stop
38
+ _attributes.action = function(root)
39
+ return cleanup(attachRoot(id, root))
40
+ end
41
+ return Vide.jsx("frame", _attributes, props.children())
42
+ end)
43
+ end
44
+ local function useModalActive()
45
+ local owner = ownerContext()
46
+ return useAtom(function()
47
+ return active(owner)
48
+ end)
49
+ end
50
+ local function useTextEditing()
51
+ return useAtom(textEditing)
52
+ end
53
+ return {
54
+ Modal = Modal,
55
+ useModalActive = useModalActive,
56
+ useTextEditing = useTextEditing,
57
+ modalOpen = modalOpen,
58
+ }
@@ -0,0 +1,35 @@
1
+ export type PressMode = {
2
+ readonly kind: "Activate";
3
+ } | {
4
+ readonly kind: "Hold";
5
+ readonly seconds: number;
6
+ } | {
7
+ readonly kind: "Repeat";
8
+ };
9
+ export interface PressState<Source> {
10
+ readonly source?: Source;
11
+ readonly elapsed: number;
12
+ readonly progress: number;
13
+ readonly fired: boolean;
14
+ readonly nextRepeatAt: number;
15
+ readonly repeatInterval: number;
16
+ }
17
+ export type PressEvent<Source> = {
18
+ readonly kind: "Begin";
19
+ readonly source: Source;
20
+ } | {
21
+ readonly kind: "Tick";
22
+ readonly delta: number;
23
+ } | {
24
+ readonly kind: "End";
25
+ readonly source: Source;
26
+ } | {
27
+ readonly kind: "Cancel";
28
+ };
29
+ export interface PressTransition<Source> {
30
+ readonly state: PressState<Source>;
31
+ readonly fire: boolean;
32
+ readonly ended: boolean;
33
+ }
34
+ export declare function idlePress<Source>(): PressState<Source>;
35
+ export declare function transition<Source>(state: PressState<Source>, mode: PressMode, event: PressEvent<Source>): PressTransition<Source>;
@@ -0,0 +1,104 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ local function idlePress()
3
+ return {
4
+ elapsed = 0,
5
+ progress = 0,
6
+ fired = false,
7
+ nextRepeatAt = 0.4,
8
+ repeatInterval = 0.15,
9
+ }
10
+ end
11
+ local function transition(state, mode, event)
12
+ if event.kind == "Begin" then
13
+ local _result
14
+ if state.source ~= nil then
15
+ _result = {
16
+ state = state,
17
+ fire = false,
18
+ ended = false,
19
+ }
20
+ else
21
+ local _object = {}
22
+ local _left = "state"
23
+ local _object_1 = table.clone(idlePress())
24
+ setmetatable(_object_1, nil)
25
+ _object_1.source = event.source
26
+ _object[_left] = _object_1
27
+ _object.fire = mode.kind == "Repeat"
28
+ _object.ended = false
29
+ _result = _object
30
+ end
31
+ return _result
32
+ end
33
+ if event.kind == "Cancel" or event.kind == "End" then
34
+ if event.kind == "End" and event.source ~= state.source then
35
+ return {
36
+ state = state,
37
+ fire = false,
38
+ ended = false,
39
+ }
40
+ end
41
+ local _object = {}
42
+ local _left = "state"
43
+ local _object_1 = table.clone(idlePress())
44
+ setmetatable(_object_1, nil)
45
+ _object_1.progress = state.progress
46
+ _object[_left] = _object_1
47
+ _object.fire = false
48
+ _object.ended = state.source ~= nil
49
+ return _object
50
+ end
51
+ local delta = math.max(0, event.delta)
52
+ if state.source == nil then
53
+ local _object = {}
54
+ local _left = "state"
55
+ local _object_1 = table.clone(state)
56
+ setmetatable(_object_1, nil)
57
+ _object_1.progress = math.max(0, state.progress - delta / 0.25)
58
+ _object[_left] = _object_1
59
+ _object.fire = false
60
+ _object.ended = false
61
+ return _object
62
+ end
63
+ local elapsed = state.elapsed + delta
64
+ if mode.kind == "Hold" then
65
+ local fire = not state.fired and elapsed >= mode.seconds
66
+ local _object = {}
67
+ local _left = "state"
68
+ local _object_1 = table.clone(state)
69
+ setmetatable(_object_1, nil)
70
+ _object_1.elapsed = elapsed
71
+ _object_1.fired = state.fired or fire
72
+ _object_1.progress = if state.fired or fire then 0 else math.min(1, elapsed / mode.seconds)
73
+ _object[_left] = _object_1
74
+ _object.fire = fire
75
+ _object.ended = false
76
+ return _object
77
+ end
78
+ if mode.kind == "Repeat" and elapsed >= state.nextRepeatAt then
79
+ local _object = {}
80
+ local _left = "state"
81
+ local _object_1 = table.clone(state)
82
+ setmetatable(_object_1, nil)
83
+ _object_1.elapsed = elapsed
84
+ _object_1.nextRepeatAt = elapsed + state.repeatInterval
85
+ _object_1.repeatInterval = math.max(0.04, state.repeatInterval * 0.85)
86
+ _object[_left] = _object_1
87
+ _object.fire = true
88
+ _object.ended = false
89
+ return _object
90
+ end
91
+ local _object = {}
92
+ local _left = "state"
93
+ local _object_1 = table.clone(state)
94
+ setmetatable(_object_1, nil)
95
+ _object_1.elapsed = elapsed
96
+ _object[_left] = _object_1
97
+ _object.fire = false
98
+ _object.ended = false
99
+ return _object
100
+ end
101
+ return {
102
+ idlePress = idlePress,
103
+ transition = transition,
104
+ }
package/out/props.d.ts ADDED
@@ -0,0 +1,12 @@
1
+ import type Vide from "@rbxts/vide";
2
+ export type Value<T> = Vide.Derivable<T>;
3
+ export type Getter<T> = () => T;
4
+ export type Children = Vide.Node;
5
+ export type ChildFactory = () => Vide.Node;
6
+ export interface Placement {
7
+ name?: string;
8
+ layoutOrder?: number;
9
+ anchorPoint?: Value<Vector2>;
10
+ position?: Value<UDim2>;
11
+ zIndex?: Value<number>;
12
+ }
package/out/props.luau ADDED
@@ -0,0 +1,2 @@
1
+ -- Compiled with roblox-ts v3.0.0
2
+ return nil
@@ -0,0 +1,30 @@
1
+ import type { Value } from "./props";
2
+ export type Owner = number | "Base";
3
+ export declare const ownerContext: import("@rbxts/vide").Context<Owner>;
4
+ export declare const closeFallbackContext: import("@rbxts/vide").Context<boolean>;
5
+ export declare const modalOpen: import("@rbxts/charm").Getter<boolean>;
6
+ export declare const selectedObject: import("@rbxts/charm").Atom<GuiObject | undefined>;
7
+ export declare const textEditing: import("@rbxts/charm").Atom<boolean>;
8
+ export declare const menuOpen: import("@rbxts/charm").Atom<boolean>;
9
+ interface Control {
10
+ readonly owner: Owner;
11
+ readonly buttonADown?: (input: InputObject) => void;
12
+ readonly buttonAUp?: (input: InputObject) => void;
13
+ }
14
+ export declare function active(owner: Owner): boolean;
15
+ export declare function openRoot(mode: "Native" | "Pointer", onClose?: () => void): number;
16
+ export declare function attachRoot(id: number, root: GuiObject): () => void;
17
+ export declare function closeRoot(id: number): void;
18
+ export declare function registerControl(object: GuiObject, control: Control, fallback?: boolean): () => void;
19
+ export declare function markControl(owner: Owner, object: GuiObject, marked: boolean): void;
20
+ export declare function useSelection(options?: {
21
+ selectFirst?: Value<boolean>;
22
+ buttonADown?: (input: InputObject) => void;
23
+ buttonAUp?: (input: InputObject) => void;
24
+ }): {
25
+ allowed: () => boolean;
26
+ selected: () => boolean;
27
+ action: (instance: GuiObject) => void;
28
+ };
29
+ export declare function startMenuRuntime(): () => void;
30
+ export {};