Layouts

Build a custom radar detector skin with HTML, CSS, sounds, and the state callback API.

layouts/Layout Authors

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 tDetector

The 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

KeyTypeDescription
radarOnbooleanDetector is active. false is the reset signal — hide everything and reset timers.
inVehiclebooleanPlayer is currently in a vehicle
distancenumberDistance in metres to the closest active radar gun. -1 when no radar is within range (≥ 300 m).
speednumberPlayer's current speed in the selected unit (rounded integer)
unitstring"mph" or "kph"
facingstring"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.

On this page

Need help?

Ask on Discord