diff --git a/arma/client/addons/bank/XEH_PREP.hpp b/arma/client/addons/bank/XEH_PREP.hpp
index c6ce19a..f1a55dc 100644
--- a/arma/client/addons/bank/XEH_PREP.hpp
+++ b/arma/client/addons/bank/XEH_PREP.hpp
@@ -1,3 +1,5 @@
PREP(handleUIEvents);
-PREP(initBankClass);
+PREP(initClass);
+PREP(initSessionService);
+PREP(initUIBridge);
PREP(openUI);
diff --git a/arma/client/addons/bank/XEH_postInitClient.sqf b/arma/client/addons/bank/XEH_postInitClient.sqf
index 2c874f5..a4cf8d6 100644
--- a/arma/client/addons/bank/XEH_postInitClient.sqf
+++ b/arma/client/addons/bank/XEH_postInitClient.sqf
@@ -1,6 +1,8 @@
#include "script_component.hpp"
-if (isNil QGVAR(BankClass)) then { call FUNC(initBankClass); };
+if (isNil QGVAR(BankClass)) then { call FUNC(initClass); };
+if (isNil QGVAR(BankSessionService)) then { call FUNC(initSessionService); };
+if (isNil QGVAR(BankUIBridge)) then { call FUNC(initUIBridge); };
[QGVAR(initBank), {
GVAR(BankClass) call ["init", []];
@@ -10,12 +12,18 @@ if (isNil QGVAR(BankClass)) then { call FUNC(initBankClass); };
params [["_data", createHashMap, [createHashMap]]];
GVAR(BankClass) call ["sync", [_data, true]];
+ if !(isNil QGVAR(BankUIBridge)) then {
+ GVAR(BankUIBridge) call ["refreshSession", []];
+ };
}] call CFUNC(addEventHandler);
[QGVAR(responseSyncBank), {
params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
GVAR(BankClass) call ["sync", [_data, _jip]];
+ if !(isNil QGVAR(BankUIBridge)) then {
+ GVAR(BankUIBridge) call ["refreshSession", []];
+ };
}] call CFUNC(addEventHandler);
[{
diff --git a/arma/client/addons/bank/config.cpp b/arma/client/addons/bank/config.cpp
index bce4c33..87ad980 100644
--- a/arma/client/addons/bank/config.cpp
+++ b/arma/client/addons/bank/config.cpp
@@ -8,6 +8,7 @@ class CfgPatches {
name = COMPONENT_NAME;
requiredVersion = REQUIRED_VERSION;
requiredAddons[] = {
+ "forge_client_common",
"forge_client_main"
};
units[] = {};
diff --git a/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf b/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf
index c9472fd..b2fcd53 100644
--- a/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf
+++ b/arma/client/addons/bank/functions/fnc_handleUIEvents.sqf
@@ -28,90 +28,49 @@ private _alert = fromJSON _message;
private _event = _alert get "event";
private _data = _alert get "data";
-private _uid = GVAR(BankClass) get "uid";
-private _account = GVAR(BankClass) get "account";
-private _bank = _account get "bank";
-private _cash = _account get "cash";
-private _earnings = _account get "earnings";
-private _pin = _account get "pin";
-private _funds = EGVAR(org,OrgClass) get "funds";
-
diag_log format ["[FORGE:Client:Bank] Handling UI event: %1 with data: %2", _event, _data];
switch (_event) do {
- // ========================================================================
- // DATA REQUESTS
- // ========================================================================
- case "bank::sync": {
- private _players = SREG(bank,IndexRegistry);
- private _accountData = createHashMapFromArray [
- ["uid", _uid],
- ["bank", _bank],
- ["cash", _cash],
- ["earnings", _earnings],
- ["org", _funds],
- ["pin", _pin],
- ["players", _players]
- ];
-
- _control ctrlWebBrowserAction ["ExecJS", format ["syncDataFromArma(%1)", toJSON _accountData]];
- };
-
- // ========================================================================
- // BANK OPERATIONS
- // ========================================================================
- case "bank::deposit": {
- private _amount = _data get "amount";
- if (_amount > _cash) exitWith { hint "Insufficient cash!"; };
-
- [SRPC(bank,requestDeposit), [_uid, _amount]] call CFUNC(serverEvent);
- };
- case "bank::withdraw": {
- private _amount = _data get "amount";
- if (_amount > _bank) exitWith { hint "Insufficient funds!"; };
-
- [SRPC(bank,requestWithdraw), [_uid, _amount]] call CFUNC(serverEvent);
- };
- case "bank::transfer": {
- private _amount = _data get "amount";
- private _from = _data get "from";
- private _target = _data get "target";
-
- if (_target isEqualTo _uid) exitWith {
- hint "Cannot transfer to yourself!";
- diag_log "[FORGE:Client:Bank] Attempted self-transfer blocked";
+ case "bank::close": {
+ if !(isNil QGVAR(BankUIBridge)) then {
+ GVAR(BankUIBridge) call ["handleClose", []];
};
- private _fromAmount = _account get _from;
- if (_amount > _fromAmount) exitWith { hint "Insufficient funds!"; };
-
- [SRPC(bank,requestTransfer), [_uid, _target, _from, _amount]] call CFUNC(serverEvent);
+ closeDialog 1;
};
- case "bank::depositEarnings": {
- private _amount = _data get "amount";
- if (_amount > _earnings) exitWith { hint "Insufficient earnings!"; };
-
- [SRPC(bank,requestDepositEarnings), [_uid, _amount]] call CFUNC(serverEvent);
+ case "bank::ready": {
+ if !(isNil QGVAR(BankUIBridge)) then {
+ GVAR(BankUIBridge) call ["handleReady", [_control, _data]];
+ };
};
- case "bank::close": { closeDialog 1; };
-
- // ========================================================================
- // ATM OPERATIONS
- // ========================================================================
- case "atm::withdraw": {
- private _amount = _data get "amount";
- if (_amount > _bank) exitWith { hint "Insufficient funds!"; };
-
- [SRPC(bank,requestWithdraw), [_uid, _amount]] call CFUNC(serverEvent);
+ case "bank::refresh": {
+ if !(isNil QGVAR(BankUIBridge)) then {
+ GVAR(BankUIBridge) call ["refreshSession", []];
+ };
};
- case "atm::deposit": {
- private _amount = _data get "amount";
- if (_amount > _cash) exitWith { hint "Insufficient cash!"; };
-
- [SRPC(bank,requestDeposit), [_uid, _amount]] call CFUNC(serverEvent);
+ case "bank::deposit::request": {
+ if !(isNil QGVAR(BankUIBridge)) then {
+ GVAR(BankUIBridge) call ["handleDepositRequest", [_data]];
+ };
+ };
+ case "bank::withdraw::request": {
+ if !(isNil QGVAR(BankUIBridge)) then {
+ GVAR(BankUIBridge) call ["handleWithdrawRequest", [_data]];
+ };
+ };
+ case "bank::transfer::request": {
+ if !(isNil QGVAR(BankUIBridge)) then {
+ GVAR(BankUIBridge) call ["handleTransferRequest", [_data]];
+ };
+ };
+ case "bank::depositEarnings::request": {
+ if !(isNil QGVAR(BankUIBridge)) then {
+ GVAR(BankUIBridge) call ["handleDepositEarningsRequest", [_data]];
+ };
+ };
+ default {
+ hint format ["Unhandled bank UI event: %1", _event];
};
- case "atm::close": { closeDialog 1; };
- default { diag_log format ["[FORGE:Client:Bank] Unhandled UI event: %1", _event]; };
};
true;
diff --git a/arma/client/addons/bank/functions/fnc_initBankClass.sqf b/arma/client/addons/bank/functions/fnc_initBankClass.sqf
deleted file mode 100644
index bf7b8e4..0000000
--- a/arma/client/addons/bank/functions/fnc_initBankClass.sqf
+++ /dev/null
@@ -1,69 +0,0 @@
-#include "..\script_component.hpp"
-
-/*
- * File: fnc_initBankClass.sqf
- * Author: IDSolutions
- * Date: 2025-12-16
- * Last Update: 2026-02-13
- * Public: No
- *
- * Description:
- * Initializes the bank class.
- *
- * Arguments:
- * None
- *
- * Return Value:
- * Bank class object [HASHMAP OBJECT]
- *
- * Example:
- * call forge_client_bank_fnc_initBankClass
- */
-
-#pragma hemtt ignore_variables ["_self"]
-GVAR(BankBaseClass) = compileFinal createHashMapFromArray [
- ["#type", "BankBaseClass"],
- ["#create", compileFinal {
- _self set ["uid", getPlayerUID player];
- _self set ["account", createHashMap];
- _self set ["isLoaded", false];
- _self set ["lastSave", time];
- }],
- ["init", compileFinal {
- private _uid = _self get "uid";
-
- [SRPC(bank,requestInitBank), [_uid]] call CFUNC(serverEvent);
-
- systemChat format ["Bank loaded for %1", (name player)];
- diag_log "[FORGE:Client:Bank] Bank Class Initialized!";
- }],
- ["save", compileFinal {
- params [["_sync", false, [false]]];
-
- private _uid = _self get "uid";
- [SRPC(bank,requestSaveBank), [_uid, _sync]] call CFUNC(serverEvent);
-
- _self set ["lastSave", time];
- }],
- ["sync", compileFinal {
- params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
-
- private _account = _self get "account";
- private _isLoaded = _self get "isLoaded";
-
- { _account set [_x, _y]; } forEach _data;
- _self set ["account", _account];
-
- if !(_isLoaded) then { _self set ["isLoaded", true]; };
- diag_log "[FORGE:Client:Bank] Sync completed";
- }],
- ["get", compileFinal {
- params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
-
- private _account = _self get "account";
- _account getOrDefault [_key, _default];
- }]
-];
-
-GVAR(BankClass) = createHashMapObject [GVAR(BankBaseClass)];
-GVAR(BankClass)
diff --git a/arma/client/addons/bank/functions/fnc_initClass.sqf b/arma/client/addons/bank/functions/fnc_initClass.sqf
new file mode 100644
index 0000000..ede4cc8
--- /dev/null
+++ b/arma/client/addons/bank/functions/fnc_initClass.sqf
@@ -0,0 +1,62 @@
+#include "..\script_component.hpp"
+
+/*
+ * File: fnc_initClass.sqf
+ * Author: IDSolutions
+ * Public: No
+ *
+ * Description:
+ * Initializes the bank class for account sync and access helpers.
+ */
+
+#pragma hemtt ignore_variables ["_self"]
+GVAR(BankBaseClass) = compileFinal createHashMapFromArray [
+ ["#type", "BankBaseClass"],
+ ["#create", compileFinal {
+ _self set ["uid", getPlayerUID player];
+ _self set ["account", createHashMapFromArray [
+ ["bank", 0],
+ ["cash", 0],
+ ["earnings", 0],
+ ["pin", 1234],
+ ["transactions", []]
+ ]];
+ _self set ["isLoaded", false];
+ _self set ["lastSave", time];
+ }],
+ ["getAccountState", compileFinal {
+ _self getOrDefault ["account", createHashMap]
+ }],
+ ["get", compileFinal {
+ params [["_key", "", [""]], ["_default", nil, [[], "", 0, false, createHashMap]]];
+
+ private _account = _self getOrDefault ["account", createHashMap];
+ _account getOrDefault [_key, _default]
+ }],
+ ["init", compileFinal {
+ [SRPC(bank,requestInitBank), [getPlayerUID player]] call CFUNC(serverEvent);
+ _self set ["lastSave", time];
+ }],
+ ["save", compileFinal {
+ [SRPC(bank,requestSaveBank), [getPlayerUID player]] call CFUNC(serverEvent);
+ _self set ["lastSave", time];
+ }],
+ ["sync", compileFinal {
+ params [["_data", createHashMap, [createHashMap]], ["_jip", false, [false]]];
+
+ private _account = _self getOrDefault ["account", createHashMap];
+ {
+ _account set [_x, _y];
+ } forEach _data;
+
+ _self set ["account", _account];
+ if !(_self getOrDefault ["isLoaded", false]) then {
+ _self set ["isLoaded", true];
+ };
+
+ true
+ }]
+];
+
+GVAR(BankClass) = createHashMapObject [GVAR(BankBaseClass)];
+GVAR(BankClass)
diff --git a/arma/client/addons/bank/functions/fnc_initSessionService.sqf b/arma/client/addons/bank/functions/fnc_initSessionService.sqf
new file mode 100644
index 0000000..155652b
--- /dev/null
+++ b/arma/client/addons/bank/functions/fnc_initSessionService.sqf
@@ -0,0 +1,80 @@
+#include "..\script_component.hpp"
+
+/*
+ * File: fnc_initSessionService.sqf
+ * Author: IDSolutions
+ * Public: No
+ *
+ * Description:
+ * Initializes the bank session service that shapes the browser payload.
+ */
+
+#pragma hemtt ignore_variables ["_self"]
+GVAR(BankSessionServiceBaseClass) = compileFinal createHashMapFromArray [
+ ["#type", "BankSessionServiceBaseClass"],
+ ["buildTransferTargets", compileFinal {
+ private _targets = [];
+
+ {
+ if (isNull _x || { _x isEqualTo player }) then {
+ continue;
+ };
+
+ private _uid = getPlayerUID _x;
+ private _name = name _x;
+ if (_uid isEqualTo "" || { _name isEqualTo "" }) then {
+ continue;
+ };
+
+ _targets pushBack (createHashMapFromArray [
+ ["name", _name],
+ ["uid", _uid]
+ ]);
+ } forEach allPlayers;
+
+ private _targetPairs = _targets apply {
+ [toLowerANSI (_x getOrDefault ["name", ""]), _x]
+ };
+ _targetPairs sort true;
+ _targetPairs apply {
+ _x param [1, createHashMap]
+ }
+ }],
+ ["buildPayload", compileFinal {
+ params [["_mode", "bank", [""]]];
+
+ private _account = if (isNil QGVAR(BankClass)) then {
+ createHashMap
+ } else {
+ GVAR(BankClass) call ["getAccountState", []]
+ };
+
+ private _orgFunds = 0;
+ private _orgName = "";
+ if !(isNil QEGVAR(org,OrgClass)) then {
+ _orgFunds = EGVAR(org,OrgClass) call ["get", ["funds", 0]];
+ _orgName = EGVAR(org,OrgClass) call ["get", ["name", ""]];
+ };
+
+ createHashMapFromArray [
+ ["session", createHashMapFromArray [
+ ["mode", ["bank", "atm"] select (toLowerANSI _mode isEqualTo "atm")],
+ ["orgFunds", _orgFunds],
+ ["orgName", _orgName],
+ ["playerName", name player],
+ ["transferTargets", _self call ["buildTransferTargets", []]],
+ ["uid", getPlayerUID player]
+ ]],
+ ["account", createHashMapFromArray [
+ ["bank", _account getOrDefault ["bank", 0]],
+ ["cash", _account getOrDefault ["cash", 0]],
+ ["earnings", _account getOrDefault ["earnings", 0]],
+ ["pin", str (_account getOrDefault ["pin", 1234])],
+ ["transactions", _account getOrDefault ["transactions", []]]
+ ]]
+ ]
+ }]
+];
+
+GVAR(BankSessionService) = createHashMapObject [GVAR(BankSessionServiceBaseClass)];
+GVAR(BankSessionService)
diff --git a/arma/client/addons/bank/functions/fnc_initUIBridge.sqf b/arma/client/addons/bank/functions/fnc_initUIBridge.sqf
new file mode 100644
index 0000000..32e1b0b
--- /dev/null
+++ b/arma/client/addons/bank/functions/fnc_initUIBridge.sqf
@@ -0,0 +1,134 @@
+#include "..\script_component.hpp"
+
+/*
+ * File: fnc_initUIBridge.sqf
+ * Author: IDSolutions
+ * Public: No
+ *
+ * Description:
+ * Initializes the bank web UI bridge.
+ */
+
+#pragma hemtt ignore_variables ["_self"]
+private _webUIDeclarations = call EFUNC(common,initWebUIBridge);
+private _webUIBridgeDeclaration = _webUIDeclarations get "bridgeDeclaration";
+
+GVAR(BankUIBridgeBaseClass) = compileFinal createHashMapFromArray [
+ ["#base", _webUIBridgeDeclaration],
+ ["#type", "BankUIBridgeBaseClass"],
+ ["#create", compileFinal {
+ _self set ["mode", "bank"];
+ }],
+ ["buildPayload", compileFinal {
+ GVAR(BankSessionService) call ["buildPayload", [_self call ["getMode", []]]]
+ }],
+ ["getActiveBrowserControl", compileFinal {
+ private _display = uiNamespace getVariable ["RscBank", displayNull];
+ if (isNull _display) exitWith {
+ _self call ["setActiveBrowserControl", [controlNull]];
+ controlNull
+ };
+
+ private _control = _display displayCtrl 1002;
+ _self call ["setActiveBrowserControl", [_control]];
+ _control
+ }],
+ ["getMode", compileFinal {
+ _self getOrDefault ["mode", "bank"]
+ }],
+ ["handleDepositEarningsRequest", compileFinal {
+ params [["_data", createHashMap, [createHashMap]]];
+
+ private _amount = floor (_data getOrDefault ["amount", 0]);
+ if (_amount <= 0) exitWith {
+ _self call ["sendNotice", ["error", "No earnings are available to deposit."]];
+ };
+
+ [SRPC(bank,requestDepositEarnings), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
+ true
+ }],
+ ["handleDepositRequest", compileFinal {
+ params [["_data", createHashMap, [createHashMap]]];
+
+ private _amount = floor (_data getOrDefault ["amount", 0]);
+ if (_amount <= 0) exitWith {
+ _self call ["sendNotice", ["error", "Enter a valid deposit amount."]];
+ };
+
+ [SRPC(bank,requestDeposit), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
+ true
+ }],
+ ["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", ["bank::hydrate", _self call ["buildPayload", []], _control]];
+ }],
+ ["handleTransferRequest", compileFinal {
+ params [["_data", createHashMap, [createHashMap]]];
+
+ private _amount = floor (_data getOrDefault ["amount", 0]);
+ private _target = _data getOrDefault ["target", ""];
+ private _from = toLowerANSI (_data getOrDefault ["from", "bank"]);
+
+ if (_target isEqualTo "") exitWith {
+ _self call ["sendNotice", ["error", "Select a transfer recipient."]];
+ };
+
+ if (_target isEqualTo getPlayerUID player) exitWith {
+ _self call ["sendNotice", ["error", "You cannot transfer funds to yourself."]];
+ };
+
+ if (_amount <= 0) exitWith {
+ _self call ["sendNotice", ["error", "Enter a valid transfer amount."]];
+ };
+
+ [SRPC(bank,requestTransfer), [getPlayerUID player, _target, _from, _amount]] call CFUNC(serverEvent);
+ true
+ }],
+ ["handleWithdrawRequest", compileFinal {
+ params [["_data", createHashMap, [createHashMap]]];
+
+ private _amount = floor (_data getOrDefault ["amount", 0]);
+ if (_amount <= 0) exitWith {
+ _self call ["sendNotice", ["error", "Enter a valid withdrawal amount."]];
+ };
+
+ [SRPC(bank,requestWithdraw), [getPlayerUID player, _amount]] call CFUNC(serverEvent);
+ true
+ }],
+ ["refreshSession", compileFinal {
+ private _control = _self call ["getActiveBrowserControl", []];
+ if (isNull _control) exitWith { false };
+
+ _self call ["sendEvent", ["bank::sync", _self call ["buildPayload", []], _control]]
+ }],
+ ["sendNotice", compileFinal {
+ params [["_type", "error", [""]], ["_message", "", [""]], ["_control", controlNull, [controlNull]]];
+
+ if (_message isEqualTo "") exitWith { false };
+
+ _self call ["sendEvent", ["bank::notice", createHashMapFromArray [
+ ["message", _message],
+ ["type", _type]
+ ], _control]]
+ }],
+ ["setMode", compileFinal {
+ params [["_mode", "bank", [""]]];
+
+ private _finalMode = toLowerANSI _mode;
+ if !(_finalMode in ["bank", "atm"]) then {
+ _finalMode = "bank";
+ };
+
+ _self set ["mode", _finalMode];
+ _finalMode
+ }]
+];
+
+GVAR(BankUIBridge) = createHashMapObject [GVAR(BankUIBridgeBaseClass)];
+GVAR(BankUIBridge)
diff --git a/arma/client/addons/bank/functions/fnc_openUI.sqf b/arma/client/addons/bank/functions/fnc_openUI.sqf
index bfce578..9a82824 100644
--- a/arma/client/addons/bank/functions/fnc_openUI.sqf
+++ b/arma/client/addons/bank/functions/fnc_openUI.sqf
@@ -31,11 +31,11 @@ _ctrl ctrlAddEventHandler ["JSDialog", {
[_control, _isConfirmDialog, _message] call FUNC(handleUIEvents);
}];
-if (_isATM) then {
- _ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\atm.html)];
-} else {
- _ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\bank.html)];
+if !(isNil QGVAR(BankUIBridge)) then {
+ GVAR(BankUIBridge) call ["setMode", [["bank", "atm"] select _isATM]];
+ GVAR(BankUIBridge) call ["setActiveBrowserControl", [_ctrl]];
};
-// _ctrl ctrlWebBrowserAction ["OpenDevConsole"];
+
+_ctrl ctrlWebBrowserAction ["LoadFile", QPATHTOF2(ui\_site\index.html)];
true;
diff --git a/arma/client/addons/bank/ui/_site/atm.css b/arma/client/addons/bank/ui/_site/atm.css
deleted file mode 100644
index 06e429c..0000000
--- a/arma/client/addons/bank/ui/_site/atm.css
+++ /dev/null
@@ -1,192 +0,0 @@
-:root {
- --bg-app: #fdfcf8;
- --bg-surface: #ffffff;
- --bg-surface-hover: #f1f5f9;
- --primary: #475569;
- --primary-hover: #1e293b;
- --text-main: #1f2937;
- --text-muted: #64748b;
- --text-inverse: #f8fafc;
- --border: #e2e8f0;
- --radius: 8px;
- --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
- --footer-bg: #1e293b;
-}
-
-body {
- font-family:
- "Inter",
- system-ui,
- -apple-system,
- sans-serif;
- margin: 0;
- padding: 0;
- background: transparent;
- color: var(--text-main);
- line-height: 1.6;
-}
-
-#app {
- min-height: 100vh;
-}
-
-main {
- display: flex;
- flex-direction: column;
- min-height: 100vh;
- padding: 3rem 0;
- box-sizing: border-box;
-}
-
-.container {
- max-width: 800px;
- width: 100%;
- background: #f1f5f9;
- margin: 0 auto;
- padding: 2rem;
- flex: 1;
- display: flex;
- flex-direction: column;
- justify-content: center;
- box-sizing: border-box;
-}
-
-/* Header */
-.header {
- text-align: center;
- margin-bottom: 3rem;
- padding-bottom: 2rem;
- border-bottom: 1px solid var(--border);
-
- h1 {
- font-size: 2.5rem;
- font-weight: 700;
- margin-bottom: 0.5rem;
- letter-spacing: -0.025em;
- color: var(--primary-hover);
- }
-
- p {
- color: var(--text-muted);
- font-size: 1.1rem;
- margin: 0;
- }
-}
-
-/* Cards */
-.card {
- background: var(--bg-surface);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- padding: 2rem;
- margin-bottom: 2rem;
- box-shadow: var(--shadow);
- text-align: center;
-
- h2 {
- margin-top: 0;
- font-size: 1.8rem;
- color: var(--primary-hover);
- }
-}
-
-/* PIN Display */
-.pin-display {
- font-size: 2.5rem;
- letter-spacing: 0.5rem;
- text-align: center;
- margin-bottom: 2rem;
- font-family: monospace;
- color: var(--primary);
-}
-
-/* Numpad */
-.numpad {
- display: grid;
- grid-template-columns: repeat(3, 1fr);
- gap: 1rem;
- max-width: 300px;
- margin: 0 auto;
-
- button {
- padding: 1.5rem;
- font-size: 1.5rem;
- background: var(--bg-surface);
- color: var(--text-main);
- border: 1px solid var(--border);
- box-shadow: var(--shadow);
- margin: 0;
-
- &:hover {
- background: var(--primary);
- color: white;
- border-color: var(--primary);
- }
- }
-}
-
-/* Kiosk Content */
-.kiosk-content {
- display: flex;
- flex-direction: column;
- align-items: center;
- width: 100%;
-}
-
-/* Kiosk Grid */
-.kiosk-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 1.5rem;
- margin-top: 2rem;
- width: 100%;
- max-width: 600px;
-}
-
-/* Kiosk Menu Stack */
-.kiosk-menu-stack {
- display: flex;
- flex-direction: column;
- gap: 1.5rem;
- margin-top: 2rem;
- width: 100%;
- max-width: 600px;
-}
-
-/* Kiosk Button */
-.kiosk-btn {
- padding: 2rem;
- font-size: 1.25rem;
- display: flex;
- flex-direction: column;
- align-items: center;
- justify-content: center;
- gap: 0.5rem;
- height: 100%;
- min-height: 120px;
- margin: 0;
-}
-
-/* Buttons */
-button {
- background: var(--primary);
- color: white;
- border: none;
- padding: 0.75rem 1.5rem;
- border-radius: var(--radius);
- cursor: pointer;
- font-size: 1rem;
- font-weight: 500;
- font-family: inherit;
- transition: all 0.2s ease;
-
- &:hover {
- background: var(--primary-hover);
- transform: translateY(-1px);
- box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
- }
-
- & + & {
- margin-left: 1rem;
- }
-}
diff --git a/arma/client/addons/bank/ui/_site/atm.html b/arma/client/addons/bank/ui/_site/atm.html
deleted file mode 100644
index 19c28f5..0000000
--- a/arma/client/addons/bank/ui/_site/atm.html
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
- ATM
-
-
-
-
-
-
-
-
-
-
-
diff --git a/arma/client/addons/bank/ui/_site/atm.js b/arma/client/addons/bank/ui/_site/atm.js
deleted file mode 100644
index b5632dc..0000000
--- a/arma/client/addons/bank/ui/_site/atm.js
+++ /dev/null
@@ -1,490 +0,0 @@
-/**
- * ATM App - Vanilla JS Kiosk Implementation
- */
-
-//=============================================================================
-// #region LIBRARY - DOM Helper
-//=============================================================================
-
-function h(tag, props = {}, ...children) {
- const el = document.createElement(tag);
- if (props) {
- Object.entries(props).forEach(([key, value]) => {
- if (key.startsWith("on") && typeof value === "function") {
- el.addEventListener(key.substring(2).toLowerCase(), value);
- } else if (key === "className") {
- el.className = value;
- } else if (key === "style" && typeof value === "object") {
- Object.assign(el.style, value);
- } else {
- el.setAttribute(key, value);
- }
- });
- }
- children.forEach((child) => {
- if (typeof child === "string" || typeof child === "number") {
- el.appendChild(document.createTextNode(child));
- } else if (child instanceof Node) {
- el.appendChild(child);
- } else if (Array.isArray(child)) {
- child.forEach((c) => {
- if (c instanceof Node) el.appendChild(c);
- });
- }
- });
- return el;
-}
-
-let _rootContainer = null;
-let _rootComponent = null;
-
-function render(component, container) {
- _rootContainer = container;
- _rootComponent = component;
- _render();
-}
-
-function _render() {
- if (_rootContainer && _rootComponent) {
- _rootContainer.innerHTML = "";
- _rootContainer.appendChild(_rootComponent());
- }
-}
-
-const createSignal = (initialValue) => {
- let _val = initialValue;
- const getValue = () => _val;
- const setValue = (newValue) => {
- _val = typeof newValue === "function" ? newValue(_val) : newValue;
- _render();
- };
- return [getValue, setValue];
-};
-
-//=============================================================================
-// #region STATE
-//=============================================================================
-
-const [getView, setView] = createSignal("pin"); // 'pin', 'menu', 'withdraw', 'custom_withdraw', 'balance'
-const [getPin, setPin] = createSignal("");
-const [getCustomAmount, setCustomAmount] = createSignal("");
-const [getMessage, setMessage] = createSignal("");
-
-//=============================================================================
-// #region UI COMPONENTS
-//=============================================================================
-
-function Header() {
- return h(
- "div",
- { className: "header", style: { marginBottom: "2rem" } },
- h("h1", null, "ATM TERMINAL"),
- h("p", null, "Global Financial Network"),
- );
-}
-
-function PinView() {
- const currentPin = getPin();
-
- const handleNumClick = (num) => {
- if (currentPin.length < 4) {
- setPin((prev) => prev + num);
- }
- };
-
- const handleClear = () => setPin("");
-
- const handleEnter = () => {
- if (currentPin.length === 4) {
- const state =
- typeof store !== "undefined"
- ? store.getState()
- : { pin: "1234" };
- if (currentPin === state.pin) {
- setView("menu");
- } else {
- setMessage("Incorrect PIN");
- setPin("");
- setTimeout(() => setMessage(""), 2000);
- }
- } else {
- setMessage("Invalid PIN Length");
- setTimeout(() => setMessage(""), 2000);
- }
- };
-
- return h(
- "div",
- { className: "card", style: { padding: "3rem 2rem" } },
- h("h2", null, "Enter Security PIN"),
- h(
- "div",
- { className: "pin-display" },
- currentPin.replace(/./g, String.fromCharCode(8226)) || "----",
- ),
- h(
- "p",
- {
- style: {
- color: "#ef4444",
- height: "1.5rem",
- textAlign: "center",
- },
- },
- getMessage(),
- ),
- h(
- "div",
- { className: "numpad" },
- ["1", "2", "3", "4", "5", "6", "7", "8", "9"].map((num) =>
- h("button", { onClick: () => handleNumClick(num) }, num),
- ),
- h(
- "button",
- {
- style: { background: "#ef4444", color: "white" },
- onClick: handleClear,
- },
- "C",
- ),
- h("button", { onClick: () => handleNumClick("0") }, "0"),
- h(
- "button",
- {
- style: { background: "#10b981", color: "white" },
- onClick: handleEnter,
- },
- String.fromCharCode(8629),
- ),
- ),
- );
-}
-
-function MenuView() {
- return h(
- "div",
- { className: "kiosk-content" },
- h(
- "h2",
- { style: { textAlign: "center", marginBottom: "1rem" } },
- "Select Transaction",
- ),
- h(
- "div",
- { className: "kiosk-menu-stack" },
- h(
- "button",
- { className: "kiosk-btn", onClick: () => setView("withdraw") },
- "Withdraw Cash",
- ),
- h(
- "button",
- { className: "kiosk-btn", onClick: () => setView("balance") },
- "Check Balance",
- ),
- h(
- "button",
- {
- className: "kiosk-btn",
- style: {
- background: "var(--bg-surface)",
- color: "var(--text-main)",
- border: "1px solid var(--border)",
- },
- onClick: () => {
- setPin("");
- setView("pin");
- sendEvent("atm::close", {});
- },
- },
- "Cancel Transaction",
- ),
- ),
- );
-}
-
-function WithdrawView() {
- const state =
- typeof store !== "undefined"
- ? store.getState()
- : { accounts: { bank: 0 } };
- const bankBalance = state.accounts?.bank || 0;
-
- const handleWithdraw = (amount) => {
- if (bankBalance >= amount) {
- if (typeof store !== "undefined") {
- store.dispatch(withdraw(amount));
- }
- sendEvent("atm::withdraw", { amount });
- setMessage(`Please take your cash: $${amount.toLocaleString()}`);
- setTimeout(() => {
- setMessage("");
- setView("menu");
- }, 3000);
- } else {
- setMessage("Insufficient Funds");
- setTimeout(() => setMessage(""), 2000);
- }
- };
-
- if (getMessage()) {
- return h(
- "div",
- {
- className: "card",
- style: { padding: "4rem", textAlign: "center" },
- },
- h("h2", { style: { color: "var(--primary)" } }, getMessage()),
- );
- }
-
- return h(
- "div",
- { className: "kiosk-content" },
- h(
- "h2",
- { style: { textAlign: "center", marginBottom: "1rem" } },
- "Select Amount",
- ),
- h(
- "div",
- { className: "kiosk-grid" },
- h(
- "button",
- { className: "kiosk-btn", onClick: () => handleWithdraw(20) },
- "$20",
- ),
- h(
- "button",
- { className: "kiosk-btn", onClick: () => handleWithdraw(50) },
- "$50",
- ),
- h(
- "button",
- { className: "kiosk-btn", onClick: () => handleWithdraw(100) },
- "$100",
- ),
- h(
- "button",
- {
- className: "kiosk-btn",
- onClick: () => {
- setCustomAmount("");
- setView("custom_withdraw");
- },
- },
- "Other Amount",
- ),
- h(
- "button",
- {
- className: "kiosk-btn",
- style: {
- gridColumn: "span 2",
- background: "var(--text-muted)",
- },
- onClick: () => setView("menu"),
- },
- "Cancel",
- ),
- ),
- );
-}
-
-function CustomWithdrawView() {
- const currentAmount = getCustomAmount();
- const state =
- typeof store !== "undefined"
- ? store.getState()
- : { accounts: { bank: 0 } };
- const bankBalance = state.accounts?.bank || 0;
-
- const handleNumClick = (num) => {
- if (currentAmount.length < 5) {
- setCustomAmount((prev) => prev + num);
- }
- };
-
- const handleClear = () => setCustomAmount("");
-
- const handleEnter = () => {
- const amount = parseInt(currentAmount, 10);
- if (amount > 0) {
- if (bankBalance >= amount) {
- if (typeof store !== "undefined") {
- store.dispatch(withdraw(amount));
- }
- sendEvent("atm::withdraw", { amount });
- setMessage(
- `Please take your cash: $${amount.toLocaleString()}`,
- );
- setTimeout(() => {
- setMessage("");
- setView("menu");
- }, 3000);
- } else {
- setMessage("Insufficient Funds");
- setTimeout(() => setMessage(""), 2000);
- }
- } else {
- setMessage("Invalid Amount");
- setTimeout(() => setMessage(""), 2000);
- }
- };
-
- if (getMessage()) {
- return h(
- "div",
- {
- className: "card",
- style: { padding: "4rem", textAlign: "center" },
- },
- h("h2", { style: { color: "var(--primary)" } }, getMessage()),
- );
- }
-
- return h(
- "div",
- { className: "card", style: { padding: "3rem 2rem" } },
- h("h2", null, "Enter Amount"),
- h(
- "div",
- { className: "pin-display" },
- currentAmount ? `$${currentAmount}` : "$0",
- ),
- h(
- "div",
- { className: "numpad" },
- ["1", "2", "3", "4", "5", "6", "7", "8", "9"].map((num) =>
- h("button", { onClick: () => handleNumClick(num) }, num),
- ),
- h(
- "button",
- {
- style: { background: "#ef4444", color: "white" },
- onClick: handleClear,
- },
- "C",
- ),
- h("button", { onClick: () => handleNumClick("0") }, "0"),
- h(
- "button",
- {
- style: { background: "#10b981", color: "white" },
- onClick: handleEnter,
- },
- String.fromCharCode(8629),
- ),
- ),
- h(
- "button",
- {
- style: {
- width: "100%",
- marginTop: "2rem",
- padding: "1rem",
- background: "var(--text-muted)",
- },
- onClick: () => setView("withdraw"),
- },
- "Cancel",
- ),
- );
-}
-
-function BalanceView() {
- const state =
- typeof store !== "undefined"
- ? store.getState()
- : { accounts: { bank: 0 } };
- const bankBalance = state.accounts?.bank || 0;
-
- return h(
- "div",
- { className: "card", style: { textAlign: "center", padding: "3rem" } },
- h("h2", { style: { color: "var(--text-muted)" } }, "Available Balance"),
- h(
- "div",
- {
- style: {
- fontSize: "4rem",
- fontWeight: "800",
- margin: "2rem 0",
- color: "var(--primary-hover)",
- },
- },
- "$" + bankBalance.toLocaleString(),
- ),
- h(
- "button",
- {
- className: "kiosk-btn",
- style: { width: "100%", maxWidth: "300px", margin: "0 auto" },
- onClick: () => setView("menu"),
- },
- "Return to Menu",
- ),
- );
-}
-
-function App() {
- const view = getView();
-
- let mainContent;
- if (view === "pin") {
- mainContent = PinView();
- } else if (view === "menu") {
- mainContent = MenuView();
- } else if (view === "withdraw") {
- mainContent = WithdrawView();
- } else if (view === "custom_withdraw") {
- mainContent = CustomWithdrawView();
- } else if (view === "balance") {
- mainContent = BalanceView();
- }
-
- return h(
- "main",
- null,
- h("div", { className: "container" }, Header(), mainContent),
- );
-}
-
-//=============================================================================
-// #region ARMA 3 INTEGRATION
-//=============================================================================
-
-function sendEvent(event, data) {
- if (typeof A3API !== "undefined") {
- A3API.SendAlert(JSON.stringify({ event, data }));
- } else {
- console.log("Event:", event, "Data:", data);
- }
-}
-
-//=============================================================================
-// #region INITIALIZATION
-//=============================================================================
-
-let initialized = false;
-
-function initATM() {
- if (initialized) return;
-
- const root = document.getElementById("app");
- if (root) {
- if (typeof store !== "undefined") {
- store.subscribe(() => _render());
- }
-
- render(App, root);
- initialized = true;
- console.log("[ATM] Interface initialized");
- }
-}
-
-if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", initATM);
-} else {
- initATM();
-}
diff --git a/arma/client/addons/bank/ui/_site/bank-ui.css b/arma/client/addons/bank/ui/_site/bank-ui.css
new file mode 100644
index 0000000..e8d03f2
--- /dev/null
+++ b/arma/client/addons/bank/ui/_site/bank-ui.css
@@ -0,0 +1,591 @@
+/* Generated by tools/build-webui.mjs for Bank UI styles. Do not edit directly. */
+:root {
+ --bank-shell-bg: #f6f4ee;
+ --bank-surface: linear-gradient(180deg, #ffffff 0%, #f4f8fd 100%);
+ --bank-border: rgba(18, 54, 93, 0.12);
+ --bank-border-strong: rgba(18, 54, 93, 0.18);
+ --bank-text-main: #142f52;
+ --bank-text-muted: #6f86a3;
+ --bank-text-subtle: #8ea2bb;
+ --bank-accent: #275a8c;
+ --bank-accent-soft: #dfeaf9;
+ --bank-accent-line: rgba(39, 90, 140, 0.12);
+ --bank-shadow: 0 16px 30px rgba(18, 36, 57, 0.08);
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html,
+body,
+#app {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+}
+
+body {
+ overflow: hidden;
+ background: transparent;
+ color: var(--bank-text-main);
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+}
+
+button,
+input,
+select {
+ font: inherit;
+}
+
+.bank-shell {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ background: var(--bank-shell-bg);
+}
+
+.bank-scroll-shell {
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.bank-layout {
+ min-height: 100%;
+ width: min(100%, 1600px);
+ margin: 0 auto;
+ display: grid;
+ grid-template-columns: 320px minmax(0, 1fr);
+ gap: 1.25rem;
+ padding: 1.25rem;
+ flex: 1 0 auto;
+}
+
+.bank-sidebar,
+.bank-main {
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.bank-main {
+ overflow: visible;
+}
+
+.bank-module,
+.bank-card,
+.bank-atm-panel {
+ background: var(--bank-surface);
+ border: 1px solid var(--bank-border);
+ border-radius: 1.3rem;
+ box-shadow: var(--bank-shadow);
+}
+
+.bank-module,
+.bank-card,
+.bank-atm-panel {
+ padding: 1rem;
+ display: flex;
+ flex-direction: column;
+}
+
+.bank-module-header,
+.bank-card-header,
+.bank-section-header,
+.bank-page-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.bank-module-header,
+.bank-card-header {
+ margin-bottom: 0.9rem;
+}
+
+.bank-page {
+ display: grid;
+ gap: 1.35rem;
+ padding: 0.1rem 0 0;
+}
+
+.bank-page-header {
+ padding-top: 0.4rem;
+}
+
+.bank-page-copy {
+ margin: 0;
+ color: var(--bank-text-muted);
+ line-height: 1.5;
+ max-width: 48rem;
+}
+
+.bank-page-divider {
+ border-top: 1px solid var(--bank-accent-line);
+}
+
+.bank-page-body {
+ display: grid;
+ gap: 1.25rem;
+ padding-bottom: 1.25rem;
+}
+
+.bank-page-section {
+ display: grid;
+ gap: 1rem;
+ padding: 1.15rem 1.2rem 1.25rem;
+ border: 1px solid var(--bank-border);
+ border-radius: 1.3rem;
+ background: rgba(255, 255, 255, 0.72);
+ box-shadow: none;
+}
+
+.bank-title,
+.bank-section-title {
+ margin: 0;
+ color: var(--bank-text-main);
+ letter-spacing: -0.02em;
+}
+
+.bank-title {
+ font-size: 1.7rem;
+}
+
+.bank-section-title {
+ font-size: 1.1rem;
+}
+
+.bank-eyebrow,
+.bank-footer-title,
+.bank-stat-label {
+ display: block;
+ font-size: 0.68rem;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ font-weight: 700;
+ color: var(--bank-text-subtle);
+}
+
+.bank-pill {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.48rem 0.8rem;
+ border-radius: 999px;
+ background: var(--bank-accent-soft);
+ color: var(--bank-accent);
+ font-size: 0.74rem;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.bank-summary-grid,
+.bank-profile-stack {
+ display: grid;
+ gap: 0.8rem;
+}
+
+.bank-summary-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.bank-stat-card,
+.bank-metric-card {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ padding: 0.9rem;
+ border-radius: 0.95rem;
+ border: 1px solid var(--bank-border);
+ background: rgba(255, 255, 255, 0.6);
+}
+
+.bank-stat-card.is-accent,
+.bank-metric-card.is-accent {
+ background: linear-gradient(180deg, #edf4fe 0%, #dfeaf9 100%);
+}
+
+.bank-stat-card.is-success,
+.bank-metric-card.is-success {
+ background: linear-gradient(180deg, #edf9f4 0%, #dff4ea 100%);
+}
+
+.bank-stat-card.is-warning,
+.bank-metric-card.is-warning {
+ background: linear-gradient(180deg, #fdf7ea 0%, #f7edd4 100%);
+}
+
+.bank-stat-value,
+.bank-metric-value {
+ min-width: 0;
+ color: var(--bank-text-main);
+ font-weight: 700;
+ overflow-wrap: anywhere;
+}
+
+.bank-stat-value {
+ font-size: 1rem;
+}
+
+.bank-metric-value {
+ font-size: 1.8rem;
+ letter-spacing: -0.03em;
+}
+
+.bank-metric-copy,
+.bank-card-copy,
+.bank-empty-copy,
+.bank-footer-copy,
+.bank-history-meta {
+ color: var(--bank-text-muted);
+ line-height: 1.45;
+}
+
+.bank-card-copy {
+ margin: 0 0 0.9rem;
+}
+
+.bank-summary-band {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.85rem;
+}
+
+.bank-action-sections {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+.bank-support-sections {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ gap: 1rem;
+}
+
+.bank-form-stack {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.bank-input,
+.bank-select {
+ width: 100%;
+ min-width: 0;
+ height: 2.9rem;
+ padding: 0 0.95rem;
+ border-radius: 0.8rem;
+ border: 1px solid var(--bank-border);
+ background: rgba(255, 255, 255, 0.82);
+ color: var(--bank-text-main);
+}
+
+.bank-action-row {
+ display: flex;
+ gap: 0.75rem;
+}
+
+.bank-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 2.85rem;
+ padding: 0.75rem 1rem;
+ border-radius: 0.8rem;
+ border: 1px solid var(--bank-border);
+ font-size: 0.82rem;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ cursor: pointer;
+ transition:
+ background-color 160ms ease,
+ color 160ms ease,
+ border-color 160ms ease;
+}
+
+.bank-btn:disabled {
+ opacity: 0.55;
+ cursor: default;
+}
+
+.bank-btn-primary {
+ background: #455a77;
+ border-color: #455a77;
+ color: #fff;
+}
+
+.bank-btn-primary:hover:not(:disabled) {
+ background: #354863;
+ border-color: #354863;
+}
+
+.bank-btn-secondary {
+ background: rgba(255, 255, 255, 0.82);
+ color: var(--bank-accent);
+}
+
+.bank-btn-secondary:hover:not(:disabled) {
+ background: #eef4fd;
+}
+
+.bank-history-list {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.bank-history-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 0.85rem 0.95rem;
+ border-radius: 0.9rem;
+ border: 1px solid var(--bank-border);
+ background: rgba(255, 255, 255, 0.6);
+}
+
+.bank-history-copy {
+ min-width: 0;
+ display: grid;
+ gap: 0.18rem;
+}
+
+.bank-history-title,
+.bank-empty-title {
+ color: var(--bank-text-main);
+ font-weight: 700;
+}
+
+.bank-history-value {
+ white-space: nowrap;
+ font-weight: 700;
+ color: var(--bank-accent);
+}
+
+.bank-empty-state {
+ display: grid;
+ gap: 0.35rem;
+ padding: 1rem 0;
+}
+
+.bank-notice-stack {
+ position: fixed;
+ top: 1.2rem;
+ right: 1.5rem;
+ z-index: 12;
+ display: grid;
+ gap: 0.65rem;
+}
+
+.bank-notice {
+ max-width: 24rem;
+ padding: 0.85rem 1rem;
+ border-radius: 0.9rem;
+ border: 1px solid var(--bank-border);
+ background: #fff;
+ box-shadow: 0 14px 28px rgba(16, 34, 56, 0.14);
+ font-size: 0.92rem;
+}
+
+.bank-notice.is-success {
+ background: #ecfdf5;
+ border-color: #bbf7d0;
+ color: #166534;
+}
+
+.bank-notice.is-error {
+ background: #fef2f2;
+ border-color: #fecaca;
+ color: #991b1b;
+}
+
+.bank-footer-bar {
+ width: 100%;
+ margin-top: auto;
+ background: #1e293b;
+ color: #f8fafc;
+}
+
+.bank-footer {
+ width: min(100%, 1600px);
+ margin: 0 auto;
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 4rem;
+ padding: 3rem 1.25rem;
+}
+
+.bank-footer-block {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.bank-footer-title {
+ margin: 0;
+ color: #f8fafc;
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ font-weight: 700;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid #475569;
+}
+
+.bank-footer-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.bank-atm-shell {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem 1rem;
+}
+
+.bank-atm-panel {
+ width: min(100%, 560px);
+ display: grid;
+ gap: 1rem;
+}
+
+.bank-atm-stack {
+ display: grid;
+ gap: 1rem;
+}
+
+.bank-pin-display,
+.bank-balance-display {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 5rem;
+ padding: 1rem;
+ border-radius: 1rem;
+ border: 1px solid var(--bank-border-strong);
+ background: rgba(255, 255, 255, 0.68);
+ color: var(--bank-text-main);
+ text-align: center;
+}
+
+.bank-pin-display {
+ font-size: 2rem;
+}
+
+.bank-balance-display {
+ font-size: 2.5rem;
+ font-weight: 800;
+ letter-spacing: -0.03em;
+}
+
+.bank-pin-indicators {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.9rem;
+}
+
+.bank-pin-indicator {
+ width: 1rem;
+ height: 1rem;
+ border-radius: 999px;
+ border: 2px solid var(--bank-accent);
+ background: transparent;
+}
+
+.bank-pin-indicator.is-filled {
+ background: var(--bank-accent);
+}
+
+.bank-keypad {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0.75rem;
+}
+
+.bank-key {
+ min-height: 3.2rem;
+ padding: 0.9rem;
+ border-radius: 0.9rem;
+ border: 1px solid var(--bank-border);
+ background: rgba(255, 255, 255, 0.82);
+ color: var(--bank-text-main);
+ font-weight: 700;
+}
+
+.bank-key.is-muted {
+ background: #eef2f8;
+ color: var(--bank-text-muted);
+}
+
+.bank-key.is-accent {
+ background: #455a77;
+ border-color: #455a77;
+ color: #fff;
+}
+
+.bank-key.is-wide {
+ grid-column: span 3;
+}
+
+.bank-atm-action-grid {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.bank-shell.is-atm {
+ background: transparent;
+ min-height: 100%;
+ justify-content: center;
+}
+
+.bank-shell.is-atm .bank-atm-shell {
+ flex: 1;
+ width: 100%;
+ min-height: 100%;
+ max-width: 100%;
+}
+
+.bank-footer-copy {
+ color: #cbd5e1;
+ line-height: 1.5;
+ margin: 0 0 0.75rem;
+}
+
+@media (max-width: 1200px) {
+ .bank-layout {
+ grid-template-columns: 1fr;
+ }
+
+ .bank-main {
+ overflow: visible;
+ }
+}
+
+@media (max-width: 900px) {
+ .bank-summary-band,
+ .bank-action-sections,
+ .bank-footer {
+ grid-template-columns: 1fr;
+ }
+
+ .bank-summary-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/arma/client/addons/bank/ui/_site/bank-ui.js b/arma/client/addons/bank/ui/_site/bank-ui.js
new file mode 100644
index 0000000..5203cfd
--- /dev/null
+++ b/arma/client/addons/bank/ui/_site/bank-ui.js
@@ -0,0 +1,1650 @@
+/* Generated by tools/build-webui.mjs for Bank UI app. Do not edit directly. */
+(function () {
+ const runtime = window.ForgeWebUI;
+ const BankApp = (window.BankApp = window.BankApp || {});
+ BankApp.runtime = runtime;
+ window.AppRuntime = runtime;
+})();
+
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+
+ const defaultSession = {
+ mode: "bank",
+ orgFunds: 0,
+ orgName: "",
+ playerName: "",
+ transferTargets: [],
+ uid: "",
+ };
+
+ const defaultAccount = {
+ bank: 0,
+ cash: 0,
+ earnings: 0,
+ pin: "1234",
+ transactions: [],
+ };
+
+ 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));
+ }
+
+ BankApp.data = {
+ account: Object.assign({}, defaultAccount),
+ session: Object.assign({}, defaultSession),
+ applyHydratePayload(payload) {
+ replaceObject(
+ this.session,
+ Object.assign({}, defaultSession, payload?.session || {}),
+ );
+ replaceObject(
+ this.account,
+ Object.assign({}, defaultAccount, payload?.account || {}),
+ );
+ },
+ };
+})();
+
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const { createSignal } = BankApp.runtime;
+
+ class BankStore {
+ constructor() {
+ [this.getMode, this.setMode] = createSignal("bank");
+ [this.getNotice, this.setNotice] = createSignal({
+ text: "",
+ type: "",
+ });
+ [this.getPendingAction, this.setPendingAction] = createSignal("");
+ [this.getAtmView, this.setAtmView] = createSignal("pin");
+ [this.getEnteredPin, this.setEnteredPin] = createSignal("");
+ [this.getCustomAmount, this.setCustomAmount] = createSignal("");
+ [this.getAccountVersion, this.setAccountVersion] = createSignal(0);
+ [this.getSessionVersion, this.setSessionVersion] = createSignal(0);
+ }
+
+ finishAction() {
+ this.setPendingAction("");
+ }
+
+ hydrateFromPayload(payload) {
+ const mode = String(payload?.session?.mode || "bank")
+ .trim()
+ .toLowerCase();
+ const currentMode = this.getMode();
+ const currentAtmView = this.getAtmView();
+
+ this.setMode(mode === "atm" ? "atm" : "bank");
+ this.setPendingAction("");
+ this.setNotice({ text: "", type: "" });
+ this.setEnteredPin("");
+ this.setCustomAmount("");
+ this.setAccountVersion(this.getAccountVersion() + 1);
+ this.setSessionVersion(this.getSessionVersion() + 1);
+
+ if (mode === "atm") {
+ this.setAtmView(currentMode === "atm" ? currentAtmView : "pin");
+ return;
+ }
+
+ this.setAtmView("dashboard");
+ }
+
+ resetAtm() {
+ this.setEnteredPin("");
+ this.setCustomAmount("");
+ this.setAtmView("pin");
+ }
+
+ startAction(action) {
+ this.setPendingAction(
+ String(action || "")
+ .trim()
+ .toLowerCase(),
+ );
+ }
+ }
+
+ BankApp.store = new BankStore();
+})();
+
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const store = BankApp.store;
+ const bridge = window.ForgeWebUI.createBridge({
+ closeEvent: "bank::close",
+ globalName: "ForgeBridge",
+ readyEvent: "bank::ready",
+ });
+
+ function hydrate(payloadData) {
+ BankApp.data.applyHydratePayload(payloadData);
+ store.hydrateFromPayload(payloadData);
+ }
+
+ bridge.on("bank::hydrate", hydrate);
+ bridge.on("bank::sync", hydrate);
+ bridge.on("bank::notice", (payloadData) => {
+ if (BankApp.actions) {
+ BankApp.actions.showNotice(
+ payloadData.type || "error",
+ payloadData.message || "Bank notice received.",
+ );
+ }
+ });
+
+ BankApp.bridge = {
+ notifyReady() {
+ return bridge.ready({ loaded: true });
+ },
+ receive: bridge.receive,
+ requestClose() {
+ return bridge.close({});
+ },
+ requestDeposit(payload) {
+ return bridge.send("bank::deposit::request", payload);
+ },
+ requestDepositEarnings(payload) {
+ return bridge.send("bank::depositEarnings::request", payload);
+ },
+ requestRefresh() {
+ return bridge.send("bank::refresh", {});
+ },
+ requestTransfer(payload) {
+ return bridge.send("bank::transfer::request", payload);
+ },
+ requestWithdraw(payload) {
+ return bridge.send("bank::withdraw::request", payload);
+ },
+ sendEvent: bridge.send,
+ };
+})();
+
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const store = BankApp.store;
+
+ let noticeTimer = null;
+
+ function getAccount() {
+ return BankApp.data?.account || {};
+ }
+
+ function getSession() {
+ return BankApp.data?.session || {};
+ }
+
+ function normalizeAmount(value) {
+ const amount = Math.floor(Number(value || 0));
+ return Number.isFinite(amount) ? amount : 0;
+ }
+
+ function showNotice(type, text) {
+ store.setNotice({ type, text });
+
+ if (noticeTimer) {
+ clearTimeout(noticeTimer);
+ }
+
+ noticeTimer = setTimeout(() => {
+ store.setNotice({ text: "", type: "" });
+ noticeTimer = null;
+ }, 3200);
+ }
+
+ function closeBank() {
+ const bridge = BankApp.bridge;
+ if (bridge && typeof bridge.requestClose === "function") {
+ const sent = bridge.requestClose();
+ if (sent) {
+ return true;
+ }
+ }
+
+ showNotice("error", "Bank bridge is unavailable.");
+ return false;
+ }
+
+ function refreshBank() {
+ const bridge = BankApp.bridge;
+ if (bridge && typeof bridge.requestRefresh === "function") {
+ const sent = bridge.requestRefresh();
+ if (sent) {
+ return true;
+ }
+ }
+
+ showNotice("error", "Bank refresh bridge is unavailable.");
+ return false;
+ }
+
+ function requestDeposit(amountValue) {
+ const amount = normalizeAmount(amountValue);
+ const account = getAccount();
+
+ if (amount <= 0) {
+ showNotice("error", "Enter a valid deposit amount.");
+ return false;
+ }
+
+ if (amount > Number(account.cash || 0)) {
+ showNotice("error", "Cash on hand cannot cover that deposit.");
+ return false;
+ }
+
+ const bridge = BankApp.bridge;
+ if (!bridge || typeof bridge.requestDeposit !== "function") {
+ showNotice("error", "Deposit bridge is unavailable.");
+ return false;
+ }
+
+ store.startAction("deposit");
+ const sent = bridge.requestDeposit({ amount });
+ if (!sent) {
+ store.finishAction();
+ showNotice("error", "Deposit bridge is unavailable.");
+ return false;
+ }
+
+ return true;
+ }
+
+ function requestWithdraw(amountValue) {
+ const amount = normalizeAmount(amountValue);
+ const account = getAccount();
+
+ if (amount <= 0) {
+ showNotice("error", "Enter a valid withdrawal amount.");
+ return false;
+ }
+
+ if (amount > Number(account.bank || 0)) {
+ showNotice("error", "Bank balance cannot cover that withdrawal.");
+ return false;
+ }
+
+ const bridge = BankApp.bridge;
+ if (!bridge || typeof bridge.requestWithdraw !== "function") {
+ showNotice("error", "Withdraw bridge is unavailable.");
+ return false;
+ }
+
+ store.startAction("withdraw");
+ const sent = bridge.requestWithdraw({ amount });
+ if (!sent) {
+ store.finishAction();
+ showNotice("error", "Withdraw bridge is unavailable.");
+ return false;
+ }
+
+ return true;
+ }
+
+ function requestTransfer(targetUid, amountValue) {
+ const amount = normalizeAmount(amountValue);
+ const session = getSession();
+ const account = getAccount();
+ const targetId = String(targetUid || "").trim();
+
+ if (!targetId) {
+ showNotice("error", "Select a transfer recipient.");
+ return false;
+ }
+
+ if (targetId === String(session.uid || "")) {
+ showNotice("error", "You cannot transfer funds to yourself.");
+ return false;
+ }
+
+ if (amount <= 0) {
+ showNotice("error", "Enter a valid transfer amount.");
+ return false;
+ }
+
+ if (amount > Number(account.bank || 0)) {
+ showNotice("error", "Bank balance cannot cover that transfer.");
+ return false;
+ }
+
+ const bridge = BankApp.bridge;
+ if (!bridge || typeof bridge.requestTransfer !== "function") {
+ showNotice("error", "Transfer bridge is unavailable.");
+ return false;
+ }
+
+ store.startAction("transfer");
+ const sent = bridge.requestTransfer({
+ amount,
+ from: "bank",
+ target: targetId,
+ });
+ if (!sent) {
+ store.finishAction();
+ showNotice("error", "Transfer bridge is unavailable.");
+ return false;
+ }
+
+ return true;
+ }
+
+ function requestDepositEarnings(amountValue) {
+ const amount = normalizeAmount(amountValue);
+ const account = getAccount();
+
+ if (amount <= 0) {
+ showNotice("error", "No earnings are available to deposit.");
+ return false;
+ }
+
+ if (amount > Number(account.earnings || 0)) {
+ showNotice(
+ "error",
+ "Pending earnings cannot cover that deposit request.",
+ );
+ return false;
+ }
+
+ const bridge = BankApp.bridge;
+ if (!bridge || typeof bridge.requestDepositEarnings !== "function") {
+ showNotice("error", "Earnings bridge is unavailable.");
+ return false;
+ }
+
+ store.startAction("depositearnings");
+ const sent = bridge.requestDepositEarnings({ amount });
+ if (!sent) {
+ store.finishAction();
+ showNotice("error", "Earnings bridge is unavailable.");
+ return false;
+ }
+
+ return true;
+ }
+
+ function appendPinDigit(digit) {
+ const nextDigit = String(digit || "").trim();
+ if (!nextDigit) {
+ return;
+ }
+
+ const currentPin = String(store.getEnteredPin() || "");
+ if (currentPin.length >= 4) {
+ return;
+ }
+
+ store.setEnteredPin(currentPin + nextDigit);
+ }
+
+ function backspacePin() {
+ const currentPin = String(store.getEnteredPin() || "");
+ store.setEnteredPin(currentPin.slice(0, -1));
+ }
+
+ function clearPin() {
+ store.setEnteredPin("");
+ }
+
+ function submitPin() {
+ const enteredPin = String(store.getEnteredPin() || "");
+ const actualPin = String(getAccount().pin || "1234");
+
+ if (enteredPin.length !== 4) {
+ showNotice("error", "Enter your four-digit access PIN.");
+ return false;
+ }
+
+ if (enteredPin !== actualPin) {
+ clearPin();
+ showNotice("error", "Incorrect PIN.");
+ return false;
+ }
+
+ clearPin();
+ store.setAtmView("menu");
+ return true;
+ }
+
+ function selectAtmView(view) {
+ const nextView = String(view || "").trim();
+ if (!nextView) {
+ return false;
+ }
+
+ if (nextView === "pin") {
+ store.resetAtm();
+ return true;
+ }
+
+ store.setCustomAmount("");
+ store.setAtmView(nextView);
+ return true;
+ }
+
+ function appendCustomAmountDigit(digit) {
+ const nextDigit = String(digit || "").trim();
+ if (!nextDigit) {
+ return;
+ }
+
+ const currentValue = String(store.getCustomAmount() || "");
+ if (currentValue.length >= 7) {
+ return;
+ }
+
+ store.setCustomAmount(currentValue + nextDigit);
+ }
+
+ function backspaceCustomAmount() {
+ const currentValue = String(store.getCustomAmount() || "");
+ store.setCustomAmount(currentValue.slice(0, -1));
+ }
+
+ function clearCustomAmount() {
+ store.setCustomAmount("");
+ }
+
+ function submitCustomAmount(kind) {
+ const amount = normalizeAmount(store.getCustomAmount());
+ const nextKind = String(kind || "")
+ .trim()
+ .toLowerCase();
+
+ if (amount <= 0) {
+ showNotice("error", "Enter a valid transaction amount.");
+ return false;
+ }
+
+ const success =
+ nextKind === "deposit"
+ ? requestDeposit(amount)
+ : requestWithdraw(amount);
+
+ if (success) {
+ store.setCustomAmount("");
+ store.setAtmView("menu");
+ }
+
+ return success;
+ }
+
+ function requestAtmAmount(kind, amount) {
+ const nextKind = String(kind || "")
+ .trim()
+ .toLowerCase();
+ const success =
+ nextKind === "deposit"
+ ? requestDeposit(amount)
+ : requestWithdraw(amount);
+
+ if (success) {
+ store.setAtmView("menu");
+ }
+
+ return success;
+ }
+
+ BankApp.actions = {
+ appendCustomAmountDigit,
+ appendPinDigit,
+ backspaceCustomAmount,
+ backspacePin,
+ clearCustomAmount,
+ clearPin,
+ closeBank,
+ refreshBank,
+ requestAtmAmount,
+ requestDeposit,
+ requestDepositEarnings,
+ requestTransfer,
+ requestWithdraw,
+ selectAtmView,
+ showNotice,
+ submitCustomAmount,
+ submitPin,
+ };
+})();
+
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const { h } = BankApp.runtime;
+ const store = BankApp.store;
+ const { account } = BankApp.data;
+
+ function formatCurrency(value) {
+ return `$${Math.round(Number(value || 0)).toLocaleString()}`;
+ }
+
+ function pending(actionName) {
+ return store.getPendingAction() === actionName;
+ }
+
+ function statCard(label, value, tone = "") {
+ return h(
+ "div",
+ {
+ className: tone
+ ? `bank-stat-card is-${tone}`
+ : "bank-stat-card",
+ },
+ h("span", { className: "bank-stat-label" }, label),
+ h("span", { className: "bank-stat-value" }, value),
+ );
+ }
+
+ function metricCard(label, value, copy, tone = "") {
+ return h(
+ "div",
+ {
+ className: tone
+ ? `bank-metric-card is-${tone}`
+ : "bank-metric-card",
+ },
+ h("span", { className: "bank-eyebrow" }, label),
+ h("span", { className: "bank-metric-value" }, value),
+ h("span", { className: "bank-metric-copy" }, copy),
+ );
+ }
+
+ function pinIndicators(value) {
+ const pin = String(value || "");
+
+ return h(
+ "div",
+ { className: "bank-pin-indicators" },
+ [0, 1, 2, 3].map((index) =>
+ h("span", {
+ className:
+ index < pin.length
+ ? "bank-pin-indicator is-filled"
+ : "bank-pin-indicator",
+ }),
+ ),
+ );
+ }
+
+ function readInputValue(id) {
+ return document.getElementById(id)?.value || "";
+ }
+
+ function clearInputValue(id) {
+ const input = document.getElementById(id);
+ if (input) {
+ input.value = "";
+ }
+ }
+
+ function keypad(onDigit, onBackspace, onClear, onEnter) {
+ const keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
+
+ return h(
+ "div",
+ { className: "bank-keypad" },
+ keys.map((digit) =>
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-key",
+ onClick: () => onDigit(digit),
+ },
+ digit,
+ ),
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-key is-muted",
+ onClick: onClear,
+ },
+ "C",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-key",
+ onClick: () => onDigit("0"),
+ },
+ "0",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-key is-accent",
+ onClick: onEnter,
+ },
+ "Enter",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-key is-wide",
+ onClick: onBackspace,
+ },
+ "Backspace",
+ ),
+ );
+ }
+
+ function transactionRows() {
+ const transactions = Array.isArray(account.transactions)
+ ? account.transactions
+ : [];
+
+ if (transactions.length === 0) {
+ return h(
+ "div",
+ { className: "bank-empty-state" },
+ h("h3", { className: "bank-empty-title" }, "No transactions"),
+ h(
+ "p",
+ { className: "bank-empty-copy" },
+ "Deposits, withdrawals, and transfers will appear here after the account begins moving funds.",
+ ),
+ );
+ }
+
+ return h(
+ "div",
+ { className: "bank-history-list" },
+ transactions
+ .slice(0, 8)
+ .map((entry) =>
+ h(
+ "div",
+ { className: "bank-history-row" },
+ h(
+ "div",
+ { className: "bank-history-copy" },
+ h(
+ "span",
+ { className: "bank-history-title" },
+ entry.type || "Transaction",
+ ),
+ h(
+ "span",
+ { className: "bank-history-meta" },
+ entry.date || "Pending timestamp",
+ ),
+ ),
+ h(
+ "span",
+ { className: "bank-history-value" },
+ formatCurrency(entry.amount || 0),
+ ),
+ ),
+ ),
+ );
+ }
+
+ BankApp.componentFns = BankApp.componentFns || {};
+ Object.assign(BankApp.componentFns, {
+ clearInputValue,
+ formatCurrency,
+ keypad,
+ metricCard,
+ pending,
+ pinIndicators,
+ readInputValue,
+ statCard,
+ transactionRows,
+ });
+})();
+
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const { h } = BankApp.runtime;
+ const store = BankApp.store;
+ const actions = BankApp.actions;
+ const { account, session } = BankApp.data;
+ const { formatCurrency, statCard } = BankApp.componentFns;
+
+ BankApp.componentFns = BankApp.componentFns || {};
+ BankApp.componentFns.BankSidebar = function BankSidebar() {
+ store.getAccountVersion();
+ store.getSessionVersion();
+
+ return h(
+ "aside",
+ { className: "bank-sidebar" },
+ h(
+ "section",
+ { className: "bank-module" },
+ h(
+ "div",
+ { className: "bank-module-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "Account"),
+ h(
+ "h2",
+ { className: "bank-section-title" },
+ "Balances",
+ ),
+ ),
+ h("span", { className: "bank-pill" }, "Live"),
+ ),
+ h(
+ "div",
+ { className: "bank-summary-grid" },
+ statCard("Bank", formatCurrency(account.bank), "accent"),
+ statCard("Cash", formatCurrency(account.cash)),
+ statCard(
+ "Earnings",
+ formatCurrency(account.earnings),
+ account.earnings > 0 ? "warning" : "",
+ ),
+ statCard(
+ "Org Funds",
+ formatCurrency(session.orgFunds),
+ session.orgFunds > 0 ? "success" : "",
+ ),
+ ),
+ ),
+ h(
+ "section",
+ { className: "bank-module" },
+ h(
+ "div",
+ { className: "bank-module-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "Profile"),
+ h(
+ "h2",
+ { className: "bank-section-title" },
+ "Account Holder",
+ ),
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ onClick: () => actions.refreshBank(),
+ },
+ "Refresh",
+ ),
+ ),
+ h(
+ "div",
+ { className: "bank-profile-stack" },
+ statCard("Name", session.playerName || "Unknown"),
+ statCard("UID", session.uid || "-"),
+ statCard(
+ "Organization",
+ session.orgName || "No active organization",
+ ),
+ ),
+ ),
+ );
+ };
+})();
+
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const { h } = BankApp.runtime;
+ const store = BankApp.store;
+ const { account, session } = BankApp.data;
+ const { formatCurrency } = BankApp.componentFns;
+
+ BankApp.componentFns = BankApp.componentFns || {};
+ BankApp.componentFns.BankFooter = function BankFooter() {
+ store.getAccountVersion();
+ store.getSessionVersion();
+
+ const sections = [
+ {
+ title: "Banking Resources",
+ items: [
+ "Account Access Policy",
+ "Transfer & Wire Guidelines",
+ "Cash Handling Schedule",
+ "Terminal Security Notice",
+ ],
+ },
+ {
+ title: "Bank Support",
+ items: session.orgName
+ ? [
+ `Organization: ${session.orgName}`,
+ `Treasury Reference: ${formatCurrency(session.orgFunds)}`,
+ `${session.transferTargets.length} transfer recipient(s) currently visible.`,
+ `Primary Ledger: ${formatCurrency(account.bank)}`,
+ ]
+ : [
+ "Organization: No active treasury link",
+ `${session.transferTargets.length} transfer recipient(s) currently visible.`,
+ `Primary Ledger: ${formatCurrency(account.bank)}`,
+ `Cash On Hand: ${formatCurrency(account.cash)}`,
+ ],
+ },
+ ];
+
+ return h(
+ "footer",
+ { className: "bank-footer-bar" },
+ h(
+ "div",
+ { className: "bank-footer" },
+ ...sections.map((section) =>
+ h(
+ "div",
+ { className: "bank-footer-block" },
+ h(
+ "h3",
+ { className: "bank-footer-title" },
+ section.title,
+ ),
+ h(
+ "ul",
+ { className: "bank-footer-list" },
+ ...(section.items || []).map((item) =>
+ h(
+ "li",
+ { className: "bank-footer-copy" },
+ item,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ };
+})();
+
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const { h } = BankApp.runtime;
+ const store = BankApp.store;
+ const actions = BankApp.actions;
+ const { account, session } = BankApp.data;
+ const {
+ clearInputValue,
+ formatCurrency,
+ metricCard,
+ pending,
+ readInputValue,
+ transactionRows,
+ } = BankApp.componentFns;
+
+ function trackAccount() {
+ store.getAccountVersion();
+ }
+
+ function trackSession() {
+ store.getSessionVersion();
+ }
+
+ function pageHeader() {
+ trackSession();
+
+ return h(
+ "div",
+ { className: "bank-page-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "Treasury Desk"),
+ h("h1", { className: "bank-title" }, "Personal Banking"),
+ ),
+ h(
+ "span",
+ { className: "bank-pill" },
+ session.playerName || "Account Holder",
+ ),
+ );
+ }
+
+ function summarySection() {
+ trackAccount();
+ trackSession();
+
+ return h(
+ "section",
+ { className: "bank-page-section bank-summary-section" },
+ h(
+ "div",
+ { className: "bank-section-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "Overview"),
+ h(
+ "h2",
+ { className: "bank-section-title" },
+ "Financial Position",
+ ),
+ ),
+ h("span", { className: "bank-pill" }, "Banking Desk"),
+ ),
+ h(
+ "div",
+ { className: "bank-summary-band" },
+ metricCard(
+ "Primary Balance",
+ formatCurrency(account.bank),
+ "Available for transfers and withdrawals.",
+ "accent",
+ ),
+ metricCard(
+ "Cash On Hand",
+ formatCurrency(account.cash),
+ "Funds currently carried by the player.",
+ ),
+ metricCard(
+ "Pending Earnings",
+ formatCurrency(account.earnings),
+ "Ready to sweep into the main account ledger.",
+ account.earnings > 0 ? "warning" : "",
+ ),
+ metricCard(
+ "Org Snapshot",
+ formatCurrency(session.orgFunds),
+ "Reference value pulled from the organization treasury.",
+ session.orgFunds > 0 ? "success" : "",
+ ),
+ ),
+ );
+ }
+
+ function actionSections() {
+ trackSession();
+
+ return h(
+ "div",
+ { className: "bank-action-sections" },
+ h(
+ "section",
+ { className: "bank-page-section" },
+ h(
+ "div",
+ { className: "bank-section-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "Movement"),
+ h(
+ "h2",
+ { className: "bank-section-title" },
+ "Deposit / Withdraw",
+ ),
+ ),
+ ),
+ h(
+ "div",
+ { className: "bank-form-stack" },
+ h("input", {
+ id: "bank-amount-input",
+ className: "bank-input",
+ type: "number",
+ min: "1",
+ placeholder: "Enter amount",
+ }),
+ h(
+ "div",
+ { className: "bank-action-row" },
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-primary",
+ disabled: pending("deposit"),
+ onClick: () => {
+ const sent = actions.requestDeposit(
+ readInputValue("bank-amount-input"),
+ );
+ if (sent) {
+ clearInputValue("bank-amount-input");
+ }
+ },
+ },
+ pending("deposit") ? "Depositing..." : "Deposit",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ disabled: pending("withdraw"),
+ onClick: () => {
+ const sent = actions.requestWithdraw(
+ readInputValue("bank-amount-input"),
+ );
+ if (sent) {
+ clearInputValue("bank-amount-input");
+ }
+ },
+ },
+ pending("withdraw") ? "Withdrawing..." : "Withdraw",
+ ),
+ ),
+ ),
+ ),
+ h(
+ "section",
+ { className: "bank-page-section" },
+ h(
+ "div",
+ { className: "bank-section-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "Transfer"),
+ h(
+ "h2",
+ { className: "bank-section-title" },
+ "Wire Funds",
+ ),
+ ),
+ ),
+ h(
+ "div",
+ { className: "bank-form-stack" },
+ h(
+ "select",
+ {
+ id: "bank-transfer-target",
+ className: "bank-select",
+ },
+ h(
+ "option",
+ { value: "" },
+ session.transferTargets.length > 0
+ ? "Select recipient"
+ : "No available recipients",
+ ),
+ session.transferTargets.map((entry) =>
+ h(
+ "option",
+ { value: entry.uid },
+ entry.name || entry.uid,
+ ),
+ ),
+ ),
+ h("input", {
+ id: "bank-transfer-amount",
+ className: "bank-input",
+ type: "number",
+ min: "1",
+ placeholder: "Enter transfer amount",
+ }),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-primary",
+ disabled:
+ pending("transfer") ||
+ session.transferTargets.length === 0,
+ onClick: () => {
+ const sent = actions.requestTransfer(
+ readInputValue("bank-transfer-target"),
+ readInputValue("bank-transfer-amount"),
+ );
+ if (sent) {
+ clearInputValue("bank-transfer-amount");
+ }
+ },
+ },
+ pending("transfer")
+ ? "Transferring..."
+ : "Transfer Funds",
+ ),
+ ),
+ ),
+ );
+ }
+
+ function supportSection() {
+ trackAccount();
+
+ return h(
+ "div",
+ { className: "bank-support-sections" },
+ h(
+ "section",
+ { className: "bank-page-section" },
+ h(
+ "div",
+ { className: "bank-section-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "Sweep"),
+ h(
+ "h2",
+ { className: "bank-section-title" },
+ "Deposit Earnings",
+ ),
+ ),
+ ),
+ h(
+ "p",
+ { className: "bank-card-copy" },
+ "Sweep pending earnings into the primary account when you want them reflected in the main balance.",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-primary",
+ disabled:
+ pending("depositearnings") ||
+ Number(account.earnings || 0) <= 0,
+ onClick: () =>
+ actions.requestDepositEarnings(account.earnings),
+ },
+ pending("depositearnings")
+ ? "Depositing..."
+ : "Deposit Earnings",
+ ),
+ ),
+ );
+ }
+
+ function historySection() {
+ trackAccount();
+
+ return h(
+ "section",
+ { className: "bank-page-section bank-history-section" },
+ h(
+ "div",
+ { className: "bank-section-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "History"),
+ h(
+ "h2",
+ { className: "bank-section-title" },
+ "Recent Transactions",
+ ),
+ ),
+ ),
+ transactionRows(),
+ );
+ }
+
+ BankApp.componentFns = BankApp.componentFns || {};
+ BankApp.componentFns.BankPageHeader = pageHeader;
+ BankApp.componentFns.BankSummarySection = summarySection;
+ BankApp.componentFns.BankActionSections = actionSections;
+ BankApp.componentFns.BankSupportSection = supportSection;
+ BankApp.componentFns.BankHistorySection = historySection;
+})();
+
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const { h } = BankApp.runtime;
+ const store = BankApp.store;
+ const actions = BankApp.actions;
+ const { account } = BankApp.data;
+ const { formatCurrency, keypad, pinIndicators } = BankApp.componentFns;
+
+ function atmMenuCard() {
+ return h(
+ "div",
+ { className: "bank-atm-action-grid" },
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-primary",
+ onClick: () => actions.selectAtmView("withdraw"),
+ },
+ "Withdraw Cash",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-primary",
+ onClick: () => actions.selectAtmView("deposit"),
+ },
+ "Deposit Cash",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ onClick: () => actions.selectAtmView("balance"),
+ },
+ "Check Balance",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ onClick: () => actions.closeBank(),
+ },
+ "Exit Terminal",
+ ),
+ );
+ }
+
+ function atmAmountMenu(kind) {
+ const label = kind === "deposit" ? "Deposit" : "Withdraw";
+ const amounts = [20, 50, 100, 500];
+
+ return h(
+ "div",
+ { className: "bank-atm-action-grid" },
+ amounts.map((amount) =>
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-primary",
+ onClick: () => actions.requestAtmAmount(kind, amount),
+ },
+ `${label} ${formatCurrency(amount)}`,
+ ),
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ onClick: () =>
+ actions.selectAtmView(
+ kind === "deposit"
+ ? "customDeposit"
+ : "customWithdraw",
+ ),
+ },
+ "Custom Amount",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ onClick: () => actions.selectAtmView("menu"),
+ },
+ "Back",
+ ),
+ );
+ }
+
+ function atmCustomAmount(kind) {
+ const label = kind === "deposit" ? "Deposit" : "Withdraw";
+
+ return h(
+ "div",
+ { className: "bank-atm-stack" },
+ h(
+ "div",
+ { className: "bank-pin-display" },
+ store.getCustomAmount()
+ ? formatCurrency(store.getCustomAmount())
+ : "$0",
+ ),
+ keypad(
+ actions.appendCustomAmountDigit,
+ actions.backspaceCustomAmount,
+ actions.clearCustomAmount,
+ () => actions.submitCustomAmount(kind),
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ onClick: () => actions.selectAtmView("menu"),
+ },
+ `Cancel ${label}`,
+ ),
+ );
+ }
+
+ BankApp.componentFns = BankApp.componentFns || {};
+ BankApp.componentFns.ATMView = function ATMView() {
+ store.getAccountVersion();
+ const atmViewName = store.getAtmView();
+ const enteredPin = String(store.getEnteredPin() || "");
+ let title = "Terminal Access";
+ let copy =
+ "Authenticate with the four-digit account PIN before using the terminal.";
+ let content = null;
+
+ switch (atmViewName) {
+ case "menu":
+ title = "ATM Menu";
+ copy =
+ "Select a banking action. The ATM can deposit, withdraw, and show the live account balance.";
+ content = atmMenuCard();
+ break;
+ case "withdraw":
+ title = "Withdraw Cash";
+ copy =
+ "Choose a preset amount or enter a custom amount for withdrawal.";
+ content = atmAmountMenu("withdraw");
+ break;
+ case "deposit":
+ title = "Deposit Cash";
+ copy =
+ "Move cash on hand back into the main bank balance from the terminal.";
+ content = atmAmountMenu("deposit");
+ break;
+ case "customWithdraw":
+ title = "Custom Withdraw";
+ copy = "Enter the exact withdrawal amount.";
+ content = atmCustomAmount("withdraw");
+ break;
+ case "customDeposit":
+ title = "Custom Deposit";
+ copy = "Enter the exact deposit amount.";
+ content = atmCustomAmount("deposit");
+ break;
+ case "balance":
+ title = "Available Balance";
+ copy = "Current bank balance available at this terminal.";
+ content = h(
+ "div",
+ { className: "bank-atm-stack" },
+ h(
+ "div",
+ { className: "bank-balance-display" },
+ formatCurrency(account.bank),
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-primary",
+ onClick: () => actions.selectAtmView("menu"),
+ },
+ "Return to Menu",
+ ),
+ );
+ break;
+ default:
+ content = h(
+ "div",
+ { className: "bank-atm-stack" },
+ h(
+ "div",
+ { className: "bank-pin-display" },
+ pinIndicators(enteredPin),
+ ),
+ keypad(
+ actions.appendPinDigit,
+ actions.backspacePin,
+ actions.clearPin,
+ actions.submitPin,
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ onClick: () => actions.closeBank(),
+ },
+ "Exit Terminal",
+ ),
+ );
+ break;
+ }
+
+ return h(
+ "div",
+ { className: "bank-atm-shell" },
+ h(
+ "section",
+ { className: "bank-atm-panel" },
+ h(
+ "div",
+ { className: "bank-panel-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "ATM"),
+ h("h1", { className: "bank-title" }, title),
+ ),
+ h("span", { className: "bank-pill" }, "Secure Terminal"),
+ ),
+ h("p", { className: "bank-panel-copy" }, copy),
+ content,
+ ),
+ );
+ };
+})();
+
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const { h } = BankApp.runtime;
+ const WindowTitleBar = window.SharedUI.componentFns.WindowTitleBar;
+ const store = BankApp.store;
+ const actions = BankApp.actions;
+
+ BankApp.componentFns = BankApp.componentFns || {};
+ BankApp.componentFns.NoticeLayer = function NoticeLayer() {
+ const notice = store.getNotice();
+
+ if (!notice.text) {
+ return null;
+ }
+
+ return h(
+ "div",
+ { className: "bank-notice-stack" },
+ h(
+ "div",
+ {
+ className:
+ notice.type === "error"
+ ? "bank-notice is-error"
+ : "bank-notice is-success",
+ },
+ notice.text,
+ ),
+ );
+ };
+
+ BankApp.components = BankApp.components || {};
+ BankApp.components.App = function App() {
+ const mode = store.getMode();
+
+ return h(
+ "div",
+ { className: mode === "atm" ? "bank-shell is-atm" : "bank-shell" },
+ mode === "atm"
+ ? null
+ : WindowTitleBar({
+ kicker: "FORGE Finance",
+ title: "Global Banking Network",
+ onClose: () => actions.closeBank(),
+ closeLabel: "Close banking interface",
+ }),
+ h("div", { id: "bank-notice-root" }),
+ mode === "atm"
+ ? h("div", { id: "bank-atm-root" })
+ : [
+ h(
+ "div",
+ {
+ className: "bank-scroll-shell",
+ "data-preserve-scroll-id": "bank-page-scroll",
+ },
+ [
+ h(
+ "div",
+ { className: "bank-layout" },
+ h("div", { id: "bank-sidebar-root" }),
+ h(
+ "main",
+ { className: "bank-main" },
+ h(
+ "div",
+ { className: "bank-page" },
+ h("div", {
+ id: "bank-page-header-root",
+ }),
+ h(
+ "p",
+ { className: "bank-page-copy" },
+ "Manage deposits, withdrawals, transfers, and earnings sweeps from the same shared financial console.",
+ ),
+ h("div", {
+ className: "bank-page-divider",
+ }),
+ h(
+ "div",
+ { className: "bank-page-body" },
+ h("div", {
+ id: "bank-summary-section-root",
+ }),
+ h("div", {
+ id: "bank-action-sections-root",
+ }),
+ h("div", {
+ id: "bank-support-section-root",
+ }),
+ h("div", {
+ id: "bank-history-section-root",
+ }),
+ ),
+ ),
+ ),
+ ),
+ h("div", { id: "bank-footer-root" }),
+ ],
+ ),
+ ],
+ );
+ };
+})();
+
+(function () {
+ const ForgeWebUI = window.ForgeWebUI;
+ const BankApp = window.BankApp;
+ const islandDefinitions = [
+ {
+ id: "bank-notice-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.NoticeLayer(),
+ },
+ {
+ id: "bank-sidebar-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.BankSidebar(),
+ },
+ {
+ id: "bank-page-header-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.BankPageHeader(),
+ },
+ {
+ id: "bank-summary-section-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.BankSummarySection(),
+ },
+ {
+ id: "bank-action-sections-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.BankActionSections(),
+ },
+ {
+ id: "bank-support-section-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.BankSupportSection(),
+ },
+ {
+ id: "bank-history-section-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.BankHistorySection(),
+ },
+ {
+ id: "bank-atm-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.ATMView(),
+ },
+ {
+ id: "bank-footer-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.BankFooter(),
+ },
+ ];
+
+ function createIslandManager() {
+ const mounts = new Map();
+
+ function sync() {
+ islandDefinitions.forEach((definition) => {
+ const container = document.getElementById(definition.id);
+ const current = mounts.get(definition.id);
+
+ if (!container) {
+ if (current) {
+ current.handle.dispose();
+ mounts.delete(definition.id);
+ }
+ return;
+ }
+
+ if (current && current.container === container) {
+ return;
+ }
+
+ if (current) {
+ current.handle.dispose();
+ }
+
+ const handle = ForgeWebUI.mount(container, definition.render, {
+ preserveScroll: definition.preserveScroll,
+ });
+ mounts.set(definition.id, {
+ container,
+ handle,
+ });
+ });
+ }
+
+ return {
+ sync,
+ };
+ }
+
+ const app = ForgeWebUI.createApp({
+ name: "bank",
+ root: "#app",
+ setup({ root }) {
+ const islandManager = createIslandManager();
+
+ ForgeWebUI.mount(root, () => BankApp.components.App(), {
+ preserveScroll: false,
+ });
+
+ if (BankApp.bridge) {
+ BankApp.bridge.notifyReady();
+ }
+
+ ForgeWebUI.effect(() => {
+ BankApp.store.getMode();
+
+ requestAnimationFrame(() => {
+ islandManager.sync();
+ });
+ });
+ },
+ });
+
+ app.start();
+})();
diff --git a/arma/client/addons/bank/ui/_site/bank.css b/arma/client/addons/bank/ui/_site/bank.css
deleted file mode 100644
index ff6e649..0000000
--- a/arma/client/addons/bank/ui/_site/bank.css
+++ /dev/null
@@ -1,444 +0,0 @@
-:root {
- --bg-app: #fdfcf8;
- --bg-surface: #ffffff;
- --bg-surface-hover: #f1f5f9;
- --primary: #475569;
- --primary-hover: #1e293b;
- --window-blue: #12325b;
- --window-blue-border: #214978;
- --window-blue-highlight: #d7e5f8;
- --text-main: #1f2937;
- --text-muted: #64748b;
- --text-inverse: #f8fafc;
- --border: #e2e8f0;
- --radius: 8px;
- --shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
- --footer-bg: #1e293b;
-}
-
-html,
-body {
- height: 100%;
-}
-
-body {
- font-family:
- "Inter",
- system-ui,
- -apple-system,
- sans-serif;
- margin: 0;
- padding: 0;
- background: var(--bg-app);
- color: var(--text-main);
- line-height: 1.6;
- overflow: hidden;
-}
-
-#app {
- height: 100vh;
- overflow: hidden;
-}
-
-.app-shell {
- height: 100vh;
- display: flex;
- flex-direction: column;
- overflow: hidden;
-}
-
-main {
- display: flex;
- flex-direction: column;
- flex: 1 1 auto;
- min-height: 0;
- overflow: auto;
- overscroll-behavior: contain;
-}
-
-.window-titlebar {
- display: flex;
- align-items: center;
- justify-content: space-between;
- gap: 1rem;
- padding: 0.8rem 1.25rem;
- background: linear-gradient(180deg, var(--window-blue) 0%, #0d2643 100%);
- border-bottom: 1px solid var(--window-blue-border);
- color: var(--text-inverse);
- box-shadow: 0 10px 24px rgb(18 50 91 / 0.24);
- position: sticky;
- top: 0;
- z-index: 30;
- flex-shrink: 0;
-}
-
-.window-titlebar-brand {
- display: flex;
- flex-direction: column;
- gap: 0.1rem;
-}
-
-.window-titlebar-kicker {
- font-size: 0.68rem;
- font-weight: 700;
- letter-spacing: 0.16em;
- text-transform: uppercase;
- color: rgb(215 229 248 / 0.78);
-}
-
-.window-titlebar-title {
- font-size: 0.95rem;
- font-weight: 700;
- letter-spacing: 0.04em;
- color: var(--text-inverse);
-}
-
-.window-titlebar-controls {
- display: flex;
- align-items: center;
- gap: 0.5rem;
-}
-
-.window-control-btn {
- min-width: 2.5rem;
- padding: 0.45rem 0.7rem;
- border-radius: 6px;
- border: 1px solid rgb(215 229 248 / 0.22);
- background: rgb(255 255 255 / 0.08);
- color: var(--window-blue-highlight);
- font-size: 0.82rem;
- font-weight: 700;
- letter-spacing: 0.08em;
- text-transform: uppercase;
- cursor: not-allowed;
- box-shadow: none;
- transform: none;
-}
-
-.window-control-btn:hover {
- background: rgb(255 255 255 / 0.08);
- box-shadow: none;
- transform: none;
-}
-
-.window-control-btn:disabled {
- opacity: 0.55;
-}
-
-.window-control-btn.is-close {
- cursor: pointer;
- opacity: 1;
- border-color: rgb(255 255 255 / 0.24);
-}
-
-.window-control-btn.is-close:hover {
- background: rgb(255 255 255 / 0.18);
-}
-
-.container {
- max-width: 1200px;
- width: 100%;
- margin: 0 auto;
- padding: 2rem;
- flex: 1;
- display: flex;
- flex-direction: column;
- box-sizing: border-box;
-}
-
-/* Navbar */
-.navbar {
- background: var(--bg-surface);
- border-bottom: 1px solid var(--border);
- box-shadow: var(--shadow);
-}
-
-.navbar-inner {
- display: flex;
- justify-content: space-between;
- align-items: center;
- max-width: 1200px;
- width: 100%;
- margin: 0 auto;
- padding: 1rem 2rem;
- box-sizing: border-box;
-}
-
-.navbar-brand {
- display: flex;
- align-items: center;
- gap: 0.75rem;
-}
-
-.navbar-title {
- font-size: 1.25rem;
- font-weight: 700;
- color: var(--primary-hover);
- letter-spacing: -0.025em;
-}
-
-.navbar-profile {
- display: flex;
- align-items: center;
- gap: 1.5rem;
-}
-
-.profile-info {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- gap: 0.125rem;
-}
-
-.profile-label {
- font-size: 0.7rem;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--text-muted);
- font-weight: 500;
-}
-
-.profile-id {
- font-size: 0.9rem;
- font-weight: 600;
- color: var(--text-main);
- font-family: "Consolas", "Monaco", monospace;
-}
-
-.content {
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 2rem;
- margin-bottom: 2rem;
-}
-
-/* Cards */
-.card {
- background: var(--bg-surface);
- border: 1px solid var(--border);
- border-radius: var(--radius);
- padding: 2rem;
- margin-bottom: 2rem;
- box-shadow: var(--shadow);
- text-align: center;
-
- h2 {
- margin-top: 0;
- font-size: 1.8rem;
- color: var(--primary-hover);
- }
-}
-
-/* Buttons */
-button {
- background: var(--primary);
- color: white;
- border: none;
- padding: 0.75rem 1.5rem;
- border-radius: var(--radius);
- cursor: pointer;
- font-size: 1rem;
- font-weight: 500;
- font-family: inherit;
- transition: all 0.2s ease;
-
- &:hover {
- background: var(--primary-hover);
- transform: translateY(-1px);
- box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
- }
-
- & + & {
- margin-left: 1rem;
- }
-}
-
-/* Forms */
-form {
- display: flex;
- flex-direction: column;
- gap: 1rem;
- text-align: left;
-
- label {
- display: block;
- margin-bottom: 0.5rem;
- color: var(--text-muted);
- font-weight: 500;
- font-size: 0.9rem;
- }
-
- input,
- select {
- width: 100%;
- padding: 0.75rem;
- border-radius: var(--radius);
- border: 1px solid var(--border);
- background: var(--bg-app);
- color: var(--text-main);
- font-family: inherit;
- font-size: 1rem;
- box-sizing: border-box;
- transition: border-color 0.2s;
-
- &:focus {
- outline: none;
- border-color: var(--primary);
- box-shadow: 0 0 0 2px rgba(59, 130, 246, 0.1);
- }
- }
-
- .form-actions {
- margin-top: 1rem;
- display: flex;
- flex-direction: column;
- gap: 1rem;
- align-items: center;
- }
-}
-
-/* Deposit/Withdraw Form */
-.balance-info {
- display: flex;
- justify-content: space-around;
- margin-bottom: 1.5rem;
- padding: 1rem;
- background: var(--bg-surface-hover);
- border-radius: var(--radius);
-}
-
-.balance-info-item {
- display: flex;
- flex-direction: column;
- align-items: center;
- gap: 0.25rem;
-}
-
-.balance-info-label {
- font-size: 0.75rem;
- text-transform: uppercase;
- letter-spacing: 0.05em;
- color: var(--text-muted);
- font-weight: 500;
-}
-
-.balance-info-value {
- font-size: 1.25rem;
- font-weight: 700;
- color: var(--primary-hover);
-
- &.cash {
- color: #fbbf24;
- }
-}
-
-.deposit-withdraw-form {
- display: flex;
- flex-direction: column;
- gap: 1rem;
-
- input {
- text-align: center;
- font-size: 1.25rem;
- padding: 1rem;
- }
-}
-
-.deposit-withdraw-buttons {
- display: flex;
- gap: 0.75rem;
-
- button {
- flex: 1;
-
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-
- &:hover {
- background: var(--primary);
- transform: none;
- box-shadow: none;
- }
- }
- }
-}
-
-.deposit-earnings-button {
- display: flex;
- gap: 0.75rem;
- width: 50%;
- margin: 0 auto;
-
- button {
- flex: 1;
-
- &:disabled {
- opacity: 0.5;
- cursor: not-allowed;
-
- &:hover {
- background: var(--primary);
- transform: none;
- box-shadow: none;
- }
- }
- }
-}
-
-/* Footer */
-.footer {
- margin-top: auto;
- background: var(--footer-bg);
- color: var(--text-inverse);
- display: block;
-
- .wrapper {
- max-width: 1200px;
- width: 100%;
- margin: 0 auto;
- padding: 3rem 2rem;
- box-sizing: border-box;
- display: grid;
- grid-template-columns: 1fr 1fr;
- gap: 4rem;
- }
-
- h3 {
- color: var(--text-inverse);
- font-size: 0.85rem;
- text-transform: uppercase;
- letter-spacing: 0.1em;
- font-weight: 700;
- margin-bottom: 1.5rem;
- border-bottom: 1px solid #475569;
- padding-bottom: 0.5rem;
- margin-right: 1rem;
- }
-
- ul {
- li {
- color: #cbd5e1;
- font-size: 0.95rem;
- margin-bottom: 0.75rem;
- cursor: pointer;
- transition: color 0.2s;
-
- &:hover {
- color: white;
- }
- }
- }
-}
-
-@media (max-width: 720px) {
- .window-titlebar {
- flex-direction: column;
- align-items: flex-start;
- }
-
- .window-titlebar-controls {
- width: 100%;
- justify-content: flex-end;
- }
-}
diff --git a/arma/client/addons/bank/ui/_site/bank.html b/arma/client/addons/bank/ui/_site/bank.html
deleted file mode 100644
index a2e2b94..0000000
--- a/arma/client/addons/bank/ui/_site/bank.html
+++ /dev/null
@@ -1,45 +0,0 @@
-
-
-
-
-
- FDIC - Global Financial Network
-
-
-
-
-
-
-
-
-
-
-
diff --git a/arma/client/addons/bank/ui/_site/bank.js b/arma/client/addons/bank/ui/_site/bank.js
deleted file mode 100644
index 01f0acc..0000000
--- a/arma/client/addons/bank/ui/_site/bank.js
+++ /dev/null
@@ -1,575 +0,0 @@
-/**
- * Bank App - Vanilla JS Implementation matching WIP UI
- */
-
-//=============================================================================
-// #region LIBRARY - DOM Helper
-//=============================================================================
-
-function h(tag, props = {}, ...children) {
- const el = document.createElement(tag);
- if (props) {
- Object.entries(props).forEach(([key, value]) => {
- if (key.startsWith("on") && typeof value === "function") {
- el.addEventListener(key.substring(2).toLowerCase(), value);
- } else if (key === "className") {
- el.className = value;
- } else if (key === "style" && typeof value === "object") {
- Object.assign(el.style, value);
- } else if (
- key === "disabled" ||
- key === "checked" ||
- key === "selected" ||
- key === "readonly"
- ) {
- if (value) el[key] = true;
- } else {
- el.setAttribute(key, value);
- }
- });
- }
- children.forEach((child) => {
- if (typeof child === "string" || typeof child === "number") {
- el.appendChild(document.createTextNode(child));
- } else if (child instanceof Node) {
- el.appendChild(child);
- } else if (Array.isArray(child)) {
- child.forEach((c) => {
- if (c instanceof Node) el.appendChild(c);
- });
- }
- });
- return el;
-}
-
-let _rootContainer = null;
-let _rootComponent = null;
-
-function render(component, container) {
- _rootContainer = container;
- _rootComponent = component;
- _render();
-}
-
-function _render() {
- if (_rootContainer && _rootComponent) {
- _rootContainer.innerHTML = "";
- _rootContainer.appendChild(_rootComponent());
- }
-}
-
-//=============================================================================
-// #region UI COMPONENTS
-//=============================================================================
-
-function Navbar() {
- const state = store.getState();
- const uid = state.uid || "Unknown";
-
- return h(
- "nav",
- { className: "navbar" },
- h(
- "div",
- { className: "navbar-inner" },
- h(
- "div",
- { className: "navbar-brand" },
- h(
- "span",
- { className: "navbar-title" },
- "FDIC - Global Financial Network",
- ),
- ),
- h(
- "div",
- { className: "navbar-profile" },
- h(
- "div",
- { className: "profile-info" },
- h("span", { className: "profile-label" }, "Account"),
- h("span", { className: "profile-id" }, uid),
- ),
- ),
- ),
- );
-}
-
-function WindowTitleBar() {
- return h(
- "div",
- { className: "window-titlebar" },
- h(
- "div",
- { className: "window-titlebar-brand" },
- h(
- "span",
- { className: "window-titlebar-kicker" },
- "FDIC Workspace",
- ),
- h(
- "span",
- { className: "window-titlebar-title" },
- "Global Financial Network",
- ),
- ),
- h(
- "div",
- { className: "window-titlebar-controls" },
- h(
- "button",
- {
- type: "button",
- className: "window-control-btn",
- disabled: true,
- title: "Minimize unavailable",
- "aria-label": "Minimize unavailable",
- },
- "-",
- ),
- h(
- "button",
- {
- type: "button",
- className: "window-control-btn",
- disabled: true,
- title: "Maximize unavailable",
- "aria-label": "Maximize unavailable",
- },
- "[ ]",
- ),
- h(
- "button",
- {
- type: "button",
- className: "window-control-btn is-close",
- onClick: () => sendEvent("bank::close", {}),
- title: "Close",
- "aria-label": "Close banking interface",
- },
- "X",
- ),
- ),
- );
-}
-
-function TransactionHistory() {
- const state = store.getState();
- const transactions = state.transactions || [];
-
- return h(
- "div",
- { className: "card" },
- h(
- "h3",
- {
- style: {
- textAlign: "left",
- borderBottom: "1px solid var(--border)",
- paddingBottom: "1rem",
- marginBottom: "1rem",
- },
- },
- "Recent Transactions",
- ),
- transactions.length === 0
- ? h(
- "p",
- { style: { color: "var(--text-muted)" } },
- "No transactions yet",
- )
- : h(
- "ul",
- { style: { listStyle: "none", padding: 0, margin: 0 } },
- transactions.slice(0, 10).map((tx) => {
- const isCredit = tx.type === "Deposit";
- return h(
- "li",
- {
- style: {
- display: "flex",
- justifyContent: "space-between",
- padding: "0.75rem 0",
- borderBottom:
- "1px solid var(--bg-surface-hover)",
- },
- },
- h(
- "div",
- { style: { textAlign: "left" } },
- h(
- "div",
- { style: { fontWeight: "500" } },
- tx.type,
- ),
- h(
- "div",
- {
- style: {
- fontSize: "0.85rem",
- color: "var(--text-muted)",
- },
- },
- tx.date,
- ),
- ),
- h(
- "div",
- {
- style: {
- fontWeight: "700",
- color: isCredit ? "#10b981" : "#ef4444",
- },
- },
- (isCredit ? "+" : "-") +
- "$" +
- Math.abs(tx.amount).toLocaleString(),
- ),
- );
- }),
- ),
- );
-}
-
-function DepositWithdrawForm() {
- const state = store.getState();
- const bankBalance = state.accounts.bank;
- const cashBalance = state.accounts.cash;
-
- const getAmount = () => {
- const input = document.getElementById("deposit-withdraw-amount");
- return parseFloat(input?.value) || 0;
- };
-
- const clearInput = () => {
- const input = document.getElementById("deposit-withdraw-amount");
- if (input) input.value = "";
- };
-
- const handleDeposit = () => {
- const amount = getAmount();
- if (!amount || amount <= 0) {
- console.log("Please enter a valid amount");
- return;
- }
- if (amount > cashBalance) {
- console.log("Insufficient cash");
- return;
- }
- sendEvent("bank::deposit", { amount });
- store.dispatch(deposit(amount));
- clearInput();
- };
-
- const handleWithdraw = () => {
- const amount = getAmount();
- if (!amount || amount <= 0) {
- console.log("Please enter a valid amount");
- return;
- }
- if (amount > bankBalance) {
- console.log("Insufficient funds");
- return;
- }
- sendEvent("bank::withdraw", { amount });
- store.dispatch(withdraw(amount));
- clearInput();
- };
-
- return h(
- "div",
- { className: "card" },
- h("h2", null, "Deposit / Withdraw"),
- h(
- "div",
- { className: "balance-info" },
- h(
- "div",
- { className: "balance-info-item" },
- h("span", { className: "balance-info-label" }, "Cash"),
- h(
- "span",
- { className: "balance-info-value cash" },
- "$" + cashBalance.toLocaleString(),
- ),
- ),
- h(
- "div",
- { className: "balance-info-item" },
- h("span", { className: "balance-info-label" }, "Bank"),
- h(
- "span",
- { className: "balance-info-value" },
- "$" + bankBalance.toLocaleString(),
- ),
- ),
- ),
- h(
- "div",
- { className: "deposit-withdraw-form" },
- h("input", {
- id: "deposit-withdraw-amount",
- type: "number",
- placeholder: "Enter amount...",
- min: "1",
- }),
- h(
- "div",
- { className: "deposit-withdraw-buttons" },
- h(
- "button",
- { onClick: handleDeposit, disabled: cashBalance <= 0 },
- "Deposit",
- ),
- h(
- "button",
- { onClick: handleWithdraw, disabled: bankBalance <= 0 },
- "Withdraw",
- ),
- ),
- ),
- );
-}
-
-function TransferForm() {
- const state = store.getState();
- const players = state.accounts.players || {};
- const currentUid = state.uid;
-
- const handleSubmit = (e) => {
- e.preventDefault();
- const formData = new FormData(e.target);
- const amount = parseFloat(formData.get("amount"));
- const playerId = formData.get("playerId");
-
- if (!amount || amount <= 0) {
- console.log("Please enter a valid amount");
- return;
- }
-
- const currentState = store.getState();
-
- if (!playerId) {
- console.log("Please select a recipient");
- return;
- }
-
- if (amount > currentState.accounts.bank) {
- console.log("Insufficient funds");
- return;
- }
-
- sendEvent("bank::transfer", { from: "bank", amount, target: playerId });
- store.dispatch(transfer("bank", amount, "player"));
- e.target.reset();
- };
-
- // Build player options
- const playerOptions = [
- h(
- "option",
- { value: "", disabled: true, selected: true },
- "Select player...",
- ),
- ];
- Object.keys(players).forEach((uid) => {
- if (uid !== currentUid && players[uid]?.name) {
- playerOptions.push(h("option", { value: uid }, players[uid].name));
- }
- });
-
- return h(
- "div",
- { className: "card" },
- h("h2", null, "Wire Transfer"),
- h(
- "form",
- { onSubmit: handleSubmit },
- h(
- "div",
- null,
- h("label", null, "Recipient"),
- h("select", { name: "playerId" }, playerOptions),
- ),
- h(
- "div",
- null,
- h("label", null, "Amount"),
- h("input", {
- name: "amount",
- type: "number",
- placeholder: "0.00",
- }),
- ),
- h("button", { type: "submit" }, "Send Funds"),
- ),
- );
-}
-
-function BankDashboard() {
- const state = store.getState();
- const bankBalance = state.accounts.bank;
- const earnings = state.accounts.earnings;
-
- return h(
- "div",
- { className: "content" },
- h(
- "div",
- { className: "card", style: { gridColumn: "span 2" } },
- h(
- "h2",
- {
- style: {
- fontSize: "1.2rem",
- color: "var(--text-muted)",
- textTransform: "uppercase",
- letterSpacing: "0.05em",
- },
- },
- "Account Balance",
- ),
- h(
- "div",
- {
- style: {
- fontSize: "2.8rem",
- fontWeight: "800",
- color: "var(--primary-hover)",
- margin: "1rem 0",
- },
- },
- "$" + bankBalance.toLocaleString(),
- ),
- h(
- "div",
- {
- style: {
- textAlign: "center",
- color: "var(--text-muted)",
- fontSize: "1.1rem",
- marginBottom: "1rem",
- },
- },
- "Pending: ",
- h(
- "span",
- { style: { color: "#fbbf24", fontWeight: "bold" } },
- "$" + earnings.toLocaleString(),
- ),
- ),
- h(
- "div",
- { className: "deposit-earnings-button" },
- h(
- "button",
- {
- onClick: () => {
- sendEvent("bank::depositEarnings", {
- amount: earnings,
- });
- store.dispatch(depositEarnings(earnings));
- },
- disabled: earnings <= 0,
- style: { width: "25%" },
- },
- "Deposit Earnings",
- ),
- ),
- ),
- DepositWithdrawForm(),
- TransferForm(),
- h("div", { style: { gridColumn: "span 2" } }, TransactionHistory()),
- );
-}
-
-function Footer() {
- return h(
- "div",
- { className: "footer" },
- h(
- "div",
- { className: "wrapper" },
- h(
- "div",
- null,
- h("h3", null, "Secure Banking"),
- h(
- "ul",
- { style: { listStyleType: "none", padding: 0 } },
- h("li", null, "FDIC Insured"),
- h("li", null, "Fraud Protection"),
- h("li", null, "24/7 Support"),
- h("li", null, "API Access"),
- ),
- ),
- h(
- "div",
- null,
- h("h3", null, "Notices"),
- h(
- "ul",
- { style: { listStyleType: "none", padding: 0 } },
- h("li", null, "Terms of Service"),
- h("li", null, "Privacy Policy"),
- h("li", null, "Interest Rates"),
- h("li", null, "Report Fraud"),
- ),
- ),
- ),
- );
-}
-
-function App() {
- return h(
- "div",
- { className: "app-shell" },
- WindowTitleBar(),
- h(
- "main",
- null,
- Navbar(),
- h("div", { className: "container" }, BankDashboard()),
- Footer(),
- ),
- );
-}
-
-//=============================================================================
-// #region ARMA 3 INTEGRATION
-//=============================================================================
-
-function sendEvent(event, data) {
- if (typeof A3API !== "undefined") {
- A3API.SendAlert(JSON.stringify({ event, data }));
- } else {
- console.log("Event:", event, "Data:", data);
- }
-}
-
-//=============================================================================
-// #region INITIALIZATION
-//=============================================================================
-
-let initialized = false;
-
-function initBank() {
- if (initialized) return;
-
- const root = document.getElementById("app");
- if (root) {
- if (typeof store !== "undefined") {
- store.subscribe(() => _render());
- }
-
- render(App, root);
- initialized = true;
- console.log("[Bank] Interface initialized");
- }
-}
-
-if (document.readyState === "loading") {
- document.addEventListener("DOMContentLoaded", initBank);
-} else {
- initBank();
-}
diff --git a/arma/client/addons/bank/ui/_site/index.html b/arma/client/addons/bank/ui/_site/index.html
new file mode 100644
index 0000000..002ba11
--- /dev/null
+++ b/arma/client/addons/bank/ui/_site/index.html
@@ -0,0 +1,64 @@
+
+
+
+
+
+
+ FORGE Banking Console
+
+
+
+
+
+
+
diff --git a/arma/client/addons/bank/ui/_site/public/fdic.png b/arma/client/addons/bank/ui/_site/public/fdic.png
deleted file mode 100644
index 579e749..0000000
Binary files a/arma/client/addons/bank/ui/_site/public/fdic.png and /dev/null differ
diff --git a/arma/client/addons/bank/ui/_site/public/fdic_co.paa b/arma/client/addons/bank/ui/_site/public/fdic_co.paa
deleted file mode 100644
index 45fe964..0000000
Binary files a/arma/client/addons/bank/ui/_site/public/fdic_co.paa and /dev/null differ
diff --git a/arma/client/addons/bank/ui/_site/public/fms.png b/arma/client/addons/bank/ui/_site/public/fms.png
deleted file mode 100644
index 553b09a..0000000
Binary files a/arma/client/addons/bank/ui/_site/public/fms.png and /dev/null differ
diff --git a/arma/client/addons/bank/ui/_site/public/fms_co.paa b/arma/client/addons/bank/ui/_site/public/fms_co.paa
deleted file mode 100644
index 9d32a24..0000000
Binary files a/arma/client/addons/bank/ui/_site/public/fms_co.paa and /dev/null differ
diff --git a/arma/client/addons/bank/ui/_site/public/gms.png b/arma/client/addons/bank/ui/_site/public/gms.png
deleted file mode 100644
index a4717b2..0000000
Binary files a/arma/client/addons/bank/ui/_site/public/gms.png and /dev/null differ
diff --git a/arma/client/addons/bank/ui/_site/public/gms_co.paa b/arma/client/addons/bank/ui/_site/public/gms_co.paa
deleted file mode 100644
index ddc6d35..0000000
Binary files a/arma/client/addons/bank/ui/_site/public/gms_co.paa and /dev/null differ
diff --git a/arma/client/addons/bank/ui/_site/store.js b/arma/client/addons/bank/ui/_site/store.js
deleted file mode 100644
index 45c195f..0000000
--- a/arma/client/addons/bank/ui/_site/store.js
+++ /dev/null
@@ -1,305 +0,0 @@
-/**
- * Banking Application Store
- * Redux-like state management for bank and ATM interfaces
- */
-
-// ============================================================================
-// REDUX CORE IMPLEMENTATION
-// ============================================================================
-
-/**
- * Creates a Redux-like store.
- * @param {Function} reducer - A function that returns the next state tree
- * @returns {Object} The store object with methods: getState, dispatch, subscribe
- */
-function createStore(reducer) {
- let state;
- let listeners = [];
-
- const getState = () => state;
-
- const dispatch = (action) => {
- state = reducer(state, action);
- listeners.forEach((listener) => listener());
- };
-
- const subscribe = (listener) => {
- listeners.push(listener);
- return () => {
- listeners = listeners.filter((l) => l !== listener);
- };
- };
-
- // Initialize state
- dispatch({});
-
- return { getState, dispatch, subscribe };
-}
-
-// ============================================================================
-// STATE
-// ============================================================================
-
-const initialState = {
- uid: "",
- accounts: {
- bank: 0,
- cash: 0,
- earnings: 0,
- org: 0,
- },
- pin: "1234",
- transactions: [],
-};
-
-// ============================================================================
-// ACTION TYPES
-// ============================================================================
-
-const DEPOSIT = "DEPOSIT";
-const DEPOSIT_EARNINGS = "DEPOSIT_EARNINGS";
-const WITHDRAW = "WITHDRAW";
-const TRANSFER = "TRANSFER";
-const UPDATE_ACCOUNTS = "UPDATE_ACCOUNTS";
-const UPDATE_PIN = "UPDATE_PIN";
-
-// ============================================================================
-// ACTION CREATORS
-// ============================================================================
-
-const deposit = (amount) => ({
- type: DEPOSIT,
- payload: amount,
-});
-
-const depositEarnings = (amount) => ({
- type: DEPOSIT_EARNINGS,
- payload: amount,
-});
-
-const withdraw = (amount) => ({
- type: WITHDRAW,
- payload: amount,
-});
-
-const transfer = (from, amount, target) => ({
- type: TRANSFER,
- from: from,
- payload: amount,
- target: target,
-});
-
-const updateAccounts = (accounts) => ({
- type: UPDATE_ACCOUNTS,
- payload: accounts,
-});
-
-const updatePin = (pin) => ({
- type: UPDATE_PIN,
- payload: pin,
-});
-
-// ============================================================================
-// REDUCER
-// ============================================================================
-
-function appReducer(state = initialState, action) {
- switch (action.type) {
- case DEPOSIT:
- if (state.accounts.cash < action.payload) {
- console.warn("Insufficient cash!");
- return state;
- }
- return {
- ...state,
- accounts: {
- ...state.accounts,
- bank: state.accounts.bank + action.payload,
- cash: state.accounts.cash - action.payload,
- },
- transactions: [
- ...state.transactions,
- {
- type: "Deposit",
- amount: action.payload,
- date: new Date().toLocaleString(),
- },
- ],
- };
-
- case DEPOSIT_EARNINGS:
- if (state.accounts.earnings < action.payload) {
- console.warn("Insufficient earnings!");
- return state;
- }
- return {
- ...state,
- accounts: {
- ...state.accounts,
- bank: state.accounts.bank + action.payload,
- earnings: state.accounts.earnings - action.payload,
- },
- transactions: [
- ...state.transactions,
- {
- type: "Deposit Earnings",
- amount: action.payload,
- date: new Date().toLocaleString(),
- },
- ],
- };
-
- case WITHDRAW:
- if (state.accounts.bank < action.payload) {
- console.warn("Insufficient funds!");
- return state;
- }
- return {
- ...state,
- accounts: {
- ...state.accounts,
- bank: state.accounts.bank - action.payload,
- cash: state.accounts.cash + action.payload,
- },
- transactions: [
- ...state.transactions,
- {
- type: "Withdraw",
- amount: action.payload,
- date: new Date().toLocaleString(),
- },
- ],
- };
-
- case TRANSFER:
- const fromAccount = action.from;
- if (state.accounts[fromAccount] < action.payload) {
- console.warn("Insufficient funds!");
- return state;
- }
-
- const newAccounts = { ...state.accounts };
- newAccounts[fromAccount] -= action.payload;
-
- return {
- ...state,
- accounts: newAccounts,
- transactions: [
- ...state.transactions,
- {
- type: "Transfer",
- amount: action.payload,
- from: fromAccount,
- target: action.target,
- date: new Date().toLocaleString(),
- },
- ],
- };
-
- case UPDATE_ACCOUNTS:
- return {
- ...state,
- accounts: {
- ...state.accounts,
- ...action.payload,
- },
- };
-
- case UPDATE_PIN:
- return {
- ...state,
- pin: String(action.payload),
- };
-
- case "SET_UID":
- return {
- ...state,
- uid: action.payload,
- };
-
- default:
- return state;
- }
-}
-
-// ============================================================================
-// STORE INSTANCE
-// ============================================================================
-
-const store = createStore(appReducer);
-
-// ============================================================================
-// ARMA 3 INTEGRATION
-// ============================================================================
-
-/**
- * Sends an event to Arma 3
- * @param {string} event - Event name
- * @param {Object} data - Event data
- */
-function sendEvent(event, data) {
- if (typeof A3API !== "undefined") {
- A3API.SendAlert(
- JSON.stringify({
- event: event,
- data: data,
- }),
- );
- } else {
- console.log("Event:", event, "Data:", data);
- }
-}
-
-/**
- * Syncs account data from Arma 3 into the store
- * @param {Object} data - Account data from Arma 3
- */
-function syncDataFromArma(data) {
- if (data && typeof data === "object") {
- const accounts = {};
-
- if (data.bank !== undefined) accounts.bank = data.bank;
- if (data.cash !== undefined) accounts.cash = data.cash;
- if (data.earnings !== undefined) accounts.earnings = data.earnings;
- if (data.org !== undefined) accounts.org = data.org;
- if (data.players !== undefined) accounts.players = data.players;
-
- if (Object.keys(accounts).length > 0) {
- store.dispatch(updateAccounts(accounts));
- }
-
- // Update UID if provided
- if (data.uid !== undefined && data.uid !== store.getState().uid) {
- store.dispatch({ type: "SET_UID", payload: data.uid });
- }
-
- // Update pin if provided
- if (data.pin !== undefined) {
- store.dispatch(updatePin(data.pin));
- }
-
- console.log(
- "[Store] Synced data from Arma:",
- store.getState().accounts,
- );
- } else {
- console.warn("[Store] Invalid data received:", data);
- }
-}
-
-// ============================================================================
-// INITIALIZATION
-// ============================================================================
-
-// Request initial data from Arma on load
-if (typeof A3API !== "undefined") {
- // Delay request slightly to ensure everything is loaded
- setTimeout(() => {
- sendEvent("bank::sync", {});
- }, 100);
-}
-
-// Expose sync function globally for Arma to call
-if (typeof window !== "undefined") {
- window.syncDataFromArma = syncDataFromArma;
-}
diff --git a/arma/client/addons/bank/ui/src/bootstrap.js b/arma/client/addons/bank/ui/src/bootstrap.js
new file mode 100644
index 0000000..6496e13
--- /dev/null
+++ b/arma/client/addons/bank/ui/src/bootstrap.js
@@ -0,0 +1,116 @@
+(function () {
+ const ForgeWebUI = window.ForgeWebUI;
+ const BankApp = window.BankApp;
+ const islandDefinitions = [
+ {
+ id: "bank-notice-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.NoticeLayer(),
+ },
+ {
+ id: "bank-sidebar-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.BankSidebar(),
+ },
+ {
+ id: "bank-page-header-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.BankPageHeader(),
+ },
+ {
+ id: "bank-summary-section-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.BankSummarySection(),
+ },
+ {
+ id: "bank-action-sections-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.BankActionSections(),
+ },
+ {
+ id: "bank-support-section-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.BankSupportSection(),
+ },
+ {
+ id: "bank-history-section-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.BankHistorySection(),
+ },
+ {
+ id: "bank-atm-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.ATMView(),
+ },
+ {
+ id: "bank-footer-root",
+ preserveScroll: false,
+ render: () => BankApp.componentFns.BankFooter(),
+ },
+ ];
+
+ function createIslandManager() {
+ const mounts = new Map();
+
+ function sync() {
+ islandDefinitions.forEach((definition) => {
+ const container = document.getElementById(definition.id);
+ const current = mounts.get(definition.id);
+
+ if (!container) {
+ if (current) {
+ current.handle.dispose();
+ mounts.delete(definition.id);
+ }
+ return;
+ }
+
+ if (current && current.container === container) {
+ return;
+ }
+
+ if (current) {
+ current.handle.dispose();
+ }
+
+ const handle = ForgeWebUI.mount(container, definition.render, {
+ preserveScroll: definition.preserveScroll,
+ });
+ mounts.set(definition.id, {
+ container,
+ handle,
+ });
+ });
+ }
+
+ return {
+ sync,
+ };
+ }
+
+ const app = ForgeWebUI.createApp({
+ name: "bank",
+ root: "#app",
+ setup({ root }) {
+ const islandManager = createIslandManager();
+
+ ForgeWebUI.mount(root, () => BankApp.components.App(), {
+ preserveScroll: false,
+ });
+
+ if (BankApp.bridge) {
+ BankApp.bridge.notifyReady();
+ }
+
+ ForgeWebUI.effect(() => {
+ BankApp.store.getMode();
+
+ requestAnimationFrame(() => {
+ islandManager.sync();
+ });
+ });
+ },
+ });
+
+ app.start();
+})();
diff --git a/arma/client/addons/bank/ui/src/bridge.js b/arma/client/addons/bank/ui/src/bridge.js
new file mode 100644
index 0000000..1ceed4e
--- /dev/null
+++ b/arma/client/addons/bank/ui/src/bridge.js
@@ -0,0 +1,51 @@
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const store = BankApp.store;
+ const bridge = window.ForgeWebUI.createBridge({
+ closeEvent: "bank::close",
+ globalName: "ForgeBridge",
+ readyEvent: "bank::ready",
+ });
+
+ function hydrate(payloadData) {
+ BankApp.data.applyHydratePayload(payloadData);
+ store.hydrateFromPayload(payloadData);
+ }
+
+ bridge.on("bank::hydrate", hydrate);
+ bridge.on("bank::sync", hydrate);
+ bridge.on("bank::notice", (payloadData) => {
+ if (BankApp.actions) {
+ BankApp.actions.showNotice(
+ payloadData.type || "error",
+ payloadData.message || "Bank notice received.",
+ );
+ }
+ });
+
+ BankApp.bridge = {
+ notifyReady() {
+ return bridge.ready({ loaded: true });
+ },
+ receive: bridge.receive,
+ requestClose() {
+ return bridge.close({});
+ },
+ requestDeposit(payload) {
+ return bridge.send("bank::deposit::request", payload);
+ },
+ requestDepositEarnings(payload) {
+ return bridge.send("bank::depositEarnings::request", payload);
+ },
+ requestRefresh() {
+ return bridge.send("bank::refresh", {});
+ },
+ requestTransfer(payload) {
+ return bridge.send("bank::transfer::request", payload);
+ },
+ requestWithdraw(payload) {
+ return bridge.send("bank::withdraw::request", payload);
+ },
+ sendEvent: bridge.send,
+ };
+})();
diff --git a/arma/client/addons/bank/ui/src/components/AppShell.js b/arma/client/addons/bank/ui/src/components/AppShell.js
new file mode 100644
index 0000000..4cb7359
--- /dev/null
+++ b/arma/client/addons/bank/ui/src/components/AppShell.js
@@ -0,0 +1,104 @@
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const { h } = BankApp.runtime;
+ const WindowTitleBar = window.SharedUI.componentFns.WindowTitleBar;
+ const store = BankApp.store;
+ const actions = BankApp.actions;
+
+ BankApp.componentFns = BankApp.componentFns || {};
+ BankApp.componentFns.NoticeLayer = function NoticeLayer() {
+ const notice = store.getNotice();
+
+ if (!notice.text) {
+ return null;
+ }
+
+ return h(
+ "div",
+ { className: "bank-notice-stack" },
+ h(
+ "div",
+ {
+ className:
+ notice.type === "error"
+ ? "bank-notice is-error"
+ : "bank-notice is-success",
+ },
+ notice.text,
+ ),
+ );
+ };
+
+ BankApp.components = BankApp.components || {};
+ BankApp.components.App = function App() {
+ const mode = store.getMode();
+
+ return h(
+ "div",
+ { className: mode === "atm" ? "bank-shell is-atm" : "bank-shell" },
+ mode === "atm"
+ ? null
+ : WindowTitleBar({
+ kicker: "FORGE Finance",
+ title: "Global Banking Network",
+ onClose: () => actions.closeBank(),
+ closeLabel: "Close banking interface",
+ }),
+ h("div", { id: "bank-notice-root" }),
+ mode === "atm"
+ ? h("div", { id: "bank-atm-root" })
+ : [
+ h(
+ "div",
+ {
+ className: "bank-scroll-shell",
+ "data-preserve-scroll-id": "bank-page-scroll",
+ },
+ [
+ h(
+ "div",
+ { className: "bank-layout" },
+ h("div", { id: "bank-sidebar-root" }),
+ h(
+ "main",
+ { className: "bank-main" },
+ h(
+ "div",
+ { className: "bank-page" },
+ h("div", {
+ id: "bank-page-header-root",
+ }),
+ h(
+ "p",
+ { className: "bank-page-copy" },
+ "Manage deposits, withdrawals, transfers, and earnings sweeps from the same shared financial console.",
+ ),
+ h("div", {
+ className: "bank-page-divider",
+ }),
+ h(
+ "div",
+ { className: "bank-page-body" },
+ h("div", {
+ id: "bank-summary-section-root",
+ }),
+ h("div", {
+ id: "bank-action-sections-root",
+ }),
+ h("div", {
+ id: "bank-support-section-root",
+ }),
+ h("div", {
+ id: "bank-history-section-root",
+ }),
+ ),
+ ),
+ ),
+ ),
+ h("div", { id: "bank-footer-root" }),
+ ],
+ ),
+ ],
+ );
+ };
+})();
diff --git a/arma/client/addons/bank/ui/src/components/BankSidebar.js b/arma/client/addons/bank/ui/src/components/BankSidebar.js
new file mode 100644
index 0000000..6199abf
--- /dev/null
+++ b/arma/client/addons/bank/ui/src/components/BankSidebar.js
@@ -0,0 +1,91 @@
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const { h } = BankApp.runtime;
+ const store = BankApp.store;
+ const actions = BankApp.actions;
+ const { account, session } = BankApp.data;
+ const { formatCurrency, statCard } = BankApp.componentFns;
+
+ BankApp.componentFns = BankApp.componentFns || {};
+ BankApp.componentFns.BankSidebar = function BankSidebar() {
+ store.getAccountVersion();
+ store.getSessionVersion();
+
+ return h(
+ "aside",
+ { className: "bank-sidebar" },
+ h(
+ "section",
+ { className: "bank-module" },
+ h(
+ "div",
+ { className: "bank-module-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "Account"),
+ h(
+ "h2",
+ { className: "bank-section-title" },
+ "Balances",
+ ),
+ ),
+ h("span", { className: "bank-pill" }, "Live"),
+ ),
+ h(
+ "div",
+ { className: "bank-summary-grid" },
+ statCard("Bank", formatCurrency(account.bank), "accent"),
+ statCard("Cash", formatCurrency(account.cash)),
+ statCard(
+ "Earnings",
+ formatCurrency(account.earnings),
+ account.earnings > 0 ? "warning" : "",
+ ),
+ statCard(
+ "Org Funds",
+ formatCurrency(session.orgFunds),
+ session.orgFunds > 0 ? "success" : "",
+ ),
+ ),
+ ),
+ h(
+ "section",
+ { className: "bank-module" },
+ h(
+ "div",
+ { className: "bank-module-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "Profile"),
+ h(
+ "h2",
+ { className: "bank-section-title" },
+ "Account Holder",
+ ),
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ onClick: () => actions.refreshBank(),
+ },
+ "Refresh",
+ ),
+ ),
+ h(
+ "div",
+ { className: "bank-profile-stack" },
+ statCard("Name", session.playerName || "Unknown"),
+ statCard("UID", session.uid || "-"),
+ statCard(
+ "Organization",
+ session.orgName || "No active organization",
+ ),
+ ),
+ ),
+ );
+ };
+})();
diff --git a/arma/client/addons/bank/ui/src/components/Footer.js b/arma/client/addons/bank/ui/src/components/Footer.js
new file mode 100644
index 0000000..607e333
--- /dev/null
+++ b/arma/client/addons/bank/ui/src/components/Footer.js
@@ -0,0 +1,72 @@
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const { h } = BankApp.runtime;
+ const store = BankApp.store;
+ const { account, session } = BankApp.data;
+ const { formatCurrency } = BankApp.componentFns;
+
+ BankApp.componentFns = BankApp.componentFns || {};
+ BankApp.componentFns.BankFooter = function BankFooter() {
+ store.getAccountVersion();
+ store.getSessionVersion();
+
+ const sections = [
+ {
+ title: "Banking Resources",
+ items: [
+ "Account Access Policy",
+ "Transfer & Wire Guidelines",
+ "Cash Handling Schedule",
+ "Terminal Security Notice",
+ ],
+ },
+ {
+ title: "Bank Support",
+ items: session.orgName
+ ? [
+ `Organization: ${session.orgName}`,
+ `Treasury Reference: ${formatCurrency(session.orgFunds)}`,
+ `${session.transferTargets.length} transfer recipient(s) currently visible.`,
+ `Primary Ledger: ${formatCurrency(account.bank)}`,
+ ]
+ : [
+ "Organization: No active treasury link",
+ `${session.transferTargets.length} transfer recipient(s) currently visible.`,
+ `Primary Ledger: ${formatCurrency(account.bank)}`,
+ `Cash On Hand: ${formatCurrency(account.cash)}`,
+ ],
+ },
+ ];
+
+ return h(
+ "footer",
+ { className: "bank-footer-bar" },
+ h(
+ "div",
+ { className: "bank-footer" },
+ ...sections.map((section) =>
+ h(
+ "div",
+ { className: "bank-footer-block" },
+ h(
+ "h3",
+ { className: "bank-footer-title" },
+ section.title,
+ ),
+ h(
+ "ul",
+ { className: "bank-footer-list" },
+ ...(section.items || []).map((item) =>
+ h(
+ "li",
+ { className: "bank-footer-copy" },
+ item,
+ ),
+ ),
+ ),
+ ),
+ ),
+ ),
+ );
+ };
+})();
diff --git a/arma/client/addons/bank/ui/src/components/common.js b/arma/client/addons/bank/ui/src/components/common.js
new file mode 100644
index 0000000..4cdd707
--- /dev/null
+++ b/arma/client/addons/bank/ui/src/components/common.js
@@ -0,0 +1,189 @@
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const { h } = BankApp.runtime;
+ const store = BankApp.store;
+ const { account } = BankApp.data;
+
+ function formatCurrency(value) {
+ return `$${Math.round(Number(value || 0)).toLocaleString()}`;
+ }
+
+ function pending(actionName) {
+ return store.getPendingAction() === actionName;
+ }
+
+ function statCard(label, value, tone = "") {
+ return h(
+ "div",
+ {
+ className: tone
+ ? `bank-stat-card is-${tone}`
+ : "bank-stat-card",
+ },
+ h("span", { className: "bank-stat-label" }, label),
+ h("span", { className: "bank-stat-value" }, value),
+ );
+ }
+
+ function metricCard(label, value, copy, tone = "") {
+ return h(
+ "div",
+ {
+ className: tone
+ ? `bank-metric-card is-${tone}`
+ : "bank-metric-card",
+ },
+ h("span", { className: "bank-eyebrow" }, label),
+ h("span", { className: "bank-metric-value" }, value),
+ h("span", { className: "bank-metric-copy" }, copy),
+ );
+ }
+
+ function pinIndicators(value) {
+ const pin = String(value || "");
+
+ return h(
+ "div",
+ { className: "bank-pin-indicators" },
+ [0, 1, 2, 3].map((index) =>
+ h("span", {
+ className:
+ index < pin.length
+ ? "bank-pin-indicator is-filled"
+ : "bank-pin-indicator",
+ }),
+ ),
+ );
+ }
+
+ function readInputValue(id) {
+ return document.getElementById(id)?.value || "";
+ }
+
+ function clearInputValue(id) {
+ const input = document.getElementById(id);
+ if (input) {
+ input.value = "";
+ }
+ }
+
+ function keypad(onDigit, onBackspace, onClear, onEnter) {
+ const keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9"];
+
+ return h(
+ "div",
+ { className: "bank-keypad" },
+ keys.map((digit) =>
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-key",
+ onClick: () => onDigit(digit),
+ },
+ digit,
+ ),
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-key is-muted",
+ onClick: onClear,
+ },
+ "C",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-key",
+ onClick: () => onDigit("0"),
+ },
+ "0",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-key is-accent",
+ onClick: onEnter,
+ },
+ "Enter",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-key is-wide",
+ onClick: onBackspace,
+ },
+ "Backspace",
+ ),
+ );
+ }
+
+ function transactionRows() {
+ const transactions = Array.isArray(account.transactions)
+ ? account.transactions
+ : [];
+
+ if (transactions.length === 0) {
+ return h(
+ "div",
+ { className: "bank-empty-state" },
+ h("h3", { className: "bank-empty-title" }, "No transactions"),
+ h(
+ "p",
+ { className: "bank-empty-copy" },
+ "Deposits, withdrawals, and transfers will appear here after the account begins moving funds.",
+ ),
+ );
+ }
+
+ return h(
+ "div",
+ { className: "bank-history-list" },
+ transactions
+ .slice(0, 8)
+ .map((entry) =>
+ h(
+ "div",
+ { className: "bank-history-row" },
+ h(
+ "div",
+ { className: "bank-history-copy" },
+ h(
+ "span",
+ { className: "bank-history-title" },
+ entry.type || "Transaction",
+ ),
+ h(
+ "span",
+ { className: "bank-history-meta" },
+ entry.date || "Pending timestamp",
+ ),
+ ),
+ h(
+ "span",
+ { className: "bank-history-value" },
+ formatCurrency(entry.amount || 0),
+ ),
+ ),
+ ),
+ );
+ }
+
+ BankApp.componentFns = BankApp.componentFns || {};
+ Object.assign(BankApp.componentFns, {
+ clearInputValue,
+ formatCurrency,
+ keypad,
+ metricCard,
+ pending,
+ pinIndicators,
+ readInputValue,
+ statCard,
+ transactionRows,
+ });
+})();
diff --git a/arma/client/addons/bank/ui/src/data.js b/arma/client/addons/bank/ui/src/data.js
new file mode 100644
index 0000000..856ca90
--- /dev/null
+++ b/arma/client/addons/bank/ui/src/data.js
@@ -0,0 +1,44 @@
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+
+ const defaultSession = {
+ mode: "bank",
+ orgFunds: 0,
+ orgName: "",
+ playerName: "",
+ transferTargets: [],
+ uid: "",
+ };
+
+ const defaultAccount = {
+ bank: 0,
+ cash: 0,
+ earnings: 0,
+ pin: "1234",
+ transactions: [],
+ };
+
+ 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));
+ }
+
+ BankApp.data = {
+ account: Object.assign({}, defaultAccount),
+ session: Object.assign({}, defaultSession),
+ applyHydratePayload(payload) {
+ replaceObject(
+ this.session,
+ Object.assign({}, defaultSession, payload?.session || {}),
+ );
+ replaceObject(
+ this.account,
+ Object.assign({}, defaultAccount, payload?.account || {}),
+ );
+ },
+ };
+})();
diff --git a/arma/client/addons/bank/ui/src/pages/ATMView.js b/arma/client/addons/bank/ui/src/pages/ATMView.js
new file mode 100644
index 0000000..a64984c
--- /dev/null
+++ b/arma/client/addons/bank/ui/src/pages/ATMView.js
@@ -0,0 +1,238 @@
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const { h } = BankApp.runtime;
+ const store = BankApp.store;
+ const actions = BankApp.actions;
+ const { account } = BankApp.data;
+ const { formatCurrency, keypad, pinIndicators } = BankApp.componentFns;
+
+ function atmMenuCard() {
+ return h(
+ "div",
+ { className: "bank-atm-action-grid" },
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-primary",
+ onClick: () => actions.selectAtmView("withdraw"),
+ },
+ "Withdraw Cash",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-primary",
+ onClick: () => actions.selectAtmView("deposit"),
+ },
+ "Deposit Cash",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ onClick: () => actions.selectAtmView("balance"),
+ },
+ "Check Balance",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ onClick: () => actions.closeBank(),
+ },
+ "Exit Terminal",
+ ),
+ );
+ }
+
+ function atmAmountMenu(kind) {
+ const label = kind === "deposit" ? "Deposit" : "Withdraw";
+ const amounts = [20, 50, 100, 500];
+
+ return h(
+ "div",
+ { className: "bank-atm-action-grid" },
+ amounts.map((amount) =>
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-primary",
+ onClick: () => actions.requestAtmAmount(kind, amount),
+ },
+ `${label} ${formatCurrency(amount)}`,
+ ),
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ onClick: () =>
+ actions.selectAtmView(
+ kind === "deposit"
+ ? "customDeposit"
+ : "customWithdraw",
+ ),
+ },
+ "Custom Amount",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ onClick: () => actions.selectAtmView("menu"),
+ },
+ "Back",
+ ),
+ );
+ }
+
+ function atmCustomAmount(kind) {
+ const label = kind === "deposit" ? "Deposit" : "Withdraw";
+
+ return h(
+ "div",
+ { className: "bank-atm-stack" },
+ h(
+ "div",
+ { className: "bank-pin-display" },
+ store.getCustomAmount()
+ ? formatCurrency(store.getCustomAmount())
+ : "$0",
+ ),
+ keypad(
+ actions.appendCustomAmountDigit,
+ actions.backspaceCustomAmount,
+ actions.clearCustomAmount,
+ () => actions.submitCustomAmount(kind),
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ onClick: () => actions.selectAtmView("menu"),
+ },
+ `Cancel ${label}`,
+ ),
+ );
+ }
+
+ BankApp.componentFns = BankApp.componentFns || {};
+ BankApp.componentFns.ATMView = function ATMView() {
+ store.getAccountVersion();
+ const atmViewName = store.getAtmView();
+ const enteredPin = String(store.getEnteredPin() || "");
+ let title = "Terminal Access";
+ let copy =
+ "Authenticate with the four-digit account PIN before using the terminal.";
+ let content = null;
+
+ switch (atmViewName) {
+ case "menu":
+ title = "ATM Menu";
+ copy =
+ "Select a banking action. The ATM can deposit, withdraw, and show the live account balance.";
+ content = atmMenuCard();
+ break;
+ case "withdraw":
+ title = "Withdraw Cash";
+ copy =
+ "Choose a preset amount or enter a custom amount for withdrawal.";
+ content = atmAmountMenu("withdraw");
+ break;
+ case "deposit":
+ title = "Deposit Cash";
+ copy =
+ "Move cash on hand back into the main bank balance from the terminal.";
+ content = atmAmountMenu("deposit");
+ break;
+ case "customWithdraw":
+ title = "Custom Withdraw";
+ copy = "Enter the exact withdrawal amount.";
+ content = atmCustomAmount("withdraw");
+ break;
+ case "customDeposit":
+ title = "Custom Deposit";
+ copy = "Enter the exact deposit amount.";
+ content = atmCustomAmount("deposit");
+ break;
+ case "balance":
+ title = "Available Balance";
+ copy = "Current bank balance available at this terminal.";
+ content = h(
+ "div",
+ { className: "bank-atm-stack" },
+ h(
+ "div",
+ { className: "bank-balance-display" },
+ formatCurrency(account.bank),
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-primary",
+ onClick: () => actions.selectAtmView("menu"),
+ },
+ "Return to Menu",
+ ),
+ );
+ break;
+ default:
+ content = h(
+ "div",
+ { className: "bank-atm-stack" },
+ h(
+ "div",
+ { className: "bank-pin-display" },
+ pinIndicators(enteredPin),
+ ),
+ keypad(
+ actions.appendPinDigit,
+ actions.backspacePin,
+ actions.clearPin,
+ actions.submitPin,
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ onClick: () => actions.closeBank(),
+ },
+ "Exit Terminal",
+ ),
+ );
+ break;
+ }
+
+ return h(
+ "div",
+ { className: "bank-atm-shell" },
+ h(
+ "section",
+ { className: "bank-atm-panel" },
+ h(
+ "div",
+ { className: "bank-panel-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "ATM"),
+ h("h1", { className: "bank-title" }, title),
+ ),
+ h("span", { className: "bank-pill" }, "Secure Terminal"),
+ ),
+ h("p", { className: "bank-panel-copy" }, copy),
+ content,
+ ),
+ );
+ };
+})();
diff --git a/arma/client/addons/bank/ui/src/pages/BankView.js b/arma/client/addons/bank/ui/src/pages/BankView.js
new file mode 100644
index 0000000..e3f8f0a
--- /dev/null
+++ b/arma/client/addons/bank/ui/src/pages/BankView.js
@@ -0,0 +1,321 @@
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const { h } = BankApp.runtime;
+ const store = BankApp.store;
+ const actions = BankApp.actions;
+ const { account, session } = BankApp.data;
+ const {
+ clearInputValue,
+ formatCurrency,
+ metricCard,
+ pending,
+ readInputValue,
+ transactionRows,
+ } = BankApp.componentFns;
+
+ function trackAccount() {
+ store.getAccountVersion();
+ }
+
+ function trackSession() {
+ store.getSessionVersion();
+ }
+
+ function pageHeader() {
+ trackSession();
+
+ return h(
+ "div",
+ { className: "bank-page-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "Treasury Desk"),
+ h("h1", { className: "bank-title" }, "Personal Banking"),
+ ),
+ h(
+ "span",
+ { className: "bank-pill" },
+ session.playerName || "Account Holder",
+ ),
+ );
+ }
+
+ function summarySection() {
+ trackAccount();
+ trackSession();
+
+ return h(
+ "section",
+ { className: "bank-page-section bank-summary-section" },
+ h(
+ "div",
+ { className: "bank-section-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "Overview"),
+ h(
+ "h2",
+ { className: "bank-section-title" },
+ "Financial Position",
+ ),
+ ),
+ h("span", { className: "bank-pill" }, "Banking Desk"),
+ ),
+ h(
+ "div",
+ { className: "bank-summary-band" },
+ metricCard(
+ "Primary Balance",
+ formatCurrency(account.bank),
+ "Available for transfers and withdrawals.",
+ "accent",
+ ),
+ metricCard(
+ "Cash On Hand",
+ formatCurrency(account.cash),
+ "Funds currently carried by the player.",
+ ),
+ metricCard(
+ "Pending Earnings",
+ formatCurrency(account.earnings),
+ "Ready to sweep into the main account ledger.",
+ account.earnings > 0 ? "warning" : "",
+ ),
+ metricCard(
+ "Org Snapshot",
+ formatCurrency(session.orgFunds),
+ "Reference value pulled from the organization treasury.",
+ session.orgFunds > 0 ? "success" : "",
+ ),
+ ),
+ );
+ }
+
+ function actionSections() {
+ trackSession();
+
+ return h(
+ "div",
+ { className: "bank-action-sections" },
+ h(
+ "section",
+ { className: "bank-page-section" },
+ h(
+ "div",
+ { className: "bank-section-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "Movement"),
+ h(
+ "h2",
+ { className: "bank-section-title" },
+ "Deposit / Withdraw",
+ ),
+ ),
+ ),
+ h(
+ "div",
+ { className: "bank-form-stack" },
+ h("input", {
+ id: "bank-amount-input",
+ className: "bank-input",
+ type: "number",
+ min: "1",
+ placeholder: "Enter amount",
+ }),
+ h(
+ "div",
+ { className: "bank-action-row" },
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-primary",
+ disabled: pending("deposit"),
+ onClick: () => {
+ const sent = actions.requestDeposit(
+ readInputValue("bank-amount-input"),
+ );
+ if (sent) {
+ clearInputValue("bank-amount-input");
+ }
+ },
+ },
+ pending("deposit") ? "Depositing..." : "Deposit",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-secondary",
+ disabled: pending("withdraw"),
+ onClick: () => {
+ const sent = actions.requestWithdraw(
+ readInputValue("bank-amount-input"),
+ );
+ if (sent) {
+ clearInputValue("bank-amount-input");
+ }
+ },
+ },
+ pending("withdraw") ? "Withdrawing..." : "Withdraw",
+ ),
+ ),
+ ),
+ ),
+ h(
+ "section",
+ { className: "bank-page-section" },
+ h(
+ "div",
+ { className: "bank-section-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "Transfer"),
+ h(
+ "h2",
+ { className: "bank-section-title" },
+ "Wire Funds",
+ ),
+ ),
+ ),
+ h(
+ "div",
+ { className: "bank-form-stack" },
+ h(
+ "select",
+ {
+ id: "bank-transfer-target",
+ className: "bank-select",
+ },
+ h(
+ "option",
+ { value: "" },
+ session.transferTargets.length > 0
+ ? "Select recipient"
+ : "No available recipients",
+ ),
+ session.transferTargets.map((entry) =>
+ h(
+ "option",
+ { value: entry.uid },
+ entry.name || entry.uid,
+ ),
+ ),
+ ),
+ h("input", {
+ id: "bank-transfer-amount",
+ className: "bank-input",
+ type: "number",
+ min: "1",
+ placeholder: "Enter transfer amount",
+ }),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-primary",
+ disabled:
+ pending("transfer") ||
+ session.transferTargets.length === 0,
+ onClick: () => {
+ const sent = actions.requestTransfer(
+ readInputValue("bank-transfer-target"),
+ readInputValue("bank-transfer-amount"),
+ );
+ if (sent) {
+ clearInputValue("bank-transfer-amount");
+ }
+ },
+ },
+ pending("transfer")
+ ? "Transferring..."
+ : "Transfer Funds",
+ ),
+ ),
+ ),
+ );
+ }
+
+ function supportSection() {
+ trackAccount();
+
+ return h(
+ "div",
+ { className: "bank-support-sections" },
+ h(
+ "section",
+ { className: "bank-page-section" },
+ h(
+ "div",
+ { className: "bank-section-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "Sweep"),
+ h(
+ "h2",
+ { className: "bank-section-title" },
+ "Deposit Earnings",
+ ),
+ ),
+ ),
+ h(
+ "p",
+ { className: "bank-card-copy" },
+ "Sweep pending earnings into the primary account when you want them reflected in the main balance.",
+ ),
+ h(
+ "button",
+ {
+ type: "button",
+ className: "bank-btn bank-btn-primary",
+ disabled:
+ pending("depositearnings") ||
+ Number(account.earnings || 0) <= 0,
+ onClick: () =>
+ actions.requestDepositEarnings(account.earnings),
+ },
+ pending("depositearnings")
+ ? "Depositing..."
+ : "Deposit Earnings",
+ ),
+ ),
+ );
+ }
+
+ function historySection() {
+ trackAccount();
+
+ return h(
+ "section",
+ { className: "bank-page-section bank-history-section" },
+ h(
+ "div",
+ { className: "bank-section-header" },
+ h(
+ "div",
+ null,
+ h("span", { className: "bank-eyebrow" }, "History"),
+ h(
+ "h2",
+ { className: "bank-section-title" },
+ "Recent Transactions",
+ ),
+ ),
+ ),
+ transactionRows(),
+ );
+ }
+
+ BankApp.componentFns = BankApp.componentFns || {};
+ BankApp.componentFns.BankPageHeader = pageHeader;
+ BankApp.componentFns.BankSummarySection = summarySection;
+ BankApp.componentFns.BankActionSections = actionSections;
+ BankApp.componentFns.BankSupportSection = supportSection;
+ BankApp.componentFns.BankHistorySection = historySection;
+})();
diff --git a/arma/client/addons/bank/ui/src/registry/events.js b/arma/client/addons/bank/ui/src/registry/events.js
new file mode 100644
index 0000000..01facaa
--- /dev/null
+++ b/arma/client/addons/bank/ui/src/registry/events.js
@@ -0,0 +1,343 @@
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const store = BankApp.store;
+
+ let noticeTimer = null;
+
+ function getAccount() {
+ return BankApp.data?.account || {};
+ }
+
+ function getSession() {
+ return BankApp.data?.session || {};
+ }
+
+ function normalizeAmount(value) {
+ const amount = Math.floor(Number(value || 0));
+ return Number.isFinite(amount) ? amount : 0;
+ }
+
+ function showNotice(type, text) {
+ store.setNotice({ type, text });
+
+ if (noticeTimer) {
+ clearTimeout(noticeTimer);
+ }
+
+ noticeTimer = setTimeout(() => {
+ store.setNotice({ text: "", type: "" });
+ noticeTimer = null;
+ }, 3200);
+ }
+
+ function closeBank() {
+ const bridge = BankApp.bridge;
+ if (bridge && typeof bridge.requestClose === "function") {
+ const sent = bridge.requestClose();
+ if (sent) {
+ return true;
+ }
+ }
+
+ showNotice("error", "Bank bridge is unavailable.");
+ return false;
+ }
+
+ function refreshBank() {
+ const bridge = BankApp.bridge;
+ if (bridge && typeof bridge.requestRefresh === "function") {
+ const sent = bridge.requestRefresh();
+ if (sent) {
+ return true;
+ }
+ }
+
+ showNotice("error", "Bank refresh bridge is unavailable.");
+ return false;
+ }
+
+ function requestDeposit(amountValue) {
+ const amount = normalizeAmount(amountValue);
+ const account = getAccount();
+
+ if (amount <= 0) {
+ showNotice("error", "Enter a valid deposit amount.");
+ return false;
+ }
+
+ if (amount > Number(account.cash || 0)) {
+ showNotice("error", "Cash on hand cannot cover that deposit.");
+ return false;
+ }
+
+ const bridge = BankApp.bridge;
+ if (!bridge || typeof bridge.requestDeposit !== "function") {
+ showNotice("error", "Deposit bridge is unavailable.");
+ return false;
+ }
+
+ store.startAction("deposit");
+ const sent = bridge.requestDeposit({ amount });
+ if (!sent) {
+ store.finishAction();
+ showNotice("error", "Deposit bridge is unavailable.");
+ return false;
+ }
+
+ return true;
+ }
+
+ function requestWithdraw(amountValue) {
+ const amount = normalizeAmount(amountValue);
+ const account = getAccount();
+
+ if (amount <= 0) {
+ showNotice("error", "Enter a valid withdrawal amount.");
+ return false;
+ }
+
+ if (amount > Number(account.bank || 0)) {
+ showNotice("error", "Bank balance cannot cover that withdrawal.");
+ return false;
+ }
+
+ const bridge = BankApp.bridge;
+ if (!bridge || typeof bridge.requestWithdraw !== "function") {
+ showNotice("error", "Withdraw bridge is unavailable.");
+ return false;
+ }
+
+ store.startAction("withdraw");
+ const sent = bridge.requestWithdraw({ amount });
+ if (!sent) {
+ store.finishAction();
+ showNotice("error", "Withdraw bridge is unavailable.");
+ return false;
+ }
+
+ return true;
+ }
+
+ function requestTransfer(targetUid, amountValue) {
+ const amount = normalizeAmount(amountValue);
+ const session = getSession();
+ const account = getAccount();
+ const targetId = String(targetUid || "").trim();
+
+ if (!targetId) {
+ showNotice("error", "Select a transfer recipient.");
+ return false;
+ }
+
+ if (targetId === String(session.uid || "")) {
+ showNotice("error", "You cannot transfer funds to yourself.");
+ return false;
+ }
+
+ if (amount <= 0) {
+ showNotice("error", "Enter a valid transfer amount.");
+ return false;
+ }
+
+ if (amount > Number(account.bank || 0)) {
+ showNotice("error", "Bank balance cannot cover that transfer.");
+ return false;
+ }
+
+ const bridge = BankApp.bridge;
+ if (!bridge || typeof bridge.requestTransfer !== "function") {
+ showNotice("error", "Transfer bridge is unavailable.");
+ return false;
+ }
+
+ store.startAction("transfer");
+ const sent = bridge.requestTransfer({
+ amount,
+ from: "bank",
+ target: targetId,
+ });
+ if (!sent) {
+ store.finishAction();
+ showNotice("error", "Transfer bridge is unavailable.");
+ return false;
+ }
+
+ return true;
+ }
+
+ function requestDepositEarnings(amountValue) {
+ const amount = normalizeAmount(amountValue);
+ const account = getAccount();
+
+ if (amount <= 0) {
+ showNotice("error", "No earnings are available to deposit.");
+ return false;
+ }
+
+ if (amount > Number(account.earnings || 0)) {
+ showNotice(
+ "error",
+ "Pending earnings cannot cover that deposit request.",
+ );
+ return false;
+ }
+
+ const bridge = BankApp.bridge;
+ if (!bridge || typeof bridge.requestDepositEarnings !== "function") {
+ showNotice("error", "Earnings bridge is unavailable.");
+ return false;
+ }
+
+ store.startAction("depositearnings");
+ const sent = bridge.requestDepositEarnings({ amount });
+ if (!sent) {
+ store.finishAction();
+ showNotice("error", "Earnings bridge is unavailable.");
+ return false;
+ }
+
+ return true;
+ }
+
+ function appendPinDigit(digit) {
+ const nextDigit = String(digit || "").trim();
+ if (!nextDigit) {
+ return;
+ }
+
+ const currentPin = String(store.getEnteredPin() || "");
+ if (currentPin.length >= 4) {
+ return;
+ }
+
+ store.setEnteredPin(currentPin + nextDigit);
+ }
+
+ function backspacePin() {
+ const currentPin = String(store.getEnteredPin() || "");
+ store.setEnteredPin(currentPin.slice(0, -1));
+ }
+
+ function clearPin() {
+ store.setEnteredPin("");
+ }
+
+ function submitPin() {
+ const enteredPin = String(store.getEnteredPin() || "");
+ const actualPin = String(getAccount().pin || "1234");
+
+ if (enteredPin.length !== 4) {
+ showNotice("error", "Enter your four-digit access PIN.");
+ return false;
+ }
+
+ if (enteredPin !== actualPin) {
+ clearPin();
+ showNotice("error", "Incorrect PIN.");
+ return false;
+ }
+
+ clearPin();
+ store.setAtmView("menu");
+ return true;
+ }
+
+ function selectAtmView(view) {
+ const nextView = String(view || "").trim();
+ if (!nextView) {
+ return false;
+ }
+
+ if (nextView === "pin") {
+ store.resetAtm();
+ return true;
+ }
+
+ store.setCustomAmount("");
+ store.setAtmView(nextView);
+ return true;
+ }
+
+ function appendCustomAmountDigit(digit) {
+ const nextDigit = String(digit || "").trim();
+ if (!nextDigit) {
+ return;
+ }
+
+ const currentValue = String(store.getCustomAmount() || "");
+ if (currentValue.length >= 7) {
+ return;
+ }
+
+ store.setCustomAmount(currentValue + nextDigit);
+ }
+
+ function backspaceCustomAmount() {
+ const currentValue = String(store.getCustomAmount() || "");
+ store.setCustomAmount(currentValue.slice(0, -1));
+ }
+
+ function clearCustomAmount() {
+ store.setCustomAmount("");
+ }
+
+ function submitCustomAmount(kind) {
+ const amount = normalizeAmount(store.getCustomAmount());
+ const nextKind = String(kind || "")
+ .trim()
+ .toLowerCase();
+
+ if (amount <= 0) {
+ showNotice("error", "Enter a valid transaction amount.");
+ return false;
+ }
+
+ const success =
+ nextKind === "deposit"
+ ? requestDeposit(amount)
+ : requestWithdraw(amount);
+
+ if (success) {
+ store.setCustomAmount("");
+ store.setAtmView("menu");
+ }
+
+ return success;
+ }
+
+ function requestAtmAmount(kind, amount) {
+ const nextKind = String(kind || "")
+ .trim()
+ .toLowerCase();
+ const success =
+ nextKind === "deposit"
+ ? requestDeposit(amount)
+ : requestWithdraw(amount);
+
+ if (success) {
+ store.setAtmView("menu");
+ }
+
+ return success;
+ }
+
+ BankApp.actions = {
+ appendCustomAmountDigit,
+ appendPinDigit,
+ backspaceCustomAmount,
+ backspacePin,
+ clearCustomAmount,
+ clearPin,
+ closeBank,
+ refreshBank,
+ requestAtmAmount,
+ requestDeposit,
+ requestDepositEarnings,
+ requestTransfer,
+ requestWithdraw,
+ selectAtmView,
+ showNotice,
+ submitCustomAmount,
+ submitPin,
+ };
+})();
diff --git a/arma/client/addons/bank/ui/src/registry/store.js b/arma/client/addons/bank/ui/src/registry/store.js
new file mode 100644
index 0000000..56b7233
--- /dev/null
+++ b/arma/client/addons/bank/ui/src/registry/store.js
@@ -0,0 +1,63 @@
+(function () {
+ const BankApp = (window.BankApp = window.BankApp || {});
+ const { createSignal } = BankApp.runtime;
+
+ class BankStore {
+ constructor() {
+ [this.getMode, this.setMode] = createSignal("bank");
+ [this.getNotice, this.setNotice] = createSignal({
+ text: "",
+ type: "",
+ });
+ [this.getPendingAction, this.setPendingAction] = createSignal("");
+ [this.getAtmView, this.setAtmView] = createSignal("pin");
+ [this.getEnteredPin, this.setEnteredPin] = createSignal("");
+ [this.getCustomAmount, this.setCustomAmount] = createSignal("");
+ [this.getAccountVersion, this.setAccountVersion] = createSignal(0);
+ [this.getSessionVersion, this.setSessionVersion] = createSignal(0);
+ }
+
+ finishAction() {
+ this.setPendingAction("");
+ }
+
+ hydrateFromPayload(payload) {
+ const mode = String(payload?.session?.mode || "bank")
+ .trim()
+ .toLowerCase();
+ const currentMode = this.getMode();
+ const currentAtmView = this.getAtmView();
+
+ this.setMode(mode === "atm" ? "atm" : "bank");
+ this.setPendingAction("");
+ this.setNotice({ text: "", type: "" });
+ this.setEnteredPin("");
+ this.setCustomAmount("");
+ this.setAccountVersion(this.getAccountVersion() + 1);
+ this.setSessionVersion(this.getSessionVersion() + 1);
+
+ if (mode === "atm") {
+ this.setAtmView(currentMode === "atm" ? currentAtmView : "pin");
+ return;
+ }
+
+ this.setAtmView("dashboard");
+ }
+
+ resetAtm() {
+ this.setEnteredPin("");
+ this.setCustomAmount("");
+ this.setAtmView("pin");
+ }
+
+ startAction(action) {
+ this.setPendingAction(
+ String(action || "")
+ .trim()
+ .toLowerCase(),
+ );
+ }
+ }
+
+ BankApp.store = new BankStore();
+})();
diff --git a/arma/client/addons/bank/ui/src/runtime.js b/arma/client/addons/bank/ui/src/runtime.js
new file mode 100644
index 0000000..b51513e
--- /dev/null
+++ b/arma/client/addons/bank/ui/src/runtime.js
@@ -0,0 +1,6 @@
+(function () {
+ const runtime = window.ForgeWebUI;
+ const BankApp = (window.BankApp = window.BankApp || {});
+ BankApp.runtime = runtime;
+ window.AppRuntime = runtime;
+})();
diff --git a/arma/client/addons/bank/ui/src/styles.css b/arma/client/addons/bank/ui/src/styles.css
new file mode 100644
index 0000000..c418b82
--- /dev/null
+++ b/arma/client/addons/bank/ui/src/styles.css
@@ -0,0 +1,590 @@
+:root {
+ --bank-shell-bg: #f6f4ee;
+ --bank-surface: linear-gradient(180deg, #ffffff 0%, #f4f8fd 100%);
+ --bank-border: rgba(18, 54, 93, 0.12);
+ --bank-border-strong: rgba(18, 54, 93, 0.18);
+ --bank-text-main: #142f52;
+ --bank-text-muted: #6f86a3;
+ --bank-text-subtle: #8ea2bb;
+ --bank-accent: #275a8c;
+ --bank-accent-soft: #dfeaf9;
+ --bank-accent-line: rgba(39, 90, 140, 0.12);
+ --bank-shadow: 0 16px 30px rgba(18, 36, 57, 0.08);
+}
+
+*,
+*::before,
+*::after {
+ box-sizing: border-box;
+}
+
+html,
+body,
+#app {
+ width: 100%;
+ height: 100%;
+ margin: 0;
+}
+
+body {
+ overflow: hidden;
+ background: transparent;
+ color: var(--bank-text-main);
+ font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
+}
+
+button,
+input,
+select {
+ font: inherit;
+}
+
+.bank-shell {
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ background: var(--bank-shell-bg);
+}
+
+.bank-scroll-shell {
+ flex: 1;
+ min-height: 0;
+ overflow: auto;
+ display: flex;
+ flex-direction: column;
+}
+
+.bank-layout {
+ min-height: 100%;
+ width: min(100%, 1600px);
+ margin: 0 auto;
+ display: grid;
+ grid-template-columns: 320px minmax(0, 1fr);
+ gap: 1.25rem;
+ padding: 1.25rem;
+ flex: 1 0 auto;
+}
+
+.bank-sidebar,
+.bank-main {
+ min-height: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 1rem;
+}
+
+.bank-main {
+ overflow: visible;
+}
+
+.bank-module,
+.bank-card,
+.bank-atm-panel {
+ background: var(--bank-surface);
+ border: 1px solid var(--bank-border);
+ border-radius: 1.3rem;
+ box-shadow: var(--bank-shadow);
+}
+
+.bank-module,
+.bank-card,
+.bank-atm-panel {
+ padding: 1rem;
+ display: flex;
+ flex-direction: column;
+}
+
+.bank-module-header,
+.bank-card-header,
+.bank-section-header,
+.bank-page-header {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 1rem;
+}
+
+.bank-module-header,
+.bank-card-header {
+ margin-bottom: 0.9rem;
+}
+
+.bank-page {
+ display: grid;
+ gap: 1.35rem;
+ padding: 0.1rem 0 0;
+}
+
+.bank-page-header {
+ padding-top: 0.4rem;
+}
+
+.bank-page-copy {
+ margin: 0;
+ color: var(--bank-text-muted);
+ line-height: 1.5;
+ max-width: 48rem;
+}
+
+.bank-page-divider {
+ border-top: 1px solid var(--bank-accent-line);
+}
+
+.bank-page-body {
+ display: grid;
+ gap: 1.25rem;
+ padding-bottom: 1.25rem;
+}
+
+.bank-page-section {
+ display: grid;
+ gap: 1rem;
+ padding: 1.15rem 1.2rem 1.25rem;
+ border: 1px solid var(--bank-border);
+ border-radius: 1.3rem;
+ background: rgba(255, 255, 255, 0.72);
+ box-shadow: none;
+}
+
+.bank-title,
+.bank-section-title {
+ margin: 0;
+ color: var(--bank-text-main);
+ letter-spacing: -0.02em;
+}
+
+.bank-title {
+ font-size: 1.7rem;
+}
+
+.bank-section-title {
+ font-size: 1.1rem;
+}
+
+.bank-eyebrow,
+.bank-footer-title,
+.bank-stat-label {
+ display: block;
+ font-size: 0.68rem;
+ letter-spacing: 0.16em;
+ text-transform: uppercase;
+ font-weight: 700;
+ color: var(--bank-text-subtle);
+}
+
+.bank-pill {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ padding: 0.48rem 0.8rem;
+ border-radius: 999px;
+ background: var(--bank-accent-soft);
+ color: var(--bank-accent);
+ font-size: 0.74rem;
+ font-weight: 700;
+ letter-spacing: 0.1em;
+ text-transform: uppercase;
+ white-space: nowrap;
+}
+
+.bank-summary-grid,
+.bank-profile-stack {
+ display: grid;
+ gap: 0.8rem;
+}
+
+.bank-summary-grid {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.bank-stat-card,
+.bank-metric-card {
+ min-width: 0;
+ display: flex;
+ flex-direction: column;
+ gap: 0.25rem;
+ padding: 0.9rem;
+ border-radius: 0.95rem;
+ border: 1px solid var(--bank-border);
+ background: rgba(255, 255, 255, 0.6);
+}
+
+.bank-stat-card.is-accent,
+.bank-metric-card.is-accent {
+ background: linear-gradient(180deg, #edf4fe 0%, #dfeaf9 100%);
+}
+
+.bank-stat-card.is-success,
+.bank-metric-card.is-success {
+ background: linear-gradient(180deg, #edf9f4 0%, #dff4ea 100%);
+}
+
+.bank-stat-card.is-warning,
+.bank-metric-card.is-warning {
+ background: linear-gradient(180deg, #fdf7ea 0%, #f7edd4 100%);
+}
+
+.bank-stat-value,
+.bank-metric-value {
+ min-width: 0;
+ color: var(--bank-text-main);
+ font-weight: 700;
+ overflow-wrap: anywhere;
+}
+
+.bank-stat-value {
+ font-size: 1rem;
+}
+
+.bank-metric-value {
+ font-size: 1.8rem;
+ letter-spacing: -0.03em;
+}
+
+.bank-metric-copy,
+.bank-card-copy,
+.bank-empty-copy,
+.bank-footer-copy,
+.bank-history-meta {
+ color: var(--bank-text-muted);
+ line-height: 1.45;
+}
+
+.bank-card-copy {
+ margin: 0 0 0.9rem;
+}
+
+.bank-summary-band {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 0.85rem;
+}
+
+.bank-action-sections {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 1rem;
+}
+
+.bank-support-sections {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr);
+ gap: 1rem;
+}
+
+.bank-form-stack {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.bank-input,
+.bank-select {
+ width: 100%;
+ min-width: 0;
+ height: 2.9rem;
+ padding: 0 0.95rem;
+ border-radius: 0.8rem;
+ border: 1px solid var(--bank-border);
+ background: rgba(255, 255, 255, 0.82);
+ color: var(--bank-text-main);
+}
+
+.bank-action-row {
+ display: flex;
+ gap: 0.75rem;
+}
+
+.bank-btn {
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 2.85rem;
+ padding: 0.75rem 1rem;
+ border-radius: 0.8rem;
+ border: 1px solid var(--bank-border);
+ font-size: 0.82rem;
+ font-weight: 700;
+ letter-spacing: 0.12em;
+ text-transform: uppercase;
+ cursor: pointer;
+ transition:
+ background-color 160ms ease,
+ color 160ms ease,
+ border-color 160ms ease;
+}
+
+.bank-btn:disabled {
+ opacity: 0.55;
+ cursor: default;
+}
+
+.bank-btn-primary {
+ background: #455a77;
+ border-color: #455a77;
+ color: #fff;
+}
+
+.bank-btn-primary:hover:not(:disabled) {
+ background: #354863;
+ border-color: #354863;
+}
+
+.bank-btn-secondary {
+ background: rgba(255, 255, 255, 0.82);
+ color: var(--bank-accent);
+}
+
+.bank-btn-secondary:hover:not(:disabled) {
+ background: #eef4fd;
+}
+
+.bank-history-list {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.bank-history-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 1rem;
+ padding: 0.85rem 0.95rem;
+ border-radius: 0.9rem;
+ border: 1px solid var(--bank-border);
+ background: rgba(255, 255, 255, 0.6);
+}
+
+.bank-history-copy {
+ min-width: 0;
+ display: grid;
+ gap: 0.18rem;
+}
+
+.bank-history-title,
+.bank-empty-title {
+ color: var(--bank-text-main);
+ font-weight: 700;
+}
+
+.bank-history-value {
+ white-space: nowrap;
+ font-weight: 700;
+ color: var(--bank-accent);
+}
+
+.bank-empty-state {
+ display: grid;
+ gap: 0.35rem;
+ padding: 1rem 0;
+}
+
+.bank-notice-stack {
+ position: fixed;
+ top: 1.2rem;
+ right: 1.5rem;
+ z-index: 12;
+ display: grid;
+ gap: 0.65rem;
+}
+
+.bank-notice {
+ max-width: 24rem;
+ padding: 0.85rem 1rem;
+ border-radius: 0.9rem;
+ border: 1px solid var(--bank-border);
+ background: #fff;
+ box-shadow: 0 14px 28px rgba(16, 34, 56, 0.14);
+ font-size: 0.92rem;
+}
+
+.bank-notice.is-success {
+ background: #ecfdf5;
+ border-color: #bbf7d0;
+ color: #166534;
+}
+
+.bank-notice.is-error {
+ background: #fef2f2;
+ border-color: #fecaca;
+ color: #991b1b;
+}
+
+.bank-footer-bar {
+ width: 100%;
+ margin-top: auto;
+ background: #1e293b;
+ color: #f8fafc;
+}
+
+.bank-footer {
+ width: min(100%, 1600px);
+ margin: 0 auto;
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 4rem;
+ padding: 3rem 1.25rem;
+}
+
+.bank-footer-block {
+ display: flex;
+ flex-direction: column;
+ gap: 0.75rem;
+}
+
+.bank-footer-title {
+ margin: 0;
+ color: #f8fafc;
+ font-size: 0.85rem;
+ text-transform: uppercase;
+ letter-spacing: 0.1em;
+ font-weight: 700;
+ padding-bottom: 0.5rem;
+ border-bottom: 1px solid #475569;
+}
+
+.bank-footer-list {
+ margin: 0;
+ padding: 0;
+ list-style: none;
+}
+
+.bank-atm-shell {
+ flex: 1;
+ min-height: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 2rem 1rem;
+}
+
+.bank-atm-panel {
+ width: min(100%, 560px);
+ display: grid;
+ gap: 1rem;
+}
+
+.bank-atm-stack {
+ display: grid;
+ gap: 1rem;
+}
+
+.bank-pin-display,
+.bank-balance-display {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ min-height: 5rem;
+ padding: 1rem;
+ border-radius: 1rem;
+ border: 1px solid var(--bank-border-strong);
+ background: rgba(255, 255, 255, 0.68);
+ color: var(--bank-text-main);
+ text-align: center;
+}
+
+.bank-pin-display {
+ font-size: 2rem;
+}
+
+.bank-balance-display {
+ font-size: 2.5rem;
+ font-weight: 800;
+ letter-spacing: -0.03em;
+}
+
+.bank-pin-indicators {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 0.9rem;
+}
+
+.bank-pin-indicator {
+ width: 1rem;
+ height: 1rem;
+ border-radius: 999px;
+ border: 2px solid var(--bank-accent);
+ background: transparent;
+}
+
+.bank-pin-indicator.is-filled {
+ background: var(--bank-accent);
+}
+
+.bank-keypad {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: 0.75rem;
+}
+
+.bank-key {
+ min-height: 3.2rem;
+ padding: 0.9rem;
+ border-radius: 0.9rem;
+ border: 1px solid var(--bank-border);
+ background: rgba(255, 255, 255, 0.82);
+ color: var(--bank-text-main);
+ font-weight: 700;
+}
+
+.bank-key.is-muted {
+ background: #eef2f8;
+ color: var(--bank-text-muted);
+}
+
+.bank-key.is-accent {
+ background: #455a77;
+ border-color: #455a77;
+ color: #fff;
+}
+
+.bank-key.is-wide {
+ grid-column: span 3;
+}
+
+.bank-atm-action-grid {
+ display: grid;
+ gap: 0.75rem;
+}
+
+.bank-shell.is-atm {
+ background: transparent;
+ min-height: 100%;
+ justify-content: center;
+}
+
+.bank-shell.is-atm .bank-atm-shell {
+ flex: 1;
+ width: 100%;
+ min-height: 100%;
+ max-width: 100%;
+}
+
+.bank-footer-copy {
+ color: #cbd5e1;
+ line-height: 1.5;
+ margin: 0 0 0.75rem;
+}
+
+@media (max-width: 1200px) {
+ .bank-layout {
+ grid-template-columns: 1fr;
+ }
+
+ .bank-main {
+ overflow: visible;
+ }
+}
+
+@media (max-width: 900px) {
+ .bank-summary-band,
+ .bank-action-sections,
+ .bank-footer {
+ grid-template-columns: 1fr;
+ }
+
+ .bank-summary-grid {
+ grid-template-columns: 1fr;
+ }
+}
diff --git a/arma/client/addons/bank/ui/ui.config.mjs b/arma/client/addons/bank/ui/ui.config.mjs
new file mode 100644
index 0000000..d323273
--- /dev/null
+++ b/arma/client/addons/bank/ui/ui.config.mjs
@@ -0,0 +1,38 @@
+export default {
+ addonName: "bank",
+ title: "FORGE Banking Console",
+ logLabel: "Bank UI",
+ outputDir: "_site",
+ jsBundles: [
+ {
+ name: "Bank UI app",
+ output: "bank-ui.js",
+ sources: [
+ "src/runtime.js",
+ "src/data.js",
+ "src/registry/store.js",
+ "src/bridge.js",
+ "src/registry/events.js",
+ "src/components/common.js",
+ "src/components/BankSidebar.js",
+ "src/components/Footer.js",
+ "src/pages/BankView.js",
+ "src/pages/ATMView.js",
+ "src/components/AppShell.js",
+ "src/bootstrap.js",
+ ],
+ },
+ ],
+ cssBundles: [
+ {
+ name: "Bank UI styles",
+ output: "bank-ui.css",
+ sources: ["src/styles.css"],
+ },
+ ],
+ site: {
+ styles: ["bank-ui.css"],
+ commonScripts: ["forge-webui.js"],
+ scripts: ["bank-ui.js"],
+ },
+};
diff --git a/arma/client/addons/garage/ui/_site/garage-ui.css b/arma/client/addons/garage/ui/_site/garage-ui.css
index 27a99d9..47bd2ea 100644
--- a/arma/client/addons/garage/ui/_site/garage-ui.css
+++ b/arma/client/addons/garage/ui/_site/garage-ui.css
@@ -218,10 +218,16 @@ button:disabled {
gap: 0.65rem;
}
+.garage-footer-bar {
+ width: 100%;
+ border-top: 1px solid rgb(18 54 93 / 0.1);
+}
+
.garage-footer {
+ width: min(100%, 1613px);
+ margin: 0 auto;
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 {
diff --git a/arma/client/addons/garage/ui/_site/garage-ui.js b/arma/client/addons/garage/ui/_site/garage-ui.js
index df7e4b9..118a535 100644
--- a/arma/client/addons/garage/ui/_site/garage-ui.js
+++ b/arma/client/addons/garage/ui/_site/garage-ui.js
@@ -1220,49 +1220,53 @@
),
h(
"footer",
- { className: "garage-footer" },
+ { className: "garage-footer-bar" },
h(
"div",
- { className: "garage-footer-block" },
+ { className: "garage-footer" },
h(
- "span",
- { className: "garage-footer-title" },
- "Storage Capacity",
+ "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(
- "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",
+ "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(
- "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.",
+ "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/components/AppShell.js b/arma/client/addons/garage/ui/src/components/AppShell.js
index 4eeb81e..6d00c24 100644
--- a/arma/client/addons/garage/ui/src/components/AppShell.js
+++ b/arma/client/addons/garage/ui/src/components/AppShell.js
@@ -776,49 +776,53 @@
),
h(
"footer",
- { className: "garage-footer" },
+ { className: "garage-footer-bar" },
h(
"div",
- { className: "garage-footer-block" },
+ { className: "garage-footer" },
h(
- "span",
- { className: "garage-footer-title" },
- "Storage Capacity",
+ "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(
- "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",
+ "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(
- "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.",
+ "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/styles.css b/arma/client/addons/garage/ui/src/styles.css
index b41390f..b45dec7 100644
--- a/arma/client/addons/garage/ui/src/styles.css
+++ b/arma/client/addons/garage/ui/src/styles.css
@@ -217,10 +217,16 @@ button:disabled {
gap: 0.65rem;
}
+.garage-footer-bar {
+ width: 100%;
+ border-top: 1px solid rgb(18 54 93 / 0.1);
+}
+
.garage-footer {
+ width: min(100%, 1613px);
+ margin: 0 auto;
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 {
diff --git a/arma/client/addons/org/ui/_site/org-ui.js b/arma/client/addons/org/ui/_site/org-ui.js
index b09fdcf..ee1321b 100644
--- a/arma/client/addons/org/ui/_site/org-ui.js
+++ b/arma/client/addons/org/ui/_site/org-ui.js
@@ -3908,7 +3908,7 @@ ${scopeSelector} .home-feedback {
"div",
{ className: "app-shell" },
WindowTitleBar({
- kicker: "ORBIS Workspace",
+ kicker: "FORGE ORBIS",
title: "Global Organization Network",
onClose: closeRegistry,
closeLabel: "Close organization interface",
@@ -3939,7 +3939,7 @@ ${scopeSelector} .home-feedback {
"div",
{ className: "app-shell" },
WindowTitleBar({
- kicker: "ORBIS Workspace",
+ kicker: "FORGE ORBIS",
title: "Global Organization Network",
onClose: closeRegistry,
closeLabel: "Close organization interface",
diff --git a/arma/client/addons/org/ui/src/components/AppShell.js b/arma/client/addons/org/ui/src/components/AppShell.js
index 7b52bfb..185b369 100644
--- a/arma/client/addons/org/ui/src/components/AppShell.js
+++ b/arma/client/addons/org/ui/src/components/AppShell.js
@@ -75,7 +75,7 @@
"div",
{ className: "app-shell" },
WindowTitleBar({
- kicker: "ORBIS Workspace",
+ kicker: "FORGE ORBIS",
title: "Global Organization Network",
onClose: closeRegistry,
closeLabel: "Close organization interface",
@@ -106,7 +106,7 @@
"div",
{ className: "app-shell" },
WindowTitleBar({
- kicker: "ORBIS Workspace",
+ kicker: "FORGE ORBIS",
title: "Global Organization Network",
onClose: closeRegistry,
closeLabel: "Close organization interface",
diff --git a/arma/client/addons/store/ui/_site/store-ui.js b/arma/client/addons/store/ui/_site/store-ui.js
index d123d15..920b347 100644
--- a/arma/client/addons/store/ui/_site/store-ui.js
+++ b/arma/client/addons/store/ui/_site/store-ui.js
@@ -1623,13 +1623,19 @@ ${scopeSelector} .store-panel-intro {
border-bottom: 1px solid var(--store-accent-line);
}
+${scopeSelector} .store-footer-bar {
+ width: 100%;
+ border-top: 1px solid rgb(18 54 93 / 0.1);
+ background: transparent;
+}
+
${scopeSelector} .store-footer {
+ width: min(100%, 1613px);
+ margin: 0 auto;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
padding: 0.95rem 1.25rem 1.15rem;
- border-top: 1px solid rgb(18 54 93 / 0.1);
- background: transparent;
}
${scopeSelector} .footer-block {
@@ -2039,39 +2045,51 @@ ${scopeSelector} .store-toast.is-error {
),
h(
"footer",
- { className: "store-footer" },
+ { className: "store-footer-bar" },
h(
"div",
- { className: "footer-block" },
+ { className: "store-footer" },
h(
- "span",
- { className: "footer-title" },
- "Procurement Desk",
+ "div",
+ { className: "footer-block" },
+ h(
+ "span",
+ { className: "footer-title" },
+ "Procurement Desk",
+ ),
+ h(
+ "span",
+ { className: "footer-copy" },
+ "Authorized supply browsing for personnel loadout preparation and mission staging.",
+ ),
),
h(
- "span",
- { className: "footer-copy" },
- "Authorized supply browsing for personnel loadout preparation and mission staging.",
+ "div",
+ { className: "footer-block" },
+ h(
+ "span",
+ { className: "footer-title" },
+ "Catalog Scope",
+ ),
+ h(
+ "span",
+ { className: "footer-copy" },
+ "Uniforms, protective gear, weapon slots, vehicles, ammunition groups, and general support inventory.",
+ ),
),
- ),
- h(
- "div",
- { className: "footer-block" },
- h("span", { className: "footer-title" }, "Catalog Scope"),
h(
- "span",
- { className: "footer-copy" },
- "Uniforms, protective gear, weapon slots, vehicles, ammunition groups, and general support inventory.",
- ),
- ),
- h(
- "div",
- { className: "footer-block" },
- h("span", { className: "footer-title" }, "Purchase Access"),
- h(
- "span",
- { className: "footer-copy" },
- `${session.approval} approval. ${availablePaymentSourceCount} payment source(s) currently available${session.orgName ? ` for ${session.orgName}.` : "."}`,
+ "div",
+ { className: "footer-block" },
+ h(
+ "span",
+ { className: "footer-title" },
+ "Purchase Access",
+ ),
+ h(
+ "span",
+ { className: "footer-copy" },
+ `${session.approval} approval. ${availablePaymentSourceCount} payment source(s) currently available${session.orgName ? ` for ${session.orgName}.` : "."}`,
+ ),
),
),
),
diff --git a/arma/client/addons/store/ui/src/components/AppShell.js b/arma/client/addons/store/ui/src/components/AppShell.js
index f33d28a..810a7e5 100644
--- a/arma/client/addons/store/ui/src/components/AppShell.js
+++ b/arma/client/addons/store/ui/src/components/AppShell.js
@@ -194,13 +194,19 @@ ${scopeSelector} .store-panel-intro {
border-bottom: 1px solid var(--store-accent-line);
}
+${scopeSelector} .store-footer-bar {
+ width: 100%;
+ border-top: 1px solid rgb(18 54 93 / 0.1);
+ background: transparent;
+}
+
${scopeSelector} .store-footer {
+ width: min(100%, 1613px);
+ margin: 0 auto;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
padding: 0.95rem 1.25rem 1.15rem;
- border-top: 1px solid rgb(18 54 93 / 0.1);
- background: transparent;
}
${scopeSelector} .footer-block {
@@ -610,39 +616,51 @@ ${scopeSelector} .store-toast.is-error {
),
h(
"footer",
- { className: "store-footer" },
+ { className: "store-footer-bar" },
h(
"div",
- { className: "footer-block" },
+ { className: "store-footer" },
h(
- "span",
- { className: "footer-title" },
- "Procurement Desk",
+ "div",
+ { className: "footer-block" },
+ h(
+ "span",
+ { className: "footer-title" },
+ "Procurement Desk",
+ ),
+ h(
+ "span",
+ { className: "footer-copy" },
+ "Authorized supply browsing for personnel loadout preparation and mission staging.",
+ ),
),
h(
- "span",
- { className: "footer-copy" },
- "Authorized supply browsing for personnel loadout preparation and mission staging.",
+ "div",
+ { className: "footer-block" },
+ h(
+ "span",
+ { className: "footer-title" },
+ "Catalog Scope",
+ ),
+ h(
+ "span",
+ { className: "footer-copy" },
+ "Uniforms, protective gear, weapon slots, vehicles, ammunition groups, and general support inventory.",
+ ),
),
- ),
- h(
- "div",
- { className: "footer-block" },
- h("span", { className: "footer-title" }, "Catalog Scope"),
h(
- "span",
- { className: "footer-copy" },
- "Uniforms, protective gear, weapon slots, vehicles, ammunition groups, and general support inventory.",
- ),
- ),
- h(
- "div",
- { className: "footer-block" },
- h("span", { className: "footer-title" }, "Purchase Access"),
- h(
- "span",
- { className: "footer-copy" },
- `${session.approval} approval. ${availablePaymentSourceCount} payment source(s) currently available${session.orgName ? ` for ${session.orgName}.` : "."}`,
+ "div",
+ { className: "footer-block" },
+ h(
+ "span",
+ { className: "footer-title" },
+ "Purchase Access",
+ ),
+ h(
+ "span",
+ { className: "footer-copy" },
+ `${session.approval} approval. ${availablePaymentSourceCount} payment source(s) currently available${session.orgName ? ` for ${session.orgName}.` : "."}`,
+ ),
),
),
),