{"id":"realstencer/uiservice","name":"uiservice","scope":"realstencer","platform":"roblox","description":"Helper framework for Roblox UI built on React, ReactRoblox, Ripple, and ReactCharm.","version":"0.2.0","latest":"0.2.0","versions":["0.1.0","0.2.0"],"license":"MIT","licenseRating":"safe","licenseCaveats":[],"licenseVerified":true,"dependencies":{"jsdotlua/react":{"version":"^17.2.1","alias":"React"},"littensy/react-charm":{"version":"^0.3.0","alias":"ReactCharm"},"jsdotlua/react-roblox":{"version":"^17.2.1","alias":"ReactRoblox"},"littensy/ripple":{"version":"^0.10.2","alias":"Ripple"}},"integrity":"7067173d7042b0b0227bedd78e9d439010073aeeca4e1b519202ab95ebfb6600","likes":0,"downloads":0,"install":"forest install realstencer/uiservice","url":"https://forest.dev/p/roblox/realstencer/uiservice","files":"https://api.forest.dev/ai/package/roblox/realstencer/uiservice/files","readme":"# uiservice\r\n\r\n`uiservice` is a small helper framework for Roblox UI built on top of React, ReactRoblox, Ripple, and ReactCharm.\r\n\r\nIt keeps the original libraries available, but adds a simpler layer for the repetitive parts:\r\n\r\n- quick element creation with `uiservice.e`\r\n- ready-made UI constructors in `uiservice.components` using Roblox instance names\r\n- automatic default props when you call `e(\"TextLabel\", { ... })` or `e(TextLabel, { ... })`\r\n- optional prop presets in `uiservice.props`\r\n- optional helpers in `uiservice.values`\r\n- easy mounting with `uiservice.mount`\r\n- lightweight Charm-backed stores with `uiservice.createStore`\r\n- Ripple bindings for React with `uiservice.useSpring`, `uiservice.useTween`, and `uiservice.useMotion`\r\n\r\nThis package is meant to be a helper and easy starting point. It is not a replacement for React, ReactRoblox, Ripple, or Charm.\r\n\r\n## Install\r\n\r\n```toml\r\n[dependencies]\r\nuiservice = \"realstencer/uiservice@0.1.0\"\r\n```\r\n\r\n## What It Exports\r\n\r\n```luau\r\nlocal UIService = require(Packages.uiservice)\r\n\r\nlocal React = UIService.React\r\nlocal e = UIService.e\r\nlocal TextLabel = UIService.TextLabel\r\nlocal TextButton = UIService.TextButton\r\nlocal TextBox = UIService.TextBox\r\nlocal CanvasGroup = UIService.CanvasGroup\r\nlocal ScreenGui = UIService.ScreenGui\r\nlocal UICorner = UIService.UICorner\r\n```\r\n\r\nMain exports:\r\n\r\n- `React`\r\n- `ReactRoblox`\r\n- `ReactCharm`\r\n- `Ripple`\r\n- `Charm`\r\n- `e(component, props, children?)`\r\n- `createComponent(className, defaults?)`\r\n- `components`\r\n- `props`\r\n- `values`\r\n- Roblox-named built-ins directly on `UIService`, such as `TextLabel`, `TextButton`, `TextBox`, `CanvasGroup`, `ViewportFrame`, `VideoFrame`, `Frame`, `ScreenGui`, `UICorner`, `UIPadding`, `UIListLayout`, `UIGridLayout`, `UIPageLayout`, and `UIStroke`\r\n- top-level helpers like `FontFace`, `AnchorPoint`, `Position`, and `Size`\r\n- `mount(target, element)`\r\n- `unmount(target)`\r\n- `createRoot(target)`\r\n- `createStore(initialState)`\r\n- `createAtom(initialValue, options?)`\r\n- `createComputed(callback, options?)`\r\n- `useAtom(atomOrSelector, dependencies?)`\r\n- `useSpring(initialValue, options?, dependencies?)`\r\n- `useTween(initialValue, options?, dependencies?)`\r\n- `useMotion(initialValue, options?, dependencies?)`\r\n- `useAnimatedBinding(initialValue, factory, dependencies?)`\r\n- `useScaleButton(options?)`\r\n\r\n## Why Use It\r\n\r\nUse `uiservice` when you want to keep real Roblox names like `TextLabel`, `TextButton`, `ImageButton`, `UICorner`, and `UIPadding`, but still move faster.\r\n\r\nIt helps in three main ways:\r\n\r\n- `components` gives you Roblox-named constructors such as `ui.TextLabel` and `ui.TextButton`\r\n- `props` gives you a basic ordered set of common properties for each instance\r\n- `values` gives you autocomplete-friendly helpers for `Color3`, `Font`, `UDim2`, size, position, anchor points, and common UI enums\r\n\r\n## Example: Simple TextLabel\r\n\r\n```luau\r\nlocal UIService = require(Packages.uiservice)\r\n\r\nlocal e = UIService.e\r\nlocal TextLabel = UIService.TextLabel\r\n\r\n\r\nlocal element = e(TextLabel, {\r\n\tAnchorPoint = Vector2.new(0.5, 0.5),\r\n\tPosition = UDim2.fromScale(0.5, 0.1),\r\n\tSize = UDim2.fromScale(0.4, 0.08),\r\n\tText = \"Title\",\r\n\tTextColor3 = Color3.fromRGB(255, 255, 255),\r\n\tFontFace = Font.fromName(\"Montserrat\"),\r\n})\r\n```\r\n\r\nThe automatic default props for `TextLabel` are still applied even if you only override a few fields. The current default set is based on [src/Props.luau](src/Props.luau).\r\n\r\n## More Supported Classes\r\n\r\nThe helper now includes automatic presets for:\r\n\r\n- `Frame`\r\n- `TextLabel`\r\n- `TextButton`\r\n- `TextBox`\r\n- `ImageLabel`\r\n- `ImageButton`\r\n- `ScrollingFrame`\r\n- `CanvasGroup`\r\n- `ViewportFrame`\r\n- `VideoFrame`\r\n- `ScreenGui`\r\n- `UICorner`\r\n- `UIPadding`\r\n- `UIListLayout`\r\n- `UIGridLayout`\r\n- `UIPageLayout`\r\n- `UIAspectRatioConstraint`\r\n- `UISizeConstraint`\r\n- `UIScale`\r\n- `UIStroke`\r\n- `UIGradient`\r\n\r\n## Example: Using Direct Roblox Names\r\n\r\n```luau\r\nlocal UIService = require(Packages.uiservice)\r\n\r\nlocal e = UIService.e\r\nlocal TextLabel = UIService.TextLabel\r\nlocal TextButton = UIService.TextButton\r\nlocal ScreenGui = UIService.ScreenGui\r\nlocal UICorner = UIService.UICorner\r\n\r\nlocal function MainMenu()\r\n\treturn e(ScreenGui, {\r\n\t\tDisplayOrder = 10,\r\n\t}, {\r\n\t\tTitle = e(TextLabel, {\r\n\t\t\tAnchorPoint = Vector2.new(0.5, 0),\r\n\t\t\tPosition = UDim2.fromScale(0.5, 0.08),\r\n\t\t\tSize = UDim2.fromScale(0.4, 0.08),\r\n\t\t\tText = \"Main Menu\",\r\n\t\t\tTextColor3 = Color3.fromRGB(255, 255, 255),\r\n\t\t\tFontFace = Font.fromName(\"Montserrat\"),\r\n\t\t\tTextScaled = true,\r\n\t\t}),\r\n\t\tPlayButton = e(TextButton, {\r\n\t\t\tAnchorPoint = Vector2.new(0.5, 0.5),\r\n\t\t\tPosition = UDim2.fromScale(0.5, 0.5),\r\n\t\t\tSize = UDim2.fromOffset(220, 54),\r\n\t\t\tBackgroundColor3 = Color3.fromRGB(66, 135, 245),\r\n\t\t\tText = \"Play\",\r\n\t\t\tTextColor3 = Color3.fromRGB(255, 255, 255),\r\n\t\t\tonActivated = function()\r\n\t\t\t\tprint(\"Play clicked\")\r\n\t\t\tend,\r\n\t\t}, {\r\n\t\t\tCorner = e(UICorner, {\r\n\t\t\t\tCornerRadius = UDim.new(0, 10),\r\n\t\t\t}),\r\n\t\t}),\r\n\t})\r\nend\r\n```\r\n\r\n## Example: Optional Preset Builder\r\n\r\nIf you do want a prefilled ordered prop table, `props` is still available:\r\n\r\n```luau\r\nlocal props = UIService.props\r\nlocal TextLabel = UIService.TextLabel\r\n\r\nlocal Title = e(TextLabel, props.TextLabel({\r\n\tText = \"Title\",\r\n\tFontFace = Font.fromName(\"Montserrat\"),\r\n}))\r\n```\r\n\r\n## Example: Mount To PlayerGui\r\n\r\n```luau\r\nlocal Players = game:GetService(\"Players\")\r\nlocal UIService = require(Packages.uiservice)\r\n\r\nlocal playerGui = Players.LocalPlayer:WaitForChild(\"PlayerGui\")\r\nUIService.mount(playerGui, MainMenu())\r\n```\r\n\r\n## Example: Game State With ReactCharm\r\n\r\n```luau\r\nlocal UIService = require(Packages.uiservice)\r\n\r\nlocal store = UIService.createStore({\r\n\tcoins = 0,\r\n\tmenuOpen = true,\r\n\tselectedTab = \"Home\",\r\n})\r\n\r\nstore.computed(\"title\", function()\r\n\treturn store.get(\"selectedTab\") .. \" Menu\"\r\nend)\r\n\r\nlocal function Header()\r\n\tlocal coins = store.use(\"coins\")\r\n\tlocal title = store.use(\"title\")\r\n\r\n\treturn UIService.e(UIService.TextLabel, {\r\n\t\tBackgroundTransparency = 1,\r\n\t\tSize = UDim2.new(1, 0, 0, 36),\r\n\t\tText = string.format(\"%s | Coins: %d\", title, coins),\r\n\t\tTextColor3 = Color3.fromRGB(255, 255, 255),\r\n\t\tTextScaled = false,\r\n\t\tTextSize = 22,\r\n\t})\r\nend\r\n\r\nstore.set(\"coins\", function(value)\r\n\treturn value + 50\r\nend)\r\n```\r\n\r\n## Example: Ripple Animation In React\r\n\r\n```luau\r\nlocal UIService = require(Packages.uiservice)\r\n\r\nlocal React = UIService.React\r\nlocal e = UIService.e\r\nlocal TextButton = UIService.TextButton\r\nlocal UICorner = UIService.UICorner\r\n\r\nlocal function AnimatedPlayButton()\r\n\tlocal scale, spring, buttonProps = UIService.useScaleButton({\r\n\t\thoverScale = 1.04,\r\n\t\tpressScale = 0.95,\r\n\t})\r\n\r\n\treturn e(TextButton, {\r\n\t\tAnchorPoint = Vector2.new(0.5, 0.5),\r\n\t\tPosition = UDim2.fromScale(0.5, 0.5),\r\n\t\tSize = scale:map(function(value)\r\n\t\t\treturn UDim2.fromOffset(240 * value, 64 * value)\r\n\t\tend),\r\n\t\tBackgroundColor3 = Color3.fromRGB(76, 201, 126),\r\n\t\tText = \"Play\",\r\n\t\tTextColor3 = Color3.fromRGB(18, 24, 18),\r\n\t\tTextScaled = true,\r\n\t\tAutoButtonColor = false,\r\n\t\t[React.Change.GuiState] = buttonProps[React.Change.GuiState],\r\n\t\tActivated = function()\r\n\t\t\tspring:setGoal(1.08)\r\n\t\t\ttask.delay(0.08, function()\r\n\t\t\t\tspring:setGoal(1)\r\n\t\t\tend)\r\n\t\tend,\r\n\t}, {\r\n\t\tCorner = e(UICorner, {\r\n\t\t\tCornerRadius = UDim.new(0, 12),\r\n\t\t}),\r\n\t})\r\nend\r\n```\r\n\r\n## Full Example: Animated Hover And Press Button\r\n\r\nThis is the pattern for a button that grows on hover and compresses on press.\r\n\r\n```luau\r\nlocal UIService = require(Packages.uiservice)\r\n\r\nlocal React = UIService.React\r\nlocal e = UIService.e\r\nlocal TextButton = UIService.TextButton\r\nlocal UICorner = UIService.UICorner\r\nlocal UIStroke = UIService.UIStroke\r\n\r\nlocal function SpringButton(props)\r\n\tlocal scale, spring, hoverProps = UIService.useScaleButton(UIService.props.TextButtonHoverScale({\r\n\t\thoverScale = 1.05,\r\n\t\tpressScale = 0.93,\r\n\t\trestScale = 1,\r\n\t}))\r\n\r\n\treturn e(TextButton, {\r\n\t\tAnchorPoint = Vector2.new(0.5, 0.5),\r\n\t\tPosition = props.Position or UDim2.fromScale(0.5, 0.5),\r\n\t\tSize = scale:map(function(value)\r\n\t\t\treturn UDim2.fromOffset(220 * value, 56 * value)\r\n\t\tend),\r\n\t\tBackgroundColor3 = props.BackgroundColor3 or Color3.fromRGB(66, 135, 245),\r\n\t\tText = props.Text or \"Play\",\r\n\t\tTextColor3 = Color3.fromRGB(255, 255, 255),\r\n\t\tFontFace = Font.fromName(\"Montserrat\"),\r\n\t\t[React.Change.GuiState] = hoverProps[React.Change.GuiState],\r\n\t\tActivated = function()\r\n\t\t\tspring:setGoal(0.9)\r\n\t\t\ttask.delay(0.06, function()\r\n\t\t\t\tspring:setGoal(1)\r\n\t\t\tend)\r\n\r\n\t\t\tif props.Activated then\r\n\t\t\t\tprops.Activated()\r\n\t\t\tend\r\n\t\tend,\r\n\t}, {\r\n\t\tCorner = e(UICorner, {\r\n\t\t\tCornerRadius = UDim.new(0, 12),\r\n\t\t}),\r\n\t\tStroke = e(UIStroke, {\r\n\t\t\tColor = Color3.fromRGB(255, 255, 255),\r\n\t\t\tTransparency = 0.35,\r\n\t\t}),\r\n\t})\r\nend\r\n```\r\n\r\n## Full Example: Shop UI With State And Animation\r\n\r\n```luau\r\nlocal UIService = require(Packages.uiservice)\r\n\r\nlocal React = UIService.React\r\nlocal e = UIService.e\r\nlocal ScreenGui = UIService.ScreenGui\r\nlocal Frame = UIService.Frame\r\nlocal TextLabel = UIService.TextLabel\r\nlocal TextButton = UIService.TextButton\r\nlocal UIListLayout = UIService.UIListLayout\r\nlocal UIPadding = UIService.UIPadding\r\nlocal UICorner = UIService.UICorner\r\nlocal UIStroke = UIService.UIStroke\r\n\r\nlocal store = UIService.createStore({\r\n\tcoins = 500,\r\n\tisPurchasing = false,\r\n\terrorMessage = nil,\r\n})\r\n\r\nlocal function ShopButton(props)\r\n\tlocal scale, spring, hoverProps = UIService.useScaleButton({\r\n\t\thoverScale = 1.04,\r\n\t\tpressScale = 0.95,\r\n\t})\r\n\r\n\treturn e(TextButton, {\r\n\t\tSize = scale:map(function(value)\r\n\t\t\treturn UDim2.fromOffset(260 * value, 52 * value)\r\n\t\tend),\r\n\t\tBackgroundColor3 = props.BackgroundColor3 or Color3.fromRGB(66, 135, 245),\r\n\t\tText = props.Text,\r\n\t\tTextColor3 = Color3.fromRGB(255, 255, 255),\r\n\t\tFontFace = Font.fromName(\"Montserrat\"),\r\n\t\t[React.Change.GuiState] = hoverProps[React.Change.GuiState],\r\n\t\tActivated = function()\r\n\t\t\tspring:setGoal(0.92)\r\n\t\t\ttask.delay(0.06, function()\r\n\t\t\t\tspring:setGoal(1)\r\n\t\t\tend)\r\n\t\t\tprops.Activated()\r\n\t\tend,\r\n\t}, {\r\n\t\tCorner = e(UICorner, {\r\n\t\t\tCornerRadius = UDim.new(0, 10),\r\n\t\t}),\r\n\t})\r\nend\r\n\r\nlocal function ShopView()\r\n\tlocal coins = store.use(\"coins\")\r\n\tlocal isPurchasing = store.use(\"isPurchasing\")\r\n\tlocal errorMessage = store.use(\"errorMessage\")\r\n\r\n\treturn e(ScreenGui, {}, {\r\n\t\tRoot = e(Frame, {\r\n\t\t\tAnchorPoint = Vector2.new(0.5, 0.5),\r\n\t\t\tPosition = UDim2.fromScale(0.5, 0.5),\r\n\t\t\tSize = UDim2.fromScale(0.34, 0.42),\r\n\t\t\tBackgroundColor3 = Color3.fromRGB(25, 28, 34),\r\n\t\t}, {\r\n\t\t\tCorner = e(UICorner, {\r\n\t\t\t\tCornerRadius = UDim.new(0, 14),\r\n\t\t\t}),\r\n\t\t\tStroke = e(UIStroke, {\r\n\t\t\t\tColor = Color3.fromRGB(255, 255, 255),\r\n\t\t\t\tTransparency = 0.6,\r\n\t\t\t}),\r\n\t\t\tPadding = e(UIPadding, {\r\n\t\t\t\tPaddingTop = UDim.new(0, 16),\r\n\t\t\t\tPaddingBottom = UDim.new(0, 16),\r\n\t\t\t\tPaddingLeft = UDim.new(0, 16),\r\n\t\t\t\tPaddingRight = UDim.new(0, 16),\r\n\t\t\t}),\r\n\t\t\tLayout = e(UIListLayout, {\r\n\t\t\t\tPadding = UDim.new(0, 10),\r\n\t\t\t}),\r\n\t\t\tTitle = e(TextLabel, {\r\n\t\t\t\tSize = UDim2.new(1, 0, 0, 34),\r\n\t\t\t\tText = string.format(\"Coins: %d\", coins),\r\n\t\t\t\tTextColor3 = Color3.fromRGB(255, 255, 255),\r\n\t\t\t\tTextScaled = false,\r\n\t\t\t\tTextSize = 26,\r\n\t\t\t\tFontFace = Font.fromName(\"Montserrat\", Enum.FontWeight.Bold),\r\n\t\t\t}),\r\n\t\t\tBuySword = e(ShopButton, {\r\n\t\t\t\tText = isPurchasing and \"Purchasing...\" or \"Buy Sword - 200\",\r\n\t\t\t\tActivated = function()\r\n\t\t\t\t\tif isPurchasing then\r\n\t\t\t\t\t\treturn\r\n\t\t\t\t\tend\r\n\r\n\t\t\t\t\tstore.batch(function()\r\n\t\t\t\t\t\tstore.set(\"isPurchasing\", true)\r\n\t\t\t\t\t\tstore.set(\"errorMessage\", nil)\r\n\t\t\t\t\tend)\r\n\r\n\t\t\t\t\tif coins < 200 then\r\n\t\t\t\t\t\tstore.batch(function()\r\n\t\t\t\t\t\t\tstore.set(\"isPurchasing\", false)\r\n\t\t\t\t\t\t\tstore.set(\"errorMessage\", \"Not enough coins\")\r\n\t\t\t\t\t\tend)\r\n\t\t\t\t\t\treturn\r\n\t\t\t\t\tend\r\n\r\n\t\t\t\t\ttask.delay(0.2, function()\r\n\t\t\t\t\t\tstore.batch(function()\r\n\t\t\t\t\t\t\tstore.set(\"coins\", function(value)\r\n\t\t\t\t\t\t\t\treturn value - 200\r\n\t\t\t\t\t\t\tend)\r\n\t\t\t\t\t\t\tstore.set(\"isPurchasing\", false)\r\n\t\t\t\t\t\tend)\r\n\t\t\t\t\tend)\r\n\t\t\t\tend,\r\n\t\t\t}),\r\n\t\t\tError = errorMessage and e(TextLabel, {\r\n\t\t\t\tSize = UDim2.new(1, 0, 0, 24),\r\n\t\t\t\tText = errorMessage,\r\n\t\t\t\tTextColor3 = Color3.fromRGB(255, 120, 120),\r\n\t\t\t\tTextScaled = false,\r\n\t\t\t\tTextSize = 18,\r\n\t\t\t}) or nil,\r\n\t\t}),\r\n\t})\r\nend\r\n```\r\n\r\nFor a real game, replace that local coin deduction with a server purchase request and update the store from the server response.\r\n\r\n## Why There Is No Separate `Types` Module\r\n\r\nThere used to be a separate `Types` module during an earlier iteration, but it was removed because the Luau analyzer in this workspace handled those cross-module type imports badly. The public helper types are now exported directly from `uiservice` itself, which keeps the package simpler and avoids those toolchain issues.\r\n\r\n## Example: Font And Stroke Defaults\r\n\r\n```luau\r\nlocal UIService = require(Packages.uiservice)\r\nlocal e = UIService.e\r\nlocal TextLabel = UIService.TextLabel\r\nlocal UIStroke = UIService.UIStroke\r\n\r\nlocal Label = e(TextLabel, {\r\n\tText = \"Hello\",\r\n\tFontFace = Font.fromName(\"Montserrat\"),\r\n})\r\n\r\nlocal Stroke = e(UIStroke, {\r\n\tColor = Color3.fromRGB(255, 255, 255),\r\n})\r\n```\r\n\r\n## Notes\r\n\r\n- `components` now keeps Roblox instance names instead of renaming them.\r\n- `e(TextLabel, { ... })` now applies the built-in default prop set automatically for supported Roblox UI classes.\r\n- `props.TextLabel()`, `props.TextButton()`, and similar helpers are optional now, not required.\r\n- `FontFace` support is available through `Font.fromName(...)`, and `UIStroke` defaults now use `StrokeSizingMode = Enum.StrokeSizingMode.Scaled` with `Thickness = 0.05`.\r\n- `components.Menu` adds `UICorner`, `UIPadding`, and `UIListLayout` automatically unless you override them in `children`.\r\n- More built-in Roblox UI classes now have presets, including `TextBox`, `CanvasGroup`, `ViewportFrame`, `VideoFrame`, `UIGridLayout`, `UIPageLayout`, `UISizeConstraint`, `UIScale`, and `UIGradient`.\r\n- `useSpring`, `useTween`, and `useMotion` return a React binding plus the underlying Ripple motor.\r\n- `createStore` is intentionally small. It helps with UI state fast, without hiding Charm itself.\r\n\r\n## Recommended Direction\r\n\r\nUse `uiservice` as a thin layer, not a replacement for the original libraries. If a screen needs something more advanced, you can always drop down to `React`, `ReactRoblox`, `Ripple`, or `Charm` directly because they are still exported.","readmeTruncated":false}