Skip to main content

UI Adapters

Pulse widget builders (toggle, slider, dropdown, …) declare widgets in a neutral syntax. An adapter translates those declarations into actual UI library calls at runtime. This separation means you can swap UI libraries without rewriting any component code.


Supported adapters​

ValueLibraryNotes
'windui'Wind UIDefault. Modern look, mobile support, built-in config persistence.
'linoria'LinoriaLibMature, widely supported.

Set uiLibrary in src/layout.ts:

export default {
uiLibrary: 'windui', // 'windui' (default) | 'linoria'
...
} satisfies LayoutConfig

The compiler reads this automatically. rb build --ui windui overrides it at the CLI level.


How the adapter is loaded​

The adapter is loaded from CDN at runtime (never inlined). The build output looks like:

if _G.__AOT_R_DESTROY then pcall(_G.__AOT_R_DESTROY) end

local _P = loadstring(game:HttpGet("https://.../bundle.lua"))()
local _A = loadstring(game:HttpGet("https://.../adapters/windui.lua"))()

-- your compiled TypeScript below

The adapter sets up the bridge between Pulse signals and the UI library before any component code runs.


Layout config — src/layout.ts​

All window configuration lives in src/layout.ts:

export default {
title: 'My Hub',
version: '1.0.0',
description: 'Enable features from the tabs above.',
discord: '', // 'https://discord.gg/...' → shows a Join Discord button
author: '', // optional credit line

toggleKey: 'RightControl', // Enum.KeyCode name — menu open/close
size: [850, 560] as [number, number],
uiLibrary: 'windui' as 'windui' | 'linoria',
theme: 'Indigo', // built-in WindUI theme name
icon: 'code-2', // Lucide icon name (lucide.dev/icons)
folder: 'MyHub', // executor save folder for configs and themes
acrylic: true, // blur effect behind window
transparency: 0.8, // 0 = opaque, 1 = transparent (ignored when acrylic = true)

openButtonMobileOnly: true, // false = show floating open button on desktop too
openButtonIcon: 'code-2',

themes: [] as LayoutConfig['themes'], // custom WindUI themes (see below)
compatExclude: [] as string[], // modules excluded from compat build

// ── Key system (optional) ────────────────────────────────────────────────
// keySystem: {
// title: 'Key Required',
// note: 'Get your key from Discord',
// saveKey: true,
// getKeyUrl: 'https://discord.gg/example',
// keys: ['KEY_1', 'KEY_2'],
// },

// ── Premium tier (optional) ─────────────────────────────────────────────
// premium: {
// keys: ['PREMIUM_KEY_1'],
// getKeyUrl: 'https://yoursite.com/premium',
// },
} satisfies LayoutConfig

WindUI themes​

Built-in themes​

All 16 built-in WindUI themes work out of the box — set theme: 'ThemeName':

Amber · CottonCandy · Crimson · Dark · Emerald · Indigo · Light · Mellowsi · Midnight · MonokaiPro · Plant · Rainbow · Red · Rose · Sky · Violet

Custom themes​

Add custom themes to the themes array — they're registered before the window opens and appear in the Settings tab theme picker:

themes: [
{
name: 'Brand',
accent: '#7c3aed',
background: '#0e0c1a',
outline: '#1e1b4b',
text: '#e8e3ff',
placeholder: '#6d6d8a',
button: '#1e1b4b',
icon: '#a78bfa',
},
],

All color fields are hex strings. name is required; all color fields are optional (each has a fallback).


Framework-managed pages​

The Home and Settings tabs are provided by the framework automatically — you do not write them. Home shows the script title, version, description, and an optional Discord link. Settings provides theme selection, config save/load, and the menu keybind picker.

:::warning Reserved names Do not name your own pages "Home" or "Settings". These names are taken by the framework and creating a page with either name produces a duplicate tab. :::


Page files​

Pages live in src/pages/. Files are loaded in filename order:

// src/pages/1_Combat.ts
definePage('Combat', { icon: 'swords' }, () => [
groupbox('left', 'Targeting', { icon: 'crosshair', mount: 'Aimbot' }),
groupbox('right', 'Visuals', { icon: 'eye', mount: 'PlayerESP' }),
])
// src/pages/2_Misc.ts
definePage('Misc', { icon: 'settings-2' }, () => [
groupbox('left', 'Movement', { icon: 'person', mount: 'SpeedHack' }),
])

groupbox options​

groupbox(
'left' | 'right', // column
'Title', // display name
{
icon: 'house', // Lucide icon
mount: 'ComponentName', // component whose widgets appear here
premium: true, // lock behind premium key
}
)

When premium: true is set, the groupbox shows a locked-state UI (key input + Copy Link + Check Key + status) until the user enters a valid premium key from layout.ts → premium.keys. Unlocking fires across all premium groupboxes simultaneously — no re-injection needed.


Compat exclude list​

Modules excluded from build/script.compat.obf.lua are listed in layout.ts:

compatExclude: [
'player/UNC.ts',
'visuals/Drawing.ts',
],

Any path listed there (relative to src/) is compiled normally into script.obf.lua but dropped from script.compat.obf.lua. Both builds are identical when the list is empty.


Adapter interface​

All widget creation goes through _UIAdapter. Call these directly in .lua files for widgets the builder API doesn't cover:

_UIAdapter:addToggle(gb, "Comp_signal", { label = "...", signal = Comp.signal })
_UIAdapter:addSlider(gb, "Comp_speed", { label = "...", signal = Comp.speed, min = 0, max = 100 })
_UIAdapter:addDropdown(gb, "Comp_mode", { label = "...", signal = Comp.mode, options = {"A","B"} })
_UIAdapter:addMultiDropdown(gb, "id", { label = "...", signal = sig, options = {...} })
_UIAdapter:addButton(gb, { label = "...", action = function() ... end })
_UIAdapter:addKeybind(gb, "id", { label = "...", key = "N", action = function() ... end })
_UIAdapter:addLabel(gb, "Static text")
_UIAdapter:addParagraph(gb, "Title", "Description text")
_UIAdapter:addSeparator(gb)

Using _UIAdapter keeps files adapter-portable. Calling library APIs directly (gb:AddToggle(...) for Linoria, WindUI internal methods) ties the file to one adapter.