diff --git a/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf
index 1eb31c0..869b624 100644
--- a/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf
+++ b/arma/client/addons/actor/functions/fnc_handleUIEvents.sqf
@@ -36,7 +36,7 @@ switch (_event) do {
case "actor::open::atm": { [true] spawn EFUNC(bank,openUI); };
case "actor::open::bank": { [] spawn EFUNC(bank,openUI); };
case "actor::open::device": { hint "Device interaction is not yet implemented."; };
- case "actor::open::garage": { hint "Garage interaction is not yet implemented."; };
+ case "actor::open::garage": { [] spawn EFUNC(garage,openUI); };
case "actor::open::vgarage": { [] spawn EFUNC(garage,openVG); };
case "actor::open::org": { [] spawn EFUNC(org,openUI); };
case "actor::open::vlocker": { [FORGE_Locker_Box, player, false] spawn AFUNC(arsenal,openBox) };
diff --git a/arma/client/addons/garage/XEH_PREP.hpp b/arma/client/addons/garage/XEH_PREP.hpp
index 98bd123..9db23f1 100644
--- a/arma/client/addons/garage/XEH_PREP.hpp
+++ b/arma/client/addons/garage/XEH_PREP.hpp
@@ -1,3 +1,8 @@
-PREP(initGarageClass);
+PREP(handleUIEvents);
+PREP(initCatalogService);
+PREP(initClass);
+PREP(initSessionService);
+PREP(initUIBridge);
PREP(initVGClass);
+PREP(openUI);
PREP(openVG);
diff --git a/arma/client/addons/garage/XEH_postInitClient.sqf b/arma/client/addons/garage/XEH_postInitClient.sqf
index 0234ac5..7d53dc5 100644
--- a/arma/client/addons/garage/XEH_postInitClient.sqf
+++ b/arma/client/addons/garage/XEH_postInitClient.sqf
@@ -1,6 +1,9 @@
#include "script_component.hpp"
-if (isNil QGVAR(GarageClass)) then { call FUNC(initGarageClass); };
+if (isNil QGVAR(GarageCatalogService)) then { call FUNC(initCatalogService); };
+if (isNil QGVAR(GarageClass)) then { call FUNC(initClass); };
+if (isNil QGVAR(GarageSessionService)) then { call FUNC(initSessionService); };
+if (isNil QGVAR(GarageUIBridge)) then { call FUNC(initUIBridge); };
if (isNil QGVAR(VGClass)) then { call FUNC(initVGClass); };
[QGVAR(initGarage), {
@@ -11,12 +14,26 @@ if (isNil QGVAR(VGClass)) then { call FUNC(initVGClass); };
params [["_data", createHashMap, [createHashMap]]];
GVAR(GarageClass) call ["sync", [_data]];
+ if !(isNil QGVAR(GarageUIBridge)) then {
+ GVAR(GarageUIBridge) call ["refreshGarage", []];
+ };
}] call CFUNC(addEventHandler);
[QGVAR(responseSyncGarage), {
params [["_data", createHashMap, [createHashMap, []]]];
GVAR(GarageClass) call ["sync", [_data]];
+ if !(isNil QGVAR(GarageUIBridge)) then {
+ GVAR(GarageUIBridge) call ["refreshGarage", []];
+ };
+}] call CFUNC(addEventHandler);
+
+[QGVAR(responseGarageAction), {
+ params [["_payload", createHashMap, [createHashMap]]];
+
+ if !(isNil QGVAR(GarageUIBridge)) then {
+ GVAR(GarageUIBridge) call ["handleActionResponse", [_payload]];
+ };
}] call CFUNC(addEventHandler);
[QGVAR(initVG), {
diff --git a/arma/client/addons/garage/config.cpp b/arma/client/addons/garage/config.cpp
index 595b77a..07836a5 100644
--- a/arma/client/addons/garage/config.cpp
+++ b/arma/client/addons/garage/config.cpp
@@ -8,6 +8,7 @@ class CfgPatches {
name = COMPONENT_NAME;
requiredVersion = REQUIRED_VERSION;
requiredAddons[] = {
+ "forge_client_common",
"forge_client_main"
};
units[] = {};
@@ -17,3 +18,5 @@ class CfgPatches {
};
#include "CfgEventHandlers.hpp"
+#include "ui\RscCommon.hpp"
+#include "ui\RscGarage.hpp"
diff --git a/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf b/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf
index 2fe5dfb..ae6ee6e 100644
--- a/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf
+++ b/arma/client/addons/garage/functions/fnc_handleUIEvents.sqf
@@ -21,3 +21,46 @@
* Example:
* call forge_client_garage_fnc_handleUIEvents;
*/
+
+params ["_control", "_isConfirmDialog", "_message"];
+
+private _alert = fromJSON _message;
+private _event = _alert get "event";
+private _data = _alert get "data";
+
+diag_log format ["[FORGE:Client:Garage] Handling UI event: %1 with data: %2", _event, _data];
+
+switch (_event) do {
+ case "garage::close": {
+ if !(isNil QGVAR(GarageUIBridge)) then {
+ GVAR(GarageUIBridge) call ["handleClose", []];
+ };
+
+ closeDialog 1;
+ };
+ case "garage::ready": {
+ if !(isNil QGVAR(GarageUIBridge)) then {
+ GVAR(GarageUIBridge) call ["handleReady", [_control, _data]];
+ };
+ };
+ case "garage::vehicle::retrieve::request": {
+ if !(isNil QGVAR(GarageUIBridge)) then {
+ GVAR(GarageUIBridge) call ["handleRetrieveRequest", [_data]];
+ };
+ };
+ case "garage::vehicle::store::request": {
+ if !(isNil QGVAR(GarageUIBridge)) then {
+ GVAR(GarageUIBridge) call ["handleStoreRequest", [_data]];
+ };
+ };
+ case "garage::refresh": {
+ if !(isNil QGVAR(GarageUIBridge)) then {
+ GVAR(GarageUIBridge) call ["refreshGarage", []];
+ };
+ };
+ default {
+ hint format ["Unhandled garage UI event: %1", _event];
+ };
+};
+
+true;
diff --git a/arma/client/addons/garage/functions/fnc_initCatalogService.sqf b/arma/client/addons/garage/functions/fnc_initCatalogService.sqf
new file mode 100644
index 0000000..748b5e4
--- /dev/null
+++ b/arma/client/addons/garage/functions/fnc_initCatalogService.sqf
@@ -0,0 +1,160 @@
+#include "..\script_component.hpp"
+
+/*
+ * File: fnc_initCatalogService.sqf
+ * Author: IDSolutions
+ * Date: 2026-03-14
+ * Public: No
+ *
+ * Description:
+ * Initializes the garage catalog service for vehicle metadata and UI-friendly shaping.
+ */
+
+#pragma hemtt ignore_variables ["_self"]
+GVAR(GarageCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
+ ["#type", "GarageCatalogServiceBaseClass"],
+ ["resolveCategory", compileFinal {
+ params [["_className", "", [""]]];
+
+ if (_className isEqualTo "") exitWith { "other" };
+
+ switch (true) do {
+ case (_className isKindOf ["Car", configFile >> "CfgVehicles"]): { "car" };
+ case (_className isKindOf ["Tank", configFile >> "CfgVehicles"]): { "armor" };
+ case (_className isKindOf ["Helicopter", configFile >> "CfgVehicles"]): { "air" };
+ case (_className isKindOf ["Plane", configFile >> "CfgVehicles"]): { "air" };
+ case (_className isKindOf ["Ship", configFile >> "CfgVehicles"]): { "naval" };
+ default { "other" };
+ }
+ }],
+ ["resolveDisplayName", compileFinal {
+ params [["_className", "", [""]]];
+
+ private _displayName = getText (configFile >> "CfgVehicles" >> _className >> "displayName");
+ if (_displayName isEqualTo "") then {
+ _displayName = _className;
+ };
+
+ _displayName
+ }],
+ ["resolvePicture", compileFinal {
+ params [["_className", "", [""]]];
+
+ private _picture = getText (configFile >> "CfgVehicles" >> _className >> "editorPreview");
+ if (_picture isEqualTo "") then {
+ _picture = getText (configFile >> "CfgVehicles" >> _className >> "picture");
+ };
+
+ _picture
+ }],
+ ["buildHitPointRows", compileFinal {
+ params [["_hitPoints", createHashMap, [createHashMap]]];
+
+ private _rows = [];
+ private _names = _hitPoints getOrDefault ["names", []];
+ private _selections = _hitPoints getOrDefault ["selections", []];
+ private _values = _hitPoints getOrDefault ["values", []];
+ private _count = count _names;
+
+ for "_index" from 0 to (_count - 1) do {
+ private _rowName = _names param [_index, ""];
+ _rows pushBack (createHashMapFromArray [
+ ["name", _rowName],
+ ["selection", _selections param [_index, ""]],
+ ["value", _values param [_index, 0]]
+ ]);
+ };
+
+ _rows
+ }],
+ ["resolveHealth", compileFinal {
+ params [["_damage", 0, [0]], ["_hitPointRows", [], [[]]]];
+
+ private _worstHitPoint = 0;
+ {
+ private _value = _x getOrDefault ["value", 0];
+ if (_value > _worstHitPoint) then {
+ _worstHitPoint = _value;
+ };
+ } forEach _hitPointRows;
+
+ 1 - ((_damage max _worstHitPoint) min 1)
+ }],
+ ["buildStoredVehicle", compileFinal {
+ params [["_plate", "", [""]], ["_vehicleData", createHashMap, [createHashMap]]];
+
+ private _className = _vehicleData getOrDefault ["classname", ""];
+ private _damage = _vehicleData getOrDefault ["damage", 0];
+ private _fuel = _vehicleData getOrDefault ["fuel", 0];
+ private _hitPoints = _vehicleData getOrDefault ["hit_points", createHashMap];
+ private _hitPointRows = _self call ["buildHitPointRows", [_hitPoints]];
+
+ createHashMapFromArray [
+ ["entryKind", "stored"],
+ ["plate", _plate],
+ ["classname", _className],
+ ["displayName", _self call ["resolveDisplayName", [_className]]],
+ ["picture", _self call ["resolvePicture", [_className]]],
+ ["category", _self call ["resolveCategory", [_className]]],
+ ["damage", _damage],
+ ["fuel", _fuel],
+ ["health", _self call ["resolveHealth", [_damage, _hitPointRows]]],
+ ["hitPoints", _hitPointRows]
+ ]
+ }],
+ ["buildNearbyVehicle", compileFinal {
+ params [
+ ["_vehicle", objNull, [objNull]],
+ ["_origin", [], [[]]]
+ ];
+
+ if (isNull _vehicle) exitWith { createHashMap };
+
+ private _className = typeOf _vehicle;
+ private _rawHitPoints = getAllHitPointsDamage _vehicle;
+ private _hitPointRows = [];
+ if (_rawHitPoints isEqualType [] && { count _rawHitPoints >= 3 }) then {
+ private _names = _rawHitPoints param [0, []];
+ private _selections = _rawHitPoints param [1, []];
+ private _values = _rawHitPoints param [2, []];
+ private _count = count _names;
+
+ for "_index" from 0 to (_count - 1) do {
+ _hitPointRows pushBack (createHashMapFromArray [
+ ["name", _names param [_index, ""]],
+ ["selection", _selections param [_index, ""]],
+ ["value", _values param [_index, 0]]
+ ]);
+ };
+ };
+
+ private _damage = damage _vehicle;
+ private _distance = if (_origin isEqualType [] && { count _origin >= 2 }) then {
+ _vehicle distance2D _origin
+ } else {
+ _vehicle distance2D player
+ };
+ private _ownerUid = _vehicle getVariable ["forge_garage_owner_uid", ""];
+ private _plate = _vehicle getVariable ["forge_garage_plate", ""];
+
+ createHashMapFromArray [
+ ["entryKind", "nearby"],
+ ["netId", netId _vehicle],
+ ["plate", _plate],
+ ["classname", _className],
+ ["displayName", _self call ["resolveDisplayName", [_className]]],
+ ["picture", _self call ["resolvePicture", [_className]]],
+ ["category", _self call ["resolveCategory", [_className]]],
+ ["damage", _damage],
+ ["fuel", fuel _vehicle],
+ ["health", _self call ["resolveHealth", [_damage, _hitPointRows]]],
+ ["hitPoints", _hitPointRows],
+ ["distance", _distance],
+ ["ownerUid", _ownerUid],
+ ["isEmpty", crew _vehicle isEqualTo []]
+ ]
+ }]
+];
+
+GVAR(GarageCatalogService) = createHashMapObject [GVAR(GarageCatalogServiceBaseClass)];
+GVAR(GarageCatalogService)
diff --git a/arma/client/addons/garage/functions/fnc_initGarageClass.sqf b/arma/client/addons/garage/functions/fnc_initClass.sqf
similarity index 90%
rename from arma/client/addons/garage/functions/fnc_initGarageClass.sqf
rename to arma/client/addons/garage/functions/fnc_initClass.sqf
index bc136e2..841550b 100644
--- a/arma/client/addons/garage/functions/fnc_initGarageClass.sqf
+++ b/arma/client/addons/garage/functions/fnc_initClass.sqf
@@ -1,7 +1,7 @@
#include "..\script_component.hpp"
/*
- * File: fnc_initGarageClass.sqf
+ * File: fnc_initClass.sqf
* Author: IDSolutions
* Date: 2025-12-17
* Last Update: 2026-02-13
@@ -18,7 +18,7 @@
* Garage class object [HASHMAP OBJECT]
*
* Example:
- * call forge_client_garage_fnc_initGarageClass
+ * call forge_client_garage_fnc_initClass
*/
#pragma hemtt ignore_variables ["_self"]
@@ -48,8 +48,8 @@ GVAR(GarageBaseClass) = compileFinal createHashMapFromArray [
["sync", compileFinal {
params [["_data", createHashMap, [createHashMap]]];
- private _garage = _self get "garage";
private _isLoaded = _self get "isLoaded";
+ private _garage = createHashMap;
{ _garage set [_x, _y]; } forEach _data;
_self set ["garage", _garage];
@@ -57,6 +57,9 @@ GVAR(GarageBaseClass) = compileFinal createHashMapFromArray [
if !(_isLoaded) then { _self set ["isLoaded", true]; };
diag_log "[FORGE:Client:Garage] Sync completed";
}],
+ ["getGarageState", compileFinal {
+ _self getOrDefault ["garage", createHashMap]
+ }],
["get", compileFinal {
params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
diff --git a/arma/client/addons/garage/functions/fnc_initSessionService.sqf b/arma/client/addons/garage/functions/fnc_initSessionService.sqf
new file mode 100644
index 0000000..7fe871b
--- /dev/null
+++ b/arma/client/addons/garage/functions/fnc_initSessionService.sqf
@@ -0,0 +1,298 @@
+#include "..\script_component.hpp"
+
+/*
+ * File: fnc_initSessionService.sqf
+ * Author: IDSolutions
+ * Date: 2026-03-14
+ * Public: No
+ *
+ * Description:
+ * Initializes the typed garage session service responsible for resolving the
+ * active garage context and building the browser hydrate payload.
+ */
+
+#pragma hemtt ignore_variables ["_self"]
+
+GVAR(GarageSessionServiceBaseClass) = compileFinal createHashMapFromArray [
+ ["#type", "GarageSessionServiceBaseClass"],
+ ["#create", compileFinal {
+ _self set ["lastContext", createHashMap];
+ }],
+ ["#delete", compileFinal {
+ _self set ["lastContext", createHashMap];
+ }],
+ ["createDefaultContext", compileFinal {
+ createHashMapFromArray [
+ ["name", "Vehicle Garage"],
+ ["anchorPosition", getPosATL player],
+ ["sourceObject", objNull],
+ ["spawnHeading", getDir player],
+ ["spawnPosition", player getPos [8, getDir player]],
+ ["spawnRadius", 6],
+ ["nearbyRadius", 30]
+ ]
+ }],
+ ["scanEntryValues", compileFinal {
+ params [
+ ["_values", [], [[]]],
+ ["_state", createHashMap, [createHashMap]]
+ ];
+
+ {
+ if (_x isEqualType "" && { (_state getOrDefault ["name", "Vehicle Garage"]) isEqualTo "Vehicle Garage" }) then {
+ _state set ["name", _x];
+ };
+
+ if (_x isEqualType "") then {
+ private _resolvedObject = _state getOrDefault ["sourceObject", objNull];
+ if (isNull _resolvedObject) then {
+ private _namedObject = missionNamespace getVariable [_x, objNull];
+ if (!isNull _namedObject) then {
+ _state set ["sourceObject", _namedObject];
+ };
+ };
+
+ if ((_state getOrDefault ["anchorPosition", []]) isEqualTo [] && { _x in allMapMarkers }) then {
+ _state set ["anchorPosition", markerPos _x];
+ };
+
+ continue;
+ };
+
+ if (_x isEqualType objNull && { isNull (_state getOrDefault ["sourceObject", objNull]) }) then {
+ _state set ["sourceObject", _x];
+ if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then {
+ _state set ["anchorPosition", getPosATL _x];
+ };
+ continue;
+ };
+
+ if (_x isEqualType 0 && { (_state getOrDefault ["spawnHeading", -1]) < 0 }) then {
+ _state set ["spawnHeading", _x];
+ continue;
+ };
+
+ if (_x isEqualType [] && { count _x > 0 }) then {
+ if (
+ { _x isEqualType 0 } count _x >= 2 &&
+ {
+ ((_state getOrDefault ["offset", []]) isEqualTo []) ||
+ ((_state getOrDefault ["anchorPosition", []]) isEqualTo [])
+ }
+ ) then {
+ if ((_state getOrDefault ["anchorPosition", []]) isEqualTo []) then {
+ _state set ["anchorPosition", _x];
+ } else {
+ _state set ["offset", _x];
+ };
+ continue;
+ };
+
+ _self call ["scanEntryValues", [_x, _state]];
+ };
+ } forEach _values;
+
+ _state
+ }],
+ ["resolveEntry", compileFinal {
+ params [["_entry", [], [[]]]];
+
+ private _state = createHashMapFromArray [
+ ["name", "Vehicle Garage"],
+ ["anchorPosition", []],
+ ["sourceObject", objNull],
+ ["offset", []],
+ ["spawnHeading", -1]
+ ];
+
+ _self call ["scanEntryValues", [_entry, _state]];
+
+ private _anchorPosition = _state getOrDefault ["anchorPosition", []];
+ private _offset = _state getOrDefault ["offset", []];
+ private _spawnPosition = if (_anchorPosition isEqualTo []) then {
+ []
+ } else {
+ if (_offset isEqualTo []) then {
+ _anchorPosition
+ } else {
+ _anchorPosition vectorAdd _offset
+ }
+ };
+
+ createHashMapFromArray [
+ ["name", _state getOrDefault ["name", "Vehicle Garage"]],
+ ["anchorPosition", _anchorPosition],
+ ["sourceObject", _state getOrDefault ["sourceObject", objNull]],
+ ["spawnHeading", _state getOrDefault ["spawnHeading", -1]],
+ ["spawnPosition", _spawnPosition]
+ ]
+ }],
+ ["resolveContext", compileFinal {
+ private _context = _self call ["createDefaultContext", []];
+ private _locations = (missionConfigFile >> "FORGE_CfgGarages" >> "locations") call BFUNC(getCfgData);
+ if !(_locations isEqualType []) exitWith {
+ _self set ["lastContext", _context];
+ _context
+ };
+
+ private _nearestEntry = [];
+ private _nearestDistance = 1e10;
+
+ {
+ private _entry = _self call ["resolveEntry", [_x]];
+ private _anchorPosition = _entry getOrDefault ["anchorPosition", []];
+ if (_anchorPosition isEqualTo []) then {
+ continue;
+ };
+
+ private _distance = player distance2D _anchorPosition;
+ if (_distance < _nearestDistance) then {
+ _nearestDistance = _distance;
+ _nearestEntry = _entry;
+ };
+ } forEach _locations;
+
+ if (_nearestEntry isEqualTo []) exitWith {
+ _self set ["lastContext", _context];
+ _context
+ };
+
+ private _anchorPosition = _nearestEntry getOrDefault ["anchorPosition", []];
+ private _garageObject = _nearestEntry getOrDefault ["sourceObject", objNull];
+ private _garageName = _nearestEntry getOrDefault ["name", "Vehicle Garage"];
+ private _spawnHeading = _nearestEntry getOrDefault ["spawnHeading", getDir player];
+ if (_spawnHeading < 0) then {
+ _spawnHeading = if (!isNull _garageObject) then { getDir _garageObject } else { getDir player };
+ };
+
+ private _spawnPosition = _nearestEntry getOrDefault ["spawnPosition", []];
+ if (_spawnPosition isEqualTo []) then {
+ _spawnPosition = if (_anchorPosition isEqualTo []) then {
+ player getPos [8, _spawnHeading]
+ } else {
+ _anchorPosition
+ };
+ };
+
+ _context set ["name", _garageName];
+ _context set ["anchorPosition", _anchorPosition];
+ _context set ["sourceObject", _garageObject];
+ _context set ["spawnHeading", _spawnHeading];
+ _context set ["spawnPosition", _spawnPosition];
+
+ _self set ["lastContext", _context];
+ _context
+ }],
+ ["getContext", compileFinal {
+ _self call ["resolveContext", []]
+ }],
+ ["buildPayload", compileFinal {
+ private _context = _self call ["getContext", []];
+ private _garageMap = if (isNil QGVAR(GarageClass)) then {
+ createHashMap
+ } else {
+ GVAR(GarageClass) call ["getGarageState", []]
+ };
+
+ private _anchorPosition = _context getOrDefault ["anchorPosition", []];
+ private _spawnPosition = _context getOrDefault ["spawnPosition", getPosATL player];
+ private _spawnRadius = _context getOrDefault ["spawnRadius", 6];
+ private _nearbyRadius = _context getOrDefault ["nearbyRadius", 30];
+ private _nearbyOrigin = [_anchorPosition, _spawnPosition] select (_anchorPosition isEqualTo []);
+
+ private _storedVehicles = [];
+ private _nearbyVehicles = [];
+ private _nearbyEntities = [];
+ private _candidateVehicles = [];
+
+ {
+ _candidateVehicles pushBackUnique _x;
+ } forEach (_nearbyOrigin nearEntities [["Car", "Tank", "Air", "Ship"], _nearbyRadius]);
+ {
+ _candidateVehicles pushBackUnique _x;
+ } forEach ((getPosATL player) nearEntities [["Car", "Tank", "Air", "Ship"], _nearbyRadius]);
+ {
+ _candidateVehicles pushBackUnique _x;
+ } forEach (nearestObjects [_nearbyOrigin, ["AllVehicles"], _nearbyRadius]);
+ {
+ _candidateVehicles pushBackUnique _x;
+ } forEach (nearestObjects [getPosATL player, ["AllVehicles"], _nearbyRadius]);
+
+ {
+ if (isNull _x) then {
+ continue;
+ };
+
+ if (_x isKindOf "CAManBase") then {
+ continue;
+ };
+
+ if !(
+ _x isKindOf "Car" ||
+ _x isKindOf "Tank" ||
+ _x isKindOf "Air" ||
+ _x isKindOf "Ship"
+ ) then {
+ continue;
+ };
+
+ _nearbyEntities pushBackUnique _x;
+ } forEach _candidateVehicles;
+
+ {
+ _storedVehicles pushBack (
+ GVAR(GarageCatalogService) call ["buildStoredVehicle", [_x, _y]]
+ );
+ } forEach _garageMap;
+
+ private _storedVehiclePairs = _storedVehicles apply {
+ [toLowerANSI (_x getOrDefault ["displayName", ""]), _x]
+ };
+ _storedVehiclePairs sort true;
+ _storedVehicles = _storedVehiclePairs apply { _x param [1, createHashMap] };
+
+ {
+ if (isNull _x) then {
+ continue;
+ };
+
+ private _builtVehicle = GVAR(GarageCatalogService) call ["buildNearbyVehicle", [_x, _nearbyOrigin]];
+ if (_builtVehicle isEqualTo createHashMap) then {
+ continue;
+ };
+
+ _nearbyVehicles pushBack _builtVehicle;
+ } forEach _nearbyEntities;
+
+ private _nearbyVehiclePairs = _nearbyVehicles apply {
+ [_x getOrDefault ["distance", 0], _x]
+ };
+ _nearbyVehiclePairs sort true;
+ _nearbyVehicles = _nearbyVehiclePairs apply { _x param [1, createHashMap] };
+
+ private _spawnBlocked = (
+ (_spawnPosition nearEntities [["Car", "Tank", "Air", "Ship"], _spawnRadius]) +
+ (nearestObjects [_spawnPosition, ["Car", "Tank", "Air", "Ship"], _spawnRadius])
+ ) isNotEqualTo [];
+
+ createHashMapFromArray [
+ ["session", createHashMapFromArray [
+ ["garageName", _context getOrDefault ["name", "Vehicle Garage"]],
+ ["capacityUsed", count _storedVehicles],
+ ["capacityMax", 5],
+ ["nearbyCount", count _nearbyVehicles],
+ ["spawnBlocked", _spawnBlocked],
+ ["spawnStatus", ["Ready", "Blocked"] select _spawnBlocked]
+ ]],
+ ["garage", createHashMapFromArray [
+ ["vehicles", _storedVehicles]
+ ]],
+ ["nearby", createHashMapFromArray [
+ ["vehicles", _nearbyVehicles]
+ ]]
+ ]
+ }]
+];
+
+GVAR(GarageSessionService) = createHashMapObject [GVAR(GarageSessionServiceBaseClass)];
+GVAR(GarageSessionService)
diff --git a/arma/client/addons/garage/functions/fnc_initUIBridge.sqf b/arma/client/addons/garage/functions/fnc_initUIBridge.sqf
new file mode 100644
index 0000000..ec15d50
--- /dev/null
+++ b/arma/client/addons/garage/functions/fnc_initUIBridge.sqf
@@ -0,0 +1,205 @@
+#include "..\script_component.hpp"
+
+/*
+ * File: fnc_initUIBridge.sqf
+ * Author: IDSolutions
+ * Date: 2026-03-14
+ * Public: No
+ *
+ * Description:
+ * Initializes the garage UI bridge for browser control state and retrieve/store actions.
+ */
+
+#pragma hemtt ignore_variables ["_self"]
+private _webUIDeclarations = call EFUNC(common,initWebUIBridge);
+private _webUIBridgeDeclaration = _webUIDeclarations get "bridgeDeclaration";
+
+GVAR(GarageUIBridgeBaseClass) = compileFinal createHashMapFromArray [
+ ["#base", _webUIBridgeDeclaration],
+ ["#type", "GarageUIBridgeBaseClass"],
+ ["#create", compileFinal {
+ _self set ["pendingStoreVehicle", objNull];
+ _self set ["pendingRetrieve", createHashMap];
+ }],
+ ["getActiveBrowserControl", compileFinal {
+ private _display = uiNamespace getVariable ["RscGarage", displayNull];
+ if (isNull _display) exitWith {
+ _self call ["setActiveBrowserControl", [controlNull]];
+ controlNull
+ };
+
+ private _control = _display displayCtrl 1006;
+ _self call ["setActiveBrowserControl", [_control]];
+ _control
+ }],
+ ["handleReady", compileFinal {
+ params [["_control", controlNull, [controlNull]], ["_data", createHashMap, [createHashMap]]];
+
+ private _screen = _self call ["getScreen", []];
+ _screen call ["setControl", [_control]];
+ _screen call ["markReady", [true]];
+
+ _self call ["flushPendingEvents", []];
+ _self call ["sendEvent", ["garage::hydrate", GVAR(GarageSessionService) call ["buildPayload", []], _control]];
+ }],
+ ["refreshGarage", compileFinal {
+ private _control = _self call ["getActiveBrowserControl", []];
+ if (isNull _control) exitWith { false };
+
+ _self call ["sendEvent", ["garage::sync", GVAR(GarageSessionService) call ["buildPayload", []], _control]]
+ }],
+ ["handleRetrieveRequest", compileFinal {
+ params [["_data", createHashMap, [createHashMap]]];
+
+ private _plate = _data getOrDefault ["plate", ""];
+ if (_plate isEqualTo "") exitWith {
+ _self call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [
+ ["message", "Select a stored vehicle to retrieve."]
+ ]]];
+ };
+
+ private _garageMap = if (isNil QGVAR(GarageClass)) then {
+ createHashMap
+ } else {
+ GVAR(GarageClass) call ["getGarageState", []]
+ };
+ private _vehicleData = _garageMap getOrDefault [_plate, createHashMap];
+ if (_vehicleData isEqualTo createHashMap) exitWith {
+ _self call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [
+ ["message", "Stored vehicle record could not be found."]
+ ]]];
+ };
+
+ private _context = GVAR(GarageSessionService) call ["getContext", []];
+ private _spawnPosition = _context getOrDefault ["spawnPosition", getPosATL player];
+ private _spawnHeading = _context getOrDefault ["spawnHeading", getDir player];
+ private _spawnRadius = _context getOrDefault ["spawnRadius", 6];
+ private _blockingVehicles = [];
+ {
+ _blockingVehicles pushBackUnique _x;
+ } forEach (_spawnPosition nearEntities [["Car", "Tank", "Air", "Ship"], _spawnRadius]);
+ {
+ _blockingVehicles pushBackUnique _x;
+ } forEach (nearestObjects [_spawnPosition, ["Car", "Tank", "Air", "Ship"], _spawnRadius]);
+ if (_blockingVehicles isNotEqualTo []) exitWith {
+ _self call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [
+ ["message", "The garage spawn area is blocked."]
+ ]]];
+ };
+
+ private _className = _vehicleData getOrDefault ["classname", ""];
+ if (_className isEqualTo "") exitWith {
+ _self call ["sendEvent", ["garage::retrieve::failure", createHashMapFromArray [
+ ["message", "Stored vehicle record is missing a classname."]
+ ]]];
+ };
+
+ private _vehicle = createVehicle [_className, _spawnPosition, [], 0, "CAN_COLLIDE"];
+ _vehicle setDir _spawnHeading;
+ _vehicle setFuel (_vehicleData getOrDefault ["fuel", 0]);
+ _vehicle setDamage (_vehicleData getOrDefault ["damage", 0]);
+
+ private _hitPoints = _vehicleData getOrDefault ["hit_points", createHashMap];
+ private _hitPointNames = _hitPoints getOrDefault ["names", []];
+ private _hitPointValues = _hitPoints getOrDefault ["values", []];
+ for "_index" from 0 to ((count _hitPointNames) - 1) do {
+ _vehicle setHitPointDamage [_hitPointNames param [_index, ""], _hitPointValues param [_index, 0]];
+ };
+
+ _vehicle setVariable ["forge_garage_plate", _plate, true];
+ _vehicle setVariable ["forge_garage_owner_uid", getPlayerUID player, true];
+
+ _self set ["pendingRetrieve", createHashMapFromArray [
+ ["plate", _plate],
+ ["vehicle", _vehicle]
+ ]];
+
+ [SRPC(garage,requestRetrieveVehicle), [getPlayerUID player, _plate]] call CFUNC(serverEvent);
+ }],
+ ["handleStoreRequest", compileFinal {
+ params [["_data", createHashMap, [createHashMap]]];
+
+ private _netId = _data getOrDefault ["netId", ""];
+ if (_netId isEqualTo "") exitWith {
+ _self call ["sendEvent", ["garage::store::failure", createHashMapFromArray [
+ ["message", "Select a nearby vehicle to store."]
+ ]]];
+ };
+
+ private _vehicle = objectFromNetId _netId;
+ if (isNull _vehicle) exitWith {
+ _self call ["sendEvent", ["garage::store::failure", createHashMapFromArray [
+ ["message", "The selected vehicle is no longer available."]
+ ]]];
+ };
+
+ if (crew _vehicle isNotEqualTo []) exitWith {
+ _self call ["sendEvent", ["garage::store::failure", createHashMapFromArray [
+ ["message", "All crew must exit the vehicle before storing it."]
+ ]]];
+ };
+
+ private _rawHitPoints = getAllHitPointsDamage _vehicle;
+ private _hitPointsJson = toJSON (createHashMapFromArray [
+ ["names", _rawHitPoints param [0, []]],
+ ["selections", _rawHitPoints param [1, []]],
+ ["values", _rawHitPoints param [2, []]]
+ ]);
+
+ _self set ["pendingStoreVehicle", _vehicle];
+ [SRPC(garage,requestStoreVehicle), [
+ getPlayerUID player,
+ typeOf _vehicle,
+ fuel _vehicle,
+ damage _vehicle,
+ _hitPointsJson
+ ]] call CFUNC(serverEvent);
+ }],
+ ["handleActionResponse", compileFinal {
+ params [["_payload", createHashMap, [createHashMap]]];
+
+ private _action = _payload getOrDefault ["action", ""];
+ private _success = _payload getOrDefault ["success", false];
+ private _message = _payload getOrDefault ["message", "Garage action failed."];
+
+ switch (_action) do {
+ case "retrieve": {
+ private _pendingRetrieve = _self getOrDefault ["pendingRetrieve", createHashMap];
+ private _vehicle = _pendingRetrieve getOrDefault ["vehicle", objNull];
+
+ if (!_success && { !isNull _vehicle }) then {
+ deleteVehicle _vehicle;
+ };
+
+ _self set ["pendingRetrieve", createHashMap];
+ _self call ["sendEvent", [[
+ "garage::retrieve::failure",
+ "garage::retrieve::success"
+ ] select _success, createHashMapFromArray [["message", _message]]]];
+ };
+ case "store": {
+ private _vehicle = _self getOrDefault ["pendingStoreVehicle", objNull];
+
+ if (_success && { !isNull _vehicle }) then {
+ deleteVehicle _vehicle;
+ };
+
+ _self set ["pendingStoreVehicle", objNull];
+ _self call ["sendEvent", [[
+ "garage::store::failure",
+ "garage::store::success"
+ ] select _success, createHashMapFromArray [["message", _message]]]];
+ };
+ };
+
+ [] spawn {
+ sleep 0.05;
+ if !(isNil QGVAR(GarageUIBridge)) then {
+ GVAR(GarageUIBridge) call ["refreshGarage", []];
+ };
+ };
+ }]
+];
+
+GVAR(GarageUIBridge) = createHashMapObject [GVAR(GarageUIBridgeBaseClass)];
+GVAR(GarageUIBridge)
diff --git a/arma/client/addons/garage/functions/fnc_openUI.sqf b/arma/client/addons/garage/functions/fnc_openUI.sqf
index 84678e0..85e31bd 100644
--- a/arma/client/addons/garage/functions/fnc_openUI.sqf
+++ b/arma/client/addons/garage/functions/fnc_openUI.sqf
@@ -19,3 +19,20 @@
* Example:
* call forge_client_garage_fnc_openUI;
*/
+
+private _display = createDialog ["RscGarage", true];
+private _ctrl = _display displayCtrl 1006;
+
+_ctrl ctrlAddEventHandler ["JSDialog", {
+ params ["_control", "_isConfirmDialog", "_message"];
+
+ [_control, _isConfirmDialog, _message] call FUNC(handleUIEvents);
+}];
+
+if !(isNil QGVAR(GarageUIBridge)) then {
+ GVAR(GarageUIBridge) call ["setActiveBrowserControl", [_ctrl]];
+};
+
+_ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\index.html)];
+
+true;
diff --git a/arma/client/addons/garage/ui/RscGarage.hpp b/arma/client/addons/garage/ui/RscGarage.hpp
index 2507825..e6f392d 100644
--- a/arma/client/addons/garage/ui/RscGarage.hpp
+++ b/arma/client/addons/garage/ui/RscGarage.hpp
@@ -1,5 +1,5 @@
class RscGarage {
- idd = 1000;
+ idd = 1005;
fadeIn = 0;
fadeOut = 0;
duration = 1e011;
@@ -10,7 +10,7 @@ class RscGarage {
class controls {
class IFrame: RscText {
type = 106;
- idc = 1001;
+ idc = 1006;
x = "safeZoneXAbs";
y = "safeZoneY";
w = "safeZoneWAbs";
diff --git a/arma/client/addons/garage/ui/_site/garage-ui.css b/arma/client/addons/garage/ui/_site/garage-ui.css
new file mode 100644
index 0000000..27a99d9
--- /dev/null
+++ b/arma/client/addons/garage/ui/_site/garage-ui.css
@@ -0,0 +1,574 @@
+/* Generated by tools/build-webui.mjs for Garage UI styles. Do not edit directly. */
+:root {
+ --garage-shell-bg: #e4e3df;
+ --garage-surface: #f5f3ef;
+ --garage-surface-alt: #ece8e2;
+ --garage-border: rgba(74, 91, 110, 0.2);
+ --garage-border-strong: rgba(20, 46, 79, 0.18);
+ --garage-text-main: #1f2d3d;
+ --garage-text-muted: #6a7787;
+ --garage-text-subtle: #8792a0;
+ --garage-accent: #12365d;
+ --garage-accent-soft: #dbe7f3;
+ --garage-accent-line: rgba(18, 54, 93, 0.12);
+ --garage-warning: #8f5f26;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+}
+
+body {
+ font-family: "Segoe UI", "Trebuchet MS", sans-serif;
+ color: var(--garage-text-main);
+ background: var(--garage-shell-bg);
+}
+
+button,
+input {
+ font: inherit;
+}
+
+button {
+ cursor: pointer;
+}
+
+button:disabled {
+ cursor: not-allowed;
+ opacity: 0.72;
+}
+
+:focus-visible {
+ outline: 2px solid rgb(18 54 93 / 0.35);
+ outline-offset: 2px;
+}
+
+#app {
+ width: 100%;
+ height: 100%;
+}
+
+.garage-shell {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ background: var(--garage-shell-bg);
+}
+
+.garage-layout {
+ flex: 1;
+ min-height: 0;
+ width: min(100%, 1613px);
+ margin: 0 auto;
+ padding: 1.25rem;
+ display: grid;
+ grid-template-columns: 308px minmax(0, 1fr);
+ gap: 1.25rem;
+}
+
+.garage-sidebar,
+.garage-main {
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.garage-main {
+ overflow: hidden;
+}
+
+.garage-module,
+.garage-panel,
+.garage-card {
+ background: linear-gradient(
+ 180deg,
+ var(--garage-surface) 0%,
+ var(--garage-surface-alt) 100%
+ );
+ border: 1px solid var(--garage-border);
+ border-radius: 1.35rem;
+}
+
+.garage-module,
+.garage-card {
+ padding: 1rem;
+}
+
+.garage-module {
+ display: grid;
+ gap: 0.85rem;
+ align-content: start;
+}
+
+.garage-panel {
+ flex: 1 1 auto;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.garage-panel-header,
+.garage-module-header,
+.garage-card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.garage-panel-header {
+ padding: 1rem 1rem 0;
+}
+
+.garage-module-header {
+ align-items: flex-start;
+}
+
+.garage-panel-intro {
+ padding: 0 1rem 1rem;
+ border-bottom: 1px solid var(--garage-accent-line);
+}
+
+.garage-dashboard {
+ flex: 1;
+ min-height: 0;
+ padding: 1rem;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ gap: 1rem;
+ align-items: stretch;
+}
+
+.garage-list-card,
+.garage-detail-card {
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.garage-detail-card {
+ grid-column: 1 / -1;
+}
+
+.garage-scroll-body {
+ flex: 1;
+ min-height: 20rem;
+ max-height: 24rem;
+ overflow: auto;
+ display: grid;
+ gap: 0.8rem;
+ padding-right: 0.2rem;
+}
+
+.garage-detail-body {
+ padding-top: 0.95rem;
+}
+
+.garage-detail-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.85fr);
+ gap: 1rem;
+}
+
+.garage-detail-meta,
+.garage-summary-grid,
+.garage-search-actions,
+.garage-category-grid,
+.garage-action-row,
+.garage-inline-meters,
+.garage-hitpoint-grid,
+.garage-footer {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.garage-detail-meta {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ margin-bottom: 1rem;
+}
+
+.garage-summary-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.garage-summary-grid > :last-child {
+ grid-column: 1 / -1;
+}
+
+.garage-search-actions,
+.garage-action-row {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.65rem;
+}
+
+.garage-category-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.65rem;
+}
+
+.garage-footer {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ padding: 0.95rem 1.25rem 1.15rem;
+ border-top: 1px solid rgb(18 54 93 / 0.1);
+}
+
+.garage-meter-stack {
+ display: grid;
+ gap: 0.75rem;
+ margin-bottom: 1rem;
+}
+
+.garage-eyebrow,
+.garage-footer-title,
+.garage-stat-label,
+.garage-meter-label,
+.garage-hitpoint-selection {
+ font-size: 0.68rem;
+ font-weight: 700;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: var(--garage-text-subtle);
+}
+
+.garage-title,
+.garage-section-title {
+ margin: 0.16rem 0 0;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ color: var(--garage-text-main);
+}
+
+.garage-title {
+ font-size: 1.1rem;
+}
+
+.garage-section-title {
+ font-size: 1.05rem;
+}
+
+.garage-copy,
+.garage-detail-note,
+.garage-empty-copy,
+.garage-footer-copy,
+.garage-vehicle-meta,
+.garage-detail-caption {
+ margin: 0;
+ font-size: 0.92rem;
+ line-height: 1.48;
+ color: var(--garage-text-muted);
+}
+
+.garage-pill,
+.garage-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.48rem 0.8rem;
+ border-radius: 999px;
+ font-size: 0.74rem;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ background: var(--garage-accent-soft);
+ color: var(--garage-accent);
+}
+
+.garage-badge.is-warning {
+ background: rgb(246 226 193 / 0.88);
+ color: var(--garage-warning);
+}
+
+.garage-search-form {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.garage-search-input {
+ width: 100%;
+ height: 2.9rem;
+ padding: 0 0.95rem;
+ border-radius: 0.8rem;
+ border: 1px solid var(--garage-border);
+ background: rgb(255 255 255 / 0.75);
+ color: var(--garage-text-main);
+}
+
+.garage-stat-card {
+ min-width: 0;
+ padding: 0.85rem;
+ border-radius: 0.85rem;
+ border: 1px solid var(--garage-border);
+ background: rgb(255 255 255 / 0.48);
+ display: flex;
+ flex-direction: column;
+ gap: 0.3rem;
+}
+
+.garage-stat-card.is-accent {
+ background: linear-gradient(
+ 180deg,
+ rgb(237 243 249 / 0.92) 0%,
+ rgb(223 232 242 / 0.72) 100%
+ );
+}
+
+.garage-stat-card.is-danger {
+ background: linear-gradient(
+ 180deg,
+ rgb(254 242 242 / 0.95) 0%,
+ rgb(252 225 225 / 0.82) 100%
+ );
+ border-color: rgb(220 151 151 / 0.38);
+}
+
+.garage-stat-value {
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--garage-text-main);
+ line-height: 1.3;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+.garage-chip {
+ min-height: 2.6rem;
+ padding: 0.68rem 0.9rem;
+ border-radius: 0.85rem;
+ border: 1px solid var(--garage-border);
+ background: rgb(255 255 255 / 0.52);
+ color: var(--garage-text-muted);
+ font-size: 0.8rem;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.garage-chip.is-active {
+ background: var(--garage-accent-soft);
+ color: var(--garage-accent);
+ border-color: rgb(18 54 93 / 0.2);
+}
+
+.garage-vehicle-item {
+ width: 100%;
+ padding: 0.9rem;
+ border-radius: 0.95rem;
+ border: 1px solid var(--garage-border);
+ background: rgb(255 255 255 / 0.48);
+ color: inherit;
+ text-align: left;
+}
+
+.garage-vehicle-item.is-selected {
+ border-color: rgb(18 54 93 / 0.24);
+ background: linear-gradient(
+ 180deg,
+ rgb(237 243 249 / 0.96) 0%,
+ rgb(223 232 242 / 0.74) 100%
+ );
+ box-shadow: 0 16px 26px rgb(18 54 93 / 0.08);
+}
+
+.garage-vehicle-item-head,
+.garage-meter-label-row,
+.garage-subsystem-header,
+.garage-hitpoint-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+}
+
+.garage-vehicle-copy,
+.garage-hitpoint-copy,
+.garage-footer-block {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.18rem;
+}
+
+.garage-vehicle-title,
+.garage-hitpoint-name,
+.garage-hitpoint-value {
+ font-size: 0.9rem;
+ font-weight: 700;
+ color: var(--garage-text-main);
+}
+
+.garage-meter {
+ display: grid;
+ gap: 0.32rem;
+}
+
+.garage-meter-track {
+ width: 100%;
+ height: 0.45rem;
+ overflow: hidden;
+ border-radius: 999px;
+ background: rgb(18 54 93 / 0.08);
+}
+
+.garage-meter-value {
+ font-size: 0.78rem;
+ font-weight: 700;
+ color: var(--garage-text-main);
+}
+
+.garage-meter-fill {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+}
+
+.garage-meter-fill.is-health {
+ background: linear-gradient(90deg, #2f7d5b 0%, #4eaa82 100%);
+}
+
+.garage-meter-fill.is-fuel {
+ background: linear-gradient(90deg, #12365d 0%, #3c6792 100%);
+}
+
+.garage-btn {
+ min-height: 2.75rem;
+ padding: 0.72rem 1rem;
+ border-radius: 0.8rem;
+ border: 1px solid var(--garage-border-strong);
+ font-size: 0.82rem;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.garage-btn-primary {
+ background: rgb(255 255 255 / 0.68);
+ color: var(--garage-accent);
+}
+
+.garage-btn-primary:hover {
+ background: rgb(219 231 243 / 0.88);
+}
+
+.garage-btn-secondary {
+ background: rgb(255 255 255 / 0.42);
+ color: var(--garage-text-muted);
+}
+
+.garage-btn-secondary:hover {
+ background: rgb(255 255 255 / 0.6);
+ color: var(--garage-text-main);
+}
+
+.garage-hitpoint-row {
+ padding: 0.72rem 0.78rem;
+ border-radius: 0.85rem;
+ border: 1px solid var(--garage-border);
+ background: rgb(255 255 255 / 0.52);
+}
+
+.garage-detail-empty,
+.garage-empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ min-height: 100%;
+}
+
+.garage-empty-title {
+ margin: 0 0 0.35rem;
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--garage-text-main);
+}
+
+.garage-empty-inline {
+ padding: 0.9rem;
+ border-radius: 0.85rem;
+ border: 1px dashed var(--garage-border);
+ color: var(--garage-text-muted);
+ background: rgb(255 255 255 / 0.36);
+}
+
+.garage-toast-stack {
+ position: fixed;
+ top: 1.2rem;
+ right: 1.5rem;
+ z-index: 10;
+ display: flex;
+ flex-direction: column;
+ gap: 0.65rem;
+}
+
+.garage-toast {
+ max-width: 24rem;
+ padding: 0.85rem 1rem;
+ border-radius: 0.9rem;
+ border: 1px solid var(--garage-border);
+ background: #fff;
+ box-shadow: 0 14px 28px rgb(16 34 56 / 0.14);
+ font-size: 0.92rem;
+}
+
+.garage-toast.is-success {
+ background: #ecfdf5;
+ border-color: #bbf7d0;
+ color: #166534;
+}
+
+.garage-toast.is-error {
+ background: #fef2f2;
+ border-color: #fecaca;
+ color: #991b1b;
+}
+
+@media (max-width: 1440px) {
+ .garage-layout {
+ grid-template-columns: 288px minmax(0, 1fr);
+ }
+
+ .garage-detail-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 1120px) {
+ .garage-layout {
+ grid-template-columns: 1fr;
+ overflow: auto;
+ }
+
+ .garage-main,
+ .garage-sidebar {
+ min-height: auto;
+ }
+
+ .garage-dashboard {
+ grid-template-columns: 1fr;
+ }
+
+ .garage-detail-card {
+ grid-column: auto;
+ }
+
+ .garage-scroll-body {
+ max-height: none;
+ min-height: 16rem;
+ }
+
+ .garage-footer {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/arma/client/addons/garage/ui/_site/garage-ui.js b/arma/client/addons/garage/ui/_site/garage-ui.js
new file mode 100644
index 0000000..df7e4b9
--- /dev/null
+++ b/arma/client/addons/garage/ui/_site/garage-ui.js
@@ -0,0 +1,1291 @@
+/* Generated by tools/build-webui.mjs for Garage UI app. Do not edit directly. */
+(function () {
+ const runtime = window.ForgeWebUI;
+ const GarageApp = (window.GarageApp = window.GarageApp || {});
+
+ GarageApp.runtime = runtime;
+ window.AppRuntime = runtime;
+})();
+
+(function () {
+ const GarageApp = (window.GarageApp = window.GarageApp || {});
+
+ const defaultSession = {
+ garageName: "Vehicle Garage",
+ capacityUsed: 0,
+ capacityMax: 5,
+ nearbyCount: 0,
+ spawnBlocked: false,
+ spawnStatus: "Ready",
+ };
+
+ const defaultGarage = {
+ vehicles: [],
+ };
+
+ const defaultNearby = {
+ vehicles: [],
+ };
+
+ function cloneValue(value) {
+ return JSON.parse(JSON.stringify(value));
+ }
+
+ function replaceObject(target, source) {
+ Object.keys(target).forEach((key) => delete target[key]);
+ Object.assign(target, cloneValue(source));
+ }
+
+ GarageApp.data = {
+ categories: [
+ { id: "all", label: "All" },
+ { id: "car", label: "Cars" },
+ { id: "armor", label: "Armor" },
+ { id: "air", label: "Air" },
+ { id: "naval", label: "Naval" },
+ { id: "other", label: "Other" },
+ ],
+ session: Object.assign({}, defaultSession),
+ garage: Object.assign({}, defaultGarage),
+ nearby: Object.assign({}, defaultNearby),
+ applyHydratePayload(payload) {
+ replaceObject(
+ this.session,
+ Object.assign({}, defaultSession, payload?.session || {}),
+ );
+ replaceObject(
+ this.garage,
+ Object.assign({}, defaultGarage, payload?.garage || {}),
+ );
+ replaceObject(
+ this.nearby,
+ Object.assign({}, defaultNearby, payload?.nearby || {}),
+ );
+ },
+ };
+})();
+
+(function () {
+ const GarageApp = (window.GarageApp = window.GarageApp || {});
+ const { createSignal } = GarageApp.runtime;
+
+ class GarageStore {
+ constructor() {
+ [this.getSelectedKind, this.setSelectedKind] = createSignal("");
+ [this.getSelectedId, this.setSelectedId] = createSignal("");
+ [this.getSearchQuery, this.setSearchQuery] = createSignal("");
+ [this.getCategoryFilter, this.setCategoryFilter] =
+ createSignal("all");
+ [this.getPendingAction, this.setPendingAction] = createSignal("");
+ [this.getNotice, this.setNotice] = createSignal({
+ type: "",
+ text: "",
+ });
+ }
+
+ getSelection() {
+ return {
+ id: this.getSelectedId(),
+ kind: this.getSelectedKind(),
+ };
+ }
+
+ clearSelection() {
+ this.setSelectedKind("");
+ this.setSelectedId("");
+ }
+
+ select(kind, id) {
+ this.setSelectedKind(String(kind || ""));
+ this.setSelectedId(String(id || ""));
+ }
+
+ startAction(action) {
+ this.setPendingAction(String(action || ""));
+ }
+
+ finishAction() {
+ this.setPendingAction("");
+ }
+
+ matchesSelection(entry) {
+ if (!entry || typeof entry !== "object") {
+ return false;
+ }
+
+ const selection = this.getSelection();
+ if (!selection.kind || !selection.id) {
+ return false;
+ }
+
+ if (selection.kind === "stored") {
+ return (
+ entry.entryKind === "stored" &&
+ String(entry.plate || "") === selection.id
+ );
+ }
+
+ if (selection.kind === "nearby") {
+ return (
+ entry.entryKind === "nearby" &&
+ String(entry.netId || "") === selection.id
+ );
+ }
+
+ return false;
+ }
+
+ ensureSelection() {
+ const garageVehicles = Array.isArray(
+ GarageApp.data?.garage?.vehicles,
+ )
+ ? GarageApp.data.garage.vehicles
+ : [];
+ const nearbyVehicles = Array.isArray(
+ GarageApp.data?.nearby?.vehicles,
+ )
+ ? GarageApp.data.nearby.vehicles
+ : [];
+ const hasCurrentSelection = [
+ ...garageVehicles,
+ ...nearbyVehicles,
+ ].some((entry) => this.matchesSelection(entry));
+
+ if (hasCurrentSelection) {
+ return;
+ }
+
+ const firstStored = garageVehicles[0] || null;
+ if (firstStored) {
+ this.select("stored", firstStored.plate || "");
+ return;
+ }
+
+ const firstNearby = nearbyVehicles[0] || null;
+ if (firstNearby) {
+ this.select("nearby", firstNearby.netId || "");
+ return;
+ }
+
+ this.clearSelection();
+ }
+
+ hydrateFromPayload() {
+ this.finishAction();
+ this.ensureSelection();
+ }
+ }
+
+ GarageApp.store = new GarageStore();
+})();
+
+(function () {
+ const GarageApp = (window.GarageApp = window.GarageApp || {});
+ const store = GarageApp.store;
+ const bridge = window.ForgeWebUI.createBridge({
+ closeEvent: "garage::close",
+ globalName: "ForgeBridge",
+ readyEvent: "garage::ready",
+ });
+
+ function requestClose() {
+ return bridge.close({});
+ }
+
+ function requestRefresh() {
+ return bridge.send("garage::refresh", {});
+ }
+
+ function requestRetrieve(payload) {
+ return bridge.send("garage::vehicle::retrieve::request", payload);
+ }
+
+ function requestStore(payload) {
+ return bridge.send("garage::vehicle::store::request", payload);
+ }
+
+ function notifyReady() {
+ return bridge.ready({ loaded: true });
+ }
+
+ function hydrate(payloadData) {
+ GarageApp.data.applyHydratePayload(payloadData);
+ store.hydrateFromPayload(payloadData);
+ }
+
+ bridge.on("garage::hydrate", hydrate);
+ bridge.on("garage::sync", hydrate);
+
+ bridge.on("garage::retrieve::success", (payloadData) => {
+ store.finishAction();
+ if (GarageApp.actions) {
+ GarageApp.actions.showNotice(
+ "success",
+ payloadData.message || "Vehicle retrieved from the garage.",
+ );
+ }
+ });
+
+ bridge.on("garage::retrieve::failure", (payloadData) => {
+ store.finishAction();
+ if (GarageApp.actions) {
+ GarageApp.actions.showNotice(
+ "error",
+ payloadData.message || "Unable to retrieve vehicle.",
+ );
+ }
+ });
+
+ bridge.on("garage::store::success", (payloadData) => {
+ store.finishAction();
+ if (GarageApp.actions) {
+ GarageApp.actions.showNotice(
+ "success",
+ payloadData.message || "Vehicle stored in the garage.",
+ );
+ }
+ });
+
+ bridge.on("garage::store::failure", (payloadData) => {
+ store.finishAction();
+ if (GarageApp.actions) {
+ GarageApp.actions.showNotice(
+ "error",
+ payloadData.message || "Unable to store vehicle.",
+ );
+ }
+ });
+
+ GarageApp.bridge = {
+ notifyReady,
+ receive: bridge.receive,
+ requestClose,
+ requestRefresh,
+ requestRetrieve,
+ requestStore,
+ sendEvent: bridge.send,
+ };
+})();
+
+(function () {
+ const GarageApp = (window.GarageApp = window.GarageApp || {});
+ const store = GarageApp.store;
+
+ let noticeTimer = null;
+
+ function getStoredVehicles() {
+ return Array.isArray(GarageApp.data?.garage?.vehicles)
+ ? GarageApp.data.garage.vehicles
+ : [];
+ }
+
+ function getNearbyVehicles() {
+ return Array.isArray(GarageApp.data?.nearby?.vehicles)
+ ? GarageApp.data.nearby.vehicles
+ : [];
+ }
+
+ function getSelectedEntry() {
+ const selection = store.getSelection();
+ if (selection.kind === "stored") {
+ return (
+ getStoredVehicles().find(
+ (vehicle) => String(vehicle.plate || "") === selection.id,
+ ) || null
+ );
+ }
+
+ if (selection.kind === "nearby") {
+ return (
+ getNearbyVehicles().find(
+ (vehicle) => String(vehicle.netId || "") === selection.id,
+ ) || null
+ );
+ }
+
+ return null;
+ }
+
+ function showNotice(type, text) {
+ store.setNotice({ type, text });
+
+ if (noticeTimer) {
+ clearTimeout(noticeTimer);
+ }
+
+ noticeTimer = setTimeout(() => {
+ store.setNotice({ type: "", text: "" });
+ noticeTimer = null;
+ }, 3200);
+ }
+
+ function closeGarage() {
+ const bridge = GarageApp.bridge;
+ if (bridge && typeof bridge.requestClose === "function") {
+ const sent = bridge.requestClose();
+ if (sent) {
+ return true;
+ }
+ }
+
+ showNotice("error", "Garage bridge is unavailable.");
+ return false;
+ }
+
+ function refreshGarage() {
+ const bridge = GarageApp.bridge;
+ if (bridge && typeof bridge.requestRefresh === "function") {
+ const sent = bridge.requestRefresh();
+ if (sent) {
+ return true;
+ }
+ }
+
+ showNotice("error", "Garage refresh bridge is unavailable.");
+ return false;
+ }
+
+ function applySearchQuery(value) {
+ store.setSearchQuery(String(value || "").trim());
+ }
+
+ function clearSearch() {
+ store.setSearchQuery("");
+ }
+
+ function selectCategory(categoryId) {
+ store.setCategoryFilter(String(categoryId || "all").trim() || "all");
+ }
+
+ function selectEntry(kind, id) {
+ store.select(kind, id);
+ }
+
+ function requestRetrieveSelected() {
+ const selectedEntry = getSelectedEntry();
+ if (!selectedEntry || selectedEntry.entryKind !== "stored") {
+ showNotice("error", "Select a stored vehicle to retrieve.");
+ return false;
+ }
+
+ if (GarageApp.data?.session?.spawnBlocked) {
+ showNotice("error", "The garage spawn area is blocked.");
+ return false;
+ }
+
+ const bridge = GarageApp.bridge;
+ if (!bridge || typeof bridge.requestRetrieve !== "function") {
+ showNotice("error", "Garage retrieve bridge is unavailable.");
+ return false;
+ }
+
+ store.startAction("retrieve");
+ const sent = bridge.requestRetrieve({
+ plate: selectedEntry.plate || "",
+ });
+
+ if (!sent) {
+ store.finishAction();
+ showNotice("error", "Garage retrieve bridge is unavailable.");
+ return false;
+ }
+
+ return true;
+ }
+
+ function requestStoreSelected() {
+ const selectedEntry = getSelectedEntry();
+ if (!selectedEntry || selectedEntry.entryKind !== "nearby") {
+ showNotice("error", "Select a nearby vehicle to store.");
+ return false;
+ }
+
+ if (selectedEntry.isEmpty === false) {
+ showNotice(
+ "error",
+ "All crew must exit the vehicle before storing it.",
+ );
+ return false;
+ }
+
+ const bridge = GarageApp.bridge;
+ if (!bridge || typeof bridge.requestStore !== "function") {
+ showNotice("error", "Garage store bridge is unavailable.");
+ return false;
+ }
+
+ store.startAction("store");
+ const sent = bridge.requestStore({
+ netId: selectedEntry.netId || "",
+ });
+
+ if (!sent) {
+ store.finishAction();
+ showNotice("error", "Garage store bridge is unavailable.");
+ return false;
+ }
+
+ return true;
+ }
+
+ GarageApp.actions = {
+ showNotice,
+ closeGarage,
+ refreshGarage,
+ applySearchQuery,
+ clearSearch,
+ selectCategory,
+ selectEntry,
+ getSelectedEntry,
+ requestRetrieveSelected,
+ requestStoreSelected,
+ };
+})();
+
+(function () {
+ const GarageApp = (window.GarageApp = window.GarageApp || {});
+ const { h } = GarageApp.runtime;
+ const WindowTitleBar = window.SharedUI.componentFns.WindowTitleBar;
+ const store = GarageApp.store;
+ const actions = GarageApp.actions;
+ const { categories, garage, nearby, session } = GarageApp.data;
+
+ function q(query, values) {
+ const needle = String(query || "")
+ .trim()
+ .toLowerCase();
+ if (!needle) {
+ return true;
+ }
+
+ return values.some((value) =>
+ String(value || "")
+ .toLowerCase()
+ .includes(needle),
+ );
+ }
+
+ function pct(value) {
+ return Math.max(0, Math.min(100, Math.round(Number(value || 0) * 100)));
+ }
+
+ function categoryLabel(category) {
+ const match = categories.find(
+ (entry) => entry.id === String(category || "other").toLowerCase(),
+ );
+ return match ? match.label : "Other";
+ }
+
+ function distanceLabel(value) {
+ return `${Math.round(Number(value || 0))} m`;
+ }
+
+ function plateLabel(value) {
+ return String(value || "").trim() || "Untracked";
+ }
+
+ function statusLabel(vehicle) {
+ if (!vehicle) {
+ return "-";
+ }
+
+ if (vehicle.entryKind === "stored") {
+ return "Stored";
+ }
+
+ return vehicle.isEmpty === false ? "Crewed" : "Ready";
+ }
+
+ function normalizeHitPointLabel(value) {
+ return String(value || "")
+ .replace(/^Hit/i, "")
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
+ .replace(/_/g, " ")
+ .trim();
+ }
+
+ function sameEntry(left, right) {
+ if (!left || !right) {
+ return false;
+ }
+
+ return (
+ String(left.entryKind || "") === String(right.entryKind || "") &&
+ String(left.plate || "") === String(right.plate || "") &&
+ String(left.netId || "") === String(right.netId || "")
+ );
+ }
+
+ function selectedEntry(state) {
+ if (state.selectedKind === "stored") {
+ return (
+ (garage.vehicles || []).find(
+ (vehicle) =>
+ String(vehicle.plate || "") === state.selectedId,
+ ) || null
+ );
+ }
+
+ if (state.selectedKind === "nearby") {
+ return (
+ (nearby.vehicles || []).find(
+ (vehicle) =>
+ String(vehicle.netId || "") === state.selectedId,
+ ) || null
+ );
+ }
+
+ return null;
+ }
+
+ function visibleVehicles(vehicles, state) {
+ return (vehicles || []).filter((vehicle) => {
+ if (
+ state.categoryFilter !== "all" &&
+ String(vehicle.category || "").toLowerCase() !==
+ state.categoryFilter
+ ) {
+ return false;
+ }
+
+ return q(state.searchQuery, [
+ vehicle.displayName,
+ vehicle.classname,
+ vehicle.plate,
+ vehicle.netId,
+ vehicle.category,
+ ]);
+ });
+ }
+
+ function stat(label, value, tone = "") {
+ return h(
+ "div",
+ {
+ className: tone
+ ? `garage-stat-card is-${tone}`
+ : "garage-stat-card",
+ },
+ h("span", { className: "garage-stat-label" }, label),
+ h("span", { className: "garage-stat-value" }, value),
+ );
+ }
+
+ function meter(label, percent, tone) {
+ return h(
+ "div",
+ { className: "garage-meter" },
+ h(
+ "div",
+ { className: "garage-meter-label-row" },
+ h("span", { className: "garage-meter-label" }, label),
+ h("span", { className: "garage-meter-value" }, `${percent}%`),
+ ),
+ h(
+ "div",
+ { className: "garage-meter-track" },
+ h("span", {
+ className: `garage-meter-fill is-${tone}`,
+ style: { width: `${percent}%` },
+ }),
+ ),
+ );
+ }
+
+ function vehicleItem(vehicle, currentSelection) {
+ const id =
+ vehicle.entryKind === "stored"
+ ? String(vehicle.plate || "")
+ : String(vehicle.netId || "");
+ const isNearby = vehicle.entryKind === "nearby";
+
+ return h(
+ "button",
+ {
+ type: "button",
+ className: sameEntry(vehicle, currentSelection)
+ ? "garage-vehicle-item is-selected"
+ : "garage-vehicle-item",
+ onClick: () => actions.selectEntry(vehicle.entryKind, id),
+ },
+ h(
+ "div",
+ { className: "garage-vehicle-item-head" },
+ h(
+ "div",
+ { className: "garage-vehicle-copy" },
+ h(
+ "span",
+ { className: "garage-vehicle-title" },
+ vehicle.displayName || vehicle.classname || "Vehicle",
+ ),
+ h(
+ "span",
+ { className: "garage-vehicle-meta" },
+ isNearby
+ ? `Nearby ${distanceLabel(vehicle.distance)}`
+ : `Plate ${plateLabel(vehicle.plate)}`,
+ ),
+ ),
+ h(
+ "span",
+ {
+ className:
+ isNearby && vehicle.isEmpty === false
+ ? "garage-badge is-warning"
+ : "garage-badge",
+ },
+ isNearby
+ ? vehicle.isEmpty === false
+ ? "Crewed"
+ : "Empty"
+ : categoryLabel(vehicle.category),
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-inline-meters" },
+ meter("Health", pct(vehicle.health), "health"),
+ meter("Fuel", pct(vehicle.fuel), "fuel"),
+ ),
+ );
+ }
+
+ function vehicleList(title, eyebrow, scrollId, vehicles, currentSelection) {
+ return h(
+ "section",
+ { className: "garage-card garage-list-card" },
+ h(
+ "div",
+ { className: "garage-card-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "garage-eyebrow" }, eyebrow),
+ h("h2", { className: "garage-section-title" }, title),
+ ),
+ h(
+ "span",
+ { className: "garage-pill" },
+ `${vehicles.length} ${vehicles.length === 1 ? "Vehicle" : "Vehicles"}`,
+ ),
+ ),
+ h(
+ "div",
+ {
+ className: "garage-card-body garage-scroll-body",
+ "data-preserve-scroll-id": scrollId,
+ },
+ vehicles.length > 0
+ ? vehicles.map((vehicle) =>
+ vehicleItem(vehicle, currentSelection),
+ )
+ : h(
+ "div",
+ { className: "garage-empty-state" },
+ h(
+ "h3",
+ { className: "garage-empty-title" },
+ "No matching vehicles",
+ ),
+ h(
+ "p",
+ { className: "garage-empty-copy" },
+ "Adjust the current search or category filter to view more records.",
+ ),
+ ),
+ ),
+ );
+ }
+
+ function hitPointRows(hitPoints) {
+ const rows = (Array.isArray(hitPoints) ? hitPoints : [])
+ .slice()
+ .sort(
+ (left, right) =>
+ Number(right.value || 0) - Number(left.value || 0),
+ )
+ .slice(0, 6)
+ .filter((row) => Number(row.value || 0) > 0);
+
+ if (rows.length === 0) {
+ return h(
+ "div",
+ { className: "garage-empty-inline" },
+ "No subsystem damage reported.",
+ );
+ }
+
+ return h(
+ "div",
+ { className: "garage-hitpoint-grid" },
+ rows.map((row) =>
+ h(
+ "div",
+ { className: "garage-hitpoint-row" },
+ h(
+ "div",
+ { className: "garage-hitpoint-copy" },
+ h(
+ "span",
+ { className: "garage-hitpoint-name" },
+ normalizeHitPointLabel(row.name) || "Subsystem",
+ ),
+ row.selection
+ ? h(
+ "span",
+ { className: "garage-hitpoint-selection" },
+ row.selection,
+ )
+ : null,
+ ),
+ h(
+ "span",
+ { className: "garage-hitpoint-value" },
+ `${Math.round(Number(row.value || 0) * 100)}%`,
+ ),
+ ),
+ ),
+ );
+ }
+
+ function detailPanel(currentSelection, state) {
+ if (!currentSelection) {
+ return h(
+ "section",
+ { className: "garage-card garage-detail-card" },
+ h(
+ "div",
+ { className: "garage-card-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "garage-eyebrow" }, "Selection"),
+ h(
+ "h2",
+ { className: "garage-section-title" },
+ "Vehicle Detail",
+ ),
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-card-body garage-detail-empty" },
+ h(
+ "h3",
+ { className: "garage-empty-title" },
+ "Select a vehicle",
+ ),
+ h(
+ "p",
+ { className: "garage-empty-copy" },
+ "Choose a stored record to retrieve or a nearby vehicle to store.",
+ ),
+ ),
+ );
+ }
+
+ const isStored = currentSelection.entryKind === "stored";
+ const pendingAction = String(state.pendingAction || "");
+ const isBusy =
+ pendingAction === "retrieve" || pendingAction === "store";
+ const canRetrieve = isStored && !session.spawnBlocked && !isBusy;
+ const canStore =
+ !isStored && currentSelection.isEmpty !== false && !isBusy;
+
+ return h(
+ "section",
+ { className: "garage-card garage-detail-card" },
+ h(
+ "div",
+ { className: "garage-card-header" },
+ h(
+ "div",
+ null,
+ h(
+ "span",
+ { className: "garage-eyebrow" },
+ isStored ? "Stored Record" : "Nearby Vehicle",
+ ),
+ h(
+ "h2",
+ { className: "garage-section-title" },
+ currentSelection.displayName ||
+ currentSelection.classname ||
+ "Vehicle",
+ ),
+ ),
+ h(
+ "span",
+ {
+ className:
+ currentSelection.entryKind === "nearby" &&
+ currentSelection.isEmpty === false
+ ? "garage-badge is-warning"
+ : "garage-badge",
+ },
+ isStored
+ ? `Plate ${plateLabel(currentSelection.plate)}`
+ : currentSelection.isEmpty === false
+ ? "Crewed"
+ : "Ready",
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-card-body garage-detail-body" },
+ h(
+ "div",
+ { className: "garage-detail-grid" },
+ h(
+ "div",
+ { className: "garage-detail-copy" },
+ h(
+ "div",
+ { className: "garage-detail-meta" },
+ stat(
+ "Category",
+ categoryLabel(currentSelection.category),
+ ),
+ stat(
+ "Status",
+ statusLabel(currentSelection),
+ currentSelection.entryKind === "nearby" &&
+ currentSelection.isEmpty === false
+ ? "danger"
+ : "",
+ ),
+ stat(
+ isStored ? "Record" : "Distance",
+ isStored
+ ? plateLabel(currentSelection.plate)
+ : distanceLabel(currentSelection.distance),
+ isStored ? "" : "accent",
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-meter-stack" },
+ meter(
+ "Health",
+ pct(currentSelection.health),
+ "health",
+ ),
+ meter("Fuel", pct(currentSelection.fuel), "fuel"),
+ ),
+ h(
+ "div",
+ { className: "garage-action-row" },
+ isStored
+ ? h(
+ "button",
+ {
+ type: "button",
+ className:
+ "garage-btn garage-btn-primary",
+ disabled: !canRetrieve,
+ onClick: () =>
+ actions.requestRetrieveSelected(),
+ },
+ pendingAction === "retrieve"
+ ? "Retrieving..."
+ : "Retrieve Vehicle",
+ )
+ : h(
+ "button",
+ {
+ type: "button",
+ className:
+ "garage-btn garage-btn-primary",
+ disabled: !canStore,
+ onClick: () =>
+ actions.requestStoreSelected(),
+ },
+ pendingAction === "store"
+ ? "Storing..."
+ : "Store Vehicle",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className:
+ "garage-btn garage-btn-secondary",
+ disabled: isBusy,
+ onClick: () => actions.refreshGarage(),
+ },
+ "Refresh",
+ ),
+ ),
+ h(
+ "p",
+ { className: "garage-detail-note" },
+ isStored
+ ? session.spawnBlocked
+ ? "The garage spawn lane is currently blocked."
+ : "Retrieve this stored vehicle into the active spawn lane."
+ : currentSelection.isEmpty === false
+ ? "Only empty nearby vehicles can be stored."
+ : "Store this nearby vehicle back into persistent garage storage.",
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-detail-subsystems" },
+ h(
+ "div",
+ { className: "garage-subsystem-header" },
+ h(
+ "span",
+ { className: "garage-eyebrow" },
+ "Subsystems",
+ ),
+ h(
+ "span",
+ { className: "garage-detail-caption" },
+ "Highest damage first",
+ ),
+ ),
+ hitPointRows(currentSelection.hitPoints),
+ ),
+ ),
+ ),
+ );
+ }
+
+ GarageApp.components = GarageApp.components || {};
+ GarageApp.components.App = function App() {
+ const state = {
+ categoryFilter: store.getCategoryFilter(),
+ notice: store.getNotice(),
+ pendingAction: store.getPendingAction(),
+ searchQuery: store.getSearchQuery(),
+ selectedId: store.getSelectedId(),
+ selectedKind: store.getSelectedKind(),
+ };
+ const currentSelection = selectedEntry(state);
+ const storedVehicles = visibleVehicles(garage.vehicles || [], state);
+ const nearbyVehicles = visibleVehicles(nearby.vehicles || [], state);
+ const searchLabel = state.searchQuery
+ ? `Search: ${state.searchQuery}`
+ : "Live";
+
+ return h(
+ "div",
+ { className: "garage-shell" },
+ WindowTitleBar({
+ kicker: "FORGE Logistics",
+ title: "Vehicle Garage",
+ onClose: () => actions.closeGarage(),
+ closeLabel: "Close garage interface",
+ }),
+ state.notice.text
+ ? h(
+ "div",
+ { className: "garage-toast-stack" },
+ h(
+ "div",
+ {
+ className:
+ state.notice.type === "error"
+ ? "garage-toast is-error"
+ : "garage-toast is-success",
+ },
+ state.notice.text,
+ ),
+ )
+ : null,
+ h(
+ "div",
+ { className: "garage-layout" },
+ h(
+ "aside",
+ { className: "garage-sidebar" },
+ h(
+ "section",
+ { className: "garage-module" },
+ h(
+ "div",
+ { className: "garage-module-header" },
+ h(
+ "div",
+ null,
+ h(
+ "span",
+ { className: "garage-eyebrow" },
+ "Search",
+ ),
+ h(
+ "h2",
+ { className: "garage-section-title" },
+ "Vehicle Records",
+ ),
+ ),
+ h(
+ "span",
+ { className: "garage-pill" },
+ searchLabel,
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-search-form" },
+ h("input", {
+ id: "garage-search-input",
+ type: "text",
+ className: "garage-search-input",
+ placeholder:
+ "Search by name, plate, or category",
+ value: state.searchQuery,
+ }),
+ h(
+ "div",
+ { className: "garage-search-actions" },
+ h(
+ "button",
+ {
+ type: "button",
+ className:
+ "garage-btn garage-btn-primary",
+ onClick: () =>
+ actions.applySearchQuery(
+ document.getElementById(
+ "garage-search-input",
+ )?.value || "",
+ ),
+ },
+ "Apply Search",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className:
+ "garage-btn garage-btn-secondary",
+ onClick: () => actions.clearSearch(),
+ },
+ "Clear",
+ ),
+ ),
+ ),
+ ),
+ h(
+ "section",
+ { className: "garage-module" },
+ h(
+ "div",
+ { className: "garage-module-header" },
+ h(
+ "div",
+ null,
+ h(
+ "span",
+ { className: "garage-eyebrow" },
+ "Filter",
+ ),
+ h(
+ "h2",
+ { className: "garage-section-title" },
+ "Vehicle Categories",
+ ),
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-category-grid" },
+ categories.map((category) =>
+ h(
+ "button",
+ {
+ type: "button",
+ className:
+ state.categoryFilter === category.id
+ ? "garage-chip is-active"
+ : "garage-chip",
+ onClick: () =>
+ actions.selectCategory(category.id),
+ },
+ category.label,
+ ),
+ ),
+ ),
+ ),
+ h(
+ "section",
+ { className: "garage-module" },
+ h(
+ "div",
+ { className: "garage-module-header" },
+ h(
+ "div",
+ null,
+ h(
+ "span",
+ { className: "garage-eyebrow" },
+ "Status",
+ ),
+ h(
+ "h2",
+ { className: "garage-section-title" },
+ "Garage Summary",
+ ),
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className:
+ "garage-btn garage-btn-secondary",
+ disabled: Boolean(state.pendingAction),
+ onClick: () => actions.refreshGarage(),
+ },
+ "Refresh",
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-summary-grid" },
+ stat(
+ "Stored",
+ `${session.capacityUsed}/${session.capacityMax}`,
+ ),
+ stat("Nearby", session.nearbyCount, "accent"),
+ stat(
+ "Spawn Lane",
+ session.spawnStatus,
+ session.spawnBlocked ? "danger" : "",
+ ),
+ ),
+ ),
+ ),
+ h(
+ "main",
+ { className: "garage-main" },
+ h(
+ "section",
+ { className: "garage-panel" },
+ h(
+ "div",
+ { className: "garage-panel-header" },
+ h(
+ "div",
+ null,
+ h(
+ "span",
+ { className: "garage-eyebrow" },
+ "Operations Bay",
+ ),
+ h(
+ "h1",
+ { className: "garage-title" },
+ session.garageName || "Vehicle Garage",
+ ),
+ ),
+ h(
+ "span",
+ { className: "garage-pill" },
+ `${session.capacityUsed}/${session.capacityMax} Stored`,
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-panel-intro" },
+ h(
+ "p",
+ { className: "garage-copy" },
+ "Retrieve stored vehicles into the active spawn lane or store nearby empty vehicles back into persistent ownership records.",
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-dashboard" },
+ vehicleList(
+ "Stored Vehicles",
+ "Persistent Records",
+ "garage-stored-list",
+ storedVehicles,
+ currentSelection,
+ ),
+ vehicleList(
+ "Nearby Vehicles",
+ "Store Window",
+ "garage-nearby-list",
+ nearbyVehicles,
+ currentSelection,
+ ),
+ detailPanel(currentSelection, state),
+ ),
+ ),
+ ),
+ ),
+ h(
+ "footer",
+ { className: "garage-footer" },
+ h(
+ "div",
+ { className: "garage-footer-block" },
+ h(
+ "span",
+ { className: "garage-footer-title" },
+ "Storage Capacity",
+ ),
+ h(
+ "span",
+ { className: "garage-footer-copy" },
+ `${session.capacityUsed} of ${session.capacityMax} vehicle slot(s) are currently occupied.`,
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-footer-block" },
+ h(
+ "span",
+ { className: "garage-footer-title" },
+ "Retrieval Window",
+ ),
+ h(
+ "span",
+ { className: "garage-footer-copy" },
+ session.spawnBlocked
+ ? "Spawn lane is blocked. Clear the bay before retrieving another vehicle."
+ : "Spawn lane is clear. Stored vehicles can be retrieved immediately.",
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-footer-block" },
+ h(
+ "span",
+ { className: "garage-footer-title" },
+ "Store Rules",
+ ),
+ h(
+ "span",
+ { className: "garage-footer-copy" },
+ "Only nearby empty vehicles can be stored. Nearby count updates from the live world state.",
+ ),
+ ),
+ ),
+ );
+ };
+})();
+
+(function () {
+ const ForgeWebUI = window.ForgeWebUI;
+ const GarageApp = window.GarageApp;
+ const app = ForgeWebUI.createApp({
+ name: "garage",
+ root: "#app",
+ setup({ root }) {
+ ForgeWebUI.mount(root, () => GarageApp.components.App(), {
+ preserveScroll: true,
+ });
+
+ if (GarageApp.bridge) {
+ GarageApp.bridge.notifyReady();
+ }
+ },
+ });
+
+ app.start();
+})();
diff --git a/arma/client/addons/garage/ui/_site/index.html b/arma/client/addons/garage/ui/_site/index.html
index ccd5165..187d47a 100644
--- a/arma/client/addons/garage/ui/_site/index.html
+++ b/arma/client/addons/garage/ui/_site/index.html
@@ -1,261 +1,64 @@
+
- Vehicle Garage
-
-
+ FORGE Vehicle Garage
-
-
-
-
-
-
-
-
-
-
-
-
-
Status
-
-
-
-
-
-
-
-
-
-
Vehicle Type
-
-
-
-
-
-
-
-
-
-
-
-
Search
-
-
-
-
-
-
-
-
-
-
-
-
-
-
🚗
-
Select a vehicle to view details
-
-
-
-
-
-
-
- Status
- Stored
-
-
- Condition
- 100%
-
-
- Fuel
- 100%
-
-
- Location
- Garage A
-
-
-
-
-
-
-
-
-
-
Specifications
-
-
- Seats
- 4
-
-
- Speed
- 180 km/h
-
-
- Cargo
- 200 kg
-
-
-
-
-
-
-
-
-
-
+
diff --git a/arma/client/addons/garage/ui/_site/script.js b/arma/client/addons/garage/ui/_site/script.js
deleted file mode 100644
index 8fc68f6..0000000
--- a/arma/client/addons/garage/ui/_site/script.js
+++ /dev/null
@@ -1,472 +0,0 @@
-/**
- * Vehicle Garage Interface
- * Handles vehicle management with spawn and store actions
- */
-
-// Mock data - sample vehicles
-const mockData = {
- vehicles: [
- // Cars
- {
- id: 1,
- name: "Sedan",
- type: "car",
- icon: "🚗",
- status: "stored",
- condition: 95,
- fuel: 80,
- location: "Garage A",
- seats: 4,
- speed: "180 km/h",
- cargo: "200 kg",
- },
- {
- id: 2,
- name: "Sports Car",
- type: "car",
- icon: "🏎️",
- status: "stored",
- condition: 100,
- fuel: 100,
- location: "Garage A",
- seats: 2,
- speed: "250 km/h",
- cargo: "50 kg",
- },
- {
- id: 3,
- name: "SUV",
- type: "car",
- icon: "🚙",
- status: "active",
- condition: 85,
- fuel: 60,
- location: "In Use",
- seats: 6,
- speed: "160 km/h",
- cargo: "400 kg",
- },
- {
- id: 4,
- name: "Hatchback",
- type: "car",
- icon: "🚗",
- status: "stored",
- condition: 90,
- fuel: 75,
- location: "Garage B",
- seats: 4,
- speed: "170 km/h",
- cargo: "250 kg",
- },
-
- // Trucks
- {
- id: 5,
- name: "Pickup Truck",
- type: "truck",
- icon: "🚛",
- status: "stored",
- condition: 88,
- fuel: 70,
- location: "Garage A",
- seats: 2,
- speed: "140 km/h",
- cargo: "800 kg",
- },
- {
- id: 6,
- name: "Delivery Van",
- type: "truck",
- icon: "🚚",
- status: "stored",
- condition: 92,
- fuel: 85,
- location: "Garage B",
- seats: 3,
- speed: "130 km/h",
- cargo: "1200 kg",
- },
- {
- id: 7,
- name: "Heavy Truck",
- type: "truck",
- icon: "🚛",
- status: "active",
- condition: 75,
- fuel: 50,
- location: "In Use",
- seats: 2,
- speed: "120 km/h",
- cargo: "2000 kg",
- },
- {
- id: 8,
- name: "Box Truck",
- type: "truck",
- icon: "📦",
- status: "stored",
- condition: 80,
- fuel: 65,
- location: "Garage A",
- seats: 3,
- speed: "110 km/h",
- cargo: "1500 kg",
- },
-
- // Aircraft
- {
- id: 9,
- name: "Helicopter",
- type: "air",
- icon: "🚁",
- status: "stored",
- condition: 95,
- fuel: 90,
- location: "Helipad",
- seats: 6,
- speed: "280 km/h",
- cargo: "500 kg",
- },
- {
- id: 10,
- name: "Light Plane",
- type: "air",
- icon: "✈️",
- status: "stored",
- condition: 100,
- fuel: 100,
- location: "Hangar",
- seats: 4,
- speed: "320 km/h",
- cargo: "300 kg",
- },
-
- // Boats
- {
- id: 11,
- name: "Speedboat",
- type: "sea",
- icon: "🚤",
- status: "stored",
- condition: 93,
- fuel: 80,
- location: "Marina",
- seats: 4,
- speed: "100 km/h",
- cargo: "150 kg",
- },
- {
- id: 12,
- name: "Yacht",
- type: "sea",
- icon: "🛥️",
- status: "stored",
- condition: 98,
- fuel: 95,
- location: "Marina",
- seats: 12,
- speed: "60 km/h",
- cargo: "800 kg",
- },
- ],
-};
-
-// State
-let selectedVehicle = null;
-let statusFilter = "all";
-let typeFilter = "all";
-let searchQuery = "";
-
-// Icons by type
-const typeIcons = {
- car: "🚗",
- truck: "🚛",
- air: "🚁",
- sea: "🚤",
-};
-
-// Initialize
-function initGarage() {
- console.log("Garage interface initializing...");
-
- setupEventHandlers();
- renderVehicles();
- updateStats();
-
- console.log("Garage interface initialized");
-}
-
-// Event Handlers
-function setupEventHandlers() {
- // Close button
- const closeBtn = document.querySelector(".close-btn");
- if (closeBtn) {
- closeBtn.addEventListener("click", () => {
- console.log("Closing garage...");
- sendEvent("garage::close", {});
- });
- }
-
- // Status filters
- const filterBtns = document.querySelectorAll(".filter-btn");
- filterBtns.forEach((btn) => {
- btn.addEventListener("click", () => {
- filterBtns.forEach((b) => b.classList.remove("active"));
- btn.classList.add("active");
- statusFilter = btn.dataset.filter;
- renderVehicles();
- });
- });
-
- // Type filters
- const typeItems = document.querySelectorAll(".type-item");
- typeItems.forEach((item) => {
- item.addEventListener("click", () => {
- typeItems.forEach((i) => i.classList.remove("active"));
- item.classList.add("active");
- typeFilter = item.dataset.type;
- renderVehicles();
- });
- });
-
- // Search
- const searchInput = document.getElementById("searchInput");
- if (searchInput) {
- searchInput.addEventListener("input", (e) => {
- searchQuery = e.target.value.toLowerCase();
- renderVehicles();
- });
- }
-
- // Spawn button
- const spawnBtn = document.getElementById("spawnBtn");
- if (spawnBtn) {
- spawnBtn.addEventListener("click", () => {
- if (selectedVehicle) {
- spawnVehicle(selectedVehicle);
- }
- });
- }
-
- // Store button
- const storeBtn = document.getElementById("storeBtn");
- if (storeBtn) {
- storeBtn.addEventListener("click", () => {
- if (selectedVehicle) {
- storeVehicle(selectedVehicle);
- }
- });
- }
-}
-
-// Render vehicles
-function renderVehicles() {
- const vehiclesGrid = document.getElementById("vehiclesGrid");
- if (!vehiclesGrid) return;
-
- vehiclesGrid.innerHTML = "";
-
- // Filter vehicles
- let filtered = mockData.vehicles;
-
- // Status filter
- if (statusFilter !== "all") {
- filtered = filtered.filter((v) => v.status === statusFilter);
- }
-
- // Type filter
- if (typeFilter !== "all") {
- filtered = filtered.filter((v) => v.type === typeFilter);
- }
-
- // Search filter
- if (searchQuery) {
- filtered = filtered.filter(
- (v) =>
- v.name.toLowerCase().includes(searchQuery) ||
- v.type.toLowerCase().includes(searchQuery),
- );
- }
-
- // Render vehicles
- filtered.forEach((vehicle) => {
- const card = document.createElement("div");
- card.className = "vehicle-card";
- if (selectedVehicle && selectedVehicle.id === vehicle.id) {
- card.classList.add("selected");
- }
-
- card.innerHTML = `
- ${vehicle.icon}
- ${vehicle.name}
- ${vehicle.type}
- ${vehicle.status}
- `;
-
- card.addEventListener("click", () => selectVehicle(vehicle));
- vehiclesGrid.appendChild(card);
- });
-
- console.log(`Rendered ${filtered.length} vehicles`);
-}
-
-// Select vehicle
-function selectVehicle(vehicle) {
- selectedVehicle = vehicle;
-
- // Update selected state in grid
- document.querySelectorAll(".vehicle-card").forEach((card) => {
- card.classList.remove("selected");
- });
- event.currentTarget.classList.add("selected");
-
- // Show details
- showVehicleDetails(vehicle);
-}
-
-// Show vehicle details
-function showVehicleDetails(vehicle) {
- const noSelection = document.getElementById("noSelection");
- const vehicleDetails = document.getElementById("vehicleDetails");
- const spawnBtn = document.getElementById("spawnBtn");
- const storeBtn = document.getElementById("storeBtn");
-
- if (noSelection) noSelection.style.display = "none";
- if (vehicleDetails) vehicleDetails.style.display = "flex";
-
- // Update details
- document.getElementById("detailIcon").textContent = vehicle.icon;
- document.getElementById("detailName").textContent = vehicle.name;
- document.getElementById("detailType").textContent = vehicle.type;
- document.getElementById("detailStatus").textContent = vehicle.status;
- document.getElementById("detailCondition").textContent =
- `${vehicle.condition}%`;
- document.getElementById("detailFuel").textContent = `${vehicle.fuel}%`;
- document.getElementById("detailLocation").textContent = vehicle.location;
- document.getElementById("detailSeats").textContent = vehicle.seats;
- document.getElementById("detailSpeed").textContent = vehicle.speed;
- document.getElementById("detailCargo").textContent = vehicle.cargo;
-
- // Show/hide action buttons based on status
- if (vehicle.status === "stored") {
- spawnBtn.style.display = "flex";
- storeBtn.style.display = "none";
- } else {
- spawnBtn.style.display = "none";
- storeBtn.style.display = "flex";
- }
-}
-
-// Spawn vehicle
-function spawnVehicle(vehicle) {
- console.log("Spawning vehicle:", vehicle.name);
-
- // Update local state
- vehicle.status = "active";
- vehicle.location = "In Use";
-
- sendEvent("garage::spawn", {
- vehicleId: vehicle.id,
- vehicleName: vehicle.name,
- vehicleType: vehicle.type,
- });
-
- // Re-render
- renderVehicles();
- updateStats();
- if (selectedVehicle && selectedVehicle.id === vehicle.id) {
- showVehicleDetails(vehicle);
- }
-}
-
-// Store vehicle
-function storeVehicle(vehicle) {
- console.log("Storing vehicle:", vehicle.name);
-
- // Update local state
- vehicle.status = "stored";
- vehicle.location = "Garage A";
-
- sendEvent("garage::store", {
- vehicleId: vehicle.id,
- vehicleName: vehicle.name,
- vehicleType: vehicle.type,
- });
-
- // Re-render
- renderVehicles();
- updateStats();
- if (selectedVehicle && selectedVehicle.id === vehicle.id) {
- showVehicleDetails(vehicle);
- }
-}
-
-// Update stats
-function updateStats() {
- const stored = mockData.vehicles.filter(
- (v) => v.status === "stored",
- ).length;
- const active = mockData.vehicles.filter(
- (v) => v.status === "active",
- ).length;
- const capacity = mockData.vehicles.length + 6; // Mock capacity
-
- document.getElementById("storedCount").textContent = stored;
- document.getElementById("activeCount").textContent = active;
- document.getElementById("capacityCount").textContent = capacity;
-}
-
-// Update garage data from external source
-function updateGarageData(data) {
- if (data.vehicles) {
- mockData.vehicles = data.vehicles;
- renderVehicles();
- updateStats();
-
- // Update selected vehicle if it still exists
- if (selectedVehicle) {
- const updated = mockData.vehicles.find(
- (v) => v.id === selectedVehicle.id,
- );
- if (updated) {
- selectedVehicle = updated;
- showVehicleDetails(updated);
- } else {
- selectedVehicle = null;
- const noSelection = document.getElementById("noSelection");
- const vehicleDetails =
- document.getElementById("vehicleDetails");
- if (noSelection) noSelection.style.display = "flex";
- if (vehicleDetails) vehicleDetails.style.display = "none";
- }
- }
- }
-}
-
-// Send event to Arma
-function sendEvent(event, data) {
- if (typeof A3API !== "undefined") {
- A3API.SendAlert(
- JSON.stringify({
- event: event,
- data: data,
- }),
- );
- } else {
- console.log("Event:", event, "Data:", data);
- }
-}
-
-// Auto-initialize
-if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", initGarage);
-} else {
- initGarage();
-}
-
-// Expose functions globally
-window.updateGarageData = updateGarageData;
-window.selectVehicle = selectVehicle;
-window.spawnVehicle = spawnVehicle;
-window.storeVehicle = storeVehicle;
diff --git a/arma/client/addons/garage/ui/_site/style.css b/arma/client/addons/garage/ui/_site/style.css
deleted file mode 100644
index a5c1ef7..0000000
--- a/arma/client/addons/garage/ui/_site/style.css
+++ /dev/null
@@ -1,605 +0,0 @@
-* {
- margin: 0;
- padding: 0;
- box-sizing: border-box;
-}
-
-body {
- height: 100vh;
- width: 100vw;
- background: rgba(0, 0, 0, 0.7);
- font-family: Arial, sans-serif;
- color: rgba(200, 220, 240, 0.95);
- overflow: hidden;
-}
-
-.garage-container {
- height: 100vh;
- width: 100vw;
- padding: 2rem;
- display: flex;
- flex-direction: column;
- gap: 1.5rem;
-}
-
-/* Header Section */
-.garage-header {
- display: flex;
- align-items: center;
- gap: 1.5rem;
- padding: 1.25rem 1.5rem;
- background: rgba(15, 20, 30, 0.9);
- border: 1px solid rgba(100, 150, 200, 0.4);
- border-radius: 4px;
- box-shadow:
- 0 0 20px rgba(100, 150, 200, 0.15),
- 0 4px 16px rgba(0, 0, 0, 0.8);
-}
-
-.garage-logo {
- width: 60px;
- height: 60px;
- background: rgba(20, 30, 45, 0.8);
- border: 2px solid rgba(100, 150, 200, 0.5);
- border-radius: 4px;
- display: flex;
- align-items: center;
- justify-content: center;
-}
-
-.logo-icon {
- font-size: 2rem;
-}
-
-.garage-info {
- flex: 1;
-}
-
-.garage-title {
- font-size: 1.5rem;
- font-weight: 600;
- letter-spacing: 0.5px;
- text-transform: uppercase;
- color: rgba(200, 220, 255, 1);
- margin-bottom: 0.25rem;
-}
-
-.garage-subtitle {
- font-size: 0.875rem;
- color: rgba(140, 160, 180, 0.8);
- letter-spacing: 0.5px;
-}
-
-.garage-stats {
- display: flex;
- gap: 1.5rem;
-}
-
-.stat-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.25rem;
- padding: 0.75rem 1.25rem;
- background: rgba(20, 30, 45, 0.7);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-radius: 4px;
-}
-
-.stat-label {
- font-size: 0.7rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(140, 160, 180, 0.8);
-}
-
-.stat-value {
- font-size: 1.25rem;
- font-weight: 600;
- color: rgba(100, 200, 150, 1);
-}
-
-.header-actions {
- display: flex;
- gap: 0.75rem;
-}
-
-.action-btn {
- padding: 0.625rem 1.25rem;
- background: rgba(20, 30, 45, 0.7);
- border: 1px solid rgba(100, 150, 200, 0.4);
- border-radius: 4px;
- color: rgba(200, 220, 240, 0.95);
- font-size: 0.875rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.action-btn:hover {
- background: rgba(30, 45, 70, 0.9);
- border-color: rgba(150, 200, 255, 0.7);
- box-shadow:
- 0 0 15px rgba(100, 150, 200, 0.2),
- inset 0 0 20px rgba(100, 150, 200, 0.05);
-}
-
-.close-btn {
- border-color: rgba(200, 100, 100, 0.4);
-}
-
-.close-btn:hover {
- border-color: rgba(255, 100, 100, 0.7);
- box-shadow:
- 0 0 15px rgba(200, 100, 100, 0.2),
- inset 0 0 20px rgba(200, 100, 100, 0.05);
-}
-
-/* Main Content */
-.garage-content {
- flex: 1;
- display: grid;
- grid-template-columns: 250px 1fr 350px;
- gap: 1.5rem;
- overflow: hidden;
-}
-
-/* Panels */
-.garage-panel {
- background: rgba(15, 20, 30, 0.9);
- border: 1px solid rgba(100, 150, 200, 0.4);
- border-left: 3px solid rgba(100, 150, 200, 0.5);
- border-radius: 4px;
- display: flex;
- flex-direction: column;
- box-shadow:
- 0 0 20px rgba(100, 150, 200, 0.1),
- 0 4px 16px rgba(0, 0, 0, 0.6);
-}
-
-.panel-header {
- padding: 1.25rem 1.5rem;
- border-bottom: 1px solid rgba(100, 150, 200, 0.2);
-}
-
-.panel-title {
- font-size: 1rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(200, 220, 255, 1);
-}
-
-.panel-content {
- flex: 1;
- padding: 1.5rem;
- overflow-y: auto;
-}
-
-/* Custom Scrollbar */
-.panel-content::-webkit-scrollbar {
- width: 8px;
-}
-
-.panel-content::-webkit-scrollbar-track {
- background: rgba(15, 20, 30, 0.5);
- border-radius: 4px;
-}
-
-.panel-content::-webkit-scrollbar-thumb {
- background: rgba(100, 150, 200, 0.3);
- border-radius: 4px;
-}
-
-.panel-content::-webkit-scrollbar-thumb:hover {
- background: rgba(100, 150, 200, 0.5);
-}
-
-/* Filters */
-.filter-section {
- margin-bottom: 2rem;
-}
-
-.filter-section:last-child {
- margin-bottom: 0;
-}
-
-.filter-title {
- font-size: 0.75rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(160, 180, 200, 0.85);
- margin-bottom: 0.75rem;
-}
-
-.filter-buttons {
- display: flex;
- gap: 0.5rem;
-}
-
-.filter-btn {
- flex: 1;
- padding: 0.625rem;
- background: rgba(20, 30, 45, 0.7);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-radius: 4px;
- color: rgba(200, 220, 240, 0.95);
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.filter-btn:hover {
- background: rgba(30, 45, 70, 0.8);
- border-color: rgba(150, 200, 255, 0.5);
-}
-
-.filter-btn.active {
- background: rgba(100, 150, 200, 0.2);
- border-color: rgba(100, 150, 200, 0.6);
- box-shadow:
- 0 0 10px rgba(100, 150, 200, 0.15),
- inset 0 0 15px rgba(100, 150, 200, 0.05);
-}
-
-.type-list {
- display: flex;
- flex-direction: column;
- gap: 0.5rem;
-}
-
-.type-item {
- display: flex;
- align-items: center;
- gap: 0.75rem;
- padding: 0.875rem;
- background: rgba(20, 30, 45, 0.7);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-left: 3px solid rgba(100, 150, 200, 0.4);
- border-radius: 4px;
- cursor: pointer;
- transition: all 0.15s ease;
- text-align: left;
-}
-
-.type-item:hover {
- background: rgba(30, 45, 70, 0.8);
- border-left-color: rgba(150, 200, 255, 0.7);
-}
-
-.type-item.active {
- background: rgba(30, 45, 70, 0.9);
- border-left-color: rgba(100, 200, 150, 0.8);
- box-shadow:
- 0 0 15px rgba(100, 200, 150, 0.15),
- inset 0 0 20px rgba(100, 200, 150, 0.05);
-}
-
-.type-icon {
- font-size: 1.5rem;
-}
-
-.type-name {
- font-size: 0.875rem;
- color: rgba(200, 220, 240, 0.95);
-}
-
-.search-input {
- width: 100%;
- padding: 0.75rem 1rem;
- background: rgba(20, 30, 45, 0.7);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-radius: 4px;
- color: rgba(200, 220, 240, 0.95);
- font-size: 0.875rem;
- transition: all 0.15s ease;
-}
-
-.search-input:focus {
- outline: none;
- border-color: rgba(150, 200, 255, 0.6);
- box-shadow:
- 0 0 15px rgba(100, 150, 200, 0.15),
- inset 0 0 20px rgba(100, 150, 200, 0.05);
-}
-
-.search-input::placeholder {
- color: rgba(100, 120, 140, 0.6);
-}
-
-/* Vehicles Grid */
-.vehicles-grid {
- display: grid;
- grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
- gap: 1rem;
-}
-
-.vehicle-card {
- background: rgba(20, 30, 45, 0.6);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-left: 3px solid rgba(100, 150, 200, 0.4);
- border-radius: 4px;
- padding: 1.25rem;
- cursor: pointer;
- transition: all 0.15s ease;
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-}
-
-.vehicle-card:hover {
- background: rgba(30, 45, 70, 0.7);
- border-left-color: rgba(150, 200, 255, 0.7);
- box-shadow:
- 0 0 15px rgba(100, 150, 200, 0.15),
- inset 0 0 20px rgba(100, 150, 200, 0.05);
-}
-
-.vehicle-card.selected {
- background: rgba(30, 45, 70, 0.8);
- border-left-color: rgba(100, 200, 150, 0.8);
- box-shadow:
- 0 0 20px rgba(100, 200, 150, 0.2),
- inset 0 0 25px rgba(100, 200, 150, 0.05);
-}
-
-.vehicle-icon {
- font-size: 3rem;
- text-align: center;
-}
-
-.vehicle-name {
- font-size: 0.95rem;
- font-weight: 600;
- color: rgba(200, 220, 255, 1);
- text-align: center;
-}
-
-.vehicle-type {
- font-size: 0.75rem;
- color: rgba(140, 160, 180, 0.85);
- text-align: center;
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.vehicle-status {
- padding: 0.375rem;
- background: rgba(100, 150, 200, 0.2);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-radius: 3px;
- font-size: 0.7rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- text-align: center;
- font-weight: 600;
-}
-
-.vehicle-status.stored {
- background: rgba(100, 150, 200, 0.2);
- border-color: rgba(100, 150, 200, 0.4);
- color: rgba(150, 200, 255, 0.9);
-}
-
-.vehicle-status.active {
- background: rgba(100, 200, 150, 0.2);
- border-color: rgba(100, 200, 150, 0.4);
- color: rgba(150, 255, 200, 0.9);
-}
-
-/* Vehicle Details */
-.no-selection {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 1rem;
- padding: 3rem 1rem;
-}
-
-.no-selection-icon {
- font-size: 4rem;
- opacity: 0.3;
-}
-
-.no-selection p {
- font-size: 0.875rem;
- color: rgba(140, 160, 180, 0.7);
-}
-
-.vehicle-details {
- display: flex;
- flex-direction: column;
- gap: 1.5rem;
-}
-
-.detail-header {
- display: flex;
- align-items: center;
- gap: 1rem;
- padding: 1.25rem;
- background: rgba(20, 30, 45, 0.6);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-left: 3px solid rgba(100, 150, 200, 0.5);
- border-radius: 4px;
-}
-
-.detail-icon {
- font-size: 3rem;
-}
-
-.detail-info {
- flex: 1;
-}
-
-.detail-name {
- font-size: 1.125rem;
- font-weight: 600;
- color: rgba(200, 220, 255, 1);
- margin-bottom: 0.25rem;
-}
-
-.detail-type {
- font-size: 0.75rem;
- color: rgba(140, 160, 180, 0.85);
- text-transform: uppercase;
- letter-spacing: 0.5px;
-}
-
-.detail-stats {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 0.75rem;
-}
-
-.detail-stat {
- padding: 0.875rem;
- background: rgba(20, 30, 45, 0.6);
- border: 1px solid rgba(100, 150, 200, 0.3);
- border-radius: 4px;
- display: flex;
- flex-direction: column;
- gap: 0.375rem;
-}
-
-.detail-label {
- font-size: 0.7rem;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(140, 160, 180, 0.85);
-}
-
-.detail-value {
- font-size: 0.95rem;
- font-weight: 600;
- color: rgba(200, 220, 240, 0.95);
-}
-
-.detail-actions {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-}
-
-.detail-btn {
- padding: 1rem;
- background: rgba(20, 30, 45, 0.7);
- border: 1px solid rgba(100, 150, 200, 0.4);
- border-radius: 4px;
- display: flex;
- align-items: center;
- justify-content: center;
- gap: 0.75rem;
- cursor: pointer;
- transition: all 0.15s ease;
-}
-
-.detail-btn:hover {
- background: rgba(30, 45, 70, 0.9);
- border-color: rgba(150, 200, 255, 0.7);
- box-shadow:
- 0 0 15px rgba(100, 150, 200, 0.2),
- inset 0 0 20px rgba(100, 150, 200, 0.05);
-}
-
-.spawn-btn {
- background: rgba(100, 150, 200, 0.2);
- border-color: rgba(100, 150, 200, 0.5);
-}
-
-.spawn-btn:hover {
- background: rgba(100, 150, 200, 0.3);
- border-color: rgba(150, 200, 255, 0.7);
-}
-
-.store-btn {
- background: rgba(200, 150, 100, 0.2);
- border-color: rgba(200, 150, 100, 0.4);
-}
-
-.store-btn:hover {
- background: rgba(200, 150, 100, 0.3);
- border-color: rgba(255, 200, 150, 0.6);
-}
-
-.btn-icon {
- font-size: 1.25rem;
-}
-
-.btn-text {
- font-size: 0.875rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(200, 220, 240, 0.95);
-}
-
-.detail-specs {
- padding: 1.25rem;
- background: rgba(20, 30, 45, 0.5);
- border: 1px solid rgba(100, 150, 200, 0.2);
- border-radius: 4px;
-}
-
-.specs-title {
- font-size: 0.875rem;
- font-weight: 600;
- text-transform: uppercase;
- letter-spacing: 0.5px;
- color: rgba(180, 200, 220, 0.9);
- margin-bottom: 1rem;
-}
-
-.specs-list {
- display: flex;
- flex-direction: column;
- gap: 0.75rem;
-}
-
-.spec-item {
- display: flex;
- justify-content: space-between;
- align-items: center;
- padding-bottom: 0.75rem;
- border-bottom: 1px solid rgba(100, 150, 200, 0.15);
-}
-
-.spec-item:last-child {
- padding-bottom: 0;
- border-bottom: none;
-}
-
-.spec-label {
- font-size: 0.8rem;
- color: rgba(140, 160, 180, 0.85);
-}
-
-.spec-value {
- font-size: 0.875rem;
- font-weight: 600;
- color: rgba(200, 220, 240, 0.95);
-}
-
-/* Responsive */
-@media (max-width: 1400px) {
- .garage-content {
- grid-template-columns: 220px 1fr 320px;
- }
-
- .vehicles-grid {
- grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
- }
-}
-
-@media (max-width: 1200px) {
- .garage-content {
- grid-template-columns: 1fr 350px;
- }
-
- .filters-panel {
- display: none;
- }
-}
diff --git a/arma/client/addons/garage/ui/src/bootstrap.js b/arma/client/addons/garage/ui/src/bootstrap.js
new file mode 100644
index 0000000..f6b5949
--- /dev/null
+++ b/arma/client/addons/garage/ui/src/bootstrap.js
@@ -0,0 +1,19 @@
+(function () {
+ const ForgeWebUI = window.ForgeWebUI;
+ const GarageApp = window.GarageApp;
+ const app = ForgeWebUI.createApp({
+ name: "garage",
+ root: "#app",
+ setup({ root }) {
+ ForgeWebUI.mount(root, () => GarageApp.components.App(), {
+ preserveScroll: true,
+ });
+
+ if (GarageApp.bridge) {
+ GarageApp.bridge.notifyReady();
+ }
+ },
+ });
+
+ app.start();
+})();
diff --git a/arma/client/addons/garage/ui/src/bridge.js b/arma/client/addons/garage/ui/src/bridge.js
new file mode 100644
index 0000000..c86b282
--- /dev/null
+++ b/arma/client/addons/garage/ui/src/bridge.js
@@ -0,0 +1,87 @@
+(function () {
+ const GarageApp = (window.GarageApp = window.GarageApp || {});
+ const store = GarageApp.store;
+ const bridge = window.ForgeWebUI.createBridge({
+ closeEvent: "garage::close",
+ globalName: "ForgeBridge",
+ readyEvent: "garage::ready",
+ });
+
+ function requestClose() {
+ return bridge.close({});
+ }
+
+ function requestRefresh() {
+ return bridge.send("garage::refresh", {});
+ }
+
+ function requestRetrieve(payload) {
+ return bridge.send("garage::vehicle::retrieve::request", payload);
+ }
+
+ function requestStore(payload) {
+ return bridge.send("garage::vehicle::store::request", payload);
+ }
+
+ function notifyReady() {
+ return bridge.ready({ loaded: true });
+ }
+
+ function hydrate(payloadData) {
+ GarageApp.data.applyHydratePayload(payloadData);
+ store.hydrateFromPayload(payloadData);
+ }
+
+ bridge.on("garage::hydrate", hydrate);
+ bridge.on("garage::sync", hydrate);
+
+ bridge.on("garage::retrieve::success", (payloadData) => {
+ store.finishAction();
+ if (GarageApp.actions) {
+ GarageApp.actions.showNotice(
+ "success",
+ payloadData.message || "Vehicle retrieved from the garage.",
+ );
+ }
+ });
+
+ bridge.on("garage::retrieve::failure", (payloadData) => {
+ store.finishAction();
+ if (GarageApp.actions) {
+ GarageApp.actions.showNotice(
+ "error",
+ payloadData.message || "Unable to retrieve vehicle.",
+ );
+ }
+ });
+
+ bridge.on("garage::store::success", (payloadData) => {
+ store.finishAction();
+ if (GarageApp.actions) {
+ GarageApp.actions.showNotice(
+ "success",
+ payloadData.message || "Vehicle stored in the garage.",
+ );
+ }
+ });
+
+ bridge.on("garage::store::failure", (payloadData) => {
+ store.finishAction();
+ if (GarageApp.actions) {
+ GarageApp.actions.showNotice(
+ "error",
+ payloadData.message || "Unable to store vehicle.",
+ );
+ }
+ });
+
+ GarageApp.bridge = {
+ notifyReady,
+ receive: bridge.receive,
+ requestClose,
+ requestRefresh,
+ requestRetrieve,
+ requestStore,
+ sendEvent: bridge.send,
+ };
+})();
diff --git a/arma/client/addons/garage/ui/src/components/AppShell.js b/arma/client/addons/garage/ui/src/components/AppShell.js
new file mode 100644
index 0000000..4eeb81e
--- /dev/null
+++ b/arma/client/addons/garage/ui/src/components/AppShell.js
@@ -0,0 +1,827 @@
+(function () {
+ const GarageApp = (window.GarageApp = window.GarageApp || {});
+ const { h } = GarageApp.runtime;
+ const WindowTitleBar = window.SharedUI.componentFns.WindowTitleBar;
+ const store = GarageApp.store;
+ const actions = GarageApp.actions;
+ const { categories, garage, nearby, session } = GarageApp.data;
+
+ function q(query, values) {
+ const needle = String(query || "")
+ .trim()
+ .toLowerCase();
+ if (!needle) {
+ return true;
+ }
+
+ return values.some((value) =>
+ String(value || "")
+ .toLowerCase()
+ .includes(needle),
+ );
+ }
+
+ function pct(value) {
+ return Math.max(0, Math.min(100, Math.round(Number(value || 0) * 100)));
+ }
+
+ function categoryLabel(category) {
+ const match = categories.find(
+ (entry) => entry.id === String(category || "other").toLowerCase(),
+ );
+ return match ? match.label : "Other";
+ }
+
+ function distanceLabel(value) {
+ return `${Math.round(Number(value || 0))} m`;
+ }
+
+ function plateLabel(value) {
+ return String(value || "").trim() || "Untracked";
+ }
+
+ function statusLabel(vehicle) {
+ if (!vehicle) {
+ return "-";
+ }
+
+ if (vehicle.entryKind === "stored") {
+ return "Stored";
+ }
+
+ return vehicle.isEmpty === false ? "Crewed" : "Ready";
+ }
+
+ function normalizeHitPointLabel(value) {
+ return String(value || "")
+ .replace(/^Hit/i, "")
+ .replace(/([a-z])([A-Z])/g, "$1 $2")
+ .replace(/_/g, " ")
+ .trim();
+ }
+
+ function sameEntry(left, right) {
+ if (!left || !right) {
+ return false;
+ }
+
+ return (
+ String(left.entryKind || "") === String(right.entryKind || "") &&
+ String(left.plate || "") === String(right.plate || "") &&
+ String(left.netId || "") === String(right.netId || "")
+ );
+ }
+
+ function selectedEntry(state) {
+ if (state.selectedKind === "stored") {
+ return (
+ (garage.vehicles || []).find(
+ (vehicle) =>
+ String(vehicle.plate || "") === state.selectedId,
+ ) || null
+ );
+ }
+
+ if (state.selectedKind === "nearby") {
+ return (
+ (nearby.vehicles || []).find(
+ (vehicle) =>
+ String(vehicle.netId || "") === state.selectedId,
+ ) || null
+ );
+ }
+
+ return null;
+ }
+
+ function visibleVehicles(vehicles, state) {
+ return (vehicles || []).filter((vehicle) => {
+ if (
+ state.categoryFilter !== "all" &&
+ String(vehicle.category || "").toLowerCase() !==
+ state.categoryFilter
+ ) {
+ return false;
+ }
+
+ return q(state.searchQuery, [
+ vehicle.displayName,
+ vehicle.classname,
+ vehicle.plate,
+ vehicle.netId,
+ vehicle.category,
+ ]);
+ });
+ }
+
+ function stat(label, value, tone = "") {
+ return h(
+ "div",
+ {
+ className: tone
+ ? `garage-stat-card is-${tone}`
+ : "garage-stat-card",
+ },
+ h("span", { className: "garage-stat-label" }, label),
+ h("span", { className: "garage-stat-value" }, value),
+ );
+ }
+
+ function meter(label, percent, tone) {
+ return h(
+ "div",
+ { className: "garage-meter" },
+ h(
+ "div",
+ { className: "garage-meter-label-row" },
+ h("span", { className: "garage-meter-label" }, label),
+ h("span", { className: "garage-meter-value" }, `${percent}%`),
+ ),
+ h(
+ "div",
+ { className: "garage-meter-track" },
+ h("span", {
+ className: `garage-meter-fill is-${tone}`,
+ style: { width: `${percent}%` },
+ }),
+ ),
+ );
+ }
+
+ function vehicleItem(vehicle, currentSelection) {
+ const id =
+ vehicle.entryKind === "stored"
+ ? String(vehicle.plate || "")
+ : String(vehicle.netId || "");
+ const isNearby = vehicle.entryKind === "nearby";
+
+ return h(
+ "button",
+ {
+ type: "button",
+ className: sameEntry(vehicle, currentSelection)
+ ? "garage-vehicle-item is-selected"
+ : "garage-vehicle-item",
+ onClick: () => actions.selectEntry(vehicle.entryKind, id),
+ },
+ h(
+ "div",
+ { className: "garage-vehicle-item-head" },
+ h(
+ "div",
+ { className: "garage-vehicle-copy" },
+ h(
+ "span",
+ { className: "garage-vehicle-title" },
+ vehicle.displayName || vehicle.classname || "Vehicle",
+ ),
+ h(
+ "span",
+ { className: "garage-vehicle-meta" },
+ isNearby
+ ? `Nearby ${distanceLabel(vehicle.distance)}`
+ : `Plate ${plateLabel(vehicle.plate)}`,
+ ),
+ ),
+ h(
+ "span",
+ {
+ className:
+ isNearby && vehicle.isEmpty === false
+ ? "garage-badge is-warning"
+ : "garage-badge",
+ },
+ isNearby
+ ? vehicle.isEmpty === false
+ ? "Crewed"
+ : "Empty"
+ : categoryLabel(vehicle.category),
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-inline-meters" },
+ meter("Health", pct(vehicle.health), "health"),
+ meter("Fuel", pct(vehicle.fuel), "fuel"),
+ ),
+ );
+ }
+
+ function vehicleList(title, eyebrow, scrollId, vehicles, currentSelection) {
+ return h(
+ "section",
+ { className: "garage-card garage-list-card" },
+ h(
+ "div",
+ { className: "garage-card-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "garage-eyebrow" }, eyebrow),
+ h("h2", { className: "garage-section-title" }, title),
+ ),
+ h(
+ "span",
+ { className: "garage-pill" },
+ `${vehicles.length} ${vehicles.length === 1 ? "Vehicle" : "Vehicles"}`,
+ ),
+ ),
+ h(
+ "div",
+ {
+ className: "garage-card-body garage-scroll-body",
+ "data-preserve-scroll-id": scrollId,
+ },
+ vehicles.length > 0
+ ? vehicles.map((vehicle) =>
+ vehicleItem(vehicle, currentSelection),
+ )
+ : h(
+ "div",
+ { className: "garage-empty-state" },
+ h(
+ "h3",
+ { className: "garage-empty-title" },
+ "No matching vehicles",
+ ),
+ h(
+ "p",
+ { className: "garage-empty-copy" },
+ "Adjust the current search or category filter to view more records.",
+ ),
+ ),
+ ),
+ );
+ }
+
+ function hitPointRows(hitPoints) {
+ const rows = (Array.isArray(hitPoints) ? hitPoints : [])
+ .slice()
+ .sort(
+ (left, right) =>
+ Number(right.value || 0) - Number(left.value || 0),
+ )
+ .slice(0, 6)
+ .filter((row) => Number(row.value || 0) > 0);
+
+ if (rows.length === 0) {
+ return h(
+ "div",
+ { className: "garage-empty-inline" },
+ "No subsystem damage reported.",
+ );
+ }
+
+ return h(
+ "div",
+ { className: "garage-hitpoint-grid" },
+ rows.map((row) =>
+ h(
+ "div",
+ { className: "garage-hitpoint-row" },
+ h(
+ "div",
+ { className: "garage-hitpoint-copy" },
+ h(
+ "span",
+ { className: "garage-hitpoint-name" },
+ normalizeHitPointLabel(row.name) || "Subsystem",
+ ),
+ row.selection
+ ? h(
+ "span",
+ { className: "garage-hitpoint-selection" },
+ row.selection,
+ )
+ : null,
+ ),
+ h(
+ "span",
+ { className: "garage-hitpoint-value" },
+ `${Math.round(Number(row.value || 0) * 100)}%`,
+ ),
+ ),
+ ),
+ );
+ }
+
+ function detailPanel(currentSelection, state) {
+ if (!currentSelection) {
+ return h(
+ "section",
+ { className: "garage-card garage-detail-card" },
+ h(
+ "div",
+ { className: "garage-card-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "garage-eyebrow" }, "Selection"),
+ h(
+ "h2",
+ { className: "garage-section-title" },
+ "Vehicle Detail",
+ ),
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-card-body garage-detail-empty" },
+ h(
+ "h3",
+ { className: "garage-empty-title" },
+ "Select a vehicle",
+ ),
+ h(
+ "p",
+ { className: "garage-empty-copy" },
+ "Choose a stored record to retrieve or a nearby vehicle to store.",
+ ),
+ ),
+ );
+ }
+
+ const isStored = currentSelection.entryKind === "stored";
+ const pendingAction = String(state.pendingAction || "");
+ const isBusy =
+ pendingAction === "retrieve" || pendingAction === "store";
+ const canRetrieve = isStored && !session.spawnBlocked && !isBusy;
+ const canStore =
+ !isStored && currentSelection.isEmpty !== false && !isBusy;
+
+ return h(
+ "section",
+ { className: "garage-card garage-detail-card" },
+ h(
+ "div",
+ { className: "garage-card-header" },
+ h(
+ "div",
+ null,
+ h(
+ "span",
+ { className: "garage-eyebrow" },
+ isStored ? "Stored Record" : "Nearby Vehicle",
+ ),
+ h(
+ "h2",
+ { className: "garage-section-title" },
+ currentSelection.displayName ||
+ currentSelection.classname ||
+ "Vehicle",
+ ),
+ ),
+ h(
+ "span",
+ {
+ className:
+ currentSelection.entryKind === "nearby" &&
+ currentSelection.isEmpty === false
+ ? "garage-badge is-warning"
+ : "garage-badge",
+ },
+ isStored
+ ? `Plate ${plateLabel(currentSelection.plate)}`
+ : currentSelection.isEmpty === false
+ ? "Crewed"
+ : "Ready",
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-card-body garage-detail-body" },
+ h(
+ "div",
+ { className: "garage-detail-grid" },
+ h(
+ "div",
+ { className: "garage-detail-copy" },
+ h(
+ "div",
+ { className: "garage-detail-meta" },
+ stat(
+ "Category",
+ categoryLabel(currentSelection.category),
+ ),
+ stat(
+ "Status",
+ statusLabel(currentSelection),
+ currentSelection.entryKind === "nearby" &&
+ currentSelection.isEmpty === false
+ ? "danger"
+ : "",
+ ),
+ stat(
+ isStored ? "Record" : "Distance",
+ isStored
+ ? plateLabel(currentSelection.plate)
+ : distanceLabel(currentSelection.distance),
+ isStored ? "" : "accent",
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-meter-stack" },
+ meter(
+ "Health",
+ pct(currentSelection.health),
+ "health",
+ ),
+ meter("Fuel", pct(currentSelection.fuel), "fuel"),
+ ),
+ h(
+ "div",
+ { className: "garage-action-row" },
+ isStored
+ ? h(
+ "button",
+ {
+ type: "button",
+ className:
+ "garage-btn garage-btn-primary",
+ disabled: !canRetrieve,
+ onClick: () =>
+ actions.requestRetrieveSelected(),
+ },
+ pendingAction === "retrieve"
+ ? "Retrieving..."
+ : "Retrieve Vehicle",
+ )
+ : h(
+ "button",
+ {
+ type: "button",
+ className:
+ "garage-btn garage-btn-primary",
+ disabled: !canStore,
+ onClick: () =>
+ actions.requestStoreSelected(),
+ },
+ pendingAction === "store"
+ ? "Storing..."
+ : "Store Vehicle",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className:
+ "garage-btn garage-btn-secondary",
+ disabled: isBusy,
+ onClick: () => actions.refreshGarage(),
+ },
+ "Refresh",
+ ),
+ ),
+ h(
+ "p",
+ { className: "garage-detail-note" },
+ isStored
+ ? session.spawnBlocked
+ ? "The garage spawn lane is currently blocked."
+ : "Retrieve this stored vehicle into the active spawn lane."
+ : currentSelection.isEmpty === false
+ ? "Only empty nearby vehicles can be stored."
+ : "Store this nearby vehicle back into persistent garage storage.",
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-detail-subsystems" },
+ h(
+ "div",
+ { className: "garage-subsystem-header" },
+ h(
+ "span",
+ { className: "garage-eyebrow" },
+ "Subsystems",
+ ),
+ h(
+ "span",
+ { className: "garage-detail-caption" },
+ "Highest damage first",
+ ),
+ ),
+ hitPointRows(currentSelection.hitPoints),
+ ),
+ ),
+ ),
+ );
+ }
+
+ GarageApp.components = GarageApp.components || {};
+ GarageApp.components.App = function App() {
+ const state = {
+ categoryFilter: store.getCategoryFilter(),
+ notice: store.getNotice(),
+ pendingAction: store.getPendingAction(),
+ searchQuery: store.getSearchQuery(),
+ selectedId: store.getSelectedId(),
+ selectedKind: store.getSelectedKind(),
+ };
+ const currentSelection = selectedEntry(state);
+ const storedVehicles = visibleVehicles(garage.vehicles || [], state);
+ const nearbyVehicles = visibleVehicles(nearby.vehicles || [], state);
+ const searchLabel = state.searchQuery
+ ? `Search: ${state.searchQuery}`
+ : "Live";
+
+ return h(
+ "div",
+ { className: "garage-shell" },
+ WindowTitleBar({
+ kicker: "FORGE Logistics",
+ title: "Vehicle Garage",
+ onClose: () => actions.closeGarage(),
+ closeLabel: "Close garage interface",
+ }),
+ state.notice.text
+ ? h(
+ "div",
+ { className: "garage-toast-stack" },
+ h(
+ "div",
+ {
+ className:
+ state.notice.type === "error"
+ ? "garage-toast is-error"
+ : "garage-toast is-success",
+ },
+ state.notice.text,
+ ),
+ )
+ : null,
+ h(
+ "div",
+ { className: "garage-layout" },
+ h(
+ "aside",
+ { className: "garage-sidebar" },
+ h(
+ "section",
+ { className: "garage-module" },
+ h(
+ "div",
+ { className: "garage-module-header" },
+ h(
+ "div",
+ null,
+ h(
+ "span",
+ { className: "garage-eyebrow" },
+ "Search",
+ ),
+ h(
+ "h2",
+ { className: "garage-section-title" },
+ "Vehicle Records",
+ ),
+ ),
+ h(
+ "span",
+ { className: "garage-pill" },
+ searchLabel,
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-search-form" },
+ h("input", {
+ id: "garage-search-input",
+ type: "text",
+ className: "garage-search-input",
+ placeholder:
+ "Search by name, plate, or category",
+ value: state.searchQuery,
+ }),
+ h(
+ "div",
+ { className: "garage-search-actions" },
+ h(
+ "button",
+ {
+ type: "button",
+ className:
+ "garage-btn garage-btn-primary",
+ onClick: () =>
+ actions.applySearchQuery(
+ document.getElementById(
+ "garage-search-input",
+ )?.value || "",
+ ),
+ },
+ "Apply Search",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className:
+ "garage-btn garage-btn-secondary",
+ onClick: () => actions.clearSearch(),
+ },
+ "Clear",
+ ),
+ ),
+ ),
+ ),
+ h(
+ "section",
+ { className: "garage-module" },
+ h(
+ "div",
+ { className: "garage-module-header" },
+ h(
+ "div",
+ null,
+ h(
+ "span",
+ { className: "garage-eyebrow" },
+ "Filter",
+ ),
+ h(
+ "h2",
+ { className: "garage-section-title" },
+ "Vehicle Categories",
+ ),
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-category-grid" },
+ categories.map((category) =>
+ h(
+ "button",
+ {
+ type: "button",
+ className:
+ state.categoryFilter === category.id
+ ? "garage-chip is-active"
+ : "garage-chip",
+ onClick: () =>
+ actions.selectCategory(category.id),
+ },
+ category.label,
+ ),
+ ),
+ ),
+ ),
+ h(
+ "section",
+ { className: "garage-module" },
+ h(
+ "div",
+ { className: "garage-module-header" },
+ h(
+ "div",
+ null,
+ h(
+ "span",
+ { className: "garage-eyebrow" },
+ "Status",
+ ),
+ h(
+ "h2",
+ { className: "garage-section-title" },
+ "Garage Summary",
+ ),
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className:
+ "garage-btn garage-btn-secondary",
+ disabled: Boolean(state.pendingAction),
+ onClick: () => actions.refreshGarage(),
+ },
+ "Refresh",
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-summary-grid" },
+ stat(
+ "Stored",
+ `${session.capacityUsed}/${session.capacityMax}`,
+ ),
+ stat("Nearby", session.nearbyCount, "accent"),
+ stat(
+ "Spawn Lane",
+ session.spawnStatus,
+ session.spawnBlocked ? "danger" : "",
+ ),
+ ),
+ ),
+ ),
+ h(
+ "main",
+ { className: "garage-main" },
+ h(
+ "section",
+ { className: "garage-panel" },
+ h(
+ "div",
+ { className: "garage-panel-header" },
+ h(
+ "div",
+ null,
+ h(
+ "span",
+ { className: "garage-eyebrow" },
+ "Operations Bay",
+ ),
+ h(
+ "h1",
+ { className: "garage-title" },
+ session.garageName || "Vehicle Garage",
+ ),
+ ),
+ h(
+ "span",
+ { className: "garage-pill" },
+ `${session.capacityUsed}/${session.capacityMax} Stored`,
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-panel-intro" },
+ h(
+ "p",
+ { className: "garage-copy" },
+ "Retrieve stored vehicles into the active spawn lane or store nearby empty vehicles back into persistent ownership records.",
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-dashboard" },
+ vehicleList(
+ "Stored Vehicles",
+ "Persistent Records",
+ "garage-stored-list",
+ storedVehicles,
+ currentSelection,
+ ),
+ vehicleList(
+ "Nearby Vehicles",
+ "Store Window",
+ "garage-nearby-list",
+ nearbyVehicles,
+ currentSelection,
+ ),
+ detailPanel(currentSelection, state),
+ ),
+ ),
+ ),
+ ),
+ h(
+ "footer",
+ { className: "garage-footer" },
+ h(
+ "div",
+ { className: "garage-footer-block" },
+ h(
+ "span",
+ { className: "garage-footer-title" },
+ "Storage Capacity",
+ ),
+ h(
+ "span",
+ { className: "garage-footer-copy" },
+ `${session.capacityUsed} of ${session.capacityMax} vehicle slot(s) are currently occupied.`,
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-footer-block" },
+ h(
+ "span",
+ { className: "garage-footer-title" },
+ "Retrieval Window",
+ ),
+ h(
+ "span",
+ { className: "garage-footer-copy" },
+ session.spawnBlocked
+ ? "Spawn lane is blocked. Clear the bay before retrieving another vehicle."
+ : "Spawn lane is clear. Stored vehicles can be retrieved immediately.",
+ ),
+ ),
+ h(
+ "div",
+ { className: "garage-footer-block" },
+ h(
+ "span",
+ { className: "garage-footer-title" },
+ "Store Rules",
+ ),
+ h(
+ "span",
+ { className: "garage-footer-copy" },
+ "Only nearby empty vehicles can be stored. Nearby count updates from the live world state.",
+ ),
+ ),
+ ),
+ );
+ };
+})();
diff --git a/arma/client/addons/garage/ui/src/data.js b/arma/client/addons/garage/ui/src/data.js
new file mode 100644
index 0000000..deca479
--- /dev/null
+++ b/arma/client/addons/garage/ui/src/data.js
@@ -0,0 +1,57 @@
+(function () {
+ const GarageApp = (window.GarageApp = window.GarageApp || {});
+
+ const defaultSession = {
+ garageName: "Vehicle Garage",
+ capacityUsed: 0,
+ capacityMax: 5,
+ nearbyCount: 0,
+ spawnBlocked: false,
+ spawnStatus: "Ready",
+ };
+
+ const defaultGarage = {
+ vehicles: [],
+ };
+
+ const defaultNearby = {
+ vehicles: [],
+ };
+
+ function cloneValue(value) {
+ return JSON.parse(JSON.stringify(value));
+ }
+
+ function replaceObject(target, source) {
+ Object.keys(target).forEach((key) => delete target[key]);
+ Object.assign(target, cloneValue(source));
+ }
+
+ GarageApp.data = {
+ categories: [
+ { id: "all", label: "All" },
+ { id: "car", label: "Cars" },
+ { id: "armor", label: "Armor" },
+ { id: "air", label: "Air" },
+ { id: "naval", label: "Naval" },
+ { id: "other", label: "Other" },
+ ],
+ session: Object.assign({}, defaultSession),
+ garage: Object.assign({}, defaultGarage),
+ nearby: Object.assign({}, defaultNearby),
+ applyHydratePayload(payload) {
+ replaceObject(
+ this.session,
+ Object.assign({}, defaultSession, payload?.session || {}),
+ );
+ replaceObject(
+ this.garage,
+ Object.assign({}, defaultGarage, payload?.garage || {}),
+ );
+ replaceObject(
+ this.nearby,
+ Object.assign({}, defaultNearby, payload?.nearby || {}),
+ );
+ },
+ };
+})();
diff --git a/arma/client/addons/garage/ui/src/registry/events.js b/arma/client/addons/garage/ui/src/registry/events.js
new file mode 100644
index 0000000..3ca41d3
--- /dev/null
+++ b/arma/client/addons/garage/ui/src/registry/events.js
@@ -0,0 +1,174 @@
+(function () {
+ const GarageApp = (window.GarageApp = window.GarageApp || {});
+ const store = GarageApp.store;
+
+ let noticeTimer = null;
+
+ function getStoredVehicles() {
+ return Array.isArray(GarageApp.data?.garage?.vehicles)
+ ? GarageApp.data.garage.vehicles
+ : [];
+ }
+
+ function getNearbyVehicles() {
+ return Array.isArray(GarageApp.data?.nearby?.vehicles)
+ ? GarageApp.data.nearby.vehicles
+ : [];
+ }
+
+ function getSelectedEntry() {
+ const selection = store.getSelection();
+ if (selection.kind === "stored") {
+ return (
+ getStoredVehicles().find(
+ (vehicle) => String(vehicle.plate || "") === selection.id,
+ ) || null
+ );
+ }
+
+ if (selection.kind === "nearby") {
+ return (
+ getNearbyVehicles().find(
+ (vehicle) => String(vehicle.netId || "") === selection.id,
+ ) || null
+ );
+ }
+
+ return null;
+ }
+
+ function showNotice(type, text) {
+ store.setNotice({ type, text });
+
+ if (noticeTimer) {
+ clearTimeout(noticeTimer);
+ }
+
+ noticeTimer = setTimeout(() => {
+ store.setNotice({ type: "", text: "" });
+ noticeTimer = null;
+ }, 3200);
+ }
+
+ function closeGarage() {
+ const bridge = GarageApp.bridge;
+ if (bridge && typeof bridge.requestClose === "function") {
+ const sent = bridge.requestClose();
+ if (sent) {
+ return true;
+ }
+ }
+
+ showNotice("error", "Garage bridge is unavailable.");
+ return false;
+ }
+
+ function refreshGarage() {
+ const bridge = GarageApp.bridge;
+ if (bridge && typeof bridge.requestRefresh === "function") {
+ const sent = bridge.requestRefresh();
+ if (sent) {
+ return true;
+ }
+ }
+
+ showNotice("error", "Garage refresh bridge is unavailable.");
+ return false;
+ }
+
+ function applySearchQuery(value) {
+ store.setSearchQuery(String(value || "").trim());
+ }
+
+ function clearSearch() {
+ store.setSearchQuery("");
+ }
+
+ function selectCategory(categoryId) {
+ store.setCategoryFilter(String(categoryId || "all").trim() || "all");
+ }
+
+ function selectEntry(kind, id) {
+ store.select(kind, id);
+ }
+
+ function requestRetrieveSelected() {
+ const selectedEntry = getSelectedEntry();
+ if (!selectedEntry || selectedEntry.entryKind !== "stored") {
+ showNotice("error", "Select a stored vehicle to retrieve.");
+ return false;
+ }
+
+ if (GarageApp.data?.session?.spawnBlocked) {
+ showNotice("error", "The garage spawn area is blocked.");
+ return false;
+ }
+
+ const bridge = GarageApp.bridge;
+ if (!bridge || typeof bridge.requestRetrieve !== "function") {
+ showNotice("error", "Garage retrieve bridge is unavailable.");
+ return false;
+ }
+
+ store.startAction("retrieve");
+ const sent = bridge.requestRetrieve({
+ plate: selectedEntry.plate || "",
+ });
+
+ if (!sent) {
+ store.finishAction();
+ showNotice("error", "Garage retrieve bridge is unavailable.");
+ return false;
+ }
+
+ return true;
+ }
+
+ function requestStoreSelected() {
+ const selectedEntry = getSelectedEntry();
+ if (!selectedEntry || selectedEntry.entryKind !== "nearby") {
+ showNotice("error", "Select a nearby vehicle to store.");
+ return false;
+ }
+
+ if (selectedEntry.isEmpty === false) {
+ showNotice(
+ "error",
+ "All crew must exit the vehicle before storing it.",
+ );
+ return false;
+ }
+
+ const bridge = GarageApp.bridge;
+ if (!bridge || typeof bridge.requestStore !== "function") {
+ showNotice("error", "Garage store bridge is unavailable.");
+ return false;
+ }
+
+ store.startAction("store");
+ const sent = bridge.requestStore({
+ netId: selectedEntry.netId || "",
+ });
+
+ if (!sent) {
+ store.finishAction();
+ showNotice("error", "Garage store bridge is unavailable.");
+ return false;
+ }
+
+ return true;
+ }
+
+ GarageApp.actions = {
+ showNotice,
+ closeGarage,
+ refreshGarage,
+ applySearchQuery,
+ clearSearch,
+ selectCategory,
+ selectEntry,
+ getSelectedEntry,
+ requestRetrieveSelected,
+ requestStoreSelected,
+ };
+})();
diff --git a/arma/client/addons/garage/ui/src/registry/store.js b/arma/client/addons/garage/ui/src/registry/store.js
new file mode 100644
index 0000000..776c5bd
--- /dev/null
+++ b/arma/client/addons/garage/ui/src/registry/store.js
@@ -0,0 +1,113 @@
+(function () {
+ const GarageApp = (window.GarageApp = window.GarageApp || {});
+ const { createSignal } = GarageApp.runtime;
+
+ class GarageStore {
+ constructor() {
+ [this.getSelectedKind, this.setSelectedKind] = createSignal("");
+ [this.getSelectedId, this.setSelectedId] = createSignal("");
+ [this.getSearchQuery, this.setSearchQuery] = createSignal("");
+ [this.getCategoryFilter, this.setCategoryFilter] =
+ createSignal("all");
+ [this.getPendingAction, this.setPendingAction] = createSignal("");
+ [this.getNotice, this.setNotice] = createSignal({
+ type: "",
+ text: "",
+ });
+ }
+
+ getSelection() {
+ return {
+ id: this.getSelectedId(),
+ kind: this.getSelectedKind(),
+ };
+ }
+
+ clearSelection() {
+ this.setSelectedKind("");
+ this.setSelectedId("");
+ }
+
+ select(kind, id) {
+ this.setSelectedKind(String(kind || ""));
+ this.setSelectedId(String(id || ""));
+ }
+
+ startAction(action) {
+ this.setPendingAction(String(action || ""));
+ }
+
+ finishAction() {
+ this.setPendingAction("");
+ }
+
+ matchesSelection(entry) {
+ if (!entry || typeof entry !== "object") {
+ return false;
+ }
+
+ const selection = this.getSelection();
+ if (!selection.kind || !selection.id) {
+ return false;
+ }
+
+ if (selection.kind === "stored") {
+ return (
+ entry.entryKind === "stored" &&
+ String(entry.plate || "") === selection.id
+ );
+ }
+
+ if (selection.kind === "nearby") {
+ return (
+ entry.entryKind === "nearby" &&
+ String(entry.netId || "") === selection.id
+ );
+ }
+
+ return false;
+ }
+
+ ensureSelection() {
+ const garageVehicles = Array.isArray(
+ GarageApp.data?.garage?.vehicles,
+ )
+ ? GarageApp.data.garage.vehicles
+ : [];
+ const nearbyVehicles = Array.isArray(
+ GarageApp.data?.nearby?.vehicles,
+ )
+ ? GarageApp.data.nearby.vehicles
+ : [];
+ const hasCurrentSelection = [
+ ...garageVehicles,
+ ...nearbyVehicles,
+ ].some((entry) => this.matchesSelection(entry));
+
+ if (hasCurrentSelection) {
+ return;
+ }
+
+ const firstStored = garageVehicles[0] || null;
+ if (firstStored) {
+ this.select("stored", firstStored.plate || "");
+ return;
+ }
+
+ const firstNearby = nearbyVehicles[0] || null;
+ if (firstNearby) {
+ this.select("nearby", firstNearby.netId || "");
+ return;
+ }
+
+ this.clearSelection();
+ }
+
+ hydrateFromPayload() {
+ this.finishAction();
+ this.ensureSelection();
+ }
+ }
+
+ GarageApp.store = new GarageStore();
+})();
diff --git a/arma/client/addons/garage/ui/src/runtime.js b/arma/client/addons/garage/ui/src/runtime.js
new file mode 100644
index 0000000..f6daa1d
--- /dev/null
+++ b/arma/client/addons/garage/ui/src/runtime.js
@@ -0,0 +1,7 @@
+(function () {
+ const runtime = window.ForgeWebUI;
+ const GarageApp = (window.GarageApp = window.GarageApp || {});
+
+ GarageApp.runtime = runtime;
+ window.AppRuntime = runtime;
+})();
diff --git a/arma/client/addons/garage/ui/src/styles.css b/arma/client/addons/garage/ui/src/styles.css
new file mode 100644
index 0000000..b41390f
--- /dev/null
+++ b/arma/client/addons/garage/ui/src/styles.css
@@ -0,0 +1,573 @@
+:root {
+ --garage-shell-bg: #e4e3df;
+ --garage-surface: #f5f3ef;
+ --garage-surface-alt: #ece8e2;
+ --garage-border: rgba(74, 91, 110, 0.2);
+ --garage-border-strong: rgba(20, 46, 79, 0.18);
+ --garage-text-main: #1f2d3d;
+ --garage-text-muted: #6a7787;
+ --garage-text-subtle: #8792a0;
+ --garage-accent: #12365d;
+ --garage-accent-soft: #dbe7f3;
+ --garage-accent-line: rgba(18, 54, 93, 0.12);
+ --garage-warning: #8f5f26;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+html,
+body {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ overflow: hidden;
+}
+
+body {
+ font-family: "Segoe UI", "Trebuchet MS", sans-serif;
+ color: var(--garage-text-main);
+ background: var(--garage-shell-bg);
+}
+
+button,
+input {
+ font: inherit;
+}
+
+button {
+ cursor: pointer;
+}
+
+button:disabled {
+ cursor: not-allowed;
+ opacity: 0.72;
+}
+
+:focus-visible {
+ outline: 2px solid rgb(18 54 93 / 0.35);
+ outline-offset: 2px;
+}
+
+#app {
+ width: 100%;
+ height: 100%;
+}
+
+.garage-shell {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ overflow: hidden;
+ background: var(--garage-shell-bg);
+}
+
+.garage-layout {
+ flex: 1;
+ min-height: 0;
+ width: min(100%, 1613px);
+ margin: 0 auto;
+ padding: 1.25rem;
+ display: grid;
+ grid-template-columns: 308px minmax(0, 1fr);
+ gap: 1.25rem;
+}
+
+.garage-sidebar,
+.garage-main {
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.garage-main {
+ overflow: hidden;
+}
+
+.garage-module,
+.garage-panel,
+.garage-card {
+ background: linear-gradient(
+ 180deg,
+ var(--garage-surface) 0%,
+ var(--garage-surface-alt) 100%
+ );
+ border: 1px solid var(--garage-border);
+ border-radius: 1.35rem;
+}
+
+.garage-module,
+.garage-card {
+ padding: 1rem;
+}
+
+.garage-module {
+ display: grid;
+ gap: 0.85rem;
+ align-content: start;
+}
+
+.garage-panel {
+ flex: 1 1 auto;
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+}
+
+.garage-panel-header,
+.garage-module-header,
+.garage-card-header {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.garage-panel-header {
+ padding: 1rem 1rem 0;
+}
+
+.garage-module-header {
+ align-items: flex-start;
+}
+
+.garage-panel-intro {
+ padding: 0 1rem 1rem;
+ border-bottom: 1px solid var(--garage-accent-line);
+}
+
+.garage-dashboard {
+ flex: 1;
+ min-height: 0;
+ padding: 1rem;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(0, 1fr);
+ gap: 1rem;
+ align-items: stretch;
+}
+
+.garage-list-card,
+.garage-detail-card {
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+}
+
+.garage-detail-card {
+ grid-column: 1 / -1;
+}
+
+.garage-scroll-body {
+ flex: 1;
+ min-height: 20rem;
+ max-height: 24rem;
+ overflow: auto;
+ display: grid;
+ gap: 0.8rem;
+ padding-right: 0.2rem;
+}
+
+.garage-detail-body {
+ padding-top: 0.95rem;
+}
+
+.garage-detail-grid {
+ display: grid;
+ grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.85fr);
+ gap: 1rem;
+}
+
+.garage-detail-meta,
+.garage-summary-grid,
+.garage-search-actions,
+.garage-category-grid,
+.garage-action-row,
+.garage-inline-meters,
+.garage-hitpoint-grid,
+.garage-footer {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.garage-detail-meta {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ margin-bottom: 1rem;
+}
+
+.garage-summary-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.garage-summary-grid > :last-child {
+ grid-column: 1 / -1;
+}
+
+.garage-search-actions,
+.garage-action-row {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.65rem;
+}
+
+.garage-category-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.65rem;
+}
+
+.garage-footer {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ padding: 0.95rem 1.25rem 1.15rem;
+ border-top: 1px solid rgb(18 54 93 / 0.1);
+}
+
+.garage-meter-stack {
+ display: grid;
+ gap: 0.75rem;
+ margin-bottom: 1rem;
+}
+
+.garage-eyebrow,
+.garage-footer-title,
+.garage-stat-label,
+.garage-meter-label,
+.garage-hitpoint-selection {
+ font-size: 0.68rem;
+ font-weight: 700;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: var(--garage-text-subtle);
+}
+
+.garage-title,
+.garage-section-title {
+ margin: 0.16rem 0 0;
+ font-weight: 700;
+ letter-spacing: -0.02em;
+ color: var(--garage-text-main);
+}
+
+.garage-title {
+ font-size: 1.1rem;
+}
+
+.garage-section-title {
+ font-size: 1.05rem;
+}
+
+.garage-copy,
+.garage-detail-note,
+.garage-empty-copy,
+.garage-footer-copy,
+.garage-vehicle-meta,
+.garage-detail-caption {
+ margin: 0;
+ font-size: 0.92rem;
+ line-height: 1.48;
+ color: var(--garage-text-muted);
+}
+
+.garage-pill,
+.garage-badge {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.48rem 0.8rem;
+ border-radius: 999px;
+ font-size: 0.74rem;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ background: var(--garage-accent-soft);
+ color: var(--garage-accent);
+}
+
+.garage-badge.is-warning {
+ background: rgb(246 226 193 / 0.88);
+ color: var(--garage-warning);
+}
+
+.garage-search-form {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.garage-search-input {
+ width: 100%;
+ height: 2.9rem;
+ padding: 0 0.95rem;
+ border-radius: 0.8rem;
+ border: 1px solid var(--garage-border);
+ background: rgb(255 255 255 / 0.75);
+ color: var(--garage-text-main);
+}
+
+.garage-stat-card {
+ min-width: 0;
+ padding: 0.85rem;
+ border-radius: 0.85rem;
+ border: 1px solid var(--garage-border);
+ background: rgb(255 255 255 / 0.48);
+ display: flex;
+ flex-direction: column;
+ gap: 0.3rem;
+}
+
+.garage-stat-card.is-accent {
+ background: linear-gradient(
+ 180deg,
+ rgb(237 243 249 / 0.92) 0%,
+ rgb(223 232 242 / 0.72) 100%
+ );
+}
+
+.garage-stat-card.is-danger {
+ background: linear-gradient(
+ 180deg,
+ rgb(254 242 242 / 0.95) 0%,
+ rgb(252 225 225 / 0.82) 100%
+ );
+ border-color: rgb(220 151 151 / 0.38);
+}
+
+.garage-stat-value {
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--garage-text-main);
+ line-height: 1.3;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+
+.garage-chip {
+ min-height: 2.6rem;
+ padding: 0.68rem 0.9rem;
+ border-radius: 0.85rem;
+ border: 1px solid var(--garage-border);
+ background: rgb(255 255 255 / 0.52);
+ color: var(--garage-text-muted);
+ font-size: 0.8rem;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.garage-chip.is-active {
+ background: var(--garage-accent-soft);
+ color: var(--garage-accent);
+ border-color: rgb(18 54 93 / 0.2);
+}
+
+.garage-vehicle-item {
+ width: 100%;
+ padding: 0.9rem;
+ border-radius: 0.95rem;
+ border: 1px solid var(--garage-border);
+ background: rgb(255 255 255 / 0.48);
+ color: inherit;
+ text-align: left;
+}
+
+.garage-vehicle-item.is-selected {
+ border-color: rgb(18 54 93 / 0.24);
+ background: linear-gradient(
+ 180deg,
+ rgb(237 243 249 / 0.96) 0%,
+ rgb(223 232 242 / 0.74) 100%
+ );
+ box-shadow: 0 16px 26px rgb(18 54 93 / 0.08);
+}
+
+.garage-vehicle-item-head,
+.garage-meter-label-row,
+.garage-subsystem-header,
+.garage-hitpoint-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 0.75rem;
+}
+
+.garage-vehicle-copy,
+.garage-hitpoint-copy,
+.garage-footer-block {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.18rem;
+}
+
+.garage-vehicle-title,
+.garage-hitpoint-name,
+.garage-hitpoint-value {
+ font-size: 0.9rem;
+ font-weight: 700;
+ color: var(--garage-text-main);
+}
+
+.garage-meter {
+ display: grid;
+ gap: 0.32rem;
+}
+
+.garage-meter-track {
+ width: 100%;
+ height: 0.45rem;
+ overflow: hidden;
+ border-radius: 999px;
+ background: rgb(18 54 93 / 0.08);
+}
+
+.garage-meter-value {
+ font-size: 0.78rem;
+ font-weight: 700;
+ color: var(--garage-text-main);
+}
+
+.garage-meter-fill {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+}
+
+.garage-meter-fill.is-health {
+ background: linear-gradient(90deg, #2f7d5b 0%, #4eaa82 100%);
+}
+
+.garage-meter-fill.is-fuel {
+ background: linear-gradient(90deg, #12365d 0%, #3c6792 100%);
+}
+
+.garage-btn {
+ min-height: 2.75rem;
+ padding: 0.72rem 1rem;
+ border-radius: 0.8rem;
+ border: 1px solid var(--garage-border-strong);
+ font-size: 0.82rem;
+ font-weight: 700;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+}
+
+.garage-btn-primary {
+ background: rgb(255 255 255 / 0.68);
+ color: var(--garage-accent);
+}
+
+.garage-btn-primary:hover {
+ background: rgb(219 231 243 / 0.88);
+}
+
+.garage-btn-secondary {
+ background: rgb(255 255 255 / 0.42);
+ color: var(--garage-text-muted);
+}
+
+.garage-btn-secondary:hover {
+ background: rgb(255 255 255 / 0.6);
+ color: var(--garage-text-main);
+}
+
+.garage-hitpoint-row {
+ padding: 0.72rem 0.78rem;
+ border-radius: 0.85rem;
+ border: 1px solid var(--garage-border);
+ background: rgb(255 255 255 / 0.52);
+}
+
+.garage-detail-empty,
+.garage-empty-state {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: center;
+ min-height: 100%;
+}
+
+.garage-empty-title {
+ margin: 0 0 0.35rem;
+ font-size: 1rem;
+ font-weight: 700;
+ color: var(--garage-text-main);
+}
+
+.garage-empty-inline {
+ padding: 0.9rem;
+ border-radius: 0.85rem;
+ border: 1px dashed var(--garage-border);
+ color: var(--garage-text-muted);
+ background: rgb(255 255 255 / 0.36);
+}
+
+.garage-toast-stack {
+ position: fixed;
+ top: 1.2rem;
+ right: 1.5rem;
+ z-index: 10;
+ display: flex;
+ flex-direction: column;
+ gap: 0.65rem;
+}
+
+.garage-toast {
+ max-width: 24rem;
+ padding: 0.85rem 1rem;
+ border-radius: 0.9rem;
+ border: 1px solid var(--garage-border);
+ background: #fff;
+ box-shadow: 0 14px 28px rgb(16 34 56 / 0.14);
+ font-size: 0.92rem;
+}
+
+.garage-toast.is-success {
+ background: #ecfdf5;
+ border-color: #bbf7d0;
+ color: #166534;
+}
+
+.garage-toast.is-error {
+ background: #fef2f2;
+ border-color: #fecaca;
+ color: #991b1b;
+}
+
+@media (max-width: 1440px) {
+ .garage-layout {
+ grid-template-columns: 288px minmax(0, 1fr);
+ }
+
+ .garage-detail-grid {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 1120px) {
+ .garage-layout {
+ grid-template-columns: 1fr;
+ overflow: auto;
+ }
+
+ .garage-main,
+ .garage-sidebar {
+ min-height: auto;
+ }
+
+ .garage-dashboard {
+ grid-template-columns: 1fr;
+ }
+
+ .garage-detail-card {
+ grid-column: auto;
+ }
+
+ .garage-scroll-body {
+ max-height: none;
+ min-height: 16rem;
+ }
+
+ .garage-footer {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/arma/client/addons/garage/ui/ui.config.mjs b/arma/client/addons/garage/ui/ui.config.mjs
new file mode 100644
index 0000000..bd3a713
--- /dev/null
+++ b/arma/client/addons/garage/ui/ui.config.mjs
@@ -0,0 +1,33 @@
+export default {
+ addonName: "garage",
+ title: "FORGE Vehicle Garage",
+ logLabel: "Garage UI",
+ outputDir: "_site",
+ jsBundles: [
+ {
+ name: "Garage UI app",
+ output: "garage-ui.js",
+ sources: [
+ "src/runtime.js",
+ "src/data.js",
+ "src/registry/store.js",
+ "src/bridge.js",
+ "src/registry/events.js",
+ "src/components/AppShell.js",
+ "src/bootstrap.js",
+ ],
+ },
+ ],
+ cssBundles: [
+ {
+ name: "Garage UI styles",
+ output: "garage-ui.css",
+ sources: ["src/styles.css"],
+ },
+ ],
+ site: {
+ styles: ["garage-ui.css"],
+ commonScripts: ["forge-webui.js"],
+ scripts: ["garage-ui.js"],
+ },
+};
diff --git a/arma/client/addons/org/XEH_PREP.hpp b/arma/client/addons/org/XEH_PREP.hpp
index e5ecefa..7d71bae 100644
--- a/arma/client/addons/org/XEH_PREP.hpp
+++ b/arma/client/addons/org/XEH_PREP.hpp
@@ -1,4 +1,4 @@
PREP(handleUIEvents);
-PREP(initOrgClass);
-PREP(initOrgUIBridge);
+PREP(initClass);
+PREP(initUIBridge);
PREP(openUI);
diff --git a/arma/client/addons/org/XEH_postInitClient.sqf b/arma/client/addons/org/XEH_postInitClient.sqf
index 2e9b559..3f9e4d9 100644
--- a/arma/client/addons/org/XEH_postInitClient.sqf
+++ b/arma/client/addons/org/XEH_postInitClient.sqf
@@ -1,7 +1,7 @@
#include "script_component.hpp"
-if (isNil QGVAR(OrgClass)) then { call FUNC(initOrgClass); };
-if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initOrgUIBridge); };
+if (isNil QGVAR(OrgClass)) then { call FUNC(initClass); };
+if (isNil QGVAR(OrgUIBridge)) then { call FUNC(initUIBridge); };
[QGVAR(initOrg), {
GVAR(OrgClass) call ["init", []];
diff --git a/arma/client/addons/org/functions/fnc_initOrgClass.sqf b/arma/client/addons/org/functions/fnc_initClass.sqf
similarity index 98%
rename from arma/client/addons/org/functions/fnc_initOrgClass.sqf
rename to arma/client/addons/org/functions/fnc_initClass.sqf
index b4ee8d5..dab354d 100644
--- a/arma/client/addons/org/functions/fnc_initOrgClass.sqf
+++ b/arma/client/addons/org/functions/fnc_initClass.sqf
@@ -1,7 +1,7 @@
#include "..\script_component.hpp"
/*
- * File: fnc_initOrgClass.sqf
+ * File: fnc_initClass.sqf
* Author: IDSolutions
* Date: 2026-02-13
* Last Update: 2026-02-13
@@ -17,7 +17,7 @@
* Org class object [HASHMAP OBJECT]
*
* Examples:
- * call forge_client_org_fnc_initOrgClass
+ * call forge_client_org_fnc_initClass
*/
#pragma hemtt ignore_variables ["_self"]
diff --git a/arma/client/addons/org/functions/fnc_initOrgUIBridge.sqf b/arma/client/addons/org/functions/fnc_initUIBridge.sqf
similarity index 98%
rename from arma/client/addons/org/functions/fnc_initOrgUIBridge.sqf
rename to arma/client/addons/org/functions/fnc_initUIBridge.sqf
index e402612..cfc5875 100644
--- a/arma/client/addons/org/functions/fnc_initOrgUIBridge.sqf
+++ b/arma/client/addons/org/functions/fnc_initUIBridge.sqf
@@ -1,7 +1,7 @@
#include "..\script_component.hpp"
/*
- * File: fnc_initOrgUIBridge.sqf
+ * File: fnc_initUIBridge.sqf
* Author: IDSolutions
* Date: 2026-03-10
* Last Update: 2026-03-13
@@ -17,7 +17,7 @@
* Org UI bridge object [HASHMAP OBJECT]
*
* Examples:
- * call forge_client_org_fnc_initOrgUIBridge
+ * call forge_client_org_fnc_initUIBridge
*/
#pragma hemtt ignore_variables ["_self"]
diff --git a/arma/client/addons/store/XEH_PREP.hpp b/arma/client/addons/store/XEH_PREP.hpp
index b8aa106..780173e 100644
--- a/arma/client/addons/store/XEH_PREP.hpp
+++ b/arma/client/addons/store/XEH_PREP.hpp
@@ -1,6 +1,6 @@
-PREP(buildStoreUIPayload);
+PREP(buildUIPayload);
PREP(handleUIEvents);
-PREP(initStoreCatalogService);
-PREP(initStoreClass);
-PREP(initStoreUIBridge);
+PREP(initCatalogService);
+PREP(initClass);
+PREP(initUIBridge);
PREP(openUI);
diff --git a/arma/client/addons/store/XEH_postInitClient.sqf b/arma/client/addons/store/XEH_postInitClient.sqf
index a14751c..bcf83fa 100644
--- a/arma/client/addons/store/XEH_postInitClient.sqf
+++ b/arma/client/addons/store/XEH_postInitClient.sqf
@@ -1,8 +1,8 @@
#include "script_component.hpp"
-if (isNil QGVAR(StoreCatalogService)) then { call FUNC(initStoreCatalogService); };
-if (isNil QGVAR(StoreClass)) then { call FUNC(initStoreClass); };
-if (isNil QGVAR(StoreUIBridge)) then { call FUNC(initStoreUIBridge); };
+if (isNil QGVAR(StoreCatalogService)) then { call FUNC(initCatalogService); };
+if (isNil QGVAR(StoreClass)) then { call FUNC(initClass); };
+if (isNil QGVAR(StoreUIBridge)) then { call FUNC(initUIBridge); };
[QGVAR(responseCheckout), {
params [["_payload", createHashMap, [createHashMap]]];
diff --git a/arma/client/addons/store/functions/fnc_buildStoreUIPayload.sqf b/arma/client/addons/store/functions/fnc_buildUIPayload.sqf
similarity index 99%
rename from arma/client/addons/store/functions/fnc_buildStoreUIPayload.sqf
rename to arma/client/addons/store/functions/fnc_buildUIPayload.sqf
index 1f47613..3b748ce 100644
--- a/arma/client/addons/store/functions/fnc_buildStoreUIPayload.sqf
+++ b/arma/client/addons/store/functions/fnc_buildUIPayload.sqf
@@ -1,7 +1,7 @@
#include "..\script_component.hpp"
/*
- * File: fnc_buildStoreUIPayload.sqf
+ * File: fnc_buildUIPayload.sqf
* Author: IDSolutions
* Date: 2026-03-13
* Public: No
diff --git a/arma/client/addons/store/functions/fnc_initStoreCatalogService.sqf b/arma/client/addons/store/functions/fnc_initCatalogService.sqf
similarity index 99%
rename from arma/client/addons/store/functions/fnc_initStoreCatalogService.sqf
rename to arma/client/addons/store/functions/fnc_initCatalogService.sqf
index 5674009..6f63302 100644
--- a/arma/client/addons/store/functions/fnc_initStoreCatalogService.sqf
+++ b/arma/client/addons/store/functions/fnc_initCatalogService.sqf
@@ -1,7 +1,7 @@
#include "..\script_component.hpp"
/*
- * File: fnc_initStoreCatalogService.sqf
+ * File: fnc_initCatalogService.sqf
* Author: IDSolutions
* Date: 2026-03-13
* Public: No
diff --git a/arma/client/addons/store/functions/fnc_initStoreClass.sqf b/arma/client/addons/store/functions/fnc_initClass.sqf
similarity index 92%
rename from arma/client/addons/store/functions/fnc_initStoreClass.sqf
rename to arma/client/addons/store/functions/fnc_initClass.sqf
index 583e131..f88d2ff 100644
--- a/arma/client/addons/store/functions/fnc_initStoreClass.sqf
+++ b/arma/client/addons/store/functions/fnc_initClass.sqf
@@ -1,7 +1,7 @@
#include "..\script_component.hpp"
/*
- * File: fnc_initStoreClass.sqf
+ * File: fnc_initClass.sqf
* Author: IDSolutions
* Date: 2026-01-28
* Last Update: 2026-03-12
@@ -17,7 +17,7 @@
* Store class object [HASHMAP OBJECT]
*
* Example:
- * call forge_client_store_fnc_initStoreClass
+ * call forge_client_store_fnc_initClass
*/
#pragma hemtt ignore_variables ["_self"]
diff --git a/arma/client/addons/store/functions/fnc_initStoreUIBridge.sqf b/arma/client/addons/store/functions/fnc_initUIBridge.sqf
similarity index 96%
rename from arma/client/addons/store/functions/fnc_initStoreUIBridge.sqf
rename to arma/client/addons/store/functions/fnc_initUIBridge.sqf
index b600593..f6cba7e 100644
--- a/arma/client/addons/store/functions/fnc_initStoreUIBridge.sqf
+++ b/arma/client/addons/store/functions/fnc_initUIBridge.sqf
@@ -1,7 +1,7 @@
#include "..\script_component.hpp"
/*
- * File: fnc_initStoreUIBridge.sqf
+ * File: fnc_initUIBridge.sqf
* Author: IDSolutions
* Date: 2026-03-10
* Last Update: 2026-03-12
@@ -48,7 +48,7 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
["handleReady", compileFinal {
params [["_control", controlNull, [controlNull]]];
- private _payload = call FUNC(buildStoreUIPayload);
+ private _payload = call FUNC(buildUIPayload);
_self call ["sendBridgeEvent", ["store::hydrate", _payload, _control]];
}],
["handleCategoryRequest", compileFinal {
@@ -78,7 +78,7 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
]]];
}],
["refreshStoreConfig", compileFinal {
- private _payload = call FUNC(buildStoreUIPayload);
+ private _payload = call FUNC(buildUIPayload);
_self call ["sendBridgeEvent", ["store::config::hydrate", _payload]];
}],
["handleCheckoutRequest", compileFinal {
diff --git a/arma/client/addons/store/ui/_site/store-ui.js b/arma/client/addons/store/ui/_site/store-ui.js
index 7fba4b4..d123d15 100644
--- a/arma/client/addons/store/ui/_site/store-ui.js
+++ b/arma/client/addons/store/ui/_site/store-ui.js
@@ -2778,6 +2778,12 @@ ${scopeSelector} .cart-line {
gap: 0.75rem;
}
+${scopeSelector} .cart-line-copy {
+ min-width: 0;
+ display: grid;
+ gap: 0.18rem;
+}
+
${scopeSelector} .cart-line-top,
${scopeSelector} .cart-line-controls,
${scopeSelector} .summary-row {
@@ -2790,13 +2796,9 @@ ${scopeSelector} .summary-row {
${scopeSelector} .cart-line-title {
font-size: 0.92rem;
font-weight: 700;
-}
-
-${scopeSelector} .cart-line-code {
- font-size: 0.72rem;
- letter-spacing: 0.14em;
- text-transform: uppercase;
- color: var(--store-text-subtle);
+ line-height: 1.32;
+ overflow-wrap: anywhere;
+ word-break: break-word;
}
${scopeSelector} .qty-controls {
@@ -3112,15 +3114,9 @@ ${scopeSelector} .cart-empty {
{ className: "cart-line-top" },
h(
"div",
- null,
- h(
- "div",
- {
- className:
- "cart-line-code",
- },
- item.code,
- ),
+ {
+ className: "cart-line-copy",
+ },
h(
"div",
{
diff --git a/arma/client/addons/store/ui/src/components/cart.js b/arma/client/addons/store/ui/src/components/cart.js
index 9678424..4249d59 100644
--- a/arma/client/addons/store/ui/src/components/cart.js
+++ b/arma/client/addons/store/ui/src/components/cart.js
@@ -150,6 +150,12 @@ ${scopeSelector} .cart-line {
gap: 0.75rem;
}
+${scopeSelector} .cart-line-copy {
+ min-width: 0;
+ display: grid;
+ gap: 0.18rem;
+}
+
${scopeSelector} .cart-line-top,
${scopeSelector} .cart-line-controls,
${scopeSelector} .summary-row {
@@ -162,13 +168,9 @@ ${scopeSelector} .summary-row {
${scopeSelector} .cart-line-title {
font-size: 0.92rem;
font-weight: 700;
-}
-
-${scopeSelector} .cart-line-code {
- font-size: 0.72rem;
- letter-spacing: 0.14em;
- text-transform: uppercase;
- color: var(--store-text-subtle);
+ line-height: 1.32;
+ overflow-wrap: anywhere;
+ word-break: break-word;
}
${scopeSelector} .qty-controls {
@@ -484,15 +486,9 @@ ${scopeSelector} .cart-empty {
{ className: "cart-line-top" },
h(
"div",
- null,
- h(
- "div",
- {
- className:
- "cart-line-code",
- },
- item.code,
- ),
+ {
+ className: "cart-line-copy",
+ },
h(
"div",
{
diff --git a/arma/server/addons/garage/XEH_preInit.sqf b/arma/server/addons/garage/XEH_preInit.sqf
index 6e5d97d..109ae47 100644
--- a/arma/server/addons/garage/XEH_preInit.sqf
+++ b/arma/server/addons/garage/XEH_preInit.sqf
@@ -65,6 +65,84 @@ PREP_RECOMPILE_END;
GVAR(GarageStore) call ["remove", [GVAR(Registry), _uid]];
}] call CFUNC(addEventHandler);
+[QGVAR(requestStoreVehicle), {
+ params [
+ ["_uid", "", [""]],
+ ["_className", "", [""]],
+ ["_fuel", 0, [0]],
+ ["_damage", 0, [0]],
+ ["_hitPointsJson", "", [""]]
+ ];
+
+ private _player = [_uid] call EFUNC(common,getPlayer);
+ if (_uid isEqualTo "" || { _className isEqualTo "" } || { _hitPointsJson isEqualTo "" }) exitWith {
+ [CRPC(garage,responseGarageAction), [createHashMapFromArray [
+ ["action", "store"],
+ ["success", false],
+ ["message", "Missing vehicle data for garage storage."]
+ ]], _player] call CFUNC(targetEvent);
+ };
+
+ private _payloadJson = toJSON (createHashMapFromArray [
+ ["classname", _className],
+ ["fuel", _fuel],
+ ["damage", _damage],
+ ["hit_points", fromJSON _hitPointsJson]
+ ]);
+
+ ["garage:add", [_uid, _payloadJson]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
+ if !(_isSuccess) exitWith {
+ [CRPC(garage,responseGarageAction), [createHashMapFromArray [
+ ["action", "store"],
+ ["success", false],
+ ["message", format ["Failed to store vehicle: %1", _result]]
+ ]], _player] call CFUNC(targetEvent);
+ };
+
+ private _garage = fromJSON _result;
+ GVAR(Registry) set [_uid, _garage];
+
+ [CRPC(garage,responseSyncGarage), [_garage], _player] call CFUNC(targetEvent);
+ [CRPC(garage,responseGarageAction), [createHashMapFromArray [
+ ["action", "store"],
+ ["success", true],
+ ["message", "Vehicle stored in garage."]
+ ]], _player] call CFUNC(targetEvent);
+}] call CFUNC(addEventHandler);
+
+[QGVAR(requestRetrieveVehicle), {
+ params [["_uid", "", [""]], ["_plate", "", [""]]];
+
+ private _player = [_uid] call EFUNC(common,getPlayer);
+ if (_uid isEqualTo "" || { _plate isEqualTo "" }) exitWith {
+ [CRPC(garage,responseGarageAction), [createHashMapFromArray [
+ ["action", "retrieve"],
+ ["success", false],
+ ["message", "Select a stored vehicle to retrieve."]
+ ]], _player] call CFUNC(targetEvent);
+ };
+
+ private _payloadJson = toJSON (createHashMapFromArray [["plate", _plate]]);
+ ["garage:remove", [_uid, _payloadJson]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
+ if !(_isSuccess) exitWith {
+ [CRPC(garage,responseGarageAction), [createHashMapFromArray [
+ ["action", "retrieve"],
+ ["success", false],
+ ["message", format ["Failed to retrieve vehicle: %1", _result]]
+ ]], _player] call CFUNC(targetEvent);
+ };
+
+ private _garage = fromJSON _result;
+ GVAR(Registry) set [_uid, _garage];
+
+ [CRPC(garage,responseSyncGarage), [_garage], _player] call CFUNC(targetEvent);
+ [CRPC(garage,responseGarageAction), [createHashMapFromArray [
+ ["action", "retrieve"],
+ ["success", true],
+ ["message", "Vehicle retrieved from garage."]
+ ]], _player] call CFUNC(targetEvent);
+}] call CFUNC(addEventHandler);
+
[QGVAR(requestInitVG), {
params [["_uid", "", [""]]];