Framework Wiki

Benjis.UI Framework

This wiki documents the current Benjis.UI clientside framework, including the theme system, windowing, panels, buttons, fonts, labels, tabs, model panels, scroll panels, list helpers, and text entry controls. It is written against the new framework files, not the older version of the wiki.

What this framework provides

A small themed VGUI layer built around factory functions such as CreateFrame, CreatePanel, CreateButton, CreateScroll, CreateTabs, CreateTextEntry, CreateModelPanel, and list/tile helpers. The framework centralises colours, font presets, animation timing, and common panel styles.

Design goals visible in the code

The new version heavily reduces repeated per-frame work by caching fonts, using local theme aliases, precomputing paint handlers, and avoiding avoidable allocations inside Paint and Think callbacks. That matters when you open lots of panels at once, such as F4 pages, inventories, admin tools, or shop interfaces.

Important: this is a clientside UI framework. It improves presentation and interaction, but it does not replace server-side validation. Any DarkRP purchase, command, weapon spawn, shipment action, rank action, or administrative action triggered from these controls must still be validated on the server.

Suggested File Load Order

Several modules depend on shared state created by earlier files, especially the theme and font systems. A safe include order for a typical clientside loader is shown below.

-- lua/autorun/client/cl_benjis_ui_loader.lua

Benjis = Benjis or {}
Benjis.UI = Benjis.UI or {}

include("benjis_ui/cl_theme.lua")
include("benjis_ui/cl_fonts.lua")
include("benjis_ui/cl_panel.lua")
include("benjis_ui/cl_window.lua")
include("benjis_ui/cl_button.lua")
include("benjis_ui/cl_scroll.lua")
include("benjis_ui/cl_list.lua")
include("benjis_ui/cl_model.lua")
include("benjis_ui/cl_tabs.lua")
include("benjis_ui/cl_textentry.lua")
Why this order? cl_theme.lua defines the palette and layout constants. cl_fonts.lua sets up font APIs and label creation. cl_panel.lua depends on the theme existing. cl_window.lua, cl_button.lua, cl_list.lua, and the other modules all build on those shared APIs.

Quick Start

The example below creates a centered window with a card container, a header label, a search field, tabs, and a button row. It demonstrates how the framework is intended to be composed.

local frame = Benjis.UI:CreateFrame("Example Menu", 960, 640, "center")

local root = Benjis.UI:CreatePanel(frame.Content, "bg")
root:Dock(FILL)
root:DockMargin(Benjis.UI.Padding, Benjis.UI.Padding, Benjis.UI.Padding, Benjis.UI.Padding)

local headerCard = Benjis.UI:CreatePanel(root, "card")
headerCard:Dock(TOP)
headerCard:SetTall(88)
headerCard:DockMargin(0, 0, 0, 12)

local title = Benjis.UI:CreateLabel(headerCard, "Framework Demo", "Header")
title:Dock(TOP)
title:DockMargin(12, 12, 12, 4)

local subtitle = Benjis.UI:CreateLabel(
    headerCard,
    "This page shows how multiple Benjis.UI controls fit together.",
    "Text"
)
subtitle:Dock(TOP)
subtitle:DockMargin(12, 0, 12, 12)

local search = Benjis.UI:CreateSearchEntry(root, {
    placeholder = "Search items, players, or categories...",
    tall = 38
})
search:Dock(TOP)
search:DockMargin(0, 0, 0, 12)

local tabs = Benjis.UI:CreateTabs(root, {
    tabHeight = 36,
    tabSpacing = 8,
    contentStyle = "transparent"
})
tabs:Dock(FILL)

tabs:AddTab("dashboard", "Dashboard", function(page)
    local panel = Benjis.UI:CreatePanel(page, "card")
    panel:Dock(FILL)

    local label = Benjis.UI:CreateLabel(page, "Dashboard content goes here.", "Text")
    label:Dock(TOP)
    label:DockMargin(12, 12, 12, 0)
end)

