# UI development

The HUD's interface is a React 18 + TypeScript app in `web/`, built with Vite into `web/build`.
This page is for developers who change that UI: how to build it, how to preview it in a browser, and the messages that pass between Lua and React.

You need this page when you edit `editable/shared/js.json` (it is compiled into the UI), or change anything in `web/src`.
Everything else — `config.lua`, locales — works without a build.

## Building the UI

You need [Node.js](https://nodejs.org) 18 or newer and pnpm.

```bash
cd koja-hud/web
pnpm install      # once, or after updating the HUD
pnpm build        # type-checks, then writes web/build
```

Restart the resource and reconnect — or press F8 and run `ensure koja-hud` — to load the new build.

:::hint{type="info" title="No pnpm?"}
`corepack enable` makes `pnpm` available with any recent Node.js.
`npx pnpm install` and `npx pnpm build` also work.
:::

| Script | What it does |
| --- | --- |
| `pnpm build` | Production build into `web/build` |
| `pnpm start` | Dev server with hot reload, for the browser |
| `pnpm start:game` | Rebuilds `web/build` on every save, for testing in-game |
| `pnpm lint` | ESLint |

:::hint{type="warning"}
`web/build` is what players download.
A server running from source without a build shows nothing, and an old build keeps showing your old `js.json`.
:::

## Previewing in a browser

`pnpm start` opens the UI in a normal browser with sample data: status bars, the speedometer, notifications, a progress bar, the text UI, the killfeed and status effects, and the settings menu.
It is the quickest way to work on styles.
The sample data lives at the top of `web/src/containers/App.tsx`.

## Project layout

| Path | Contents |
| --- | --- |
| `web/src/containers/App.tsx` | Root: start-up sync with the game, HUD, settings, vehicle menu |
| `web/src/components/Hud/` | One folder per element: `Status`, `CarHud`, `Notify`, `Progressbar`, `Textui`, `Informations`, `Keybinds`, `Compass`, `Weapon`, `Watermark`, `Killfeed`, `Warnings`, `StatusEffects` |
| `web/src/components/Hud/*/styles/` | The style variants of status, speedometer and notifications |
| `web/src/components/Settings/` | Settings menu, layout editor, controls (switch, slider, colour, dropdown) |
| `web/src/components/VehicleMenu/` | Vehicle control menu |
| `web/src/providers/` | Settings (stored in browser storage), locale, visibility |
| `web/src/utils/sanitizeHtml.ts` | The allowlist for HTML in notifications |

## Messages from Lua to the UI

Lua sends `SendNUIMessage({ action = name, data = payload })` through `KOJA.Client.SendReactMessage(name, payload)`; React subscribes with `useNuiEvent(name, handler)`.

| Action | Payload | Effect |
| --- | --- | --- |
| `koja_hud:openHud` | `true` | Show the HUD after the character loads |
| `koja_hud:refreshHud` | partial `HudData` | Merge new values — see below |
| `koja_hud:showCarHud` | boolean | Entered or left a vehicle |
| `koja_hud:toggleHud` | boolean | `ToggleHUD` export |
| `koja_hud:pauseHud` | boolean | Pause menu opened or closed |
| `koja_hud:hideComponents` | `{ status, carhud, … }` | Forced-hidden components |
| `koja_hud:setTalking` | boolean | Voice chat activity |
| `koja_hud:sendNotify` | notification | New notification |
| `koja_hud:startProgressbar` | `{ label, icon, time, … }` | Start the progress bar |
| `koja_hud:cancelProgressbar` | — | Stop it |
| `koja_hud:startTextui` | `{ input, type, desc }` | Show the text UI |
| `koja_hud:cancelTextui` | — | Hide it |
| `koja_hud:killfeed` | `{ killer, victim, weapon, distance, headshot, self }` | New killfeed entry |
| `koja_hud:killstreakReset` | `{}` | The player died |
| `koja_hud:statuseffects` | list of effects | Full list of active effects |
| `koja_hud:minimapRect` | `{ enabled, left, bottom, width, height }` | Where the minimap is, for the layout editor |
| `koja_hud:openSettings` | `true` | Open the settings menu |
| `koja_hud:openVehicleMenu` | vehicle state | Open the vehicle menu |
| `koja_hud:closeVehicleMenu` | `true` | Close it |
| `setLocale` | locale object | Texts from `locales/<code>.json` |

Note the casing: `startTextui` and `cancelTextui` here, while the public net events are `koja_hud:startTextUI` and `koja_hud:cancelTextUI`.

### `koja_hud:refreshHud`

Every field is optional; whatever is sent is merged into the current state.

```lua
KOJA.Client.SendReactMessage('koja_hud:refreshHud', {
    status = {
        { id = 'health', status = 87 },   -- health, shield, food, water, stamina, oxygen, voice, stress
    },
    informations = { cash = 1200, bank = 54000, job = 'Police', id = 12 },
    car = {
        speed = 23,                 -- metres per second; the UI converts to km/h or mph
        gear = 3,                   -- or 'N', 'R'
        engine = true,
        keys = false,               -- vehicle locked
        fuel = { type = 'gas', min = 64, max = 100 },   -- type 'gas' or 'electric'
        nitro = 80,
        seatbelt = true,
        rpm = 0.42,                 -- 0-1
        engineHealth = 96,
        lights = true, highbeam = false,
        indicators = 0,             -- GetVehicleIndicatorLights bitmask
        cruise = false
    },
    compass = { heading = 270 },
    weapon = { active = true, name = 'Pistol', ammo = 9, total = 48 },
    watermark = { text = 'MY SERVER' }
})
```

## Callbacks from the UI to Lua

React calls `fetchNui(name, body)`, which posts to `https://koja-hud/<name>`; Lua answers in `RegisterNUICallback`.

| Callback | Body | Does |
| --- | --- | --- |
| `koja_hud:getConfig` | — | Returns server defaults, enabled features (`compass`, `watermark`) and the keys allowed for binds |
| `loadLocale` | — | Lua replies with a `setLocale` message |
| `koja_hud:closeSettings` | — | Releases NUI focus |
| `koja_hud:updateMinimap` | `{ x, y, scale }` | Moves and resizes the minimap |
| `koja_hud:updateMinimapPosition` | `{ x, y }` | Moves the minimap |
| `koja_hud:updateMinimapVisibility` | `{ mode }` | `always`, `vehicle`, `foot`, `never` |
| `koja_hud:updateKillfeed` | `{ enabled, anyDistance, distance }` | Player's killfeed filter |
| `koja_hud:updateCustomBinds` | `{ binds = [{ key, command }] }` | Player's custom binds |
| `koja_hud:updateBuiltinBinds` | `{ binds = [{ id, key, command }] }` | Player's keys for the HUD's own actions |
| `koja_hud:closeVehicleMenu` | — | Releases NUI focus |
| `koja_hud:vehicleAction` | `{ action, index? }` | `engine`, `seatbelt`, `lights`, `indicatorLeft`, `indicatorRight`, `hazard`, `door` (with `index` 0–5); returns the new vehicle state |

On start the UI calls `getConfig` and then sends the player's stored binds, minimap position, minimap visibility and killfeed filter back to Lua, so player choices apply from the first frame.

## Adding a setting

1. Add the default under `default_settings` in `editable/shared/js.json`.
2. Add a control to a window in `options_settings`, with `id` set to the setting's path — the menu builds itself from that list.
3. Add its texts to `ui.settings` in every locale file.
4. Read it in your component with `const { settings } = useSettings()`.
5. `pnpm build`.

The settings type is inferred from `js.json`, so TypeScript knows about the new field without further changes.
