Skip to main content

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
ValueLibrary
'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('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.