tabs:AddTab("settings", "Settings", function(page)
    local save = Benjis.UI:CreateButton(page, "Save Settings")
    save:Dock(TOP)
    save:DockMargin(0, 12, 0, 0)
    save.DoClick = function()
        chat.AddText(Color(0, 200, 0), "Saved settings")
    end
end)

Theme System

The theme file defines the shared palette and the two main global layout constants used throughout the framework: Benjis.UI.AnimTime and Benjis.UI.Padding.

Benjis.UI.Theme = {
    BG      = Color(18, 18, 18, 235),
    Panel   = Color(25, 25, 25, 150),
    Hover   = Color(35, 35, 35, 220),
    Accent  = Color(200, 60, 60),
    Divider = Color(0, 0, 0, 180),
    Text    = color_white,
    Muted   = Color(160, 160, 160),
    _CardOutline = Color(0, 0, 0, 120),
}

Benjis.UI.AnimTime = 0.25
Benjis.UI.Padding  = 12

Theme fields

KeyPurposeUsed by
BGMain background colour for windows and some text entries.Frames, panel style bg, text entries.
PanelDefault panel/body surface.Buttons, content panels, rows, tabs, item tiles.
HoverHover/active surface colour.Buttons, tabs, text entry states, tile hovers.
AccentHighlight colour.Scroll grip, tab underline, cursor/highlight states.
DividerOutline or separator colour.Text entry border.
TextPrimary foreground text colour.Labels, buttons, tabs, rows, tiles.
MutedSecondary foreground text colour.Close button, placeholder text, tile amount text.
_CardOutlineInternal helper colour for the card outline.CreatePanel(..., "card").

Best practice for theme overrides

Do not mutate the existing colour tables in place if you want to override the theme. Replace entries with new Color(...) values instead. This matches the framework’s own guidance and prevents hard-to-track issues caused by shared colour references.

-- Good
Benjis.UI.Theme.Accent = Color(52, 152, 219)
Benjis.UI.Theme.Panel  = Color(22, 22, 26, 190)

-- Avoid in-place mutation patterns like this
-- Benjis.UI.Theme.Accent.r = 52

Theme override example for a police terminal

local oldAccent = Benjis.UI.Theme.Accent
local oldHover  = Benjis.UI.Theme.Hover

Benjis.UI.Theme.Accent = Color(40, 120, 255)
Benjis.UI.Theme.Hover  = Color(32, 42, 72, 220)

local frame = Benjis.UI:CreateFrame("Police MDT", 1100, 700, "sidebar_right")

-- restore later if you only want temporary styling
Benjis.UI.Theme.Accent = oldAccent
Benjis.UI.Theme.Hover  = oldHover

Fonts and Labels

The font module handles font creation, caching, preset registration, preset lookup, and the CreateLabel helper. Fonts are resolution-scaled using a cached factor derived from ScrH(), clamped between 0.85 and 1.25. When the screen size changes, the cache is reset and rebuilt on demand.

Available APIs

FunctionPurpose
Benjis.UI:GetFont(name, size, opts)Creates or returns a cached surface font ID.
Benjis.UI:RegisterFontPreset(id, data)Registers a preset name for reuse.
Benjis.UI:GetFontPreset(id)Returns the font ID for a preset, with fallback behaviour if the preset is missing.
Benjis.UI:CreateLabel(parent, text, preset)Creates a wrapped DLabel using a preset and the theme text colour.

Built-in font presets

Title  = { font = "Roboto",      size = 20, options = { weight = 800 } }
Header = { font = "Roboto",      size = 18, options = { weight = 700 } }
Text   = { font = "Roboto",      size = 16, options = { weight = 500 } }
Small  = { font = "Roboto",      size = 13, options = { weight = 500 } }
Button = { font = "Roboto",      size = 16, options = { weight = 600 } }
Mono   = { font = "Courier New", size = 14, options = { weight = 500 } }

Manual font creation

local fontID = Benjis.UI:GetFont("Tahoma", 18, {
    weight = 700,
    outline = false,
    antialias = true
})

hook.Add("HUDPaint", "ExampleFontUsage", function()
    draw.SimpleText("Benjis.UI custom font", fontID, 40, 40, color_white)
end)

Registering a new preset

