From 21b430e9dd9f58f6e51e08f24b93641a9a9f6195 Mon Sep 17 00:00:00 2001 From: paradoxuum Date: Fri, 19 Jul 2024 05:42:29 +0100 Subject: [PATCH] refactor(ui): port ui to vide --- packages/ui/package.json | 13 +- packages/ui/src/app/app.tsx | 34 +- packages/ui/src/app/config.ts | 7 +- packages/ui/src/app/terminal-app.tsx | 52 ++- .../src/components/history/history-line.tsx | 47 +-- .../src/components/history/history-list.tsx | 65 ++-- .../ui/src/components/suggestions/badge.tsx | 37 +-- .../suggestions/main-suggestion.tsx | 155 ++++----- .../suggestions/suggestion-list.tsx | 80 ++--- .../components/suggestions/suggestions.tsx | 73 ++--- .../ui/src/components/suggestions/util.ts | 8 +- .../ui/src/components/terminal-text-field.tsx | 297 ++++++++---------- .../ui/src/components/terminal-window.tsx | 186 +++++------ packages/ui/src/components/terminal.tsx | 18 +- packages/ui/src/components/ui/frame.tsx | 43 ++- packages/ui/src/components/ui/group.tsx | 35 +-- packages/ui/src/components/ui/layer.tsx | 8 +- packages/ui/src/components/ui/outline.tsx | 112 +++---- packages/ui/src/components/ui/padding.tsx | 13 +- .../ui/src/components/ui/scrolling-frame.tsx | 84 +++-- .../ui/src/components/ui/shortcut-group.tsx | 108 ------- packages/ui/src/components/ui/text-field.tsx | 108 +++---- packages/ui/src/components/ui/text.tsx | 50 ++- packages/ui/src/hooks/use-api.ts | 15 + packages/ui/src/hooks/use-atom.ts | 13 + packages/ui/src/hooks/use-event.ts | 65 ++++ packages/ui/src/hooks/use-motion.ts | 34 +- packages/ui/src/hooks/use-px.ts | 83 +++-- packages/ui/src/hooks/use-store.ts | 4 - packages/ui/src/providers/api-provider.tsx | 12 - .../ui/src/providers/options-provider.tsx | 42 --- packages/ui/src/providers/root-provider.tsx | 31 -- packages/ui/src/store.ts | 35 +++ packages/ui/src/store/app/app-selectors.ts | 3 - packages/ui/src/store/app/app-slice.ts | 13 - packages/ui/src/store/app/index.ts | 2 - .../ui/src/store/command/command-selectors.ts | 3 - .../ui/src/store/command/command-slice.ts | 18 -- packages/ui/src/store/command/index.ts | 2 - .../ui/src/store/history/history-slice.ts | 50 --- packages/ui/src/store/history/index.ts | 1 - packages/ui/src/store/index.ts | 17 - packages/ui/src/store/suggestion/index.ts | 2 - .../store/suggestion/suggestion-selectors.ts | 3 - .../src/store/suggestion/suggestion-slice.ts | 15 - packages/ui/src/store/text/index.ts | 2 - packages/ui/src/store/text/text-selectors.ts | 5 - packages/ui/src/store/text/text-slice.ts | 33 -- packages/ui/src/types.ts | 7 +- packages/ui/src/util/argument.ts | 35 ++- packages/ui/src/util/suggestion.ts | 28 +- packages/ui/tsconfig.json | 4 +- yarn.lock | 85 ++--- 53 files changed, 946 insertions(+), 1349 deletions(-) delete mode 100644 packages/ui/src/components/ui/shortcut-group.tsx create mode 100644 packages/ui/src/hooks/use-api.ts create mode 100644 packages/ui/src/hooks/use-atom.ts create mode 100644 packages/ui/src/hooks/use-event.ts delete mode 100644 packages/ui/src/hooks/use-store.ts delete mode 100644 packages/ui/src/providers/api-provider.tsx delete mode 100644 packages/ui/src/providers/options-provider.tsx delete mode 100644 packages/ui/src/providers/root-provider.tsx create mode 100644 packages/ui/src/store.ts delete mode 100644 packages/ui/src/store/app/app-selectors.ts delete mode 100644 packages/ui/src/store/app/app-slice.ts delete mode 100644 packages/ui/src/store/app/index.ts delete mode 100644 packages/ui/src/store/command/command-selectors.ts delete mode 100644 packages/ui/src/store/command/command-slice.ts delete mode 100644 packages/ui/src/store/command/index.ts delete mode 100644 packages/ui/src/store/history/history-slice.ts delete mode 100644 packages/ui/src/store/history/index.ts delete mode 100644 packages/ui/src/store/index.ts delete mode 100644 packages/ui/src/store/suggestion/index.ts delete mode 100644 packages/ui/src/store/suggestion/suggestion-selectors.ts delete mode 100644 packages/ui/src/store/suggestion/suggestion-slice.ts delete mode 100644 packages/ui/src/store/text/index.ts delete mode 100644 packages/ui/src/store/text/text-selectors.ts delete mode 100644 packages/ui/src/store/text/text-slice.ts diff --git a/packages/ui/package.json b/packages/ui/package.json index 5ba33da9..087a3171 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -28,7 +28,8 @@ }, "scripts": { "prepack": "yarn build", - "build": "shx rm -rf out && rbxtsc --verbose" + "build": "shx rm -rf out && rbxtsc --verbose", + "dev": "rbxtsc --watch" }, "devDependencies": { "@rbxts/centurion": "workspace:^", @@ -40,13 +41,11 @@ }, "dependencies": { "@rbxts/beacon": "^2.1.1", - "@rbxts/pretty-react-hooks": "^0.5.2", - "@rbxts/react": "^0.4.0", - "@rbxts/react-reflex": "^0.3.4", - "@rbxts/react-roblox": "^0.4.0", - "@rbxts/reflex": "^4.3.1", + "@rbxts/charm": "^0.5.2", "@rbxts/ripple": "^0.8.2", - "@rbxts/services": "^1.5.4" + "@rbxts/services": "^1.5.4", + "@rbxts/set-timeout": "^1.1.2", + "@rbxts/vide": "^0.4.1" }, "peerDependencies": { "@rbxts/centurion": "workspace:^" diff --git a/packages/ui/src/app/app.tsx b/packages/ui/src/app/app.tsx index 837d52c8..ace2ce94 100644 --- a/packages/ui/src/app/app.tsx +++ b/packages/ui/src/app/app.tsx @@ -1,15 +1,12 @@ -const config = script.Parent?.FindFirstChild("config"); -if (config !== undefined && classIs(config, "ModuleScript")) { - require(config); -} +import "./config"; import { Signal } from "@rbxts/beacon"; import { ClientAPI } from "@rbxts/centurion"; -import React, { StrictMode } from "@rbxts/react"; -import { createPortal, createRoot } from "@rbxts/react-roblox"; import { ContentProvider, Players } from "@rbxts/services"; +import Vide, { mount } from "@rbxts/vide"; import { DEFAULT_INTERFACE_OPTIONS } from "../constants/options"; -import { RootProvider } from "../providers/root-provider"; +import { useAPI } from "../hooks/use-api"; +import { usePx } from "../hooks/use-px"; import { InterfaceOptions } from "../types"; import { TerminalApp } from "./terminal-app"; @@ -26,9 +23,6 @@ export namespace CenturionUI { options: Partial = {}, ): (api: ClientAPI) => void { return (api) => { - const root = createRoot(new Instance("Folder")); - const target = Players.LocalPlayer.WaitForChild("PlayerGui"); - // Attempt to preload font task.spawn(() => { const fontFamily = ( @@ -49,20 +43,12 @@ export namespace CenturionUI { } }); - root.render( - createPortal( - - - - - , - target, - ), - ); + const target = Players.LocalPlayer.WaitForChild("PlayerGui"); + mount(() => { + useAPI(api); + usePx(); + return ; + }, target); }; } } diff --git a/packages/ui/src/app/config.ts b/packages/ui/src/app/config.ts index f73d6ee3..d24272f3 100644 --- a/packages/ui/src/app/config.ts +++ b/packages/ui/src/app/config.ts @@ -1,7 +1,6 @@ -declare const _G: { __DEV__: boolean }; - -const RunService = game.GetService("RunService"); +import { RunService } from "@rbxts/services"; +import Vide from "@rbxts/vide"; if (RunService.IsStudio() && RunService.IsClient()) { - _G.__DEV__ = true; + Vide.strict = true; } diff --git a/packages/ui/src/app/terminal-app.tsx b/packages/ui/src/app/terminal-app.tsx index 875802c7..e56e6eda 100644 --- a/packages/ui/src/app/terminal-app.tsx +++ b/packages/ui/src/app/terminal-app.tsx @@ -1,43 +1,41 @@ -import { useEventListener } from "@rbxts/pretty-react-hooks"; -import React, { useContext, useMemo } from "@rbxts/react"; -import { useSelector } from "@rbxts/react-reflex"; import { UserInputService } from "@rbxts/services"; +import Vide, { derive } from "@rbxts/vide"; import Terminal from "../components/terminal"; import { Layer } from "../components/ui/layer"; -import { OptionsContext } from "../providers/options-provider"; -import { store } from "../store"; -import { selectVisible } from "../store/app"; +import { useAtom } from "../hooks/use-atom"; +import { useEvent } from "../hooks/use-event"; +import { + interfaceOptions, + interfaceVisible, + mouseOverInterface, +} from "../store"; -export function TerminalApp() { - const options = useContext(OptionsContext); - const visible = useSelector(selectVisible); +const MOUSE_INPUT_TYPES = new Set([ + Enum.UserInputType.MouseButton1, + Enum.UserInputType.MouseButton2, + Enum.UserInputType.Touch, +]); - const validKeys = useMemo(() => { - return new Set(options.activationKeys); - }, [options]); +export function TerminalApp() { + const options = useAtom(interfaceOptions); + const visible = useAtom(interfaceVisible); - const mouseInputTypes = useMemo(() => { - return new Set([ - Enum.UserInputType.MouseButton1, - Enum.UserInputType.MouseButton2, - Enum.UserInputType.Touch, - ]); - }, []); + const validKeys = derive(() => new Set(options().activationKeys)); - useEventListener(UserInputService.InputBegan, (input, gameProcessed) => { - if (validKeys.has(input.KeyCode) && !gameProcessed) { - store.setVisible(!visible); + useEvent(UserInputService.InputBegan, (input, gameProcessed) => { + if (validKeys().has(input.KeyCode) && !gameProcessed) { + interfaceVisible(!visible()); } else if ( - options.hideOnLostFocus && - mouseInputTypes.has(input.UserInputType) && - !options.isMouseOnGUI + options().hideOnLostFocus && + MOUSE_INPUT_TYPES.has(input.UserInputType) && + !mouseOverInterface() ) { - store.setVisible(false); + interfaceVisible(false); } }); return ( - + options().displayOrder ?? 0} visible={visible}> ); diff --git a/packages/ui/src/components/history/history-line.tsx b/packages/ui/src/components/history/history-line.tsx index e3463003..3c221d1b 100644 --- a/packages/ui/src/components/history/history-line.tsx +++ b/packages/ui/src/components/history/history-line.tsx @@ -1,9 +1,9 @@ import { HistoryEntry } from "@rbxts/centurion"; -import { BindingOrValue } from "@rbxts/pretty-react-hooks"; -import React, { useContext, useMemo } from "@rbxts/react"; +import Vide, { Derivable, derive } from "@rbxts/vide"; import { HISTORY_TEXT_SIZE } from "../../constants/text"; -import { usePx } from "../../hooks/use-px"; -import { OptionsContext } from "../../providers/options-provider"; +import { useAtom } from "../../hooks/use-atom"; +import { px } from "../../hooks/use-px"; +import { interfaceOptions } from "../../store"; import { Frame } from "../ui/frame"; import { Group } from "../ui/group"; import { Outline } from "../ui/outline"; @@ -12,44 +12,46 @@ import { TextField } from "../ui/text-field"; interface HistoryLineProps { data: HistoryEntry; - size?: BindingOrValue; - position?: BindingOrValue; - order?: BindingOrValue; + size?: Derivable; + position?: Derivable; + order?: Derivable; } export function HistoryLine({ data, size, position, order }: HistoryLineProps) { - const options = useContext(OptionsContext); - const px = usePx(); - const date = useMemo(() => { + const options = useAtom(interfaceOptions); + + const date = derive(() => { const dateTime = DateTime.fromUnixTimestamp(data.sentAt).FormatLocalTime( "LT", "en-us", ); const dateParts = dateTime.split(" "); return `${dateParts[0]} ${dateParts[1]}`; - }, [data]); + }); return ( options().palette.surface} size={UDim2.fromOffset(px(76), px(HISTORY_TEXT_SIZE + 4))} cornerRadius={new UDim(0, px(4))} > options().palette.text} + textSize={() => px(HISTORY_TEXT_SIZE)} richText={true} /> px(1)} innerTransparency={0.25} - innerColor={ - data.success ? options.palette.success : options.palette.error - } + innerColor={() => { + return data.success + ? options().palette.success + : options().palette.error; + }} outerThickness={0} cornerRadius={new UDim(0, px(4))} /> @@ -57,15 +59,18 @@ export function HistoryLine({ data, size, position, order }: HistoryLineProps) { new UDim2(1, -px(84), 1, 0)} position={UDim2.fromScale(1, 0)} text={data.text} textSize={px(HISTORY_TEXT_SIZE)} - textColor={data.success ? options.palette.text : options.palette.error} + textColor={() => { + const palette = options().palette; + return data.success ? palette.text : palette.error; + }} textEditable={false} textXAlignment="Left" clearTextOnFocus={false} - font={options.font.medium} + font={() => options().font.medium} richText /> diff --git a/packages/ui/src/components/history/history-list.tsx b/packages/ui/src/components/history/history-list.tsx index fb0718dc..d7f00ebe 100644 --- a/packages/ui/src/components/history/history-list.tsx +++ b/packages/ui/src/components/history/history-list.tsx @@ -1,7 +1,7 @@ -import { BindingOrValue, mapBinding } from "@rbxts/pretty-react-hooks"; -import React, { useBinding, useContext, useEffect } from "@rbxts/react"; -import { usePx } from "../../hooks/use-px"; -import { OptionsContext } from "../../providers/options-provider"; +import Vide, { Derivable, effect, For, read, source } from "@rbxts/vide"; +import { useAtom } from "../../hooks/use-atom"; +import { px } from "../../hooks/use-px"; +import { interfaceOptions } from "../../store"; import { HistoryLineData } from "../../types"; import { ScrollingFrame } from "../ui/scrolling-frame"; import { HistoryLine } from "./history-line"; @@ -12,11 +12,11 @@ export interface HistoryData { } interface HistoryListProps { - data: HistoryData; - size?: BindingOrValue; - position?: BindingOrValue; + data: Derivable; + size?: Derivable; + position?: Derivable; maxHeight?: number; - scrollingEnabled?: BindingOrValue; + scrollingEnabled?: Derivable; } export function HistoryList({ @@ -25,42 +25,41 @@ export function HistoryList({ position, maxHeight, }: HistoryListProps) { - const px = usePx(); - const options = useContext(OptionsContext); + const options = useAtom(interfaceOptions); - const [scrollingEnabled, setScrollingEnabled] = useBinding(false); - const [canvasSize, setCanvasSize] = useBinding(new UDim2()); - const [canvasPosition, setCanvasPosition] = useBinding(new Vector2()); + const scrollingEnabled = source(false); + const canvasSize = source(new UDim2()); + const canvasPos = source(new Vector2()); - useEffect(() => { - const height = data.height - px(8); - setCanvasSize(new UDim2(0, 0, 0, height)); - setCanvasPosition(new Vector2(0, height)); + effect(() => { + const height = read(data).height - px(8); + canvasSize(new UDim2(0, 0, 0, height)); + canvasPos(new Vector2(0, height)); - if (maxHeight !== undefined) setScrollingEnabled(height > maxHeight); - }, [data, px]); + if (maxHeight !== undefined) scrollingEnabled(height > maxHeight); + }); return ( { - return val ? 10 : 0; - })} + canvasPosition={canvasPos} + scrollBarColor={() => options().palette.subtext} + scrollBarThickness={() => (scrollingEnabled() ? 10 : 0)} scrollingEnabled={scrollingEnabled} > - {data.lines.map((data, i) => { - return ( - - ); - })} + read(data).lines}> + {(line: HistoryLineData, index: () => number) => { + return ( + + ); + }} + diff --git a/packages/ui/src/components/suggestions/badge.tsx b/packages/ui/src/components/suggestions/badge.tsx index d924dd6a..c906b89a 100644 --- a/packages/ui/src/components/suggestions/badge.tsx +++ b/packages/ui/src/components/suggestions/badge.tsx @@ -1,23 +1,21 @@ -import { BindingOrValue } from "@rbxts/pretty-react-hooks"; -import React, { useContext } from "@rbxts/react"; -import { usePx } from "../../hooks/use-px"; -import { OptionsContext } from "../../providers/options-provider"; +import Vide, { Derivable } from "@rbxts/vide"; +import { useAtom } from "../../hooks/use-atom"; +import { px } from "../../hooks/use-px"; +import { interfaceOptions } from "../../store"; import { Frame } from "../ui/frame"; import { Text } from "../ui/text"; interface BadgeProps { - anchorPoint?: BindingOrValue; - size?: BindingOrValue; - position?: BindingOrValue; - color?: BindingOrValue; - text?: BindingOrValue; - textColor?: BindingOrValue; - textSize?: BindingOrValue; - visible?: BindingOrValue; - + anchorPoint?: Derivable; + size?: Derivable; + position?: Derivable; + color?: Derivable; + text?: Derivable; + textColor?: Derivable; + textSize?: Derivable; + visible?: Derivable; onTextBoundsChange?: (textBounds: Vector2) => void; - - children?: JSX.Element; + children?: Vide.Node; backgroundTransparency?: number; } @@ -34,8 +32,7 @@ export function Badge({ children, backgroundTransparency, }: BadgeProps) { - const options = useContext(OptionsContext); - const px = usePx(); + const options = useAtom(interfaceOptions); return ( onTextBoundsChange?.(rbx.TextBounds), - }} + font={() => options().font.bold} + textBoundsChanged={onTextBoundsChange} /> {children} diff --git a/packages/ui/src/components/suggestions/main-suggestion.tsx b/packages/ui/src/components/suggestions/main-suggestion.tsx index 456f1900..2b285fdb 100644 --- a/packages/ui/src/components/suggestions/main-suggestion.tsx +++ b/packages/ui/src/components/suggestions/main-suggestion.tsx @@ -1,45 +1,26 @@ -import { BindingOrValue, lerpBinding } from "@rbxts/pretty-react-hooks"; -import React, { Binding, useContext, useEffect } from "@rbxts/react"; +import Vide, { Derivable, effect, read } from "@rbxts/vide"; import { springs } from "../../constants/springs"; import { SUGGESTION_TEXT_SIZE, SUGGESTION_TITLE_TEXT_SIZE, } from "../../constants/text"; +import { useAtom } from "../../hooks/use-atom"; import { useMotion } from "../../hooks/use-motion"; -import { usePx } from "../../hooks/use-px"; -import { - InterfaceOptionsWithState, - OptionsContext, -} from "../../providers/options-provider"; -import { ArgumentSuggestion, CommandSuggestion, Suggestion } from "../../types"; +import { px } from "../../hooks/use-px"; +import { interfaceOptions, mouseOverInterface } from "../../store"; +import { Suggestion } from "../../types"; import { Frame } from "../ui/frame"; import { Padding } from "../ui/padding"; -import { ShortcutGroup } from "../ui/shortcut-group"; import { Text } from "../ui/text"; import { Badge } from "./badge"; import { SuggestionTextBounds } from "./types"; import { highlightMatching } from "./util"; export interface MainSuggestionProps { - suggestion?: Suggestion; - argument: BindingOrValue; - currentText?: string; - size: BindingOrValue; - sizes: Binding; -} - -function showKeybindsGui(props: { - options: InterfaceOptionsWithState; - suggestion: Suggestion | undefined; -}) { - if ( - props.suggestion && - props.suggestion.main.type === "command" && - (props.suggestion.main as CommandSuggestion).shortcuts !== undefined && - props.options.showShortcutSuggestions - ) { - return true; - } + suggestion: Derivable; + currentText?: Derivable; + size: Derivable; + sizes: Derivable; } export function MainSuggestion({ @@ -47,50 +28,54 @@ export function MainSuggestion({ currentText, size, sizes, - argument, }: MainSuggestionProps) { - const px = usePx(); - const options = useContext(OptionsContext); + const options = useAtom(interfaceOptions); const [titleHeight, titleHeightMotion] = useMotion(0); const [badgeWidth, badgeWidthMotion] = useMotion(0); - useEffect(() => { + effect(() => { titleHeightMotion.spring( - suggestion?.main.description !== undefined ? 1 : 0, + read(suggestion)?.description !== undefined ? 1 : 0, springs.responsive, ); - }, [suggestion]); + }); return ( options().palette.background} + backgroundTransparency={() => options().backgroundTransparency ?? 0} cornerRadius={new UDim(0, px(8))} clipsDescendants={false} - event={{ - MouseEnter: () => options.setMouseOnGUI(true), - MouseLeave: () => options.setMouseOnGUI(false), - }} + mouseEnter={() => mouseOverInterface(true)} + mouseLeave={() => mouseOverInterface(false)} > - UDim2.fromOffset(math.round(width), px(24)), - )} + size={() => { + return UDim2.fromOffset(badgeWidth(), px(24)); + }} position={UDim2.fromScale(1, 0)} - color={options.palette.highlight} - text={ - argument && suggestion !== undefined - ? (suggestion.main as ArgumentSuggestion).dataType - : "" - } - textColor={options.palette.surface} + color={() => options().palette.highlight} + text={() => { + const currentSuggestion = read(suggestion); + return currentSuggestion !== undefined && + currentSuggestion.type === "argument" + ? currentSuggestion.dataType + : ""; + }} + textColor={() => options().palette.surface} textSize={px(SUGGESTION_TEXT_SIZE)} - visible={argument} + visible={() => { + const currentSuggestion = read(suggestion); + return ( + currentSuggestion !== undefined && + currentSuggestion.type === "argument" + ); + }} onTextBoundsChange={(textBounds) => badgeWidthMotion.spring(textBounds.X + px(8), { mass: 0.5, @@ -99,48 +84,38 @@ export function MainSuggestion({ } /> - {showKeybindsGui({ options: options, suggestion: suggestion }) ? ( - - ) : ( - <> - )} - val.title)} - position={lerpBinding( - titleHeight, - UDim2.fromOffset(0, 0), - UDim2.fromOffset(0, px(-4)), - )} - text={ - argument - ? suggestion?.main.title + size={() => read(sizes).title} + position={() => { + return UDim2.fromOffset(0, 0).Lerp( + UDim2.fromOffset(0, px(-4)), + titleHeight(), + ); + }} + text={() => { + const currentSuggestion = read(suggestion); + return currentSuggestion?.type === "argument" + ? currentSuggestion.title : highlightMatching( - options.palette.highlight, - suggestion?.main.title, - currentText, - ) - } + options().palette.highlight, + currentSuggestion?.title, + read(currentText), + ); + }} textSize={px(SUGGESTION_TITLE_TEXT_SIZE)} - textColor={options.palette.text} + textColor={() => options().palette.text} textXAlignment="Left" textYAlignment="Top" richText={true} - font={options.font.bold} + font={() => options().font.bold} /> val.description)} + size={() => read(sizes).description} position={UDim2.fromOffset(0, px(SUGGESTION_TITLE_TEXT_SIZE))} - text={suggestion?.main.description ?? ""} + text={() => read(suggestion)?.description ?? ""} textSize={px(SUGGESTION_TEXT_SIZE)} - textColor={options.palette.subtext} + textColor={() => options().palette.subtext} textXAlignment="Left" textYAlignment="Top" textWrapped={true} @@ -149,14 +124,16 @@ export function MainSuggestion({ new UDim2(1, 0, 0, val.errorTextHeight))} + size={() => new UDim2(1, 0, 0, read(sizes).errorTextHeight)} position={UDim2.fromScale(0, 1)} - text={ - argument && suggestion !== undefined - ? (suggestion.main as ArgumentSuggestion).error ?? "" - : "" - } - textColor={options.palette.error} + text={() => { + const currentSuggestion = read(suggestion); + return currentSuggestion !== undefined && + currentSuggestion.type === "argument" + ? currentSuggestion.error ?? "" + : ""; + }} + textColor={() => options().palette.error} textSize={px(SUGGESTION_TEXT_SIZE)} textWrapped={true} textXAlignment="Left" diff --git a/packages/ui/src/components/suggestions/suggestion-list.tsx b/packages/ui/src/components/suggestions/suggestion-list.tsx index 83144e03..7ce14fc3 100644 --- a/packages/ui/src/components/suggestions/suggestion-list.tsx +++ b/packages/ui/src/components/suggestions/suggestion-list.tsx @@ -1,8 +1,8 @@ -import { BindingOrValue } from "@rbxts/pretty-react-hooks"; -import React, { useContext } from "@rbxts/react"; +import Vide, { Derivable, For, read } from "@rbxts/vide"; import { SUGGESTION_TEXT_SIZE } from "../../constants/text"; -import { usePx } from "../../hooks/use-px"; -import { OptionsContext } from "../../providers/options-provider"; +import { useAtom } from "../../hooks/use-atom"; +import { px } from "../../hooks/use-px"; +import { interfaceOptions, mouseOverInterface } from "../../store"; import { Suggestion } from "../../types"; import { Frame } from "../ui/frame"; import { Group } from "../ui/group"; @@ -11,9 +11,9 @@ import { Text } from "../ui/text"; import { highlightMatching } from "./util"; export interface SuggestionListProps { - suggestion?: Suggestion; - currentText?: string; - size: BindingOrValue; + suggestion?: Derivable; + currentText?: Derivable; + size: Derivable; } export function SuggestionList({ @@ -21,46 +21,48 @@ export function SuggestionList({ currentText, size, }: SuggestionListProps) { - const px = usePx(); - const options = useContext(OptionsContext); + const options = useAtom(interfaceOptions); return ( options.setMouseOnGUI(true), - MouseLeave: () => options.setMouseOnGUI(false), - }} + mouseEnter={() => mouseOverInterface(true)} + mouseLeave={() => mouseOverInterface(false)} > - {suggestion?.others?.map((name, i) => { - return ( - - + read(suggestion)?.others ?? []}> + {(name: string, i: () => number) => { + return ( + options().palette.background} + backgroundTransparency={() => + options().backgroundTransparency ?? 0 + } + cornerRadius={new UDim(0, px(8))} + clipsDescendants={true} + > + - - - ); - })} + + highlightMatching( + options().palette.highlight, + name, + read(currentText), + ) + } + textColor={() => options().palette.text} + textSize={px(SUGGESTION_TEXT_SIZE)} + textXAlignment="Left" + richText={true} + /> + + ); + }} + ); } diff --git a/packages/ui/src/components/suggestions/suggestions.tsx b/packages/ui/src/components/suggestions/suggestions.tsx index 61eb58b4..2b010b5c 100644 --- a/packages/ui/src/components/suggestions/suggestions.tsx +++ b/packages/ui/src/components/suggestions/suggestions.tsx @@ -1,21 +1,18 @@ -import React, { - useBinding, - useContext, - useEffect, - useMemo, -} from "@rbxts/react"; -import { useSelector } from "@rbxts/react-reflex"; import { TextService } from "@rbxts/services"; +import Vide, { cleanup, effect, source } from "@rbxts/vide"; import { springs } from "../../constants/springs"; import { SUGGESTION_TEXT_SIZE, SUGGESTION_TITLE_TEXT_SIZE, } from "../../constants/text"; +import { useAtom } from "../../hooks/use-atom"; import { useMotion } from "../../hooks/use-motion"; -import { usePx } from "../../hooks/use-px"; -import { OptionsContext } from "../../providers/options-provider"; -import { selectSuggestion } from "../../store/suggestion"; -import { selectText } from "../../store/text"; +import { px } from "../../hooks/use-px"; +import { + currentSuggestion, + currentTextPart, + interfaceOptions, +} from "../../store"; import { Group } from "../ui/group"; import { MainSuggestion } from "./main-suggestion"; import { SuggestionList } from "./suggestion-list"; @@ -27,28 +24,17 @@ const MAX_BADGE_WIDTH = 80; const PADDING = 8; export function Suggestions() { - const options = useContext(OptionsContext); - const px = usePx(); - - const terminalText = useSelector(selectText); - const currentTextPart = useMemo(() => { - if ( - terminalText.index === -1 || - terminalText.index >= terminalText.parts.size() - ) { - return; - } - return terminalText.parts[terminalText.index]; - }, [terminalText]); + const options = useAtom(interfaceOptions); + const textPart = useAtom(currentTextPart); - const textBoundsParams = useMemo( - () => new Instance("GetTextBoundsParams"), - [], - ); + const textBoundsParams = new Instance("GetTextBoundsParams"); + cleanup(() => { + textBoundsParams.Destroy(); + }); // Suggestions - const currentSuggestion = useSelector(selectSuggestion); - const [sizes, setSizes] = useBinding({ + const suggestion = useAtom(currentSuggestion); + const sizes = source({ title: UDim2.fromOffset(0, px(SUGGESTION_TITLE_TEXT_SIZE)), description: UDim2.fromOffset(0, px(SUGGESTION_TEXT_SIZE)), errorTextHeight: 0, @@ -61,29 +47,29 @@ export function Suggestions() { ); // Resize window based on suggestions - useEffect(() => { - if (currentSuggestion === undefined) { + effect(() => { + const current = suggestion(); + if (current === undefined) { suggestionSizeMotion.spring(new UDim2()); otherSuggestionSizeMotion.spring(new UDim2()); return; } - const mainSuggestion = currentSuggestion.main; - const otherSuggestions = currentSuggestion.others; + const otherSuggestions = current.others; if (otherSuggestions.isEmpty()) { otherSuggestionSizeMotion.spring(new UDim2()); } const textBounds = getSuggestionTextBounds( - options, - mainSuggestion, + options(), + current, px(SUGGESTION_TITLE_TEXT_SIZE), px(SUGGESTION_TEXT_SIZE), px(MAX_SUGGESTION_WIDTH), px(MAX_BADGE_WIDTH), ); - setSizes(textBounds); + sizes(textBounds); let windowWidth = math.max(textBounds.title.X.Offset, textBounds.description.X.Offset) + @@ -108,7 +94,7 @@ export function Suggestions() { if (!otherSuggestions.isEmpty()) { let maxSuggestionWidth = 0; - textBoundsParams.Font = options.font.regular; + textBoundsParams.Font = options().font.regular; textBoundsParams.Size = px(SUGGESTION_TEXT_SIZE); textBoundsParams.Width = math.huge; @@ -136,7 +122,7 @@ export function Suggestions() { UDim2.fromOffset(windowWidth, windowHeight), springs.responsive, ); - }, [currentSuggestion, px]); + }); return ( diff --git a/packages/ui/src/components/suggestions/util.ts b/packages/ui/src/components/suggestions/util.ts index 2beb2186..1d93d756 100644 --- a/packages/ui/src/components/suggestions/util.ts +++ b/packages/ui/src/components/suggestions/util.ts @@ -1,9 +1,5 @@ import { TextService } from "@rbxts/services"; -import { - ArgumentSuggestion, - CommandSuggestion, - InterfaceOptions, -} from "../../types"; +import { InterfaceOptions, Suggestion } from "../../types"; import { SuggestionTextBounds } from "./types"; const TEXT_BOUNDS_PARAMS = new Instance("GetTextBoundsParams"); @@ -36,7 +32,7 @@ export function highlightMatching( export function getSuggestionTextBounds( options: InterfaceOptions, - suggestion: ArgumentSuggestion | CommandSuggestion, + suggestion: Suggestion, titleTextSize: number, textSize: number, maxWidth: number, diff --git a/packages/ui/src/components/terminal-text-field.tsx b/packages/ui/src/components/terminal-text-field.tsx index a0a3f289..a7b06140 100644 --- a/packages/ui/src/components/terminal-text-field.tsx +++ b/packages/ui/src/components/terminal-text-field.tsx @@ -2,30 +2,25 @@ import { endsWithSpace, formatPartsAsPath, } from "@rbxts/centurion/out/shared/util/string"; -import { - BindingOrValue, - getBindingValue, - useEventListener, - useMountEffect, -} from "@rbxts/pretty-react-hooks"; -import React, { - useBinding, - useCallback, - useContext, - useEffect, - useRef, - useState, -} from "@rbxts/react"; -import { useSelector } from "@rbxts/react-reflex"; +import { subscribe } from "@rbxts/charm"; import { UserInputService } from "@rbxts/services"; -import { usePx } from "../hooks/use-px"; -import { useStore } from "../hooks/use-store"; -import { ApiContext } from "../providers/api-provider"; -import { OptionsContext } from "../providers/options-provider"; -import { selectVisible } from "../store/app"; -import { selectCommand } from "../store/command"; -import { selectSuggestion } from "../store/suggestion"; -import { selectValid } from "../store/text"; +import Vide, { Derivable, effect, source } from "@rbxts/vide"; +import { getAPI } from "../hooks/use-api"; +import { useAtom } from "../hooks/use-atom"; +import { useEvent } from "../hooks/use-event"; +import { px } from "../hooks/use-px"; +import { + commandHistory, + commandHistoryIndex, + currentArgIndex, + currentCommandPath, + currentSuggestion, + interfaceOptions, + interfaceVisible, + terminalText, + terminalTextParts, + terminalTextValid, +} from "../store"; import { getArgumentNames } from "../util/argument"; import { Frame } from "./ui/frame"; import { Padding } from "./ui/padding"; @@ -33,10 +28,10 @@ import { Text } from "./ui/text"; import { TextField } from "./ui/text-field"; interface TerminalTextFieldProps { - anchorPoint?: BindingOrValue; - size?: BindingOrValue; - position?: BindingOrValue; - backgroundTransparency?: BindingOrValue; + anchorPoint?: Derivable; + size?: Derivable; + position?: Derivable; + backgroundTransparency?: Derivable; onTextChange?: (text: string) => void; onSubmit?: (text: string) => void; } @@ -51,23 +46,29 @@ export function TerminalTextField({ onTextChange, onSubmit, }: TerminalTextFieldProps) { - const px = usePx(); - const ref = useRef(); - const api = useContext(ApiContext); - const options = useContext(OptionsContext); - const store = useStore(); - - const appVisible = useSelector(selectVisible); - const currentSuggestion = useSelector(selectSuggestion); - const [text, setText] = useBinding(""); - const [suggestionText, setSuggestionText] = useBinding(""); - const [valid, setValid] = useState(false); - - const traverseHistory = useCallback((up: boolean) => { - const history = store.getState().history.commandHistory; + const api = getAPI(); + const options = useAtom(interfaceOptions); + const visible = useAtom(interfaceVisible); + const valid = useAtom(terminalTextValid); + + const ref = source(undefined); + const text = source(""); + const suggestionText = source(""); + + // Focus text field when terminal becomes visible + effect(() => { + if (visible()) { + ref()?.CaptureFocus(); + } else { + ref()?.ReleaseFocus(); + } + }); + + const traverseHistory = (up: boolean) => { + const history = commandHistory(); if (history.isEmpty()) return; - const historyIndex = store.getState().history.commandHistoryIndex; + const historyIndex = commandHistoryIndex(); if ((up && historyIndex === 0) || (!up && historyIndex === -1)) return; let newIndex: number; @@ -78,78 +79,50 @@ export function TerminalTextField({ } const newText = newIndex !== -1 ? history[newIndex] : ""; - setText(newText); - setSuggestionText(""); - store.setCommandHistoryIndex(newIndex); + text(newText); + suggestionText(""); + commandHistoryIndex(newIndex); - if (ref.current !== undefined) { - ref.current.CursorPosition = newText.size() + 1; + const textBox = ref(); + if (textBox !== undefined) { + textBox.CursorPosition = newText.size() + 1; } - }, []); - - useEffect(() => { - if (ref.current === undefined) return; - - if (appVisible) { - ref.current.CaptureFocus(); - } else { - ref.current.ReleaseFocus(); - } - }, [appVisible]); - - useMountEffect(() => { - store.subscribe(selectValid, (valid) => { - setValid(valid); - }); - - store.subscribe(selectCommand, (command) => { - if (!store.getState().text.valid) return; - setValid(command !== undefined); - }); - }); + }; - useEffect(() => { - if (currentSuggestion === undefined) { - setSuggestionText(""); + subscribe(currentSuggestion, (suggestion) => { + if (suggestion === undefined) { + suggestionText(""); return; } - const state = store.getState(); - const atNextPart = endsWithSpace(state.text.value); + const atNextPart = endsWithSpace(terminalText()); + const textParts = terminalTextParts(); const suggestionStartIndex = - state.text.parts.size() > 0 - ? (!atNextPart - ? state.text.parts[state.text.parts.size() - 1].size() - : 0) + 1 + textParts.size() > 0 + ? (!atNextPart ? textParts[textParts.size() - 1].size() : 0) + 1 : -1; - const command = state.command.path; - const argIndex = state.command.argIndex; - if ( - currentSuggestion.main.type === "command" && - suggestionStartIndex > -1 - ) { - setSuggestionText( - getBindingValue(text) + - currentSuggestion.main.title.sub(suggestionStartIndex), - ); + if (suggestion.type === "command" && suggestionStartIndex > -1) { + suggestionText(text() + suggestion.title.sub(suggestionStartIndex)); return; } + const command = currentCommandPath(); + const argIndex = currentArgIndex(); if ( - currentSuggestion.main.type !== "argument" || + suggestion.type !== "argument" || command === undefined || argIndex === undefined ) { return; } - let newText = getBindingValue(text); + let newText = text(); const argNames = getArgumentNames(api.registry, command); for (const i of $range(argIndex, argNames.size() - 1)) { if (i === argIndex && !atNextPart) { - if (!currentSuggestion.others.isEmpty()) { - newText += currentSuggestion.others[0].sub(suggestionStartIndex); + if (!suggestion.others.isEmpty()) { + newText += suggestion.others[0].sub(suggestionStartIndex); } newText += " "; @@ -158,11 +131,12 @@ export function TerminalTextField({ newText = `${newText}${argNames[i]} `; } - setSuggestionText(newText); - }, [currentSuggestion]); + suggestionText(newText); + }); - useEventListener(UserInputService.InputBegan, (input) => { - if (ref.current === undefined || !ref.current.IsFocused()) return; + useEvent(UserInputService.InputBegan, (input) => { + const textBox = ref(); + if (textBox === undefined || !textBox?.IsFocused()) return; if (input.KeyCode === Enum.KeyCode.Up) { traverseHistory(true); @@ -172,28 +146,27 @@ export function TerminalTextField({ if (input.KeyCode !== Enum.KeyCode.Tab) return; - const state = store.getState(); - const commandPath = state.command.path; - // Handle command suggestions + const commandPath = currentCommandPath(); + const suggestion = currentSuggestion(); if (commandPath === undefined) { - const suggestionTitle = currentSuggestion?.main.title; + const suggestionTitle = suggestion?.title; if (suggestionTitle === undefined) return; - const currentText = text.getValue(); + const currentText = text(); + const textParts = terminalTextParts(); + let newText = ""; if (endsWithSpace(currentText)) { newText = currentText + suggestionTitle; - } else if (!state.text.parts.isEmpty()) { - const textPartSize = - state.text.parts[state.text.parts.size() - 1].size(); + } else if (!textParts.isEmpty()) { + const textPartSize = textParts[textParts.size() - 1].size(); newText = currentText.sub(0, currentText.size() - textPartSize) + suggestionTitle; } - const suggestionTextParts = suggestionText - .getValue() + const suggestionTextParts = suggestionText() .gsub("%s+", " ")[0] .split(" "); const nextCommand = api.registry.getCommandByString( @@ -206,46 +179,45 @@ export function TerminalTextField({ newText += " "; } - setSuggestionText(""); - setText(newText); - ref.current.CursorPosition = newText.size() + 1; + suggestionText(""); + text(newText); + textBox.CursorPosition = newText.size() + 1; return; } // Handle argument suggestions if ( commandPath === undefined || - currentSuggestion === undefined || - currentSuggestion.others.isEmpty() + suggestion === undefined || + suggestion.others.isEmpty() ) { return; } - const argIndex = state.command.argIndex; + const argIndex = currentArgIndex(); const commandArgs = api.registry.getCommand(commandPath)?.options.arguments; if (argIndex === undefined || commandArgs === undefined) return; - let newText = getBindingValue(text); + let newText = text(); - const parts = state.text.parts; + const parts = terminalTextParts(); if (!endsWithSpace(newText) && !parts.isEmpty()) { newText = newText.sub(0, newText.size() - parts[parts.size() - 1].size()); } - let suggestion = currentSuggestion.others[0]; - - if (string.match(suggestion, "%s")[0] !== undefined) { - suggestion = `"${suggestion}"`; + let otherSuggestion = suggestion.others[0]; + if (string.match(otherSuggestion, "%s")[0] !== undefined) { + otherSuggestion = `"${otherSuggestion}"`; } - newText += suggestion; + newText += otherSuggestion; if (argIndex < commandArgs.size() - 1) { newText += " "; } - setSuggestionText(""); - setText(newText); - ref.current.CursorPosition = newText.size() + 1; + suggestionText(""); + text(newText); + textBox.CursorPosition = newText.size() + 1; }); return ( @@ -253,55 +225,56 @@ export function TerminalTextField({ anchorPoint={anchorPoint} size={size} position={position} - backgroundColor={options.palette.surface} + backgroundColor={() => options().palette.surface} backgroundTransparency={backgroundTransparency} cornerRadius={new UDim(0, px(4))} > ref(instance)} size={UDim2.fromScale(1, 1)} - placeholderText="Enter command..." - text={text} + text={() => { + let value = text(); + + // Remove line breaks + if (value.match("[\n\r]")[0] !== undefined) { + value = value.gsub("[\n\r]", "")[0]; + } + + // Remove all tabs from text input - we use these for autocompletion + if (value.match("\t")[0] !== undefined) { + value = value.gsub("\t", "")[0]; + } + + onTextChange?.(value); + return value; + }} textSize={px(TEXT_SIZE)} - textColor={valid ? options.palette.success : options.palette.error} - placeholderColor={options.palette.subtext} + textColor={() => { + return valid() ? options().palette.success : options().palette.error; + }} textXAlignment="Left" + placeholderText="Enter command..." + placeholderColor={() => options().palette.subtext} + font={() => options().font.medium} clearTextOnFocus={false} - font={options.font.medium} - ref={ref} - event={{ - FocusLost: (rbx, enterPressed) => { - if (!enterPressed) return; - store.addCommandHistory(rbx.Text, api.options.historyLength); - store.setCommandHistoryIndex(-1); - onSubmit?.(rbx.Text); - ref.current?.CaptureFocus(); - setText(""); - }, + focusLost={(enterPressed) => { + if (!enterPressed) return; + + const textBox = ref(); + if (textBox === undefined) return; + + // TODO Limit + commandHistory((prev) => [...prev, textBox.Text]); + commandHistoryIndex(-1); + onSubmit?.(textBox.Text); + textBox.CaptureFocus(); + text(""); }} - change={{ - Text: (rbx) => { - let newText = rbx.Text; - - // Remove line breaks - if (newText.match("[\n\r]")[0] !== undefined) { - newText = newText.gsub("[\n\r]", "")[0]; - } - - // Remove all tabs from text input - we use these for autocompletion - if (newText.match("\t")[0] !== undefined) { - newText = newText.gsub("\t", "")[0]; - } - - // Reset command history index - if (store.getState().history.commandHistoryIndex !== -1) { - store.setCommandHistoryIndex(-1); - } - - setText(newText); - onTextChange?.(newText); - }, + textChanged={(currentText) => { + if (commandHistoryIndex() !== -1) commandHistoryIndex(-1); + text(currentText); }} zIndex={2} /> @@ -309,10 +282,10 @@ export function TerminalTextField({ options().palette.subtext} textSize={px(TEXT_SIZE)} textXAlignment="Left" - font={options.font.medium} + font={() => options().font.medium} /> ); diff --git a/packages/ui/src/components/terminal-window.tsx b/packages/ui/src/components/terminal-window.tsx index 7390d434..d471cbea 100644 --- a/packages/ui/src/components/terminal-window.tsx +++ b/packages/ui/src/components/terminal-window.tsx @@ -1,5 +1,4 @@ import { - CommandOptions, HistoryEntry, ImmutableRegistryPath, RegistryPath, @@ -8,18 +7,27 @@ import { ArrayUtil } from "@rbxts/centurion/out/shared/util/data"; import { endsWithSpace, formatPartsAsPath, - splitString, } from "@rbxts/centurion/out/shared/util/string"; -import { useEventListener, useLatestCallback } from "@rbxts/pretty-react-hooks"; -import React, { useContext, useEffect, useMemo, useState } from "@rbxts/react"; import { TextService } from "@rbxts/services"; +import Vide, { cleanup, effect, source } from "@rbxts/vide"; import { HISTORY_TEXT_SIZE } from "../constants/text"; +import { getAPI } from "../hooks/use-api"; +import { useAtom } from "../hooks/use-atom"; +import { useEvent } from "../hooks/use-event"; import { useMotion } from "../hooks/use-motion"; -import { usePx } from "../hooks/use-px"; -import { useStore } from "../hooks/use-store"; -import { ApiContext } from "../providers/api-provider"; -import { OptionsContext } from "../providers/options-provider"; +import { px } from "../hooks/use-px"; +import { + currentArgIndex, + currentCommandPath, + currentSuggestion, + interfaceOptions, + mouseOverInterface, + terminalText, + terminalTextParts, + terminalTextValid, +} from "../store"; import { HistoryLineData, Suggestion } from "../types"; +import { getMissingArgs } from "../util/argument"; import { getArgumentSuggestion, getCommandSuggestion, @@ -33,75 +41,36 @@ const MAX_HEIGHT = HISTORY_TEXT_SIZE * 10; const TEXT_FIELD_HEIGHT = 40; export function TerminalWindow() { - const px = usePx(); - const store = useStore(); - const api = useContext(ApiContext); - const options = useContext(OptionsContext); + const api = getAPI(); + const options = useAtom(interfaceOptions); + const missingArgs = source([]); - const [history, setHistory] = useState( - api.dispatcher.getHistory(), - ); - const [historyData, setHistoryData] = useState({ + const history = source(api.dispatcher.getHistory()); + const historyData = source({ lines: [], height: 0, }); const [historyHeight, historyHeightMotion] = useMotion(0); - const textBoundsParams = useMemo(() => { - const params = new Instance("GetTextBoundsParams"); - params.Width = math.huge; - params.Font = options.font.regular; - return params; - }, []); - - const windowHeightBinding = useMemo(() => { - return historyHeight.map((y) => { - return new UDim2(1, 0, 0, math.ceil(px(TEXT_FIELD_HEIGHT + 16) + y)); - }); - }, [px]); - - const checkMissingArgs = useLatestCallback( - (path: ImmutableRegistryPath, command: CommandOptions) => { - if (command.arguments === undefined || command.arguments.isEmpty()) { - return undefined; - } - - const storeState = store.getState(); - const lastPartIndex = storeState.text.parts.size() - path.size() - 1; - const missingArgs: string[] = []; - - let index = 0; - for (const arg of command.arguments) { - if (arg.optional) break; - if (index > lastPartIndex) { - missingArgs.push(`${arg.name}`); - } - index++; - } - - if (missingArgs.isEmpty()) return undefined; - - let text = "Missing required argument"; - if (missingArgs.size() !== 1) { - text += "s"; - } - return `${text}: ${missingArgs.join(", ")}`; - }, - ); + useEvent(api.dispatcher.historyUpdated, (entries) => history([...entries])); - useEventListener(api.dispatcher.historyUpdated, (entries) => { - setHistory([...entries]); + const textBoundsParams = new Instance("GetTextBoundsParams"); + textBoundsParams.Width = math.huge; + textBoundsParams.Font = options().font.regular; + cleanup(() => { + textBoundsParams.Destroy(); }); // Handle history updates - useEffect(() => { - const historySize = history.size(); + effect(() => { + const entries = history(); + const historySize = entries.size(); let totalHeight = historySize > 0 ? px(8) + (historySize - 1) * px(8) : 0; textBoundsParams.Size = HISTORY_TEXT_SIZE; const historyLines: HistoryLineData[] = []; - for (const entry of history) { + for (const entry of entries) { textBoundsParams.Text = entry.text; const textSize = TextService.GetTextBoundsAsync(textBoundsParams); const lineHeight = px(textSize.Y + 4); @@ -116,22 +85,27 @@ export function TerminalWindow() { tension: 300, friction: 15, }); - setHistoryData({ + historyData({ lines: historyLines, height: totalHeight, }); - }, [history, px]); + }); return ( options.setMouseOnGUI(true), - MouseLeave: () => options.setMouseOnGUI(false), + size={() => { + return new UDim2( + 1, + 0, + 0, + math.ceil(px(TEXT_FIELD_HEIGHT + 16) + historyHeight()), + ); }} + backgroundColor={() => options().palette.background} + backgroundTransparency={() => options().backgroundTransparency ?? 0} + cornerRadius={new UDim(0, px(8))} + mouseEnter={() => mouseOverInterface(true)} + mouseLeave={() => mouseOverInterface(false)} > @@ -145,25 +119,25 @@ export function TerminalWindow() { anchorPoint={new Vector2(0, 1)} size={new UDim2(1, 0, 0, px(TEXT_FIELD_HEIGHT))} position={UDim2.fromScale(0, 1)} - backgroundTransparency={options.backgroundTransparency} + backgroundTransparency={() => options().backgroundTransparency ?? 0} onTextChange={(text) => { - const parts = splitString(text, " "); - store.setText(text, parts); + terminalText(text); + terminalTextValid(false); + const parts = terminalTextParts(); if (parts.isEmpty()) { - store.setSuggestion(undefined); - store.setCommand(undefined); - store.setArgIndex(undefined); + currentCommandPath(undefined); + currentSuggestion(undefined); + currentArgIndex(undefined); return; } - store.flush(); // If the text ends in a space, we want to count that as having traversed // to the next "part" of the text. This means we should include the previous // text part as part of the parent path. const atNextPart = endsWithSpace(text); - let commandPath = store.getState().command.path; + let commandPath = currentCommandPath(); let atCommand = false; if ( commandPath !== undefined && @@ -197,15 +171,11 @@ export function TerminalWindow() { commandPath = ImmutableRegistryPath.fromPath(currentPath); } - if (!atCommand && store.getState().command.path !== undefined) { - store.setCommand(undefined); - store.flush(); - } else if ( - atCommand && - commandPath !== store.getState().command.path - ) { - store.setCommand(commandPath); - store.flush(); + const currentPath = currentCommandPath(); + if (!atCommand && currentPath !== undefined) { + currentCommandPath(undefined); + } else if (atCommand && commandPath !== currentPath) { + currentCommandPath(commandPath); } const command = @@ -213,12 +183,17 @@ export function TerminalWindow() { ? api.registry.getCommand(commandPath)?.options : undefined; if (commandPath !== undefined && command !== undefined) { - store.setTextValid( - checkMissingArgs(commandPath, command as CommandOptions) === - undefined, + const missing = getMissingArgs(api.registry, commandPath, parts); + missingArgs(missing); + + const noArgs = command.arguments?.isEmpty() ?? true; + terminalTextValid( + noArgs || + missing.isEmpty() || + (atNextPart && missing.size() === 1), ); } else { - store.setTextValid(false); + terminalTextValid(false); } // Update suggestions @@ -253,20 +228,23 @@ export function TerminalWindow() { parts.size() - commandPath.size() - (atNextPart ? 0 : 1); if (argIndex >= argCount) return; - store.setArgIndex(argIndex); + currentArgIndex(argIndex); suggestion = getArgumentSuggestion( api.registry, commandPath, argIndex, currentTextPart, ); + + if (suggestion?.error !== undefined) { + terminalTextValid(false); + } } - store.setSuggestion(suggestion); + currentSuggestion(suggestion); }} - onSubmit={(text) => { - const storeState = store.getState(); - const commandPath = storeState.command.path; + onSubmit={() => { + const commandPath = currentCommandPath(); const command = commandPath !== undefined ? api.registry.getCommand(commandPath) @@ -281,23 +259,17 @@ export function TerminalWindow() { return; } - const argCheckMessage = checkMissingArgs( - commandPath, - command.options as CommandOptions, - ); - if (argCheckMessage !== undefined) { + const missing = missingArgs(); + if (!missing.isEmpty()) { api.dispatcher.addHistoryEntry({ success: false, - text: argCheckMessage, + text: `Missing arguments: ${missing.join(", ")}`, sentAt: os.time(), }); return; } - const args = ArrayUtil.slice( - storeState.text.parts, - commandPath.size(), - ); + const args = ArrayUtil.slice(terminalTextParts(), commandPath.size()); api.dispatcher.run(commandPath, args); }} /> diff --git a/packages/ui/src/components/terminal.tsx b/packages/ui/src/components/terminal.tsx index 85491f14..0bb486e0 100644 --- a/packages/ui/src/components/terminal.tsx +++ b/packages/ui/src/components/terminal.tsx @@ -1,21 +1,21 @@ -import React, { useContext } from "@rbxts/react"; import { GuiService } from "@rbxts/services"; -import { usePx } from "../hooks/use-px"; -import { OptionsContext } from "../providers/options-provider"; +import Vide from "@rbxts/vide"; +import { useAtom } from "../hooks/use-atom"; +import { px } from "../hooks/use-px"; +import { interfaceOptions } from "../store"; import { Suggestions } from "./suggestions"; import { TerminalWindow } from "./terminal-window"; import { Group } from "./ui/group"; export default function Terminal() { - const px = usePx(); - const options = useContext(OptionsContext); + const options = useAtom(interfaceOptions); return ( options().anchorPoint ?? new Vector2(0, 0)} + size={() => options().size ?? new UDim2(0, px(1024), 1, 0)} + position={() => + options().position ?? UDim2.fromOffset(px(16), px(8) + GuiService.GetGuiInset()[0].Y) } > diff --git a/packages/ui/src/components/ui/frame.tsx b/packages/ui/src/components/ui/frame.tsx index 9e849941..7c26eab4 100644 --- a/packages/ui/src/components/ui/frame.tsx +++ b/packages/ui/src/components/ui/frame.tsx @@ -1,29 +1,27 @@ -import { BindingOrValue } from "@rbxts/pretty-react-hooks"; -import React, { InferEnumNames, Ref, forwardRef } from "@rbxts/react"; +import Vide, { ActionAttributes, Derivable, InferEnumNames } from "@rbxts/vide"; export interface FrameProps - extends React.PropsWithChildren { - ref?: React.Ref; - event?: React.InstanceEvent; - change?: React.InstanceChangeEvent; - size?: BindingOrValue; - position?: BindingOrValue; - anchorPoint?: BindingOrValue; - rotation?: BindingOrValue; - backgroundColor?: BindingOrValue; - backgroundTransparency?: BindingOrValue; - clipsDescendants?: BindingOrValue; - visible?: BindingOrValue; - zIndex?: BindingOrValue; - layoutOrder?: BindingOrValue; - cornerRadius?: BindingOrValue; + extends ActionAttributes { + size?: Derivable; + position?: Derivable; + anchorPoint?: Derivable; + rotation?: Derivable; + backgroundColor?: Derivable; + backgroundTransparency?: Derivable; + clipsDescendants?: Derivable; + visible?: Derivable; + zIndex?: Derivable; + layoutOrder?: Derivable; + cornerRadius?: Derivable; automaticSize?: InferEnumNames; + children?: Vide.Node; + mouseEnter?: () => void; + mouseLeave?: () => void; } -export const Frame = forwardRef((props: FrameProps, ref: Ref) => { +export function Frame(props: FrameProps) { return ( ) => { ZIndex={props.zIndex} LayoutOrder={props.layoutOrder} BorderSizePixel={0} - Event={props.event || {}} - Change={props.change || {}} + MouseEnter={props.mouseEnter} + MouseLeave={props.mouseLeave} + action={props.action} > {props.cornerRadius && } {props.children} ); -}); +} diff --git a/packages/ui/src/components/ui/group.tsx b/packages/ui/src/components/ui/group.tsx index 131e709a..5a9ffd1f 100644 --- a/packages/ui/src/components/ui/group.tsx +++ b/packages/ui/src/components/ui/group.tsx @@ -1,24 +1,21 @@ -import { BindingOrValue } from "@rbxts/pretty-react-hooks"; -import React, { forwardRef } from "@rbxts/react"; +import Vide, { Derivable } from "@rbxts/vide"; -interface GroupProps extends React.PropsWithChildren { - ref?: React.Ref; - event?: React.InstanceEvent; - change?: React.InstanceChangeEvent; - size?: BindingOrValue; - position?: BindingOrValue; - anchorPoint?: BindingOrValue; - rotation?: BindingOrValue; - clipsDescendants?: BindingOrValue; - layoutOrder?: BindingOrValue; - visible?: BindingOrValue; - zIndex?: BindingOrValue; +interface GroupProps extends Vide.PropsWithChildren { + size?: Derivable; + position?: Derivable; + anchorPoint?: Derivable; + rotation?: Derivable; + clipsDescendants?: Derivable; + layoutOrder?: Derivable; + visible?: Derivable; + zIndex?: Derivable; + mouseEnter?: () => void; + mouseLeave?: () => void; } -export const Group = forwardRef((props: GroupProps, ref: React.Ref) => { +export function Group(props: GroupProps) { return ( ) => { Visible={props.visible} ZIndex={props.zIndex} BackgroundTransparency={1} - Event={props.event || {}} - Change={props.change || {}} + MouseEnter={props.mouseEnter} + MouseLeave={props.mouseLeave} > {props.children} ); -}); +} diff --git a/packages/ui/src/components/ui/layer.tsx b/packages/ui/src/components/ui/layer.tsx index 33ee1515..eebc72dd 100644 --- a/packages/ui/src/components/ui/layer.tsx +++ b/packages/ui/src/components/ui/layer.tsx @@ -1,10 +1,10 @@ -import React from "@rbxts/react"; +import Vide, { Derivable } from "@rbxts/vide"; import { IS_EDIT } from "../../constants/util"; import { Group } from "./group"; -interface LayerProps extends React.PropsWithChildren { - visible?: boolean; - displayOrder?: number; +interface LayerProps extends Vide.PropsWithChildren { + visible?: Derivable; + displayOrder?: Derivable; } export function Layer({ visible, displayOrder, children }: LayerProps) { diff --git a/packages/ui/src/components/ui/outline.tsx b/packages/ui/src/components/ui/outline.tsx index ea5c1391..9dc81244 100644 --- a/packages/ui/src/components/ui/outline.tsx +++ b/packages/ui/src/components/ui/outline.tsx @@ -1,27 +1,33 @@ -import { - BindingOrValue, - blend, - composeBindings, -} from "@rbxts/pretty-react-hooks"; -import React, { useMemo } from "@rbxts/react"; -import { usePx } from "../../hooks/use-px"; +import Vide, { Derivable, derive, read } from "@rbxts/vide"; +import { px } from "../../hooks/use-px"; import { Group } from "./group"; -interface OutlineProps extends React.PropsWithChildren { - readonly outlineTransparency?: BindingOrValue; - readonly innerColor?: BindingOrValue; - readonly outerColor?: BindingOrValue; - readonly innerTransparency?: BindingOrValue; - readonly outerTransparency?: BindingOrValue; - readonly innerThickness?: BindingOrValue; - readonly outerThickness?: BindingOrValue; - readonly cornerRadius?: BindingOrValue; +interface OutlineProps { + outlineTransparency?: Derivable; + innerColor?: Derivable; + outerColor?: Derivable; + innerTransparency?: Derivable; + outerTransparency?: Derivable; + innerThickness?: Derivable; + outerThickness?: Derivable; + cornerRadius?: Derivable; + children?: Vide.Node; } function ceilEven(n: number) { return math.ceil(n / 2) * 2; } +export function blend(...transparencies: number[]) { + let result = 1; + + for (const transparency of transparencies) { + result *= 1 - transparency; + } + + return 1 - result; +} + const DEFAULT_INNER_COLOR = new Color3(1, 1, 1); const DEFAULT_OUTER_COLOR = new Color3(0, 0, 0); @@ -36,72 +42,52 @@ export function Outline({ cornerRadius, children, }: OutlineProps) { - const px = usePx(); - const properties = { innerThickness: innerThickness ?? px(3), outerThickness: outerThickness ?? px(1.5), cornerRadius: cornerRadius ?? new UDim(0, px(8)), }; - const innerStyle = useMemo(() => { - const size = composeBindings(properties.innerThickness, (thickness) => { - return new UDim2( - 1, - ceilEven(-2 * thickness), - 1, - ceilEven(-2 * thickness), - ); - }); - - const position = composeBindings(properties.innerThickness, (thickness) => { - return new UDim2(0, thickness, 0, thickness); - }); - - const radius = composeBindings( - properties.cornerRadius, - properties.innerThickness, - (radius, thickness) => { - return radius.sub(new UDim(0, thickness)); - }, + const innerStyle = derive(() => { + const thickness = read(properties.innerThickness); + const size = new UDim2( + 1, + ceilEven(-2 * thickness), + 1, + ceilEven(-2 * thickness), ); - const transparency = composeBindings( - outlineTransparency, - innerTransparency, - (a, b) => { - return math.clamp(blend(a, b), 0, 1); - }, + const position = new UDim2(0, thickness, 0, thickness); + const radius = read(properties.cornerRadius).sub(new UDim(0, thickness)); + const transparency = math.clamp( + blend(read(outlineTransparency), read(innerTransparency)), + 0, + 1, ); return { size, position, radius, transparency }; - }, [ - properties.innerThickness, - innerTransparency, - properties.cornerRadius, - outlineTransparency, - px, - ]); + }); - const outerStyle = useMemo(() => { - const transparency = composeBindings( - outlineTransparency, - outerTransparency, - (a, b) => { - return math.clamp(blend(a, b), 0, 1); - }, + const outerStyle = derive(() => { + const transparency = math.clamp( + blend(read(outlineTransparency), read(outerTransparency)), + 0, + 1, ); return { transparency }; - }, [outlineTransparency, outerTransparency]); + }); return ( <> - - + innerStyle().size} + position={() => innerStyle().position} + > + innerStyle().radius} /> innerStyle().transparency} Thickness={innerThickness} > {children} @@ -112,7 +98,7 @@ export function Outline({ outerStyle().transparency} Thickness={outerThickness} > {children} diff --git a/packages/ui/src/components/ui/padding.tsx b/packages/ui/src/components/ui/padding.tsx index 4c1f33f0..7e9e3e27 100644 --- a/packages/ui/src/components/ui/padding.tsx +++ b/packages/ui/src/components/ui/padding.tsx @@ -1,12 +1,11 @@ -import { BindingOrValue } from "@rbxts/pretty-react-hooks"; -import React from "@rbxts/react"; +import Vide, { Derivable } from "@rbxts/vide"; interface Props { - all?: BindingOrValue; - left?: BindingOrValue; - right?: BindingOrValue; - top?: BindingOrValue; - bottom?: BindingOrValue; + all?: Derivable; + left?: Derivable; + right?: Derivable; + top?: Derivable; + bottom?: Derivable; } export function Padding({ all, left, right, top, bottom }: Props) { diff --git a/packages/ui/src/components/ui/scrolling-frame.tsx b/packages/ui/src/components/ui/scrolling-frame.tsx index 7ffb15be..385c9144 100644 --- a/packages/ui/src/components/ui/scrolling-frame.tsx +++ b/packages/ui/src/components/ui/scrolling-frame.tsx @@ -1,50 +1,44 @@ -import { BindingOrValue } from "@rbxts/pretty-react-hooks"; -import React, { Ref, forwardRef } from "@rbxts/react"; +import Vide, { Derivable, InferEnumNames } from "@rbxts/vide"; import { FrameProps } from "./frame"; export interface ScrollingFrameProps extends FrameProps { - automaticSize?: React.InferEnumNames; - automaticCanvasSize?: React.InferEnumNames; - scrollingDirection?: React.InferEnumNames; - scrollBarThickness?: BindingOrValue; - scrollBarColor?: BindingOrValue; - scrollBarTransparency?: BindingOrValue; - scrollingEnabled?: BindingOrValue; - canvasSize?: BindingOrValue; - canvasPosition?: BindingOrValue; + automaticSize?: InferEnumNames; + automaticCanvasSize?: InferEnumNames; + scrollingDirection?: InferEnumNames; + scrollBarThickness?: Derivable; + scrollBarColor?: Derivable; + scrollBarTransparency?: Derivable; + scrollingEnabled?: Derivable; + canvasSize?: Derivable; + canvasPosition?: Derivable; } -export const ScrollingFrame = forwardRef( - (props: ScrollingFrameProps, ref: Ref) => { - return ( - - {props.cornerRadius && } - {props.children} - - ); - }, -); +export function ScrollingFrame(props: ScrollingFrameProps) { + return ( + + {props.cornerRadius && } + {props.children} + + ); +} diff --git a/packages/ui/src/components/ui/shortcut-group.tsx b/packages/ui/src/components/ui/shortcut-group.tsx deleted file mode 100644 index 235ee4ab..00000000 --- a/packages/ui/src/components/ui/shortcut-group.tsx +++ /dev/null @@ -1,108 +0,0 @@ -import { CommandShortcut } from "@rbxts/centurion"; -import { - getModifierKeyName, - getShortcutKeycodes, - isModifierKey, -} from "@rbxts/centurion/out/client/util"; -import React, { useContext, useState } from "@rbxts/react"; -import { usePx } from "../../hooks/use-px"; -import { - InterfaceOptionsWithState, - OptionsContext, -} from "../../providers/options-provider"; -import { Badge } from "../suggestions/badge"; -import { Frame } from "./frame"; -import { Padding } from "./padding"; - -interface KeyProps { - options: InterfaceOptionsWithState; - keyCode: Enum.KeyCode; -} - -function Key(props: KeyProps) { - const options = props.options; - const isModifier = isModifierKey(props.keyCode); - - const [size, setSize] = useState(new UDim2(0, 24, 0, 24)); - - return ( - - { - setSize(new UDim2(0, math.max(24, textBounds.X + 12), 0, 24)); - }} - backgroundTransparency={isModifier ? 0.5 : 0} - > - {isModifier ? ( - - ) : ( - - )} - - - ); -} - -interface ShortcutGroupProps { - shortcuts?: CommandShortcut[]; -} -export function ShortcutGroup(props: ShortcutGroupProps) { - const options = useContext(OptionsContext); - const px = usePx(); - - return ( - - - - - {props.shortcuts?.map((shortcut, index) => { - const shortcuts = getShortcutKeycodes(shortcut); - - return ( - - - - {shortcuts.map((key) => { - return ( - - ); - })} - - ); - })} - - ); -} diff --git a/packages/ui/src/components/ui/text-field.tsx b/packages/ui/src/components/ui/text-field.tsx index 21d5485a..8e00bc98 100644 --- a/packages/ui/src/components/ui/text-field.tsx +++ b/packages/ui/src/components/ui/text-field.tsx @@ -1,60 +1,60 @@ -import { Ref, forwardRef, useContext } from "@rbxts/react"; - -import { BindingOrValue } from "@rbxts/pretty-react-hooks"; -import React from "@rbxts/react"; -import { OptionsContext } from "../../providers/options-provider"; +import Vide, { Derivable, read } from "@rbxts/vide"; +import { useAtom } from "../../hooks/use-atom"; +import { interfaceOptions } from "../../store"; import { TextProps } from "./text"; interface TextFieldProps extends TextProps { - text?: BindingOrValue; - placeholderText?: BindingOrValue; - placeholderColor?: BindingOrValue; - clearTextOnFocus?: BindingOrValue; - multiLine?: BindingOrValue; - textEditable?: BindingOrValue; + text?: Derivable; + placeholderText?: Derivable; + placeholderColor?: Derivable; + clearTextOnFocus?: Derivable; + multiLine?: Derivable; + textEditable?: Derivable; + focusLost?: (enterPressed: boolean) => void; + textChanged?: (text: string) => void; } -export const TextField = forwardRef( - (props: TextFieldProps, ref: Ref) => { - const options = useContext(OptionsContext); +export function TextField(props: TextFieldProps) { + const options = useAtom(interfaceOptions); - return ( - - {props.cornerRadius && } - {props.children} - - ); - }, -); + return ( + { + return read(props.font) ?? options().font.regular; + }} + Text={props.text} + TextColor3={props.textColor} + TextSize={props.textSize} + TextTransparency={props.textTransparency} + TextTruncate={props.textTruncate} + TextWrapped={props.textWrapped} + TextXAlignment={props.textXAlignment} + TextYAlignment={props.textYAlignment} + TextScaled={props.textScaled} + RichText={props.richText} + AutomaticSize={props.textAutoResize} + Size={props.size} + Position={props.position} + AnchorPoint={props.anchorPoint} + BackgroundColor3={props.backgroundColor} + BackgroundTransparency={props.backgroundTransparency ?? 1} + ClipsDescendants={props.clipsDescendants} + Visible={props.visible} + ZIndex={props.zIndex} + LayoutOrder={props.layoutOrder} + BorderSizePixel={0} + FocusLost={props.focusLost} + TextChanged={props.textChanged} + action={props.action} + > + {props.cornerRadius && } + {props.children} + + ); +} diff --git a/packages/ui/src/components/ui/text.tsx b/packages/ui/src/components/ui/text.tsx index d76fb474..1d74568d 100644 --- a/packages/ui/src/components/ui/text.tsx +++ b/packages/ui/src/components/ui/text.tsx @@ -1,36 +1,34 @@ -import { BindingOrValue } from "@rbxts/pretty-react-hooks"; -import React, { Ref, forwardRef, useContext } from "@rbxts/react"; -import { usePx } from "../../hooks/use-px"; -import { OptionsContext } from "../../providers/options-provider"; +import Vide, { Derivable, InferEnumNames, read } from "@rbxts/vide"; +import { useAtom } from "../../hooks/use-atom"; +import { px } from "../../hooks/use-px"; +import { interfaceOptions } from "../../store"; import { FrameProps } from "./frame"; export interface TextProps extends FrameProps { - font?: Font; - text?: BindingOrValue; - textColor?: BindingOrValue; - textSize?: BindingOrValue; - textTransparency?: BindingOrValue; - textWrapped?: BindingOrValue; - textXAlignment?: React.InferEnumNames; - textYAlignment?: React.InferEnumNames; - textTruncate?: React.InferEnumNames; - textScaled?: BindingOrValue; - textHeight?: BindingOrValue; + font?: Derivable; + text?: Derivable; + textColor?: Derivable; + textSize?: Derivable; + textTransparency?: Derivable; + textWrapped?: Derivable; + textXAlignment?: InferEnumNames; + textYAlignment?: InferEnumNames; + textTruncate?: InferEnumNames; + textScaled?: Derivable; + textHeight?: Derivable; textAutoResize?: "X" | "Y" | "XY"; - richText?: BindingOrValue; - maxVisibleGraphemes?: BindingOrValue; + richText?: Derivable; + maxVisibleGraphemes?: Derivable; + textBoundsChanged?: (textBounds: Vector2) => void; } -export const Text = forwardRef((props: TextProps, ref: Ref) => { - const px = usePx(); - const options = useContext(OptionsContext); +export function Text(props: TextProps) { + const options = useAtom(interfaceOptions); return ( read(props.font) ?? options().font.regular} Text={props.text} TextColor3={props.textColor} TextSize={props.textSize ?? px(16)} @@ -53,11 +51,11 @@ export const Text = forwardRef((props: TextProps, ref: Ref) => { Visible={props.visible} ZIndex={props.zIndex} LayoutOrder={props.layoutOrder} - Change={props.change || {}} - Event={props.event || {}} + action={props.action} + TextBoundsChanged={props.textBoundsChanged} > {props.cornerRadius && } {props.children} ); -}); +} diff --git a/packages/ui/src/hooks/use-api.ts b/packages/ui/src/hooks/use-api.ts new file mode 100644 index 00000000..b7cfe5df --- /dev/null +++ b/packages/ui/src/hooks/use-api.ts @@ -0,0 +1,15 @@ +import { ClientAPI } from "@rbxts/centurion"; +import { source } from "@rbxts/vide"; + +const api = source(); + +export function getAPI() { + const value = api(); + assert(value !== undefined, "Client API has not been set"); + return value; +} + +export function useAPI(clientAPI: ClientAPI) { + api(clientAPI); + return api; +} diff --git a/packages/ui/src/hooks/use-atom.ts b/packages/ui/src/hooks/use-atom.ts new file mode 100644 index 00000000..daed3130 --- /dev/null +++ b/packages/ui/src/hooks/use-atom.ts @@ -0,0 +1,13 @@ +import { Molecule, subscribe } from "@rbxts/charm"; +import { cleanup, source } from "@rbxts/vide"; + +export function useAtom(atom: Molecule) { + const state = source(atom()); + const disconnect = subscribe(atom, state); + + cleanup(() => { + disconnect(); + }); + + return state; +} diff --git a/packages/ui/src/hooks/use-event.ts b/packages/ui/src/hooks/use-event.ts new file mode 100644 index 00000000..758256cf --- /dev/null +++ b/packages/ui/src/hooks/use-event.ts @@ -0,0 +1,65 @@ +import { cleanup } from "@rbxts/vide"; + +type EventLike = + | { Connect(callback: T): ConnectionLike } + | { connect(callback: T): ConnectionLike } + | { subscribe(callback: T): ConnectionLike }; + +type ConnectionLike = + | { Disconnect(): void } + | { disconnect(): void } + | (() => void); + +const connect = (event: EventLike, callback: Callback): ConnectionLike => { + if (typeIs(event, "RBXScriptSignal")) { + // With deferred events, a "hard disconnect" is necessary to avoid causing + // state updates after a component unmounts. Use 'Connected' to check if + // the connection is still valid before invoking the callback. + // https://devforum.roblox.com/t/deferred-engine-events/2276564/99 + const connection = event.Connect((...args: unknown[]) => { + if (connection.Connected) { + return callback(...args); + } + }); + return connection; + } + + if ("Connect" in event) { + return event.Connect(callback); + } + + if ("connect" in event) { + return event.connect(callback); + } + + if ("subscribe" in event) { + return event.subscribe(callback); + } + + throw "Event-like object does not have a supported connect method."; +}; + +const disconnect = (connection: ConnectionLike) => { + if (typeIs(connection, "function")) { + connection(); + } else if ( + typeIs(connection, "RBXScriptConnection") || + "Disconnect" in connection + ) { + connection.Disconnect(); + } else if ("disconnect" in connection) { + connection.disconnect(); + } else { + throw "Connection-like object does not have a supported disconnect method."; + } +}; + +export function useEvent( + event: T, + listener: T extends EventLike ? U : never, +) { + const connection = connect(event, listener); + cleanup(() => { + disconnect(connection); + }); +} diff --git a/packages/ui/src/hooks/use-motion.ts b/packages/ui/src/hooks/use-motion.ts index 0a688c4d..33f07941 100644 --- a/packages/ui/src/hooks/use-motion.ts +++ b/packages/ui/src/hooks/use-motion.ts @@ -1,30 +1,18 @@ -import { useEventListener } from "@rbxts/pretty-react-hooks"; -import { Binding, useBinding, useMemo } from "@rbxts/react"; -import { Motion, MotionGoal, createMotion } from "@rbxts/ripple"; -import { RunService } from "@rbxts/services"; +import { MotionGoal, createMotion } from "@rbxts/ripple"; +import { cleanup, source } from "@rbxts/vide"; -export function useMotion( - initialValue: number, -): LuaTuple<[Binding, Motion]>; +type NonStrict = T extends number ? number : T; -export function useMotion( - initialValue: T, -): LuaTuple<[Binding, Motion]>; +export function useMotion(initialValue: NonStrict) { + const motion = createMotion(initialValue); + const state = source(initialValue); -export function useMotion(initialValue: T) { - const motion = useMemo(() => { - return createMotion(initialValue); - }, []); + motion.onStep(state); + motion.start(); - const [binding, setValue] = useBinding(initialValue); - - useEventListener(RunService.Heartbeat, (delta) => { - const value = motion.step(delta); - - if (value !== binding.getValue()) { - setValue(value); - } + cleanup(() => { + motion.stop(); }); - return $tuple(binding, motion); + return $tuple(state, motion); } diff --git a/packages/ui/src/hooks/use-px.ts b/packages/ui/src/hooks/use-px.ts index 8225a4f4..55faff0a 100644 --- a/packages/ui/src/hooks/use-px.ts +++ b/packages/ui/src/hooks/use-px.ts @@ -1,9 +1,12 @@ -import { - useCamera, - useDebounceState, - useEventListener, -} from "@rbxts/pretty-react-hooks"; -import { useMemo } from "@rbxts/react"; +import { Workspace } from "@rbxts/services"; +import { source } from "@rbxts/vide"; +import { useEvent } from "./use-event"; + +const BASE_RESOLUTION = new Vector2(1280, 832); +const MIN_SCALE = 0.75; +const DOMINANT_AXIS = 0.5; + +const scale = source(1); interface ScaleFunction { /** @@ -28,48 +31,44 @@ interface ScaleFunction { ceil: (pixels: number) => number; } -const BASE_RESOLUTION = new Vector2(1280, 832); -const MIN_SCALE = 0.75; -const DOMINANT_AXIS = 0.5; - /** - * @see https://discord.com/channels/476080952636997633/476080952636997635/1146857136358432900 + * Rounds and scales a number to the current `px` unit. Includes additional + * methods for edge cases. + * + * @param value The number to scale. + * @returns A number in scaled `px` units. */ -function calculateScale(viewport: Vector2) { - const width = math.log(viewport.X / BASE_RESOLUTION.X, 2); - const height = math.log(viewport.Y / BASE_RESOLUTION.Y, 2); - const centered = width + (height - width) * DOMINANT_AXIS; +export const px = setmetatable( + { + even: (value: number) => math.round(value * scale() * 0.5) * 2, + scale: (value: number) => value * scale(), + floor: (value: number) => math.floor(value * scale()), + ceil: (value: number) => math.ceil(value * scale()), + }, + { + __call: (_, value) => math.round((value as number) * scale()), + }, +) as ScaleFunction; - return math.max(2 ** centered, MIN_SCALE); -} +/** + * Scales the current `px` unit based on the current viewport size. Should be + * called once in `Vide.mount`. + */ +export function usePx() { + const camera = Workspace.CurrentCamera; + assert(camera, "CurrentCamera is not set"); -export function usePx(): ScaleFunction { - const camera = useCamera(); + const updateScale = () => { + const width = math.log(camera.ViewportSize.X / BASE_RESOLUTION.X, 2); + const height = math.log(camera.ViewportSize.Y / BASE_RESOLUTION.Y, 2); + const centered = width + (height - width) * DOMINANT_AXIS; - const [scale, setScale] = useDebounceState( - calculateScale(camera.ViewportSize), - { - wait: 0.2, - leading: true, - }, - ); + scale(math.max(2 ** centered, MIN_SCALE)); + }; - useEventListener(camera.GetPropertyChangedSignal("ViewportSize"), () => { - setScale(calculateScale(camera.ViewportSize)); + useEvent(camera.GetPropertyChangedSignal("ViewportSize"), () => { + updateScale(); }); - return useMemo(() => { - const api = { - even: (value: number) => math.round(value * scale * 0.5) * 2, - scale: (value: number) => value * scale, - floor: (value: number) => math.floor(value * scale), - ceil: (value: number) => math.ceil(value * scale), - }; - - setmetatable(api, { - __call: (_, value) => math.round((value as number) * scale), - }); - - return api as ScaleFunction; - }, [scale]); + updateScale(); } diff --git a/packages/ui/src/hooks/use-store.ts b/packages/ui/src/hooks/use-store.ts deleted file mode 100644 index 4d092ae3..00000000 --- a/packages/ui/src/hooks/use-store.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { UseProducerHook, useProducer } from "@rbxts/react-reflex"; -import { RootStore } from "../store"; - -export const useStore: UseProducerHook = useProducer; diff --git a/packages/ui/src/providers/api-provider.tsx b/packages/ui/src/providers/api-provider.tsx deleted file mode 100644 index cbe8a76e..00000000 --- a/packages/ui/src/providers/api-provider.tsx +++ /dev/null @@ -1,12 +0,0 @@ -import { ClientAPI } from "@rbxts/centurion"; -import React, { createContext } from "@rbxts/react"; - -interface ApiProviderProps extends React.PropsWithChildren { - api: ClientAPI; -} - -export const ApiContext = createContext({} as never); - -export function ApiProvider({ api, children }: ApiProviderProps) { - return {children}; -} diff --git a/packages/ui/src/providers/options-provider.tsx b/packages/ui/src/providers/options-provider.tsx deleted file mode 100644 index 5c37577f..00000000 --- a/packages/ui/src/providers/options-provider.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Signal } from "@rbxts/beacon"; -import { useEventListener } from "@rbxts/pretty-react-hooks"; -import React, { createContext, useState } from "@rbxts/react"; -import { InterfaceOptions } from "../types"; - -export interface InterfaceOptionsWithState extends InterfaceOptions { - isMouseOnGUI: boolean; - setMouseOnGUI: (newState: boolean) => void; -} - -// we can safely assign to {} as the value will always be assigned in the provider -export const OptionsContext = createContext( - {} as never, -); - -export interface OptionsProviderProps extends React.PropsWithChildren { - value: InterfaceOptions; - changed: Signal<[options: Partial]>; -} - -export function OptionsProvider({ - value, - changed, - children, -}: OptionsProviderProps) { - const [isMouseOnGUI, setMouseOnGUI] = useState(false); - const [options, setOptions] = useState({ - ...value, - }); - - useEventListener(changed, (options) => { - setOptions((prev) => ({ ...prev, ...options })); - }); - - return ( - - {children} - - ); -} diff --git a/packages/ui/src/providers/root-provider.tsx b/packages/ui/src/providers/root-provider.tsx deleted file mode 100644 index b217eace..00000000 --- a/packages/ui/src/providers/root-provider.tsx +++ /dev/null @@ -1,31 +0,0 @@ -import { Signal } from "@rbxts/beacon"; -import { ClientAPI } from "@rbxts/centurion"; -import React from "@rbxts/react"; -import { ReflexProvider } from "@rbxts/react-reflex"; -import { store } from "../store"; -import { InterfaceOptions } from "../types"; -import { ApiProvider } from "./api-provider"; -import { OptionsProvider } from "./options-provider"; - -interface RootProviderProps extends React.PropsWithChildren { - api: ClientAPI; - options: InterfaceOptions; - optionsChanged: Signal<[options: Partial]>; -} - -export function RootProvider({ - api, - options, - optionsChanged, - children, -}: RootProviderProps) { - return ( - - - - {children} - - - - ); -} diff --git a/packages/ui/src/store.ts b/packages/ui/src/store.ts new file mode 100644 index 00000000..78491683 --- /dev/null +++ b/packages/ui/src/store.ts @@ -0,0 +1,35 @@ +import { ImmutableRegistryPath } from "@rbxts/centurion"; +import { splitString } from "@rbxts/centurion/out/shared/util/string"; +import { atom, computed } from "@rbxts/charm"; +import { DEFAULT_INTERFACE_OPTIONS } from "./constants/options"; +import { InterfaceOptions, Suggestion } from "./types"; + +export const interfaceVisible = atom(false); +export const interfaceOptions = atom( + DEFAULT_INTERFACE_OPTIONS, +); +export const mouseOverInterface = atom(false); + +export const currentCommandPath = atom( + undefined, +); +export const currentArgIndex = atom(undefined); + +export const commandHistory = atom([]); +export const commandHistoryIndex = atom(-1); + +export const currentSuggestion = atom(undefined); +export const terminalText = atom(""); +export const terminalTextParts = computed(() => { + return splitString(terminalText(), " "); +}); +export const currentTextPart = computed(() => { + const text = terminalText(); + const textParts = terminalTextParts(); + + const endsWithSpace = textParts.size() > 0 && text.match("%s$").size() > 0; + const index = endsWithSpace ? textParts.size() : textParts.size() - 1; + if (index === -1 || index >= textParts.size()) return; + return textParts[index]; +}); +export const terminalTextValid = atom(false); diff --git a/packages/ui/src/store/app/app-selectors.ts b/packages/ui/src/store/app/app-selectors.ts deleted file mode 100644 index 86728b53..00000000 --- a/packages/ui/src/store/app/app-selectors.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { RootState } from ".."; - -export const selectVisible = (state: RootState) => state.app.visible; diff --git a/packages/ui/src/store/app/app-slice.ts b/packages/ui/src/store/app/app-slice.ts deleted file mode 100644 index d036ba49..00000000 --- a/packages/ui/src/store/app/app-slice.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { createProducer } from "@rbxts/reflex"; - -export interface AppState { - visible: boolean; -} - -const initialAppState: AppState = { - visible: false, -}; - -export const appSlice = createProducer(initialAppState, { - setVisible: (state, visible: boolean) => ({ ...state, visible }), -}); diff --git a/packages/ui/src/store/app/index.ts b/packages/ui/src/store/app/index.ts deleted file mode 100644 index 39817702..00000000 --- a/packages/ui/src/store/app/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./app-selectors"; -export * from "./app-slice"; diff --git a/packages/ui/src/store/command/command-selectors.ts b/packages/ui/src/store/command/command-selectors.ts deleted file mode 100644 index 54c7c4ae..00000000 --- a/packages/ui/src/store/command/command-selectors.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { RootState } from ".."; - -export const selectCommand = (state: RootState) => state.command.path; diff --git a/packages/ui/src/store/command/command-slice.ts b/packages/ui/src/store/command/command-slice.ts deleted file mode 100644 index e4080d8b..00000000 --- a/packages/ui/src/store/command/command-slice.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { ImmutableRegistryPath } from "@rbxts/centurion"; -import { createProducer } from "@rbxts/reflex"; - -export interface CommandState { - path?: ImmutableRegistryPath; - argIndex?: number; -} - -const initialCommandState: CommandState = {}; - -export const commandSlice = createProducer(initialCommandState, { - setCommand: (state, path?: ImmutableRegistryPath) => ({ - ...state, - path, - }), - - setArgIndex: (state, index?: number) => ({ ...state, argIndex: index }), -}); diff --git a/packages/ui/src/store/command/index.ts b/packages/ui/src/store/command/index.ts deleted file mode 100644 index 89da2c7e..00000000 --- a/packages/ui/src/store/command/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./command-selectors"; -export * from "./command-slice"; diff --git a/packages/ui/src/store/history/history-slice.ts b/packages/ui/src/store/history/history-slice.ts deleted file mode 100644 index 704a4b5a..00000000 --- a/packages/ui/src/store/history/history-slice.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { createProducer } from "@rbxts/reflex"; - -export interface HistoryState { - commandHistory: string[]; - commandHistoryIndex: number; -} - -const initialHistoryState: HistoryState = { - commandHistory: [], - commandHistoryIndex: -1, -}; - -/** - * Limits an array by removing the first n (limit) elements if - * the array's size exceeds the limit. - * - * @param array The array to limit - * @param limit The limit - */ -function limitArray(array: T[], limit: number) { - if (array.size() <= limit) return; - for (const i of $range(0, math.min(array.size() - 1, limit - 1))) { - array.remove(i); - } -} - -export const historySlice = createProducer(initialHistoryState, { - addCommandHistory: (state, command: string, limit: number) => { - if ( - state.commandHistory.size() > 0 && - state.commandHistory[state.commandHistory.size() - 1] === command - ) { - return state; - } - - const commandHistory = [...state.commandHistory]; - limitArray(commandHistory, limit); - commandHistory.push(command); - - return { - ...state, - commandHistory, - }; - }, - - setCommandHistoryIndex: (state, index: number) => ({ - ...state, - commandHistoryIndex: index, - }), -}); diff --git a/packages/ui/src/store/history/index.ts b/packages/ui/src/store/history/index.ts deleted file mode 100644 index 5c1afbb2..00000000 --- a/packages/ui/src/store/history/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from "./history-slice"; diff --git a/packages/ui/src/store/index.ts b/packages/ui/src/store/index.ts deleted file mode 100644 index 05305ba7..00000000 --- a/packages/ui/src/store/index.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { InferState, combineProducers } from "@rbxts/reflex"; -import { appSlice } from "./app"; -import { commandSlice } from "./command"; -import { historySlice } from "./history"; -import { suggestionSlice } from "./suggestion"; -import { textSlice } from "./text"; - -export type RootStore = typeof store; -export type RootState = InferState; - -export const store = combineProducers({ - app: appSlice, - command: commandSlice, - history: historySlice, - suggestion: suggestionSlice, - text: textSlice, -}); diff --git a/packages/ui/src/store/suggestion/index.ts b/packages/ui/src/store/suggestion/index.ts deleted file mode 100644 index 370a98c5..00000000 --- a/packages/ui/src/store/suggestion/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./suggestion-selectors"; -export * from "./suggestion-slice"; diff --git a/packages/ui/src/store/suggestion/suggestion-selectors.ts b/packages/ui/src/store/suggestion/suggestion-selectors.ts deleted file mode 100644 index 19a2c4ba..00000000 --- a/packages/ui/src/store/suggestion/suggestion-selectors.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { RootState } from ".."; - -export const selectSuggestion = (state: RootState) => state.suggestion.current; diff --git a/packages/ui/src/store/suggestion/suggestion-slice.ts b/packages/ui/src/store/suggestion/suggestion-slice.ts deleted file mode 100644 index e0309287..00000000 --- a/packages/ui/src/store/suggestion/suggestion-slice.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { createProducer } from "@rbxts/reflex"; -import { Suggestion } from "../../types"; - -export interface SuggestionState { - current?: Suggestion; -} - -const initialSuggestionState: SuggestionState = {}; - -export const suggestionSlice = createProducer(initialSuggestionState, { - setSuggestion: (state, suggestion?: Suggestion) => ({ - ...state, - current: suggestion, - }), -}); diff --git a/packages/ui/src/store/text/index.ts b/packages/ui/src/store/text/index.ts deleted file mode 100644 index aa1ec332..00000000 --- a/packages/ui/src/store/text/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from "./text-selectors"; -export * from "./text-slice"; diff --git a/packages/ui/src/store/text/text-selectors.ts b/packages/ui/src/store/text/text-selectors.ts deleted file mode 100644 index 7fe7dbe6..00000000 --- a/packages/ui/src/store/text/text-selectors.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { RootState } from ".."; - -export const selectText = (state: RootState) => state.text; - -export const selectValid = (state: RootState) => state.text.valid; diff --git a/packages/ui/src/store/text/text-slice.ts b/packages/ui/src/store/text/text-slice.ts deleted file mode 100644 index 5095b514..00000000 --- a/packages/ui/src/store/text/text-slice.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { createProducer } from "@rbxts/reflex"; - -export interface TextState { - value: string; - parts: string[]; - index: number; - valid: boolean; -} - -const initialTextState: TextState = { - value: "", - parts: [], - index: -1, - valid: false, -}; - -export const textSlice = createProducer(initialTextState, { - setText: (state, text: string, textParts: string[]) => { - const endsWithSpace = textParts.size() > 0 && text.match("%s$").size() > 0; - - return { - ...state, - value: text, - parts: textParts, - index: endsWithSpace ? textParts.size() : textParts.size() - 1, - }; - }, - - setTextValid: (state, valid: boolean) => ({ - ...state, - valid, - }), -}); diff --git a/packages/ui/src/types.ts b/packages/ui/src/types.ts index 7a8b0498..7232deb2 100644 --- a/packages/ui/src/types.ts +++ b/packages/ui/src/types.ts @@ -26,6 +26,7 @@ export interface HistoryLineData { export interface CommandSuggestion { type: "command"; title: string; + others: string[]; description?: string; shortcuts?: CommandShortcut[]; } @@ -33,13 +34,11 @@ export interface CommandSuggestion { export interface ArgumentSuggestion { type: "argument"; title: string; + others: string[]; description?: string; dataType: string; optional: boolean; error?: string; } -export interface Suggestion { - main: ArgumentSuggestion | CommandSuggestion; - others: string[]; -} +export type Suggestion = CommandSuggestion | ArgumentSuggestion; diff --git a/packages/ui/src/util/argument.ts b/packages/ui/src/util/argument.ts index 5164022f..5c48eef5 100644 --- a/packages/ui/src/util/argument.ts +++ b/packages/ui/src/util/argument.ts @@ -1,4 +1,8 @@ -import { BaseRegistry, RegistryPath } from "@rbxts/centurion"; +import { + BaseRegistry, + ImmutableRegistryPath, + RegistryPath, +} from "@rbxts/centurion"; import { IS_EDIT } from "../constants/util"; export function getArgumentNames(registry: BaseRegistry, path: RegistryPath) { @@ -13,3 +17,32 @@ export function getArgumentNames(registry: BaseRegistry, path: RegistryPath) { return command.options.arguments.map((arg) => arg.name); } + +export function getMissingArgs( + registry: BaseRegistry, + path: ImmutableRegistryPath, + text: string[], +) { + const commandOptions = registry.getCommand(path)?.options; + if ( + commandOptions === undefined || + commandOptions.arguments === undefined || + commandOptions.arguments.isEmpty() + ) { + return []; + } + + const lastPartIndex = text.size() - path.size() - 1; + const missingArgs: string[] = []; + + let index = 0; + for (const arg of commandOptions.arguments) { + if (arg.optional) break; + if (index > lastPartIndex) { + missingArgs.push(arg.name); + } + index++; + } + + return missingArgs; +} diff --git a/packages/ui/src/util/suggestion.ts b/packages/ui/src/util/suggestion.ts index c82224e1..a4dab4a1 100644 --- a/packages/ui/src/util/suggestion.ts +++ b/packages/ui/src/util/suggestion.ts @@ -1,7 +1,7 @@ import { BaseRegistry, CommandOptions, RegistryPath } from "@rbxts/centurion"; import { ArrayUtil } from "@rbxts/centurion/out/shared/util/data"; import { Players } from "@rbxts/services"; -import { Suggestion } from "../types"; +import { ArgumentSuggestion, Suggestion } from "../types"; const MAX_OTHER_SUGGESTIONS = 3; @@ -41,7 +41,7 @@ export function getArgumentSuggestion( path: RegistryPath, index: number, text?: string, -): Suggestion | undefined { +): ArgumentSuggestion | undefined { const command = registry.getCommand(path); if (command === undefined) return; @@ -90,15 +90,13 @@ export function getArgumentSuggestion( ).map((index) => argSuggestions[index]); return { - main: { - type: "argument", - title: arg.name, - description: arg.description, - dataType: typeObject.name, - optional: arg.optional ?? false, - error: errorText, - }, + type: "argument", + title: arg.name, others: otherSuggestions, + description: arg.description, + dataType: typeObject.name, + optional: arg.optional ?? false, + error: errorText, }; } @@ -133,12 +131,10 @@ export function getCommandSuggestion( : []; return { - main: { - type: "command", - title: firstPath.tail(), - description: mainData.description, - shortcuts: (mainData as CommandOptions).shortcuts, - }, + type: "command", + title: firstPath.tail(), others: otherNames, + description: mainData.description, + shortcuts: (mainData as CommandOptions).shortcuts, }; } diff --git a/packages/ui/tsconfig.json b/packages/ui/tsconfig.json index 4547aefd..7c68f343 100644 --- a/packages/ui/tsconfig.json +++ b/packages/ui/tsconfig.json @@ -4,8 +4,8 @@ "allowSyntheticDefaultImports": true, "downlevelIteration": true, "jsx": "react", - "jsxFactory": "React.createElement", - "jsxFragmentFactory": "React.Fragment", + "jsxFactory": "Vide.jsx", + "jsxFragmentFactory": "Vide.Fragment", "module": "commonjs", "moduleResolution": "Node", "noLib": true, diff --git a/yarn.lock b/yarn.lock index e17747bd..b2babb29 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1231,15 +1231,13 @@ __metadata: dependencies: "@rbxts/beacon": "npm:^2.1.1" "@rbxts/centurion": "workspace:^" + "@rbxts/charm": "npm:^0.5.2" "@rbxts/compiler-types": "npm:2.3.0-types.1" - "@rbxts/pretty-react-hooks": "npm:^0.5.2" - "@rbxts/react": "npm:^0.4.0" - "@rbxts/react-reflex": "npm:^0.3.4" - "@rbxts/react-roblox": "npm:^0.4.0" - "@rbxts/reflex": "npm:^4.3.1" "@rbxts/ripple": "npm:^0.8.2" "@rbxts/services": "npm:^1.5.4" + "@rbxts/set-timeout": "npm:^1.1.2" "@rbxts/types": "npm:^1.0.789" + "@rbxts/vide": "npm:^0.4.1" roblox-ts: "npm:2.3.0-dev-26ec859" shx: "npm:^0.3.4" typescript: "npm:~5.5.3" @@ -1265,6 +1263,18 @@ __metadata: languageName: unknown linkType: soft +"@rbxts/charm@npm:^0.5.2": + version: 0.5.2 + resolution: "@rbxts/charm@npm:0.5.2" + peerDependencies: + "@rbxts/react": "*" + peerDependenciesMeta: + "@rbxts/react": + optional: true + checksum: 10c0/9c8701000e6a935348a90b37c6c30116ea4a9e474f32aea238e017f41a596a4855bd99799185f887d5b2b163710cc9dab45507e276e2f2fcd206132f1ff1b1b2 + languageName: node + linkType: hard + "@rbxts/compiler-types@npm:2.3.0-types.1, @rbxts/compiler-types@npm:^2.3.0-types.1": version: 2.3.0-types.1 resolution: "@rbxts/compiler-types@npm:2.3.0-types.1" @@ -1272,13 +1282,6 @@ __metadata: languageName: node linkType: hard -"@rbxts/flipper@npm:^2.0.1": - version: 2.0.1 - resolution: "@rbxts/flipper@npm:2.0.1" - checksum: 10c0/ac32b7bb1ac92516cc5b0efca9a60be3a3dd536281f3bf5d66ec1b7a62f6b2e28ef0802639b8ad3abb3928b545d476bfe9e820a3cb7654f539bfc7452f9893d3 - languageName: node - linkType: hard - "@rbxts/jest-globals@npm:0.1.0, @rbxts/jest-globals@npm:^0.1.0": version: 0.1.0 resolution: "@rbxts/jest-globals@npm:0.1.0" @@ -1329,62 +1332,13 @@ __metadata: languageName: node linkType: hard -"@rbxts/pretty-react-hooks@npm:^0.5.2": - version: 0.5.2 - resolution: "@rbxts/pretty-react-hooks@npm:0.5.2" - dependencies: - "@rbxts/flipper": "npm:^2.0.1" - "@rbxts/react": "npm:*" - "@rbxts/react-roblox": "npm:*" - "@rbxts/services": "npm:^1.5.1" - "@rbxts/set-timeout": "npm:^1.1.2" - checksum: 10c0/5327a7a6282ac008b6ea7aad5005ae4e4e0c6145658cc279d67456268c99a65b9a40dce803c31eeaed166171fc4b5e0f2b4754852ffae0c49542708273fdf54d - languageName: node - linkType: hard - -"@rbxts/react-reflex@npm:^0.3.4": - version: 0.3.4 - resolution: "@rbxts/react-reflex@npm:0.3.4" - peerDependencies: - "@rbxts/react": "*" - "@rbxts/reflex": "*" - checksum: 10c0/2869f78dc990dfadcd4abe87267aea938432844444f26f2085c8a151195684851043df88f997919dadee917ab0e8ddd1b54e4927c80cfdf26c8c9c1af6305cad - languageName: node - linkType: hard - -"@rbxts/react-roblox@npm:*, @rbxts/react-roblox@npm:^0.4.0": - version: 0.4.0 - resolution: "@rbxts/react-roblox@npm:0.4.0" - dependencies: - "@rbxts/react": "npm:0.4.0" - "@rbxts/react-vendor": "npm:0.4.0" - checksum: 10c0/144dcf8d1cfe9a513f9bb08d851e0d1cff211be640a126bf1808599507b99382226cbd3561f7e2a2aa2810ac0bcd630a207e284da380c01a6e69055041bf3325 - languageName: node - linkType: hard - -"@rbxts/react-vendor@npm:*, @rbxts/react-vendor@npm:0.4.0": +"@rbxts/react-vendor@npm:*": version: 0.4.0 resolution: "@rbxts/react-vendor@npm:0.4.0" checksum: 10c0/e1f3fccb13a55dc8751f93e5bfb66d40ffd188e296f351801fdce25bd3df5cb5ed2d4ae67b8b067e950875ff49b6349c44c4d8b09b448e446e102c5735a9a388 languageName: node linkType: hard -"@rbxts/react@npm:*, @rbxts/react@npm:0.4.0, @rbxts/react@npm:^0.4.0": - version: 0.4.0 - resolution: "@rbxts/react@npm:0.4.0" - dependencies: - "@rbxts/react-vendor": "npm:0.4.0" - checksum: 10c0/bbb0612019af214e43ed427478e255992a6b3b58322937d17a6af442a8e1b6578ce945d382a5f460fc393897fed9d649698c2838450347265ebef592ec8cfdf5 - languageName: node - linkType: hard - -"@rbxts/reflex@npm:^4.3.1": - version: 4.3.1 - resolution: "@rbxts/reflex@npm:4.3.1" - checksum: 10c0/7c45f2e9ebc223e6ffc792a1b2bf8899883455b0717fca58306cbe6fc62f2acafb2421e45bafaccd7b851ffb752b7febcdbd9734f59ad96a8fae2935faf2aa0e - languageName: node - linkType: hard - "@rbxts/ripple@npm:^0.8.2": version: 0.8.2 resolution: "@rbxts/ripple@npm:0.8.2" @@ -1422,6 +1376,13 @@ __metadata: languageName: node linkType: hard +"@rbxts/vide@npm:^0.4.1": + version: 0.4.1 + resolution: "@rbxts/vide@npm:0.4.1" + checksum: 10c0/8b5f02fc16dcd800cedabbb71c4fd652746aa293b8ee73330c2c1bd2ab104211d808b995f41232096711689be606085d8dbf14b1d4cdc178320a7b41d0911330 + languageName: node + linkType: hard + "@roblox-ts/luau-ast@npm:=2.0.0": version: 2.0.0 resolution: "@roblox-ts/luau-ast@npm:2.0.0"