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
| Value | Library | Notes |
|---|---|---|
'windui' | Wind UI | Default. Modern look, mobile support, built-in config persistence. |
'linoria' | LinoriaLib | Mature, 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.