Benjis.UI:RegisterFontPreset("JobCardTitle", {
    font = "Roboto",
    size = 22,
    options = {
        weight = 900,
        antialias = true
    }
})

local lbl = Benjis.UI:CreateLabel(parent, "Civil Protection", "JobCardTitle")

Using labels correctly

CreateLabel creates a wrapped label, enables vertical auto-stretch, sets its alignment to left/vertical-center, and disables mouse input. It is suited for descriptions, body text, and titles where you want standardised typography with minimal setup.

local desc = Benjis.UI:CreateLabel(panel,
    "This shipment can only be bought by Gun Dealers and requires a valid license.",
    "Text"
)
desc:Dock(TOP)
desc:DockMargin(12, 0, 12, 12)
Missing preset behaviour: GetFontPreset does not hard-crash when a preset does not exist. It logs through ErrorNoHalt and falls back to the Text preset if available, or Default as a final fallback.

Frames and Windows

CreateFrame(title, w, h, style) creates an animated DFrame with a custom header, close button, blur-backed paint, and a themed frame.Content panel for your body layout.

Supported styles

StyleOpen animationClose animationTypical use
sidebarSlides in from the left edge.Slides back out to the left.F4 menus, side inventories, admin drawers.
sidebar_rightSlides in from the right edge.Slides back out to the right.Context panels, secondary inspectors, side tools.
centerStarts lower and transparent, then moves upward while fading in.Moves downward slightly while fading out.Confirm dialogs, settings windows, modal pages.

Minimal example

local frame = Benjis.UI:CreateFrame("F4 Menu", 1100, 700, "sidebar")

local content = frame.Content
local body = Benjis.UI:CreatePanel(content, "transparent")
body:Dock(FILL)
body:DockMargin(Benjis.UI.Padding, Benjis.UI.Padding, Benjis.UI.Padding, Benjis.UI.Padding)

Closing a frame safely

local frame = Benjis.UI:CreateFrame("Example", 700, 500, "center")

local btn = Benjis.UI:CreateButton(frame.Content, "Close")
btn:Dock(TOP)
btn:DockMargin(12, 12, 12, 0)
btn.DoClick = function()
    if frame.CloseAnimated then
        frame:CloseAnimated()
    else
        frame:Remove()
    end
end

Header and content structure

The frame internally creates a 56px tall header with the title on the left and a custom close button on the right. Everything below that header is exposed as frame.Content, which is itself a themed DPanel. In practice, you should treat frame.Content as your main container and build all layout inside it.

Frame used as a DarkRP command menu

concommand.Add("open_job_terminal", function()
    if IsValid(BenjisJobTerminal) then
        BenjisJobTerminal:Remove()
    end

    local frame = Benjis.UI:CreateFrame("Job Terminal", 980, 640, "sidebar_right")
    BenjisJobTerminal = frame

    local left = Benjis.UI:CreatePanel(frame.Content, "panel")
    left:Dock(LEFT)
    left:SetWide(260)
    left:DockMargin(12, 12, 12, 12)

    local right = Benjis.UI:CreatePanel(frame.Content, "card")
    right:Dock(FILL)
    right:DockMargin(0, 12, 12, 12)
end)

Panels

CreatePanel(parent, paintStyle) returns a themed DPanel with a pre-selected paint function. The style is resolved once when the panel is created rather than on every frame.

Supported paint styles

StyleEffectWhen to use it
panelDraws a flat box with Theme.Panel.Default content containers and standard group boxes.
bgDraws a flat box with Theme.BG.Deeper backgrounds and inner window roots.
transparentDoes not draw anything.Pure layout containers, docking wrappers, spacing shells.
cardDraws a rounded card with outline.Feature blocks, setting groups, stats cards, inventory sections.

Examples

local root = Benjis.UI:CreatePanel(frame.Content, "transparent")
root:Dock(FILL)

local sidebar = Benjis.UI:CreatePanel(root, "panel")
sidebar:Dock(LEFT)
sidebar:SetWide(240)

local content = Benjis.UI:CreatePanel(root, "bg")
content:Dock(FILL)
content:DockMargin(12, 0, 0, 0)

