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