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 overrides
Image 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.