local card = Benjis.UI:CreatePanel(content, "card")
card:Dock(TOP)
card:SetTall(120)
card:DockMargin(12, 12, 12, 0)

Unknown styles

If you pass an unknown style name, the framework falls back to the default panel painter. That gives safe behaviour instead of a broken panel.

Buttons and Confirm Dialogs

CreateButton(parent, text, tall) creates a themed DButton with a hover animation. The button stores the visible label in btn.Label, and also exposes btn:SetLabel(txt) so you can update the display text later.

Basic button

local btn = Benjis.UI:CreateButton(parent, "Save Settings")
btn:Dock(TOP)
btn:DockMargin(0, 0, 0, 8)

btn.DoClick = function()
    chat.AddText(Color(0, 255, 0), "Settings saved")
end

Changing label text dynamically

local ready = false
local btn = Benjis.UI:CreateButton(parent, "Start")
btn:Dock(TOP)

btn.DoClick = function(self)
    ready = not ready
    self:SetLabel(ready and "Stop" or "Start")
end

Button list example

for i = 1, 5 do
    local btn = Benjis.UI:CreateButton(parent, "Option " .. i)
    btn:Dock(TOP)
    btn:DockMargin(0, 0, 0, 6)
    btn.DoClick = function()
        print("Clicked option", i)
    end
end

How hover rendering works

The button smoothly lerps an internal hover value toward 1 while hovered and back toward 0 otherwise. That value is then passed into Benjis.UI:LerpColor to blend between Theme.Panel and Theme.Hover.

LerpColor caveat

Do not store the return value of Benjis.UI:LerpColor. The framework uses one shared mutable colour buffer for performance. Consume the result immediately in a draw call. Storing it for later will produce incorrect colours because the next LerpColor call overwrites that same buffer.
-- Correct
local function PaintButton(self, w, h)
    draw.RoundedBox(0, 0, 0, w, h,
        Benjis.UI:LerpColor(self.Hover, Benjis.UI.Theme.Panel, Benjis.UI.Theme.Hover)
    )
end

-- Wrong
local col = Benjis.UI:LerpColor(0.5, color_white, color_black)
-- storing this and reusing it later is unsafe

Confirm dialog

Benjis.UI:Confirm(title, text, yesText, noText, yesFunc, noFunc) is the framework’s styled replacement for Derma_Query. It creates a centered animated frame, a message label, and two side-by-side buttons.

Benjis.UI:Confirm(
    "Delete Shipment",
    "Are you sure you want to delete this shipment preset?",
    "Delete",
    "Cancel",
    function()
        print("Confirmed delete")
    end,
    function()
        print("Cancelled")
    end
)

Using Confirm before a DarkRP action

deleteButton.DoClick = function()
    Benjis.UI:Confirm(
        "Clear Warrants",
        "This will request a warrant reset for all players. Continue?",
        "Proceed",
        "Abort",
        function()
            net.Start("BenjisPolice_RequestWarrantReset")
            net.SendToServer()
        end
    )
end
The UI confirmation is not a security layer. The net receiver must still validate that the sender has the correct permissions server-side.

Scroll Panels

CreateScroll(parent) returns a styled DScrollPanel with a slim 6px scrollbar. The up and down buttons, plus the bar background, are intentionally not painted. Only the grip is drawn, using Theme.Accent.

Simple scroll list

local scroll = Benjis.UI:CreateScroll(parent)
scroll:Dock(FILL)

for i = 1, 25 do
    local btn = Benjis.UI:CreateButton(scroll, "Entry " .. i)
    btn:Dock(TOP)
    btn:DockMargin(0, 0, 0, 8)
end

Scrollable card stack

local scroll = Benjis.UI:CreateScroll(parent)
scroll:Dock(FILL)

for i = 1, 8 do
    local card = Benjis.UI:CreatePanel(scroll, "card")
    card:Dock(TOP)
    card:SetTall(100)
    card:DockMargin(0, 0, 8, 12)

    local lbl = Benjis.UI:CreateLabel(card, "Card #" .. i, "Header")
    lbl:Dock(TOP)
    lbl:DockMargin(12, 12, 12, 0)
