Building UI
UI is declared with fluent widget builders returned from defineComponent, and laid out in src/pages/*.ts files. The adapter (Linoria or WindUI) handles all widget creation automatically.
Choosing a UI library
Set uiLibrary in src/layout.ts:
export default {
uiLibrary: 'windui', // 'windui' (default) | 'linoria'
...
} satisfies LayoutConfig
| Value | Library |
|---|---|
'windui' | Wind UI — modern look, mobile support, built-in config persistence |
'linoria' | LinoriaLib — mature, widely supported |
The compiler reads this automatically. rb build --ui windui overrides it.
Widget builders
Return an array of widget builders from defineComponent. Each one is bound to a signal with .bind(signal).
toggle
toggle('Speed Hack').bind(enabled)
toggle('Speed Hack', { default: true, tip: 'Enables speed' }).bind(enabled)
Signal must be PulseSignal<boolean>.
slider
slider('Walk Speed', { min: 16, max: 250 }).bind(speed)
slider('FOV', { min: 1, max: 120, suffix: '°' }).bind(fov)
slider('Speed', { min: 0, max: 100, default: 50 }).bind(speed)
Signal must be PulseSignal<number>.
dropdown
dropdown('Mode', { options: ['Normal', 'Rage', 'Legit'] }).bind(mode)
dropdown('Mode', { options: ['Normal', 'Rage'], default: 'Rage' }).bind(mode)
Signal must be PulseSignal<string>.
multidropdown
multidropdown('Remove', { options: ['Decor', 'Trees', 'Rocks'] }).bind(selected)
Signal must be PulseSignal<string[]>.
button
button('Reset', () => speed.set(16))
button('Apply', someFunction)
keybind
keybind('Activate', { default: 'F' }).bind(enabled)
keybind('Lock', { default: 'RightBracket' }).bind(lockEnabled)
default matches Enum.KeyCode names without the prefix.
separator
separator()
A horizontal divider line.
label
label('Advanced Settings')
Static text inside a groupbox.
Page layout — definePage
Pages live in src/pages/. Files are loaded in filename order — 1_Combat.ts before 2_Visuals.ts.
// src/pages/1_Combat.ts
definePage('Combat', { icon: 'swords' }, () => [
groupbox('left', 'Player', { icon: 'person', mount: 'SpeedHack' }),
groupbox('left', 'Player', { icon: 'person', mount: 'FOVChanger' }),
groupbox('right', 'Visuals', { icon: 'eye', mount: 'PlayerESP' }),
])
// src/pages/2_Visuals.ts
definePage('Visuals', { icon: 'eye' }, () => [
groupbox('left', 'Targeting', { icon: 'crosshair', mount: 'Aimbot' }),
groupbox('right', 'Damage', { icon: 'zap', mount: 'KillAura' }),
])
The mental model is Next.js pages — filename determines order, each file is one tab.
groupbox options
groupbox(
'left' | 'right', // column
'Title', // display name
{
icon: 'house', // Lucide icon name (lucide.dev/icons)
mount: 'ComponentName', // component whose widgets go here
premium: true, // lock behind premium key — shows inline key-entry UI
}
)
All widgets declared in SpeedHack's return array appear inside its mounted groupbox, in declaration order.
When premium: true is set, the groupbox renders a locked state showing:
- A key input field
- Copy Link button (opens
premium.getKeyUrl) - Check Key button
- Status indicator (Valid / Invalid / Copied)
When the user enters a valid premium key, all premium groupboxes across the script unlock live — no re-injection needed.
Reserved page names
"Home" and "Settings" are built-in tabs created by the framework. Do not name your pages "Home" or "Settings":
// ✗ conflicts with built-in Home tab
definePage('Home', ...)
// ✓ use any other name
definePage('Combat', { icon: 'swords' }, () => [...])
layout.ts — window config
// src/layout.ts
export default {
title: 'MyScript', // window title bar
version: '1.0.0', // shown in Home tab info card
description: 'A powerful script.', // shown in Home tab
discord: '', // if set, "Join Discord" button in Home tab
author: '', // optional credit
toggleKey: 'RightControl', // Enum.KeyCode name
size: [850, 560] as [number, number],
uiLibrary: 'windui' as 'windui' | 'linoria',
theme: 'Indigo', // WindUI theme name
icon: 'code-2', // Lucide icon for the window
folder: 'MyScript', // save/config folder name
acrylic: true,
transparency: 0.8,
openButtonMobileOnly: true,
openButtonIcon: 'code-2',
themes: [] as LayoutConfig['themes'],
compatExclude: [] as string[],
// ── Key system (optional) — gates entire UI behind a key ────────────────
// keySystem: {
// title: 'Key Required',
// note: 'Get your key from Discord',
// saveKey: true,
// getKeyUrl: 'https://discord.gg/example',
// keys: ['KEY_1', 'KEY_2'], // static list
// // OR: validatorUrl: 'https://yoursite.com/validate?key=',
// },
// ── Premium tier (optional) — locks { premium: true } groupboxes ────────
// premium: {
// keys: ['PREMIUM_KEY_1'],
// // OR: validatorUrl: 'https://yoursite.com/premium?key=',
// getKeyUrl: 'https://yoursite.com/premium',
// },
} satisfies LayoutConfig
Adapter-agnostic notifications
_PulseNotify('Feature activated.', 3)
Both adapters define this. Never call _Library:Notify(...) directly — that is Linoria-only.
Settings tab
The Settings tab (theme picker, save/load config, menu keybind) is provided by the CDN adapter — you don't write or copy it. It appears automatically.