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.tspremium.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.