end

Lists and Item Tiles

The list module provides four helpers: CreateList, AddListRow, CreateGrid, and AddItemTile. These cover common scrollable rows and grid-style item tiles.

CreateList(parent)

This is a convenience wrapper around CreateScroll that docks the scroll panel to fill its parent.

local list = Benjis.UI:CreateList(parent)

AddListRow(list, text, stripColor, iconPath)

This adds a clickable row implemented as a DButton. It supports an optional coloured strip on the left and an optional icon image. The text offset automatically changes if an icon is supplied.

local list = Benjis.UI:CreateList(parent)

for _, ply in ipairs(player.GetAll()) do
    local row = Benjis.UI:AddListRow(
        list,
        ply:Nick(),
        team.GetColor(ply:Team()),
        "icon16/user.png"
    )

    row.DoClick = function()
        print("Selected player:", ply:Nick())
    end
end

Player list without icons

for _, ply in ipairs(player.GetAll()) do
    local row = Benjis.UI:AddListRow(list, ply:Nick(), team.GetColor(ply:Team()))
    row.DoClick = function()
        chat.AddText(color_white, "Viewing ", Color(255, 200, 0), ply:Nick())
    end
end

CreateGrid(parent, spacing)

This returns two values: a scroll panel and a DIconLayout. The scroll panel is docked to fill its parent, and the icon layout is created inside it. Use the layout object for adding tiles.

local scroll, grid = Benjis.UI:CreateGrid(parent, 12)
scroll:Dock(FILL)

AddItemTile(grid, data)

This creates a 110x140 button tile with a hover state, model preview, item name, and quantity string. The data table is expected to contain at least name, amount, and model.

local _, grid = Benjis.UI:CreateGrid(parent, 12)

local tile = Benjis.UI:AddItemTile(grid, {
    name = "Pistol Ammo",
    amount = 3,
    model = "models/items/boxsrounds.mdl"
})

tile.DoClick = function()
    print("Clicked ammo tile")
end

Updating a tile after creation

The tile exposes tile:SetName(str) and tile:SetAmount(n), which update the prebuilt display strings without rebuilding the panel.

local tile = Benjis.UI:AddItemTile(grid, {
    name = "Lockpick",
    amount = 1,
    model = "models/weapons/w_crowbar.mdl"
})

-- later
tile:SetAmount(2)
tile:SetName("Advanced Lockpick")

Inventory grid example

local scroll, grid = Benjis.UI:CreateGrid(parent, 10)
scroll:Dock(FILL)

local items = {
    { name = "Health Kit", amount = 2, model = "models/items/healthkit.mdl" },
    { name = "Armor Battery", amount = 1, model = "models/items/battery.mdl" },
    { name = "Lockpick", amount = 3, model = "models/weapons/w_crowbar.mdl" }
}

for _, item in ipairs(items) do
    local tile = Benjis.UI:AddItemTile(grid, item)
    tile.DoClick = function()
        print("Use item:", item.name)
    end
end

Model Panels

The model module provides a static preview panel and an interactive preview panel.

CreateModelPanel(parent, model)

This creates a non-interactive DModelPanel with fixed camera settings, disabled idle rotation, and visibility-aware manual painting toggling. It is intended for cheap static previews in tiles, cards, and lists.

local mdl = Benjis.UI:CreateModelPanel(parent, "models/player/kleiner.mdl")
mdl:SetSize(220, 320)
mdl:Dock(LEFT)
mdl:DockMargin(0, 0, 12, 0)

CreateInteractiveModelPanel(parent, model)

This version allows drag-to-rotate and mousewheel zoom. It tracks the current yaw in pnl.Angles and the camera distance in pnl.Zoom.

local mdl = Benjis.UI:CreateInteractiveModelPanel(parent, LocalPlayer():GetModel())
mdl:Dock(FILL)

Character preview example

local card = Benjis.UI:CreatePanel(parent, "card")
card:Dock(FILL)

local mdl = Benjis.UI:CreateInteractiveModelPanel(card, LocalPlayer():GetModel())
mdl:Dock(FILL)

