Layouts
Build a custom radar detector skin with HTML, CSS, sounds, and the state callback API.
Overview
A layout is a self-contained folder under layouts/ containing a ui.html entry point plus any icons and sounds it needs. The SolidJS HUD panel fetches the HTML at runtime, injects it into the page, and calls a JavaScript callback every time the radar state changes.
Lua does no presentation logic — the layout decides what to show and when.
Folders in layouts/ are auto-discovered on server start and appear in the HUD Layout selector in /tDet.
Folder structure
layouts/
sounds/ shared sound files — available to all layouts
blip.wav
gps_connected.wav
ka_band.wav
MyLayout/
ui.html required — layout entry point
icons/ PNG / SVG assets
sounds/ optional layout-specific sound overridesImage and audio src attributes in ui.html are automatically rewritten to be absolute before injection, so use simple relative paths:
<img src="icons/radar_frame.png" />For sounds, use the injected window.__tdetSoundsDir variable (points to the shared layouts/sounds/ folder) rather than hard-coding a path:
var soundsDir = window.__tdetSoundsDir || './sounds/';
var blip = new Audio(soundsDir + 'blip.ogg');window.__tdetLayoutDir is also available and points to the current layout's own folder, for layout-specific assets.
Create a layout
Create the folder
layouts/MyLayout/ — the folder name is what players see in the /tDet layout selector.
Add ui.html
This is the full layout. It can contain all HTML, CSS, and a single <script data-layout-script> block for logic.
Add assets
Drop icons and sounds into icons/ and sounds/. The script tries .ogg for sounds and falls back to .wav.
Restart the resource
restart tDetectorThe layout appears in /tDet → HUD Layout.
State callback
Register window.__tdetLayoutUpdate inside a <script data-layout-script> block. It is called after every state update.
<script data-layout-script>
(function () {
window.__tdetLayoutUpdate = function (state, root) {
// state — object with the keys below
// root — this layout's container element (use for querySelector)
};
// Optional: called when the layout is replaced or the component unmounts.
// Cancel timers, remove listeners, reset internal variables here.
window.__tdetLayoutDestroy = function () {};
})();
</script>Use an IIFE
Always wrap layout scripts in an immediately-invoked function expression (function () { … })(). Multiple layouts may be injected over a session — an IIFE prevents variable collisions between layout loads.
Available state keys
| Key | Type | Description |
|---|---|---|
radarOn | boolean | Detector is active. false is the reset signal — hide everything and reset timers. |
inVehicle | boolean | Player is currently in a vehicle |
distance | number | Distance in metres to the closest active radar gun. -1 when no radar is within range (≥ 300 m). |
speed | number | Player's current speed in the selected unit (rounded integer) |
unit | string | "mph" or "kph" |
facing | string | "front" — radar source is ahead. "back" — radar source is behind. |
State lifecycle
radarOn: false— sent once when the detector is turned off. Reset all timers, hide all elements, clear any internal state.radarOn: true, inVehicle: true— normal operation. Update the display each call.- Updates arrive every ~500 ms while the detector is on.
Sounds
Shared sounds live in layouts/sounds/. Load them using the injected window.__tdetSoundsDir path helper:
var soundsDir = window.__tdetSoundsDir || './sounds/';
function makeSound(name) {
var a = new Audio(soundsDir + name + '.ogg');
a.onerror = function () { a.src = soundsDir + name + '.wav'; }; // wav fallback
return a;
}
var blip = makeSound('blip');
function play(audio) {
audio.cloneNode().play().catch(function () {});
}Using cloneNode() allows the same sound to overlap itself — important for a blip that fires repeatedly at short intervals.
Layout-specific sounds can live in MyLayout/sounds/ and are accessed via window.__tdetLayoutDir:
var mySound = new Audio(window.__tdetLayoutDir + 'sounds/custom.ogg');Minimal example
<div>
<style>
.det { width: 200px; padding: 12px; background: #111; color: #0f0;
font-family: monospace; border-radius: 4px; }
.det-hidden { display: none; }
</style>
<div class="det" id="det-root">
<div id="det-distance">No signal</div>
<div id="det-speed"></div>
</div>
</div>
<script data-layout-script>
(function () {
function el(id) { return document.getElementById(id); }
window.__tdetLayoutUpdate = function (state) {
if (!state.radarOn) {
el('det-distance').textContent = 'No signal';
el('det-speed').textContent = '';
return;
}
var dist = state.distance;
el('det-distance').textContent =
dist !== -1 ? (state.facing === 'front' ? 'FRONT' : 'REAR') + ' — ' + Math.round(dist) + 'm' : 'Scanning…';
el('det-speed').textContent =
(state.speed || 0) + ' ' + (state.unit || 'mph');
};
})();
</script>UI Studio
Build and publish custom layouts with the UI Studio. See the Marketplace docs for the full walkthrough — creating drafts, design vs HTML mode, lint rules, bundle limits, forking, and publishing.
Default layout reference
The built-in Youniden7 layout is a direct port of the original Youniden detector display. It uses eight proximity rings, front/rear direction arrows, a speed readout, and a startup boot sequence. The source is at layouts/Youniden7/ui.html and is the best starting point for a custom skin.