local apply = Benjis.UI:CreateButton(card, "Apply Outfit")
apply:Dock(BOTTOM)
apply:DockMargin(12, 12, 12, 12)

Static tile preview example

local tile = Benjis.UI:AddItemTile(grid, {
    name = "MP5",
    amount = 1,
    model = "models/weapons/w_smg1.mdl"
})
The static model panel disables idle animation and avoids needless rendering work when hidden. The interactive version only updates the camera when zoom changes and only updates the entity angle when the user is actively dragging and the mouse has actually moved.

Tabs

CreateTabs(parent, opts) provides a lightweight tab/page switcher for interfaces such as F4 subpages, inventories, admin panels, chatboxes, and settings menus. It does not use DPropertySheet. Instead, it manages a button strip and one page panel per tab.

Options

OptionPurposeDefault
tabHeightHeight of the tab button strip.34
tabSpacingSpacing between tab buttons.6
contentStylePaint style used for the content area panel."transparent"
onTabChangedCallback triggered after switching tabs.nil

Main methods

MethodPurpose
tabs:AddTab(id, title, buildFunc)Creates a button and page for a new tab.
tabs:SetTabLabel(id, title)Changes the display text and button width.
tabs:SetActiveTab(id)Shows the target page and hides all others.
tabs:GetActiveTab()Returns the active tab ID.
tabs:GetPage(id)Returns the page panel for a tab ID.

Basic tabs example

local tabs = Benjis.UI:CreateTabs(parent, {
    tabHeight = 36,
    tabSpacing = 8,
    contentStyle = "transparent"
})
tabs:Dock(FILL)

tabs:AddTab("jobs", "Jobs", function(page)
    local lbl = Benjis.UI:CreateLabel(page, "Available jobs go here.", "Text")
    lbl:Dock(TOP)
end)

tabs:AddTab("shipments", "Shipments", function(page)
    local lbl = Benjis.UI:CreateLabel(page, "Shipment listing goes here.", "Text")
    lbl:Dock(TOP)
end)

Using onTabChanged

local tabs = Benjis.UI:CreateTabs(parent, {
    onTabChanged = function(self, newID, oldID, page, button)
        print("Changed tab from", oldID, "to", newID)
    end
})

Changing a tab title later

tabs:SetTabLabel("jobs", "Jobs (12)")

DarkRP F4 style page layout example

local frame = Benjis.UI:CreateFrame("City Menu", 1180, 720, "sidebar")

local tabs = Benjis.UI:CreateTabs(frame.Content, {
    tabHeight = 40,
    tabSpacing = 10,
    contentStyle = "transparent"
})
tabs:Dock(FILL)
tabs:DockMargin(12, 12, 12, 12)

tabs:AddTab("jobs", "Jobs", function(page)
    local scroll = Benjis.UI:CreateScroll(page)
    scroll:Dock(FILL)
end)

tabs:AddTab("entities", "Entities", function(page)
    local scroll = Benjis.UI:CreateScroll(page)
    scroll:Dock(FILL)
end)

tabs:AddTab("weapons", "Weapons", function(page)
    local scroll = Benjis.UI:CreateScroll(page)
    scroll:Dock(FILL)
end)

tabs:AddTab("settings", "Settings", function(page)
    local entry = Benjis.UI:CreateTextEntry(page, { placeholder = "Search settings..." })
    entry:Dock(TOP)
end)

Text Entries and Search Boxes

The text entry module provides CreateTextEntry(parent, opts) and CreateSearchEntry(parent, opts). These are themed wrappers around DTextEntry with hover/focus interpolation, placeholder rendering, custom colours, and state setters.

Supported options

OptionPurpose
tallControl height. Default 36.
fontExplicit font ID to use.
presetFont preset name used if font is not supplied.
textColorTyped text colour.
cursorColorCursor colour.
highlightColorSelection highlight colour.
placeholderPlaceholder text shown while empty and unfocused.
placeholderColorPlaceholder text colour.
bgColorIdle background colour.
hoverColorHover background colour.
activeColorFocus background colour.
outlineColorBorder colour.
textInsetX, textInsetYText inset/padding.
roundedRounded corner size. Defaults to 6.
updateOnTypeWhether text updates on each typed character.
enterAllowedWhether pressing Enter is allowed.
numericNumeric-only input.
multilineEnable multiline mode.
valueInitial value.

Basic text entry

local entry = Benjis.UI:CreateTextEntry(parent, {
    placeholder = "Enter title...",
    tall = 38
})
entry:Dock(TOP)
entry:DockMargin(0, 0, 0, 12)

Numeric entry

local amountEntry = Benjis.UI:CreateTextEntry(parent, {
    placeholder = "Amount",
    numeric = true,
    value = 1,
    tall = 36
})

Multiline notes field

local notes = Benjis.UI:CreateTextEntry(parent, {
    placeholder = "Internal notes...",
    multiline = true,
    enterAllowed = true,
    tall = 140,
    textInsetY = 8
})
notes:Dock(TOP)

Search entry

CreateSearchEntry is a small wrapper that sets the default placeholder to Search..., increases left text inset to make space for an icon, and paints a magnifier icon on the left.

local search = Benjis.UI:CreateSearchEntry(parent, {
    placeholder = "Search warrants...",
    updateOnType = true
})
search:Dock(TOP)

search.OnValueChange = function(self, value)
    print("Search changed:", value)
end

Changing state colours at runtime

local entry = Benjis.UI:CreateTextEntry(parent, {
    placeholder = "Custom palette"
})

entry:SetStateColors(
    Color(12, 16, 22, 235),
    Color(18, 28, 42, 235),
    Color(24, 36, 58, 235),
    Color(0, 0, 0, 180)
)

entry:SetPlaceholderColor(Color(140, 165, 190))
The module explicitly notes that clientside validation is presentation-only. Never trust input from these entries for money, ranks, jobs, inventory actions, database writes, or administrative actions without strict server-side checks.

Recommended Usage Patterns

1. Use layout wrappers aggressively

Use transparent panels as layout shells. Keep visible paint styles for actual visual sections only. That keeps your structure readable and prevents unnecessary paint work.

local shell = Benjis.UI:CreatePanel(frame.Content, "transparent")
shell:Dock(FILL)
shell:DockMargin(12, 12, 12, 12)

local left = Benjis.UI:CreatePanel(shell, "panel")
left:Dock(LEFT)
left:SetWide(260)

local right = Benjis.UI:CreatePanel(shell, "transparent")
right:Dock(FILL)
right:DockMargin(12, 0, 0, 0)

2. Keep UI creation separate from action logic

Build the panel structure in one function and keep data loading, filtering, and net message handling in separate functions. That makes the framework easy to reuse across DarkRP jobs, entity menus, and admin tools.

local function BuildPlayerListPage(parent)
    local list = Benjis.UI:CreateList(parent)

    for _, ply in ipairs(player.GetAll()) do
        local row = Benjis.UI:AddListRow(list, ply:Nick(), team.GetColor(ply:Team()))
        row.DoClick = function()
            OpenPlayerInspector(ply)
        end
    end
end

3. Use theme and presets, not hardcoded styling

If every page uses the same theme and font presets, you can rebrand or rebalance contrast later without rewriting every menu.

4. Prefer static model panels in dense grids

Use CreateModelPanel for tiles and list entries. Reserve CreateInteractiveModelPanel for one or two prominent previews such as character customisation or item inspection.

Important Caveats

LerpColor returns shared state

Do not cache it. Pass it directly into draw.RoundedBox, draw.SimpleText, or another immediate draw call.

Theme aliases are captured

Some modules localise theme references when controls are created. If you replace the theme after panels already exist, existing controls may still be using earlier references until rebuilt.

Text entries do not secure anything

Clients can still spoof input. Validate values server-side, clamp lengths, check permissions, and rate-limit requests.

Interactive model panels cost more

They are optimised, but they still do more work than static previews. Avoid filling huge scrolling inventories with interactive model panels.

Extended Examples

Example 1: Searchable command list

local frame = Benjis.UI:CreateFrame("Command Browser", 900, 620, "center")

local root = Benjis.UI:CreatePanel(frame.Content, "transparent")
root:Dock(FILL)
root:DockMargin(12, 12, 12, 12)

local search = Benjis.UI:CreateSearchEntry(root, {
    placeholder = "Search chat commands...",
    updateOnType = true
})
search:Dock(TOP)
search:DockMargin(0, 0, 0, 12)

local list = Benjis.UI:CreateList(root)

local commands = {
    { name = "/dropmoney", color = Color(46, 204, 113) },
    { name = "/lockdown",  color = Color(231, 76, 60) },
    { name = "/wanted",    color = Color(241, 196, 15) },
    { name = "/job",       color = Color(52, 152, 219) }
}

local function RebuildList(filter)
    list:Clear()

    filter = string.Trim(string.lower(filter or ""))

    for _, cmd in ipairs(commands) do
        if filter == "" or string.find(string.lower(cmd.name), filter, 1, true) then
            local row = Benjis.UI:AddListRow(list, cmd.name, cmd.color, "icon16/script.png")
            row.DoClick = function()
                SetClipboardText(cmd.name)
            end
        end
    end
end

search.OnValueChange = function(self, value)
    RebuildList(value)
end

RebuildList()

Example 2: Simple inventory viewer

local frame = Benjis.UI:CreateFrame("Inventory", 1000, 700, "sidebar_right")

local wrap = Benjis.UI:CreatePanel(frame.Content, "transparent")
wrap:Dock(FILL)
wrap:DockMargin(12, 12, 12, 12)

local search = Benjis.UI:CreateSearchEntry(wrap, {
    placeholder = "Search inventory...",
    updateOnType = true
})
search:Dock(TOP)
search:DockMargin(0, 0, 0, 12)

local scroll, grid = Benjis.UI:CreateGrid(wrap, 10)
scroll:Dock(FILL)

local items = {
    { name = "Lockpick", amount = 2, model = "models/weapons/w_crowbar.mdl" },
    { name = "Medkit", amount = 1, model = "models/items/healthkit.mdl" },
    { name = "Battery", amount = 4, model = "models/items/battery.mdl" }
}

local function RebuildInventory(filter)
    grid:Clear()
    filter = string.Trim(string.lower(filter or ""))

    for _, item in ipairs(items) do
        if filter == "" or string.find(string.lower(item.name), filter, 1, true) then
            local tile = Benjis.UI:AddItemTile(grid, item)
            tile.DoClick = function()
                print("Selected item:", item.name)
            end
        end
    end
end

search.OnValueChange = function(self, value)
    RebuildInventory(value)
end

RebuildInventory()

Example 3: Administrative inspector with tabs

local frame = Benjis.UI:CreateFrame("Admin Inspector", 1180, 760, "sidebar")

local tabs = Benjis.UI:CreateTabs(frame.Content, {
    tabHeight = 40,
    tabSpacing = 8,
    contentStyle = "transparent",
    onTabChanged = function(self, newID, oldID)
        print("Switched from", oldID, "to", newID)
    end
})
tabs:Dock(FILL)
tabs:DockMargin(12, 12, 12, 12)

tabs:AddTab("players", "Players", function(page)
    local list = Benjis.UI:CreateList(page)

    for _, ply in ipairs(player.GetAll()) do
        local row = Benjis.UI:AddListRow(page, ply:Nick(), team.GetColor(ply:Team()), "icon16/user.png")
        row.DoClick = function()
            print("Inspect player", ply:SteamID())
        end
    end
end)

tabs:AddTab("logs", "Logs", function(page)
    local box = Benjis.UI:CreateTextEntry(page, {
        multiline = true,
        tall = 400,
        value = "Recent moderation actions will appear here..."
    })
    box:Dock(FILL)
end)

tabs:AddTab("preview", "Character Preview", function(page)
    local mdl = Benjis.UI:CreateInteractiveModelPanel(page, LocalPlayer():GetModel())
    mdl:Dock(FILL)
end)
In the final example, the players tab should create list rows inside the list panel, not directly on the page, when used in production. Keep parent references precise when building reusable pages.