Merge development into master: shared Web UI runtime, bridge-driven UIs, and server-authoritative store flow #1

Merged
J.Schmidt92 merged 37 commits from development into master 2026-03-14 20:12:08 -05:00
137 changed files with 6007 additions and 5061 deletions
Showing only changes of commit e0e6121a5c - Show all commits

View File

@ -1,3 +1,4 @@
PREP(buildPortalPayload);
PREP(handleUIEvents);
PREP(initOrgClass);
PREP(openUI);

View File

@ -18,6 +18,32 @@ if (isNil QGVAR(OrgClass)) then { call FUNC(initOrgClass); };
GVAR(OrgClass) call ["sync", [_data, _jip]];
}] call CFUNC(addEventHandler);
[QGVAR(responseCreateOrg), {
params [["_payload", createHashMap, [createHashMap]]];
private _control = uiNamespace getVariable [QGVAR(PendingBrowserControl), controlNull];
uiNamespace setVariable [QGVAR(PendingBrowserControl), controlNull];
private _success = _payload getOrDefault ["success", false];
if (!_success) exitWith {
if (_control isNotEqualTo controlNull) then {
private _json = toJSON (createHashMapFromArray [
["message", _payload getOrDefault ["message", "Organization registration failed."]]
]);
_control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.receiveCreateFailure(%1)", _json]];
};
};
private _orgData = _payload getOrDefault ["org", createHashMap];
GVAR(OrgClass) call ["sync", [_orgData, true]];
if (_control isNotEqualTo controlNull) then {
private _json = toJSON (call FUNC(buildPortalPayload));
_control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.receiveCreateSuccess(%1)", _json]];
};
}] call CFUNC(addEventHandler);
[{
EGVAR(actor,ActorClass) get "isLoaded";
}, {

View File

@ -0,0 +1,129 @@
#include "..\script_component.hpp"
/*
* Author: IDSolutions
* Builds the web portal payload from the synced org class.
*
* Arguments:
* None
*
* Return Value:
* Portal payload <HASHMAP>
*
* Example:
* call forge_client_org_fnc_buildPortalPayload;
*
* Public: No
*/
private _orgData = GVAR(OrgClass) get "org";
private _name = _orgData getOrDefault ["name", "Unknown Organization"];
private _id = _orgData getOrDefault ["id", ""];
private _ownerUid = _orgData getOrDefault ["owner", ""];
private _funds = _orgData getOrDefault ["funds", 0];
private _reputation = _orgData getOrDefault ["reputation", 0];
private _assetsRaw = _orgData getOrDefault ["assets", createHashMap];
private _membersRaw = _orgData getOrDefault ["members", createHashMap];
private _fleetRaw = _orgData getOrDefault ["fleet", createHashMap];
private _headquarters = _orgData getOrDefault ["headquarters", "ArmA Verse"];
private _type = _orgData getOrDefault ["type", "Organization"];
private _status = _orgData getOrDefault ["status", "Operational"];
private _isDefaultOrg = (_orgData getOrDefault ["default", false])
|| {toLower _id isEqualTo "default"}
|| {toLower _ownerUid isEqualTo "server"};
private _playerName = name player;
private _playerUid = getPlayerUID player;
private _playerVar = vehicleVarName player;
private _sessionRole = "Member";
private _sessionIsCeo = _isDefaultOrg && {_playerVar isEqualTo "ceo"};
private _ownerName = ["", "Server"] select (toLower _ownerUid isEqualTo "server");
private _membersList = [];
{
private _memberData = _y;
private _memberName = _memberData getOrDefault ["name", "Unknown"];
private _memberUid = _memberData getOrDefault ["uid", ""];
if (_memberUid isEqualTo _ownerUid && {_ownerName isEqualTo ""}) then { _ownerName = _memberName; };
if (_memberUid isEqualTo _playerUid) then { _sessionRole = "Member"; };
_membersList pushBack (createHashMapFromArray [["name", _memberName]]);
} forEach _membersRaw;
if (_ownerName isEqualTo "" && { _ownerUid isEqualTo _playerUid }) then { _ownerName = _playerName; };
if (_ownerName isEqualTo "" && { _ownerUid isNotEqualTo "" }) then { _ownerName = "Unknown Owner"; };
if (_ownerUid isEqualTo _playerUid) then { _sessionRole = "Leader"; };
private _assetsList = [];
{
private _assetData = _y;
_assetsList pushBack (createHashMapFromArray [
["name", _assetData getOrDefault ["name", "Unknown Asset"]],
["type", _assetData getOrDefault ["type", "items"]],
["quantity", str (_assetData getOrDefault ["quantity", 0])]
]);
} forEach _assetsRaw;
private _fleetList = [];
{
private _vehicleData = _y;
_fleetList pushBack (createHashMapFromArray [
["name", _vehicleData getOrDefault ["name", "Unknown Vehicle"]],
["type", _vehicleData getOrDefault ["type", "other"]],
["status", _vehicleData getOrDefault ["status", "Unknown"]],
["damage", _vehicleData getOrDefault ["damage", "0%"]]
]);
} forEach _fleetRaw;
private _roadmap = [
createHashMapFromArray [
["name", "Contracts Board"],
["status", "Planned"],
["detail", "Track payouts, assignments, and claim approvals."]
],
createHashMapFromArray [
["name", "Diplomacy"],
["status", "Future Review"],
["detail", "Possible future module pending a full design and scope review."]
],
createHashMapFromArray [
["name", "Logistics Queue"],
["status", "Future Review"],
["detail", "Possible future module pending a full design and scope review."]
],
createHashMapFromArray [
["name", "Permissions"],
["status", "Future Review"],
["detail", "Possible future module pending a full design and scope review."]
]
];
createHashMapFromArray [
["session", createHashMapFromArray [
["actorName", _playerName],
["actorUid", _playerUid],
["role", _sessionRole],
["ceo", _sessionIsCeo]
]],
["portalData", createHashMapFromArray [
["org", createHashMapFromArray [
["name", _name],
["tag", _id],
["type", _type],
["status", _status],
["headquarters", _headquarters],
["owner", _ownerName],
["ownerUid", _ownerUid],
["isDefault", _isDefaultOrg]
]],
["funds", _funds],
["reputation", _reputation],
["members", _membersList],
["fleet", _fleetList],
["assets", _assetsList],
["activity", []],
["roadmap", _roadmap]
]]
]

View File

@ -30,159 +30,35 @@ private _fnc_execBridge = {
_control ctrlWebBrowserAction ["ExecJS", format ["OrgUIBridge.%1(%2)", _fnName, _json]];
};
private _fnc_buildPortalPayload = {
private _orgData = GVAR(OrgClass) get "org";
private _name = _orgData getOrDefault ["name", "Unknown Organization"];
private _id = _orgData getOrDefault ["id", ""];
private _ownerUid = _orgData getOrDefault ["owner", ""];
private _funds = _orgData getOrDefault ["funds", 0];
private _reputation = _orgData getOrDefault ["reputation", 0];
private _assetsRaw = _orgData getOrDefault ["assets", createHashMap];
private _membersRaw = _orgData getOrDefault ["members", createHashMap];
private _fleetRaw = _orgData getOrDefault ["fleet", createHashMap];
private _headquarters = _orgData getOrDefault ["headquarters", "ArmA Verse"];
private _type = _orgData getOrDefault ["type", "Organization"];
private _status = _orgData getOrDefault ["status", "Operational"];
private _isDefaultOrg = (_orgData getOrDefault ["default", false])
|| {toLower _id isEqualTo "default"}
|| {toLower _ownerUid isEqualTo "server"};
private _playerName = name player;
private _playerUid = getPlayerUID player;
private _playerVar = vehicleVarName player;
private _sessionRole = "Member";
private _sessionIsCeo = _isDefaultOrg && {_playerVar isEqualTo "ceo"};
private _ownerName = ["", "Server"] select (toLower _ownerUid isEqualTo "server");
private _membersList = [];
{
private _memberData = _y;
private _memberName = _memberData getOrDefault ["name", "Unknown"];
private _memberUid = _memberData getOrDefault ["uid", ""];
if (_memberUid isEqualTo _ownerUid && {_ownerName isEqualTo ""}) then {
_ownerName = _memberName;
};
if (_memberUid isEqualTo _playerUid) then {
_sessionRole = "Member";
};
_membersList pushBack (createHashMapFromArray [
["name", _memberName]
]);
} forEach _membersRaw;
if (_ownerName isEqualTo "" && {_ownerUid isEqualTo _playerUid}) then {
_ownerName = _playerName;
};
if (_ownerName isEqualTo "" && {_ownerUid isNotEqualTo ""}) then {
_ownerName = "Unknown Owner";
};
if (_ownerUid isEqualTo _playerUid) then {
_sessionRole = "Leader";
};
private _assetsList = [];
{
private _assetData = _y;
_assetsList pushBack (createHashMapFromArray [
["name", _assetData getOrDefault ["name", "Unknown Asset"]],
["type", _assetData getOrDefault ["type", "items"]],
["quantity", str (_assetData getOrDefault ["quantity", 0])]
]);
} forEach _assetsRaw;
private _fleetList = [];
{
private _vehicleData = _y;
_fleetList pushBack (createHashMapFromArray [
["name", _vehicleData getOrDefault ["name", "Unknown Vehicle"]],
["type", _vehicleData getOrDefault ["type", "other"]],
["status", _vehicleData getOrDefault ["status", "Unknown"]],
["damage", _vehicleData getOrDefault ["damage", "0%"]]
]);
} forEach _fleetRaw;
private _roadmap = [
createHashMapFromArray [
["name", "Contracts Board"],
["status", "Planned"],
["detail", "Track payouts, assignments, and claim approvals."]
],
createHashMapFromArray [
["name", "Diplomacy"],
["status", "Future Review"],
["detail", "Possible future module pending a full design and scope review."]
],
createHashMapFromArray [
["name", "Logistics Queue"],
["status", "Future Review"],
["detail", "Possible future module pending a full design and scope review."]
],
createHashMapFromArray [
["name", "Permissions"],
["status", "Future Review"],
["detail", "Possible future module pending a full design and scope review."]
]
];
createHashMapFromArray [
["session", createHashMapFromArray [
["actorName", _playerName],
["actorUid", _playerUid],
["role", _sessionRole],
["ceo", _sessionIsCeo]
]],
["portalData", createHashMapFromArray [
["org", createHashMapFromArray [
["name", _name],
["tag", _id],
["type", _type],
["status", _status],
["headquarters", _headquarters],
["owner", _ownerName],
["ownerUid", _ownerUid],
["isDefault", _isDefaultOrg]
]],
["funds", _funds],
["reputation", _reputation],
["members", _membersList],
["fleet", _fleetList],
["assets", _assetsList],
["activity", []],
["roadmap", _roadmap]
]]
]
};
diag_log format ["[FORGE:Client:Org] Handling UI event: %1 with data: %2", _event, _data];
switch (_event) do {
case "org::close": { closeDialog 1; };
case "org::login::request": {
private _email = toLower (_data getOrDefault ["email", ""]);
private _password = _data getOrDefault ["password", ""];
private _orgData = GVAR(OrgClass) get "org";
private _orgId = _orgData getOrDefault ["id", ""];
private _orgName = _orgData getOrDefault ["name", ""];
if (_email isEqualTo "" || {_password isEqualTo ""}) exitWith {
[_control, "receiveLoginFailure", createHashMapFromArray [
["message", "Enter both email and password."]
]] call _fnc_execBridge;
};
if (_orgId isEqualTo "" && {_orgName isEqualTo ""}) exitWith {
[_control, "receiveLoginFailure", createHashMapFromArray [
["message", "No organization data is available for this player."]
]] call _fnc_execBridge;
};
[_control, "receiveLoginSuccess", call _fnc_buildPortalPayload] call _fnc_execBridge;
private _payload = call FUNC(buildPortalPayload);
[_control, "receiveLoginSuccess", _payload] call _fnc_execBridge;
};
case "org::create::request": {
private _orgName = _data getOrDefault ["orgName", ""];
if (_orgName isEqualTo "") exitWith {
[_control, "receiveCreateFailure", createHashMapFromArray [
["message", "Enter an organization name."]
]] call _fnc_execBridge;
};
uiNamespace setVariable [QGVAR(PendingBrowserControl), _control];
[SRPC(org,requestCreateOrg), [getPlayerUID player, _orgName]] call CFUNC(serverEvent);
};
case "org::ready": {
[_control, "receive", createHashMapFromArray [

View File

@ -30,6 +30,17 @@
store.failLogin("Arma login bridge is unavailable.");
}
function requestCreateOrg(registration) {
store.startCreate();
const sent = sendEvent("org::create::request", registration);
if (sent) {
return;
}
store.failCreate("Arma registration bridge is unavailable.");
}
function receive(eventOrPayload, data = {}) {
const event =
typeof eventOrPayload === "object" && eventOrPayload !== null
@ -49,18 +60,33 @@
store.failLogin(payloadData.message || "Authentication failed.");
return;
}
if (event === "org::create::success") {
store.completeCreate(payloadData);
return;
}
if (event === "org::create::failure") {
store.failCreate(
payloadData.message || "Organization registration failed.",
);
}
}
RegistryApp.bridge = {
requestLogin,
requestCreateOrg,
receive,
sendEvent,
};
window.OrgUIBridge = {
requestLogin,
requestCreateOrg,
receive,
receiveLoginSuccess: (data) => receive("org::login::success", data),
receiveLoginFailure: (data) => receive("org::login::failure", data),
receiveCreateSuccess: (data) => receive("org::create::success", data),
receiveCreateFailure: (data) => receive("org::create::failure", data),
};
})();

View File

@ -0,0 +1,107 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
const store = RegistryApp.store;
RegistryApp.components = RegistryApp.components || {};
RegistryApp.components.App = function App() {
const Navbar = window.SharedUI.componentFns.Navbar;
const Header = window.SharedUI.componentFns.Header;
const Footer = window.SharedUI.componentFns.Footer;
const HomeView = RegistryApp.componentFns.HomeView;
const RegistrationView = RegistryApp.componentFns.RegistrationView;
const PortalApp =
window.OrgPortal && window.OrgPortal.components
? window.OrgPortal.components.App
: null;
const view = store.getView();
const viewLabel =
view === "create"
? "Organization Registration"
: view === "portal"
? "Organization Portal"
: "Entry Hub";
const actionLabel = view === "portal" ? "Sign Out" : "Close";
const footerSections = [
{
title: "Registry Resources",
items: [
"Registration Guidelines",
"Tax & Fee Schedule",
"Legal Compliance",
"Trademark Database",
],
},
{
title: "Bureau Support",
items: [
"Office: Sector 7 Admin Block",
"Hours: 0800 - 1600 (GST)",
"Helpdesk: 555-01-REGISTRY",
"support@org-bureau.gov",
],
},
];
function closeRegistry() {
if (
typeof A3API !== "undefined" &&
typeof A3API.SendAlert === "function"
) {
A3API.SendAlert(
JSON.stringify({
event: "org::close",
data: {},
}),
);
return;
}
store.setView("home");
}
if (view === "portal" && PortalApp) {
return h(
"div",
null,
Navbar({
title: "Global Organization Network",
viewLabel,
actionLabel,
onAction: closeRegistry,
}),
PortalApp(),
);
}
let mainContent;
if (view === "home") {
mainContent = HomeView();
} else if (view === "create") {
mainContent = RegistrationView();
}
return h(
"main",
null,
Navbar({
title: "Global Organization Network",
viewLabel,
actionLabel,
onAction: closeRegistry,
}),
h(
"div",
{ className: "container" },
Header({
title: "Global Organization Network",
onTitleClick: () => store.setView("home"),
}),
mainContent,
),
Footer({ sections: footerSections }),
);
};
})();

View File

@ -1,40 +1,29 @@
(function () {
const SharedUI = (window.SharedUI = window.SharedUI || {});
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
RegistryApp.componentFns = RegistryApp.componentFns || {};
SharedUI.componentFns = SharedUI.componentFns || {};
RegistryApp.componentFns.Footer = function Footer() {
SharedUI.componentFns.Footer = function Footer({ sections = [] }) {
return h(
"div",
{ className: "footer" },
h(
"div",
{ className: "wrapper" },
h(
"div",
null,
h("h3", null, "Registry Resources"),
...sections.map((section) =>
h(
"ul",
{ style: { listStyleType: "none", padding: 0 } },
h("li", null, "Registration Guidelines"),
h("li", null, "Tax & Fee Schedule"),
h("li", null, "Legal Compliance"),
h("li", null, "Trademark Database"),
),
),
h(
"div",
null,
h("h3", null, "Bureau Support"),
h(
"ul",
{ style: { listStyleType: "none", padding: 0 } },
h("li", null, "Office: Sector 7 Admin Block"),
h("li", null, "Hours: 0800 - 1600 (GST)"),
h("li", null, "Helpdesk: 555-01-REGISTRY"),
h("li", null, "support@org-bureau.gov"),
"div",
null,
h("h3", null, section.title),
h(
"ul",
{ style: { listStyleType: "none", padding: 0 } },
...(section.items || []).map((item) =>
h("li", null, item),
),
),
),
),
),

View File

@ -1,84 +0,0 @@
.split-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
align-items: center;
width: 100%;
}
.info-panel {
text-align: left;
padding: 1rem;
}
.app-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 rgb(59 130 246 / 0.1);
}
}
}
.form-actions {
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
}
.cancel-link {
font-size: 0.9rem;
color: var(--text-muted);
cursor: pointer;
text-decoration: underline;
&:hover {
color: var(--primary);
}
}
.form-feedback {
padding: 0.85rem 1rem;
border-radius: var(--radius);
font-size: 0.92rem;
&.is-error {
background: #fef2f2;
border: 1px solid #fecaca;
color: #991b1b;
}
}
@media (max-width: 960px) {
.split-container {
grid-template-columns: 1fr;
}
}

View File

@ -1,23 +1,27 @@
(function () {
const SharedUI = (window.SharedUI = window.SharedUI || {});
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
const store = RegistryApp.store;
RegistryApp.componentFns = RegistryApp.componentFns || {};
SharedUI.componentFns = SharedUI.componentFns || {};
RegistryApp.componentFns.Header = function Header({ title }) {
SharedUI.componentFns.Header = function Header({
title,
subtitle = "Organization Registration & Management Portal",
onTitleClick = null,
}) {
return h(
"div",
{ className: "header" },
h(
"h1",
{
style: { cursor: "pointer" },
onClick: () => store.setView("home"),
style: { cursor: onTitleClick ? "pointer" : "default" },
onClick: onTitleClick,
},
title,
),
h("p", null, "Organization Registration & Management Portal"),
h("p", null, subtitle),
);
};
})();

View File

@ -0,0 +1,35 @@
(function () {
const SharedUI = (window.SharedUI = window.SharedUI || {});
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
SharedUI.componentFns = SharedUI.componentFns || {};
SharedUI.componentFns.Hero = function Hero({
className = "",
kicker = "",
title = "",
subtitle = "",
meta = "",
}) {
const finalClassName = [
"card org-panel org-span-12 org-page-header",
className,
]
.filter(Boolean)
.join(" ");
return h(
"section",
{ className: finalClassName },
h(
"div",
{ className: "org-page-heading" },
h("span", { className: "org-page-kicker" }, kicker),
h("h1", { className: "org-page-title" }, title),
h("p", { className: "org-page-subtitle" }, subtitle),
h("span", { className: "org-page-meta" }, meta),
),
);
};
})();

View File

@ -1,12 +0,0 @@
.content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
@media (max-width: 960px) {
.content {
grid-template-columns: 1fr;
}
}

View File

@ -1,40 +0,0 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
const store = RegistryApp.store;
RegistryApp.componentFns = RegistryApp.componentFns || {};
RegistryApp.componentFns.HomeView = function HomeView() {
return h(
"div",
{ className: "content" },
h(
"div",
{ className: "card" },
h("h2", null, "Create Organization"),
h(
"p",
null,
"Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly.",
),
h(
"button",
{ onClick: () => store.setView("create") },
"Register",
),
),
h(
"div",
{ className: "card" },
h("h2", null, "Organization Portal"),
h(
"p",
null,
"Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink.",
),
h("button", { onClick: () => store.setView("login") }, "Login"),
),
);
};
})();

View File

@ -1,48 +0,0 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
const store = RegistryApp.store;
RegistryApp.components = RegistryApp.components || {};
RegistryApp.components.App = function App() {
const Navbar = RegistryApp.componentFns.Navbar;
const Header = RegistryApp.componentFns.Header;
const HomeView = RegistryApp.componentFns.HomeView;
const LoginForm = RegistryApp.componentFns.LoginForm;
const CreateOrgForm = RegistryApp.componentFns.CreateOrgForm;
const Footer = RegistryApp.componentFns.Footer;
const PortalApp =
window.OrgPortal && window.OrgPortal.components
? window.OrgPortal.components.App
: null;
const view = store.getView();
if (view === "portal" && PortalApp) {
return h("div", null, Navbar(), PortalApp());
}
let mainContent;
if (view === "home") {
mainContent = HomeView();
} else if (view === "login") {
mainContent = LoginForm();
} else if (view === "create") {
mainContent = CreateOrgForm();
}
return h(
"main",
null,
Navbar(),
h(
"div",
{ className: "container" },
Header({ title: "Global Organization Network" }),
mainContent,
),
Footer(),
);
};
})();

View File

@ -1,96 +0,0 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
const store = RegistryApp.store;
RegistryApp.componentFns = RegistryApp.componentFns || {};
RegistryApp.componentFns.LoginForm = function LoginForm() {
const bridge = RegistryApp.bridge;
const isAuthenticating = store.getIsAuthenticating();
const loginError = store.getLoginError();
const handleLogin = () => {
const data = {
email: String(
document.getElementById("org-login-email")?.value || "",
),
password: String(
document.getElementById("org-login-password")?.value || "",
),
};
if (!bridge) {
store.failLogin("Login bridge is not available.");
return;
}
bridge.requestLogin(data);
};
return h(
"div",
{
className: "card",
style: { maxWidth: "400px", margin: "0 auto" },
},
h("h2", null, "Organization Login"),
h(
"div",
{ className: "app-form" },
h(
"div",
null,
h("label", null, "Email"),
h("input", {
id: "org-login-email",
type: "text",
placeholder: "admin@spearnet.mil",
}),
),
h(
"div",
null,
h("label", null, "Password"),
h("input", {
id: "org-login-password",
type: "password",
placeholder: "********",
disabled: isAuthenticating,
}),
),
loginError
? h(
"div",
{ className: "form-feedback is-error" },
loginError,
)
: null,
h(
"div",
{ className: "form-actions" },
h(
"button",
{
type: "button",
style: { width: "100%" },
onClick: handleLogin,
disabled: isAuthenticating,
},
isAuthenticating
? "Authenticating..."
: "Access Authenticator",
),
h(
"span",
{
className: "cancel-link",
onClick: () => store.setView("home"),
},
"Cancel / Return to Main",
),
),
),
);
};
})();

View File

@ -0,0 +1,190 @@
(function () {
const SharedUI = (window.SharedUI = window.SharedUI || {});
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h, ensureScopedStyle } = RegistryApp.runtime;
const scopeAttr = "data-ui-modal";
const scopeSelector = `[${scopeAttr}]`;
const modalCss = `
${scopeSelector} {
position: fixed;
inset: 0;
background: rgb(15 23 42 / 0.38);
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
z-index: 20;
}
${scopeSelector} .app-modal-card {
width: min(100%, 30rem);
margin-bottom: 0;
text-align: left;
}
${scopeSelector} .app-modal-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
${scopeSelector} .app-modal-title {
margin: 0;
color: var(--primary-hover);
font-size: 1.45rem;
}
${scopeSelector} .app-modal-close {
width: 2.25rem;
height: 2.25rem;
padding: 0;
background: var(--bg-surface);
color: var(--text-main);
border: 1px solid var(--border);
box-shadow: none;
transform: none;
}
${scopeSelector} .app-modal-close:hover {
background: var(--bg-surface-hover);
color: var(--text-main);
box-shadow: none;
transform: none;
}
${scopeSelector} .app-modal-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
${scopeSelector} .app-modal-form label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-muted);
font-weight: 500;
font-size: 0.9rem;
}
${scopeSelector} .app-modal-form input,
${scopeSelector} .app-modal-form 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, box-shadow 0.2s;
}
${scopeSelector} .app-modal-form input:focus,
${scopeSelector} .app-modal-form select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12);
}
${scopeSelector} .app-modal-form input:disabled,
${scopeSelector} .app-modal-form select:disabled {
background: #f1f5f9;
color: var(--text-muted);
cursor: not-allowed;
}
${scopeSelector} .app-modal-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 0.5rem;
}
${scopeSelector} .app-modal-actions button + button,
${scopeSelector} .app-modal-danger-actions button + button {
margin-left: 0;
}
${scopeSelector} .app-modal-danger {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 1rem;
padding: 1rem;
border: 1px solid #fecaca;
border-radius: var(--radius);
background: #fff1f2;
align-items: flex-start;
}
${scopeSelector} .app-modal-danger p {
margin: 0;
color: var(--text-main);
}
${scopeSelector} .app-modal-danger-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
@media (max-width: 960px) {
${scopeSelector} .app-modal-head,
${scopeSelector} .app-modal-danger {
flex-direction: column;
align-items: flex-start;
}
}
`;
SharedUI.componentFns = SharedUI.componentFns || {};
SharedUI.componentFns.Modal = function Modal({
title = "",
body = null,
onClose = null,
}) {
ensureScopedStyle("shared-modal", modalCss);
return h(
"div",
{
className: "app-modal-backdrop",
[scopeAttr]: "",
onClick: (e) => {
if (e.target === e.currentTarget && onClose) {
onClose();
}
},
},
h(
"div",
{ className: "card app-modal-card" },
h(
"div",
{ className: "app-modal-head" },
h(
"div",
null,
h("h2", { className: "app-modal-title" }, title),
),
h(
"button",
{
type: "button",
className: "app-modal-close",
onClick: onClose,
"aria-label": "Close dialog",
},
"x",
),
),
body,
),
);
};
})();

View File

@ -1,79 +0,0 @@
.app-navbar {
background: var(--bg-surface);
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow);
}
.app-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;
}
.app-navbar-brand {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
.app-navbar-kicker {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
font-weight: 600;
}
.app-navbar-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--primary-hover);
letter-spacing: -0.025em;
}
.app-navbar-actions {
display: flex;
align-items: center;
gap: 1.5rem;
}
.app-navbar-view {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
font-weight: 600;
}
.app-close-btn {
background: transparent;
color: var(--text-muted);
border: 1px solid var(--border);
padding: 0.5rem 1rem;
font-size: 0.85rem;
&:hover {
background: var(--bg-surface-hover);
color: var(--primary-hover);
border-color: var(--primary);
transform: none;
box-shadow: none;
}
}
@media (max-width: 960px) {
.app-navbar-inner {
flex-direction: column;
align-items: flex-start;
padding: 1rem 1.5rem;
}
.app-navbar-actions {
align-items: flex-start;
}
}

View File

@ -1,54 +1,113 @@
(function () {
const SharedUI = (window.SharedUI = window.SharedUI || {});
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
const store = RegistryApp.store;
const { h, ensureScopedStyle } = RegistryApp.runtime;
const scopeAttr = "data-ui-navbar";
const scopeSelector = `[${scopeAttr}]`;
const navbarCss = `
${scopeSelector} {
background: var(--bg-surface);
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow);
}
RegistryApp.componentFns = RegistryApp.componentFns || {};
${scopeSelector} .app-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;
}
function closeRegistry() {
if (
typeof A3API !== "undefined" &&
typeof A3API.SendAlert === "function"
) {
A3API.SendAlert(
JSON.stringify({
event: "org::close",
data: {},
}),
);
return;
}
${scopeSelector} .app-navbar-brand {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
store.setView("home");
${scopeSelector} .app-navbar-kicker {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
font-weight: 600;
}
${scopeSelector} .app-navbar-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--primary-hover);
letter-spacing: -0.025em;
}
${scopeSelector} .app-navbar-actions {
display: flex;
align-items: center;
gap: 1.5rem;
}
${scopeSelector} .app-navbar-view {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
font-weight: 600;
}
${scopeSelector} .app-close-btn {
background: transparent;
color: var(--text-muted);
border: 1px solid var(--border);
padding: 0.5rem 1rem;
font-size: 0.85rem;
}
${scopeSelector} .app-close-btn:hover {
background: var(--bg-surface-hover);
color: var(--primary-hover);
border-color: var(--primary);
transform: none;
box-shadow: none;
}
@media (max-width: 960px) {
${scopeSelector} .app-navbar-inner {
flex-direction: column;
align-items: flex-start;
padding: 1rem 1.5rem;
}
RegistryApp.componentFns.Navbar = function Navbar() {
const view = store.getView();
const viewLabel =
view === "login"
? "Organization Login"
: view === "create"
? "Organization Registration"
: view === "portal"
? "Organization Portal"
: "Entry Hub";
const actionLabel = view === "portal" ? "Sign Out" : "Close";
${scopeSelector} .app-navbar-actions {
align-items: flex-start;
}
}
`;
SharedUI.componentFns = SharedUI.componentFns || {};
SharedUI.componentFns.Navbar = function Navbar({
kicker = "ORBIS",
title = "",
viewLabel = "",
actionLabel = "",
onAction = null,
}) {
ensureScopedStyle("shared-navbar", navbarCss);
return h(
"nav",
{ className: "app-navbar" },
{ className: "app-navbar", [scopeAttr]: "" },
h(
"div",
{ className: "app-navbar-inner" },
h(
"div",
{ className: "app-navbar-brand" },
h("span", { className: "app-navbar-kicker" }, "ORBIS"),
h(
"span",
{ className: "app-navbar-title" },
"Global Organization Network",
),
h("span", { className: "app-navbar-kicker" }, kicker),
h("span", { className: "app-navbar-title" }, title),
),
h(
"div",
@ -59,7 +118,7 @@
{
type: "button",
className: "app-close-btn",
onClick: closeRegistry,
onClick: onAction,
},
actionLabel,
),

View File

@ -0,0 +1,83 @@
(function () {
const SharedUI = (window.SharedUI = window.SharedUI || {});
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h, ensureScopedStyle } = RegistryApp.runtime;
const scopeAttr = "data-ui-panel-card";
const scopeSelector = `[${scopeAttr}]`;
const panelCardCss = `
${scopeSelector} .org-panel-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.5rem;
}
${scopeSelector} .org-eyebrow {
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 0.4rem;
}
${scopeSelector} .org-panel-title {
margin: 0;
color: var(--primary-hover);
font-size: 1.45rem;
}
${scopeSelector} .org-panel-subtitle {
margin: 0.35rem 0 0;
color: var(--text-muted);
font-size: 0.95rem;
}
@media (max-width: 960px) {
${scopeSelector} .org-panel-head {
flex-direction: column;
align-items: flex-start;
}
}
`;
SharedUI.componentFns = SharedUI.componentFns || {};
SharedUI.componentFns.PanelCard = function PanelCard({
className = "",
eyebrow = "",
title = "",
subtitle = "",
headerExtras = null,
body = null,
rootProps = {},
}) {
const finalClassName = ["card org-panel", className]
.filter(Boolean)
.join(" ");
ensureScopedStyle("shared-panel-card", panelCardCss);
return h(
"section",
{ className: finalClassName, [scopeAttr]: "", ...rootProps },
h(
"div",
{ className: "org-panel-head" },
h(
"div",
null,
eyebrow
? h("div", { className: "org-eyebrow" }, eyebrow)
: null,
h("h2", { className: "org-panel-title" }, title),
subtitle
? h("p", { className: "org-panel-subtitle" }, subtitle)
: null,
),
headerExtras,
),
body,
);
};
})();

View File

@ -0,0 +1,79 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const scopeAttr = "data-ui-activity-card";
const scopeSelector = `[${scopeAttr}]`;
const activityCardCss = `
${scopeSelector} .org-activity-list {
display: flex;
flex-direction: column;
flex: 1;
gap: 0.85rem;
min-height: 0;
overflow: auto;
padding-right: 0.35rem;
scrollbar-width: thin;
scrollbar-color: #94a3b8 #e2e8f0;
}
${scopeSelector} .org-activity-row {
padding: 1rem;
border: 1px solid var(--border);
border-left: 3px solid #94a3b8;
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-activity-row:nth-child(even) {
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
border-color: rgb(148 163 184 / 0.45);
border-left-color: #64748b;
}
${scopeSelector} .org-activity-row p {
margin: 0;
color: var(--text-main);
}
${scopeSelector} .org-activity-time {
display: inline-block;
margin-bottom: 0.35rem;
color: var(--text-muted);
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.ActivityCard = function ActivityCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
ensureScopedStyle("portal-activity-card", activityCardCss);
return PanelCard({
className: "org-scroll-panel org-span-6",
title: "Command Feed",
subtitle: "Recent organization-level actions and updates.",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
{ className: "org-activity-list" },
...portalData.activity.map((item) =>
h(
"article",
{ className: "org-activity-row" },
h(
"span",
{ className: "org-activity-time" },
item.time,
),
h("p", null, item.text),
),
),
),
});
};
})();

View File

@ -0,0 +1,94 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const actions = OrgPortal.actions;
const scopeAttr = "data-ui-assets-card";
const scopeSelector = `[${scopeAttr}]`;
const assetsCardCss = `
${scopeSelector} .org-simple-list {
display: flex;
flex-direction: column;
flex: 1;
gap: 0.85rem;
min-height: 0;
overflow: auto;
padding-right: 0.35rem;
scrollbar-width: thin;
scrollbar-color: #94a3b8 #e2e8f0;
}
${scopeSelector} .org-simple-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-simple-row:nth-child(even) {
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
border-color: rgb(148 163 184 / 0.45);
}
${scopeSelector} .org-simple-name {
color: var(--primary-hover);
}
${scopeSelector} .org-simple-meta {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 1rem;
}
@media (max-width: 960px) {
${scopeSelector} .org-simple-row {
flex-direction: column;
align-items: flex-start;
}
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.AssetsCard = function AssetsCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
const SimpleStat = OrgPortal.componentFns.SimpleStat;
ensureScopedStyle("portal-assets-card", assetsCardCss);
return PanelCard({
className: "org-scroll-panel org-span-7",
title: "Assets",
subtitle: "Inventory supplies and equipment with quantity totals.",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
{ className: "org-simple-list" },
...portalData.assets.map((asset) =>
h(
"article",
{ className: "org-simple-row" },
h(
"strong",
{ className: "org-simple-name" },
asset.name,
),
h(
"div",
{ className: "org-simple-meta" },
SimpleStat(
"Type",
actions.formatAssetType(asset.type),
),
SimpleStat("Quantity", asset.quantity),
),
),
),
),
});
};
})();

View File

@ -0,0 +1,70 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const permissions = OrgPortal.permissions;
const actions = OrgPortal.actions;
const scopeAttr = "data-ui-danger-card";
const scopeSelector = `[${scopeAttr}]`;
const dangerCardCss = `
${scopeSelector} {
border-color: #fecaca;
background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%);
}
${scopeSelector} .org-danger-copy {
margin-bottom: 1rem;
}
${scopeSelector} .org-danger-copy strong,
${scopeSelector} .org-danger-copy p {
display: block;
}
${scopeSelector} .org-danger-copy p {
margin: 0.4rem 0 0;
color: var(--text-muted);
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.DangerCard = function DangerCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
ensureScopedStyle("portal-danger-card", dangerCardCss);
if (!permissions.canDisbandOrg()) {
return null;
}
return PanelCard({
className: "org-span-12 org-danger-panel",
title: "Organization Controls",
subtitle:
"Leader-only actions for membership and permanent organization removal.",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
null,
h(
"div",
{ className: "org-danger-copy" },
h("strong", null, "Disband organization"),
h(
"p",
null,
"This removes the organization and revokes access to the portal for all members.",
),
),
h(
"button",
{
type: "button",
className: "org-danger-btn",
onClick: () => actions.openModal("disband"),
},
"Disband Organization",
),
),
});
};
})();

View File

@ -0,0 +1,101 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const actions = OrgPortal.actions;
const scopeAttr = "data-ui-fleet-card";
const scopeSelector = `[${scopeAttr}]`;
const fleetCardCss = `
${scopeSelector} .org-simple-list {
display: flex;
flex-direction: column;
flex: 1;
gap: 0.85rem;
min-height: 0;
overflow: auto;
padding-right: 0.35rem;
scrollbar-width: thin;
scrollbar-color: #94a3b8 #e2e8f0;
}
${scopeSelector} {
min-height: 32.5rem;
max-height: 32.5rem;
}
${scopeSelector} .org-simple-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-simple-row:nth-child(even) {
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
border-color: rgb(148 163 184 / 0.45);
}
${scopeSelector} .org-simple-name {
color: var(--primary-hover);
}
${scopeSelector} .org-simple-meta {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 1rem;
}
@media (max-width: 960px) {
${scopeSelector} .org-simple-row {
flex-direction: column;
align-items: flex-start;
}
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.FleetCard = function FleetCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
const SimpleStat = OrgPortal.componentFns.SimpleStat;
ensureScopedStyle("portal-fleet-card", fleetCardCss);
return PanelCard({
className: "org-scroll-panel org-span-7",
title: "Fleet",
subtitle:
"Individual vehicles with type, status, and overall damage.",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
{ className: "org-simple-list" },
...portalData.fleet.map((unit) =>
h(
"article",
{ className: "org-simple-row" },
h(
"strong",
{ className: "org-simple-name" },
unit.name,
),
h(
"div",
{ className: "org-simple-meta" },
SimpleStat(
"Type",
actions.formatVehicleType(unit.type),
),
SimpleStat("Status", unit.status),
SimpleStat("Damage", unit.damage),
),
),
),
),
});
};
})();

View File

@ -0,0 +1,105 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const scopeAttr = "data-ui-future-card";
const scopeSelector = `[${scopeAttr}]`;
const futureCardCss = `
${scopeSelector} .org-roadmap-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
flex: 1;
min-height: 0;
overflow: auto;
padding-right: 0.35rem;
scrollbar-width: thin;
scrollbar-color: #94a3b8 #e2e8f0;
}
${scopeSelector} .org-roadmap-card {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.7rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-roadmap-card:nth-child(4n + 2),
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) {
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
border-color: rgb(100 116 139 / 0.4);
}
${scopeSelector} .org-roadmap-card p {
margin: 0;
color: var(--text-main);
}
${scopeSelector} .org-list-tag {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.2rem 0.55rem;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: #e2e8f0;
color: var(--primary-hover);
}
${scopeSelector} .org-roadmap-card:nth-child(4n + 2) .org-list-tag,
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {
background: #cbd5e1;
color: #1e293b;
}
@media (max-width: 960px) {
${scopeSelector} .org-roadmap-grid {
grid-template-columns: 1fr;
}
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) {
background: #f8fafc;
border-color: var(--border);
}
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {
background: #e2e8f0;
color: var(--primary-hover);
}
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.FutureCard = function FutureCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
ensureScopedStyle("portal-future-card", futureCardCss);
return PanelCard({
className: "org-scroll-panel org-span-6",
title: "Expansion Slots",
subtitle:
"Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
{ className: "org-roadmap-grid" },
...portalData.roadmap.map((item) =>
h(
"article",
{ className: "org-roadmap-card" },
h("span", { className: "org-list-tag" }, item.status),
h("strong", null, item.name),
h("p", null, item.detail),
),
),
),
});
};
})();

View File

@ -1,42 +1,83 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { h, ensureScopedStyle } = OrgPortal.runtime;
const store = OrgPortal.store;
const permissions = OrgPortal.permissions;
const actions = OrgPortal.actions;
const scopeAttr = "data-ui-members-card";
const scopeSelector = `[${scopeAttr}]`;
const membersCardCss = `
${scopeSelector} .org-name-list {
display: flex;
flex-direction: column;
flex: 1;
gap: 0.85rem;
min-height: 0;
overflow: auto;
padding-right: 0.35rem;
scrollbar-width: thin;
scrollbar-color: #94a3b8 #e2e8f0;
}
${scopeSelector} .org-name-row {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-name-row:nth-child(even) {
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
border-color: rgb(148 163 184 / 0.45);
}
${scopeSelector} .org-name-row button {
margin-left: auto;
}
@media (max-width: 960px) {
${scopeSelector} .org-name-row {
flex-direction: column;
align-items: flex-start;
}
${scopeSelector} .org-name-row button {
margin-left: 0;
}
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.MembersCard = function MembersCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
const members = store.getMembers();
const allowMemberManagement = permissions.canManageMembers();
ensureScopedStyle("portal-members-card", membersCardCss);
return h(
"section",
{ className: "card org-panel org-scroll-panel org-span-5" },
h(
"div",
{ className: "org-panel-head" },
h(
"div",
null,
h("h2", { className: "org-panel-title" }, "Members"),
h(
"p",
{ className: "org-panel-subtitle" },
"Current roster listing with member removal controls.",
),
),
),
h(
return PanelCard({
className: "org-scroll-panel org-span-5",
title: "Members",
subtitle:
"Current roster listing. The organization owner cannot be removed.",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
{ className: "org-name-list" },
...members.map((member) =>
h(
...members.map((member) => {
const canRemoveMember =
allowMemberManagement &&
!actions.isOwnerMember(member.name);
return h(
"article",
{ className: "org-name-row" },
h("strong", null, member.name),
allowMemberManagement
canRemoveMember
? h(
"button",
{
@ -67,9 +108,9 @@
),
)
: null,
),
),
);
}),
),
);
});
};
})();

View File

@ -0,0 +1,77 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const scopeAttr = "data-ui-metric-card";
const scopeSelector = `[${scopeAttr}]`;
const metricCardCss = `
${scopeSelector} {
display: flex;
flex-direction: column;
gap: 0.45rem;
padding: 1rem;
border-radius: var(--radius);
border: 1px solid var(--border);
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
}
${scopeSelector}:nth-child(4n + 2),
${scopeSelector}:nth-child(4n + 3) {
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(226 232 240) 100%);
border-color: rgb(100 116 139 / 0.35);
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);
}
${scopeSelector} .org-metric-label {
font-size: 0.76rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
}
${scopeSelector} .org-metric-value {
font-size: 1.8rem;
color: var(--primary-hover);
line-height: 1.1;
}
${scopeSelector}:nth-child(4n + 2) .org-metric-value,
${scopeSelector}:nth-child(4n + 3) .org-metric-value {
color: #334155;
}
${scopeSelector} .org-metric-note {
color: var(--text-muted);
font-size: 0.9rem;
}
@media (max-width: 960px) {
${scopeSelector}:nth-child(4n + 3) {
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
border-color: var(--border);
box-shadow: none;
}
${scopeSelector}:nth-child(4n + 3) .org-metric-value {
color: var(--primary-hover);
}
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.MetricCard = function MetricCard(
label,
value,
note,
) {
ensureScopedStyle("portal-metric-card", metricCardCss);
return h(
"div",
{ className: "org-metric-card", [scopeAttr]: "" },
h("span", { className: "org-metric-label" }, label),
h("strong", { className: "org-metric-value" }, value),
h("span", { className: "org-metric-note" }, note),
);
};
})();

View File

@ -8,6 +8,7 @@
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.ModalLayer = function ModalLayer() {
const Modal = window.SharedUI.componentFns.Modal;
const modal = store.getModal();
if (!modal) {
return null;
@ -24,7 +25,7 @@
title = "Run Payroll";
body = h(
"div",
{ className: "org-modal-form" },
{ className: "app-modal-form" },
h(
"div",
null,
@ -39,7 +40,7 @@
),
h(
"div",
{ className: "org-modal-actions" },
{ className: "app-modal-actions" },
h(
"button",
{
@ -75,7 +76,7 @@
title = "Send Funds";
body = h(
"div",
{ className: "org-modal-form" },
{ className: "app-modal-form" },
h(
"div",
null,
@ -104,7 +105,7 @@
),
h(
"div",
{ className: "org-modal-actions" },
{ className: "app-modal-actions" },
h(
"button",
{
@ -146,7 +147,7 @@
title = "Assign Credit Line";
body = h(
"div",
{ className: "org-modal-form" },
{ className: "app-modal-form" },
h(
"div",
null,
@ -172,7 +173,7 @@
),
h(
"div",
{ className: "org-modal-actions" },
{ className: "app-modal-actions" },
h(
"button",
{
@ -214,7 +215,7 @@
title = "Disband Organization";
body = h(
"div",
{ className: "org-danger-confirm" },
{ className: "app-modal-danger" },
h(
"p",
null,
@ -224,7 +225,7 @@
),
h(
"div",
{ className: "org-danger-actions" },
{ className: "app-modal-danger-actions" },
h(
"button",
{
@ -247,40 +248,10 @@
);
}
return h(
"div",
{
className: "org-modal-backdrop",
onClick: (e) => {
if (e.target === e.currentTarget) {
actions.closeModal();
}
},
},
h(
"div",
{ className: "card org-modal-card" },
h(
"div",
{ className: "org-modal-head" },
h(
"div",
null,
h("h2", { className: "org-panel-title" }, title),
),
h(
"button",
{
type: "button",
className: "org-modal-close",
onClick: () => actions.closeModal(),
"aria-label": "Close dialog",
},
"x",
),
),
body,
),
);
return Modal({
title,
body,
onClose: () => actions.closeModal(),
});
};
})();

View File

@ -1,35 +1,90 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { h, ensureScopedStyle } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const store = OrgPortal.store;
const actions = OrgPortal.actions;
const scopeAttr = "data-ui-overview-card";
const scopeSelector = `[${scopeAttr}]`;
const overviewCardCss = `
${scopeSelector} .org-hero-grid {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 1.5rem;
align-items: start;
}
${scopeSelector} .org-summary {
margin: 0;
font-size: 1.05rem;
color: var(--text-main);
}
${scopeSelector} .org-meta-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
${scopeSelector} .org-meta-item {
display: flex;
flex-direction: column;
gap: 0.4rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-meta-item:nth-child(even) {
background: linear-gradient(180deg, rgb(241 245 249) 0%, rgb(226 232 240) 100%);
border-color: rgb(148 163 184 / 0.45);
}
${scopeSelector} .org-meta-label {
font-size: 0.76rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
${scopeSelector} .org-meta-value {
font-size: 1rem;
font-weight: 600;
color: var(--primary-hover);
}
${scopeSelector} .org-metric-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
@media (max-width: 960px) {
${scopeSelector} .org-hero-grid,
${scopeSelector} .org-meta-row,
${scopeSelector} .org-metric-grid {
grid-template-columns: 1fr;
}
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.OverviewCard = function OverviewCard() {
const MetricCard = OrgPortal.componentFns.MetricCard;
const PanelCard = window.SharedUI.componentFns.PanelCard;
const readiness = actions.getAssetReadiness();
const headquarters = portalData.org.headquarters || "ArmA Verse";
ensureScopedStyle("portal-overview-card", overviewCardCss);
return h(
"section",
{ className: "card org-panel org-span-12" },
h(
"div",
{ className: "org-panel-head" },
h(
"div",
null,
h("div", { className: "org-eyebrow" }, portalData.org.tag),
h(
"h2",
{ className: "org-panel-title" },
"Organization Overview",
),
),
),
h(
return PanelCard({
className: "org-span-12",
eyebrow: portalData.org.tag,
title: "Organization Overview",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
{ className: "org-hero-grid" },
h(
@ -115,6 +170,6 @@
),
),
),
);
});
};
})();

View File

@ -0,0 +1,39 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const scopeAttr = "data-ui-simple-stat";
const scopeSelector = `[${scopeAttr}]`;
const simpleStatCss = `
${scopeSelector} {
display: flex;
flex-direction: column;
gap: 0.2rem;
min-width: 90px;
}
${scopeSelector} .org-simple-label {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
${scopeSelector} .org-simple-value {
font-size: 0.95rem;
color: var(--text-main);
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.SimpleStat = function SimpleStat(label, value) {
ensureScopedStyle("portal-simple-stat", simpleStatCss);
return h(
"div",
{ className: "org-simple-stat", [scopeAttr]: "" },
h("span", { className: "org-simple-label" }, label),
h("strong", { className: "org-simple-value" }, value),
);
};
})();

View File

@ -0,0 +1,430 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle, createSignal } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const store = OrgPortal.store;
const permissions = OrgPortal.permissions;
const actions = OrgPortal.actions;
const scopeAttr = "data-ui-treasury-card";
const scopeSelector = `[${scopeAttr}]`;
const [getTreasuryTab, setTreasuryTab] = createSignal("overview");
const [getTreasuryMenuOpen, setTreasuryMenuOpen] = createSignal(false);
const treasuryCardCss = `
${scopeSelector} .org-treasury-menu {
position: relative;
}
${scopeSelector} .org-menu-btn {
width: 2.75rem;
height: 2.75rem;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
border: 1px solid var(--border);
background: #f8fafc;
color: var(--text-muted);
}
${scopeSelector} .org-menu-btn:hover {
color: var(--primary-hover);
border-color: rgb(148 163 184 / 0.65);
}
${scopeSelector} .org-menu-btn svg {
width: 1.1rem;
height: 1.1rem;
}
${scopeSelector} .org-menu-dropdown {
position: absolute;
top: calc(100% + 0.6rem);
right: 0;
min-width: 10.5rem;
padding: 0.45rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #fff;
box-shadow: 0 12px 28px rgb(15 23 42 / 0.12);
display: flex;
flex-direction: column;
gap: 0.35rem;
z-index: 5;
}
${scopeSelector} .org-menu-option + .org-menu-option {
margin-left: 0;
}
${scopeSelector} .org-menu-option {
width: 100%;
justify-content: flex-start;
background: transparent;
color: var(--text-main);
border: 1px solid transparent;
}
${scopeSelector} .org-menu-option:hover {
background: #f8fafc;
border-color: rgb(148 163 184 / 0.35);
}
${scopeSelector} .org-menu-option.is-active {
background: rgb(226 232 240 / 0.7);
color: var(--primary-hover);
border-color: rgb(148 163 184 / 0.35);
}
${scopeSelector} .org-finance-meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
${scopeSelector} .org-finance-meta > div {
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
${scopeSelector} .org-meta-label {
font-size: 0.76rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
${scopeSelector} .org-action-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1rem;
}
${scopeSelector} .org-action-grid button + button {
margin-left: 0;
}
${scopeSelector} .org-action-grid button {
width: 100%;
}
${scopeSelector} .org-access-note {
margin: 0 0 1rem;
color: var(--text-muted);
font-size: 0.95rem;
}
${scopeSelector} .org-credit-summary {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.85rem 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-credit-summary strong {
font-size: 1rem;
}
${scopeSelector} .org-credit-summary span:last-child {
font-size: 0.92rem;
line-height: 1.45;
}
${scopeSelector} .org-credit-lines-list {
display: flex;
flex-direction: column;
gap: 0.85rem;
}
${scopeSelector} .org-credit-line-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-credit-line-row:nth-child(even) {
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
border-color: rgb(148 163 184 / 0.45);
}
${scopeSelector} .org-credit-line-member {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
${scopeSelector} .org-credit-line-label {
font-size: 0.76rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
${scopeSelector} .org-credit-line-empty {
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
color: var(--text-muted);
}
@media (max-width: 960px) {
${scopeSelector} .org-finance-meta {
grid-template-columns: 1fr;
}
${scopeSelector} .org-credit-line-row {
flex-direction: column;
align-items: flex-start;
}
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.TreasuryCard = function TreasuryCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
const creditLines = store.getCreditLines();
const allowTreasuryActions = permissions.canManageTreasury();
const activeTab = getTreasuryTab();
const isMenuOpen = getTreasuryMenuOpen();
const activeCreditLabel =
creditLines.length === 1
? "1 active credit line"
: `${creditLines.length} active credit lines`;
ensureScopedStyle("portal-treasury-card", treasuryCardCss);
return PanelCard({
className: "org-span-5",
title: "Treasury",
subtitle: "Organization funds, reputation, and member payouts.",
headerExtras: h(
"div",
{ className: "org-treasury-menu" },
h(
"button",
{
type: "button",
className: "org-menu-btn",
title: "Treasury views",
"aria-label": "Treasury views",
onClick: () => setTreasuryMenuOpen((open) => !open),
},
h(
"svg",
{
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
"aria-hidden": "true",
},
h("line", { x1: "4", y1: "7", x2: "20", y2: "7" }),
h("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
h("line", { x1: "4", y1: "17", x2: "20", y2: "17" }),
),
),
isMenuOpen
? h(
"div",
{ className: "org-menu-dropdown" },
h(
"button",
{
type: "button",
className:
activeTab === "overview"
? "org-menu-option is-active"
: "org-menu-option",
onClick: () => {
setTreasuryTab("overview");
setTreasuryMenuOpen(false);
},
},
"Overview",
),
h(
"button",
{
type: "button",
className:
activeTab === "credit"
? "org-menu-option is-active"
: "org-menu-option",
onClick: () => {
setTreasuryTab("credit");
setTreasuryMenuOpen(false);
},
},
"Credit Lines",
),
)
: null,
),
rootProps: { [scopeAttr]: "" },
body: h(
"div",
null,
activeTab === "credit"
? creditLines.length > 0
? h(
"div",
{ className: "org-credit-lines-list" },
...creditLines.map((line) =>
h(
"article",
{ className: "org-credit-line-row" },
h(
"div",
{
className:
"org-credit-line-member",
},
h(
"span",
{
className:
"org-credit-line-label",
},
"Member",
),
h("strong", null, line.member),
),
h(
"div",
{
className:
"org-credit-line-member",
},
h(
"span",
{
className:
"org-credit-line-label",
},
"Amount",
),
h(
"strong",
null,
actions.formatCurrency(
line.amount,
),
),
),
),
),
)
: h(
"div",
{ className: "org-credit-line-empty" },
"No active credit lines.",
)
: h(
"div",
null,
h(
"div",
{ className: "org-finance-meta" },
h(
"div",
null,
h(
"span",
{ className: "org-meta-label" },
"Funds",
),
h(
"strong",
null,
actions.formatCurrency(store.getFunds()),
),
),
h(
"div",
null,
h(
"span",
{ className: "org-meta-label" },
"Reputation",
),
h("strong", null, `${portalData.reputation}`),
),
),
allowTreasuryActions
? h(
"div",
{ className: "org-action-grid" },
h(
"button",
{
type: "button",
onClick: () =>
actions.openModal("payroll"),
},
"Run Payroll",
),
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () =>
actions.openModal("transfer"),
},
"Send Funds",
),
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () =>
actions.openModal("credit"),
},
"Credit Line",
),
)
: h(
"p",
{ className: "org-access-note" },
"Only the organization leader or CEO can manage treasury actions.",
),
h(
"div",
{ className: "org-credit-summary" },
h(
"span",
{ className: "org-meta-label" },
"Credit Line Status",
),
h("strong", null, activeCreditLabel),
h(
"span",
null,
creditLines.length > 0
? "Open the Credit Lines tab to review assigned members and amounts."
: "Assign a credit line to create the first approved member limit.",
),
),
),
),
});
};
})();

View File

@ -6,55 +6,41 @@
<title>ORBIS - Global Organization Network</title>
<script>
const addonRoot = "forge\\forge_client\\addons\\org\\ui\\_site\\";
const styleFiles = [
"base.css",
"components\\navbar.css",
"components\\homeView.css",
"components\\forms.css",
"portal\\components\\controls.css",
"portal\\components\\layout.css",
"portal\\components\\portalHeader.css",
"portal\\components\\overviewCard.css",
"portal\\components\\metricCard.css",
"portal\\components\\simpleList.css",
"portal\\components\\simpleStat.css",
"portal\\components\\treasuryCard.css",
"portal\\components\\activityCard.css",
"portal\\components\\futureCard.css",
"portal\\components\\dangerCard.css",
"portal\\components\\modalLayer.css",
];
const styleFiles = ["base.css", "controls.css", "hero.css"];
const scriptFiles = [
"runtime.js",
"logic\\registryStore.js",
"logic\\portalStore.js",
"logic\\portalPermissions.js",
"logic\\portalActions.js",
"state.js",
"bridge.js",
"portal\\runtime.js",
"portal\\data.js",
"portal\\store.js",
"portal\\permissions.js",
"portal\\actions.js",
"portal\\components\\metricCard.js",
"portal\\components\\simpleStat.js",
"portal\\components\\portalHeader.js",
"portal\\components\\overviewCard.js",
"portal\\components\\fleetCard.js",
"portal\\components\\treasuryCard.js",
"portal\\components\\assetsCard.js",
"portal\\components\\membersCard.js",
"portal\\components\\activityCard.js",
"portal\\components\\futureCard.js",
"portal\\components\\dangerCard.js",
"portal\\components\\modalLayer.js",
"portal\\components\\disbandedView.js",
"portal\\components\\footer.js",
"portal\\components\\index.js",
"components\\navbar.js",
"components\\header.js",
"components\\loginForm.js",
"components\\createOrgForm.js",
"components\\homeView.js",
"components\\hero.js",
"components\\footer.js",
"components\\index.js",
"components\\modal.js",
"components\\panelCard.js",
"components\\portal\\metricCard.js",
"components\\portal\\simpleStat.js",
"components\\portal\\overviewCard.js",
"components\\portal\\fleetCard.js",
"components\\portal\\treasuryCard.js",
"components\\portal\\assetsCard.js",
"components\\portal\\membersCard.js",
"components\\portal\\activityCard.js",
"components\\portal\\futureCard.js",
"components\\portal\\dangerCard.js",
"components\\portal\\modalLayer.js",
"views\\DisbandedView.js",
"views\\PortalView.js",
"views\\RegistrationView.js",
"views\\HomeView.js",
"components\\AppShell.js",
"bootstrap.js",
];

View File

@ -0,0 +1,318 @@
(function () {
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
SharedLogic.createPortalActions = function createPortalActions({
portalData,
store,
permissions,
registryStore,
}) {
class OrgPortalActions {
constructor() {
this.treasuryNoticeTimer = null;
}
formatCurrency(value) {
return "$" + value.toLocaleString();
}
formatVehicleType(type) {
if (!type) {
return "";
}
return type.charAt(0).toUpperCase() + type.slice(1);
}
formatAssetType(type) {
if (!type) {
return "";
}
return type.charAt(0).toUpperCase() + type.slice(1);
}
formatDisplayName(value) {
if (!value) {
return "";
}
return String(value)
.trim()
.split(/\s+/)
.map((part) => {
if (!part) {
return "";
}
return (
part.charAt(0).toUpperCase() +
part.slice(1).toLowerCase()
);
})
.join(" ");
}
getAssetReadiness() {
if (portalData.fleet.length === 0) {
return null;
}
const total = portalData.fleet.reduce(
(sum, unit) => sum + (100 - parseInt(unit.damage, 10)),
0,
);
return Math.round(total / portalData.fleet.length);
}
showTreasuryNotice(type, text) {
store.setTreasuryNotice({ type, text });
if (this.treasuryNoticeTimer) {
clearTimeout(this.treasuryNoticeTimer);
}
this.treasuryNoticeTimer = setTimeout(() => {
store.setTreasuryNotice({ type: "", text: "" });
this.treasuryNoticeTimer = null;
}, 3500);
}
parseAmount(value) {
const amount = Number(value);
return Number.isFinite(amount) ? Math.round(amount) : 0;
}
getInputValue(id) {
const el = document.getElementById(id);
return el ? el.value : "";
}
isOwnerMember(memberName) {
return (
String(memberName || "")
.trim()
.toLowerCase() ===
String(portalData.org.owner || "")
.trim()
.toLowerCase()
);
}
closePortal() {
if (
typeof A3API !== "undefined" &&
typeof A3API.SendAlert === "function"
) {
A3API.SendAlert(
JSON.stringify({
event: "org::close",
data: {},
}),
);
return;
}
if (registryStore) {
registryStore.setView("home");
}
}
openModal(type) {
if (
(type === "payroll" ||
type === "transfer" ||
type === "credit") &&
!permissions.canManageTreasury()
) {
this.showTreasuryNotice(
"error",
"Only the organization leader or CEO can manage treasury actions.",
);
return;
}
if (type === "disband" && !permissions.canDisbandOrg()) {
return;
}
store.setModal({ type });
}
closeModal() {
store.setModal(null);
}
removeMember(memberName) {
if (!permissions.canManageMembers()) {
return false;
}
if (this.isOwnerMember(memberName)) {
return false;
}
store.setMembers((currentMembers) =>
currentMembers.filter(
(member) => member.name !== memberName,
),
);
store.setCreditLines((currentLines) =>
currentLines.filter((line) => line.member !== memberName),
);
return true;
}
disbandOrganization() {
if (!permissions.canDisbandOrg()) {
return false;
}
store.setOrgDisbanded(true);
this.closeModal();
return true;
}
runPayroll(amountPerMember) {
if (!permissions.canManageTreasury()) {
this.showTreasuryNotice(
"error",
"Only the organization leader or CEO can manage treasury actions.",
);
return false;
}
const members = store.getMembers();
const funds = store.getFunds();
if (members.length === 0) {
this.showTreasuryNotice(
"error",
"No members available for payroll.",
);
return false;
}
if (amountPerMember <= 0) {
this.showTreasuryNotice(
"error",
"Enter a valid payroll amount.",
);
return false;
}
const total = amountPerMember * members.length;
if (total > funds) {
this.showTreasuryNotice(
"error",
"Insufficient org funds for payroll.",
);
return false;
}
store.setFunds(funds - total);
this.showTreasuryNotice(
"success",
`Payroll sent to ${members.length} members for ${this.formatCurrency(total)}.`,
);
return true;
}
sendFundsToMember(memberName, amount) {
if (!permissions.canManageTreasury()) {
this.showTreasuryNotice(
"error",
"Only the organization leader or CEO can manage treasury actions.",
);
return false;
}
const funds = store.getFunds();
if (!memberName) {
this.showTreasuryNotice(
"error",
"Select a member to receive funds.",
);
return false;
}
if (amount <= 0) {
this.showTreasuryNotice(
"error",
"Enter a valid transfer amount.",
);
return false;
}
if (amount > funds) {
this.showTreasuryNotice(
"error",
"Insufficient org funds for this transfer.",
);
return false;
}
store.setFunds(funds - amount);
this.showTreasuryNotice(
"success",
`${this.formatCurrency(amount)} sent to ${memberName}.`,
);
return true;
}
grantCreditLine(memberName, amount) {
if (!permissions.canManageTreasury()) {
this.showTreasuryNotice(
"error",
"Only the organization leader or CEO can manage treasury actions.",
);
return false;
}
if (!memberName) {
this.showTreasuryNotice(
"error",
"Select a member for the credit line.",
);
return false;
}
if (amount <= 0) {
this.showTreasuryNotice(
"error",
"Enter a valid credit line amount.",
);
return false;
}
store.setCreditLines((currentLines) => {
const existingIndex = currentLines.findIndex(
(line) => line.member === memberName,
);
if (existingIndex === -1) {
return [
...currentLines,
{ member: memberName, amount },
];
}
const updatedLines = [...currentLines];
updatedLines[existingIndex] = {
member: memberName,
amount,
};
return updatedLines;
});
this.showTreasuryNotice(
"success",
`Credit line of ${this.formatCurrency(amount)} assigned to ${memberName}.`,
);
return true;
}
}
return new OrgPortalActions();
};
})();

View File

@ -0,0 +1,75 @@
(function () {
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
SharedLogic.createPortalPermissions = function createPortalPermissions({
portalData,
session,
}) {
class OrgPortalPermissions {
getNormalizedRole() {
return String(session.role || "")
.trim()
.toUpperCase();
}
isDefaultOrg() {
return (
portalData.org.isDefault === true ||
String(portalData.org.tag || "")
.trim()
.toUpperCase() === "DEFAULT"
);
}
isOrgOwner() {
const ownerUid = String(
portalData.org.ownerUid || portalData.org.owner || "",
)
.trim()
.toLowerCase();
const actorUid = String(session.actorUid || "")
.trim()
.toLowerCase();
if (ownerUid && actorUid) {
return actorUid === ownerUid;
}
return (
String(session.actorName || "")
.trim()
.toLowerCase() ===
String(portalData.org.owner || "")
.trim()
.toLowerCase()
);
}
isSessionCeo() {
return session.ceo === true;
}
isOrgLeaderOrCeo() {
return (
this.isOrgOwner() ||
this.getNormalizedRole() === "LEADER" ||
(this.isDefaultOrg() && this.isSessionCeo())
);
}
canManageMembers() {
return this.isOrgLeaderOrCeo();
}
canManageTreasury() {
return this.isOrgLeaderOrCeo();
}
canDisbandOrg() {
return this.isOrgLeaderOrCeo();
}
}
return new OrgPortalPermissions();
};
})();

View File

@ -0,0 +1,38 @@
(function () {
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
SharedLogic.createPortalStore = function createPortalStore({
createSignal,
portalData,
}) {
class OrgPortalStore {
constructor() {
[this.getFunds, this.setFunds] = createSignal(portalData.funds);
[this.getMembers, this.setMembers] = createSignal([
...portalData.members,
]);
[this.getCreditLines, this.setCreditLines] = createSignal([]);
[this.getTreasuryNotice, this.setTreasuryNotice] = createSignal(
{
type: "",
text: "",
},
);
[this.getModal, this.setModal] = createSignal(null);
[this.getOrgDisbanded, this.setOrgDisbanded] =
createSignal(false);
}
hydrateFromPayload(payload) {
this.setFunds(payload.portalData.funds || 0);
this.setMembers([...(payload.portalData.members || [])]);
this.setCreditLines([]);
this.setTreasuryNotice({ type: "", text: "" });
this.setModal(null);
this.setOrgDisbanded(false);
}
}
return new OrgPortalStore();
};
})();

View File

@ -0,0 +1,69 @@
(function () {
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
SharedLogic.createRegistryStore = function createRegistryStore({
createSignal,
onHydratePortal,
}) {
class RegistryStore {
constructor() {
[this.getView, this.setView] = createSignal("home");
[this.getIsAuthenticating, this.setIsAuthenticating] =
createSignal(false);
[this.getLoginError, this.setLoginError] = createSignal("");
[this.getIsCreating, this.setIsCreating] = createSignal(false);
[this.getCreateError, this.setCreateError] = createSignal("");
}
startLogin() {
this.setLoginError("");
this.setIsAuthenticating(true);
}
startCreate() {
this.setCreateError("");
this.setIsCreating(true);
}
failLogin(message) {
this.setIsAuthenticating(false);
this.setLoginError(message || "Authentication failed.");
}
failCreate(message) {
this.setIsCreating(false);
this.setCreateError(
message || "Organization registration failed.",
);
}
hydratePortal(payload) {
return Boolean(onHydratePortal && onHydratePortal(payload));
}
completeLogin(payload) {
if (!this.hydratePortal(payload)) {
this.failLogin("Login response was missing portal data.");
return;
}
this.setLoginError("");
this.setIsAuthenticating(false);
}
completeCreate(payload) {
if (!this.hydratePortal(payload)) {
this.failCreate(
"Organization registration response was missing portal data.",
);
return;
}
this.setCreateError("");
this.setIsCreating(false);
}
}
return new RegistryStore();
};
})();

View File

@ -4,289 +4,12 @@
const store = OrgPortal.store;
const permissions = OrgPortal.permissions;
const registryStore = window.RegistryApp.store;
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
class OrgPortalActions {
constructor() {
this.treasuryNoticeTimer = null;
}
formatCurrency(value) {
return "$" + value.toLocaleString();
}
formatVehicleType(type) {
if (!type) {
return "";
}
return type.charAt(0).toUpperCase() + type.slice(1);
}
formatAssetType(type) {
if (!type) {
return "";
}
return type.charAt(0).toUpperCase() + type.slice(1);
}
formatDisplayName(value) {
if (!value) {
return "";
}
return String(value)
.trim()
.split(/\s+/)
.map((part) => {
if (!part) {
return "";
}
return (
part.charAt(0).toUpperCase() +
part.slice(1).toLowerCase()
);
})
.join(" ");
}
getAssetReadiness() {
if (portalData.fleet.length === 0) {
return null;
}
const total = portalData.fleet.reduce(
(sum, unit) => sum + (100 - parseInt(unit.damage, 10)),
0,
);
return Math.round(total / portalData.fleet.length);
}
showTreasuryNotice(type, text) {
store.setTreasuryNotice({ type, text });
if (this.treasuryNoticeTimer) {
clearTimeout(this.treasuryNoticeTimer);
}
this.treasuryNoticeTimer = setTimeout(() => {
store.setTreasuryNotice({ type: "", text: "" });
this.treasuryNoticeTimer = null;
}, 3500);
}
parseAmount(value) {
const amount = Number(value);
return Number.isFinite(amount) ? Math.round(amount) : 0;
}
getInputValue(id) {
const el = document.getElementById(id);
return el ? el.value : "";
}
closePortal() {
if (
typeof A3API !== "undefined" &&
typeof A3API.SendAlert === "function"
) {
A3API.SendAlert(
JSON.stringify({
event: "org::close",
data: {},
}),
);
return;
}
if (registryStore) {
registryStore.setView("home");
}
}
openModal(type) {
if (
(type === "payroll" ||
type === "transfer" ||
type === "credit") &&
!permissions.canManageTreasury()
) {
this.showTreasuryNotice(
"error",
"Only the organization leader or CEO can manage treasury actions.",
);
return;
}
if (type === "disband" && !permissions.canDisbandOrg()) {
return;
}
store.setModal({ type });
}
closeModal() {
store.setModal(null);
}
removeMember(memberName) {
if (!permissions.canManageMembers()) {
return false;
}
store.setMembers((currentMembers) =>
currentMembers.filter((member) => member.name !== memberName),
);
store.setCreditLines((currentLines) =>
currentLines.filter((line) => line.member !== memberName),
);
return true;
}
disbandOrganization() {
if (!permissions.canDisbandOrg()) {
return false;
}
store.setOrgDisbanded(true);
this.closeModal();
return true;
}
runPayroll(amountPerMember) {
if (!permissions.canManageTreasury()) {
this.showTreasuryNotice(
"error",
"Only the organization leader or CEO can manage treasury actions.",
);
return false;
}
const members = store.getMembers();
const funds = store.getFunds();
if (members.length === 0) {
this.showTreasuryNotice(
"error",
"No members available for payroll.",
);
return false;
}
if (amountPerMember <= 0) {
this.showTreasuryNotice(
"error",
"Enter a valid payroll amount.",
);
return false;
}
const total = amountPerMember * members.length;
if (total > funds) {
this.showTreasuryNotice(
"error",
"Insufficient org funds for payroll.",
);
return false;
}
store.setFunds(funds - total);
this.showTreasuryNotice(
"success",
`Payroll sent to ${members.length} members for ${this.formatCurrency(total)}.`,
);
return true;
}
sendFundsToMember(memberName, amount) {
if (!permissions.canManageTreasury()) {
this.showTreasuryNotice(
"error",
"Only the organization leader or CEO can manage treasury actions.",
);
return false;
}
const funds = store.getFunds();
if (!memberName) {
this.showTreasuryNotice(
"error",
"Select a member to receive funds.",
);
return false;
}
if (amount <= 0) {
this.showTreasuryNotice(
"error",
"Enter a valid transfer amount.",
);
return false;
}
if (amount > funds) {
this.showTreasuryNotice(
"error",
"Insufficient org funds for this transfer.",
);
return false;
}
store.setFunds(funds - amount);
this.showTreasuryNotice(
"success",
`${this.formatCurrency(amount)} sent to ${memberName}.`,
);
return true;
}
grantCreditLine(memberName, amount) {
if (!permissions.canManageTreasury()) {
this.showTreasuryNotice(
"error",
"Only the organization leader or CEO can manage treasury actions.",
);
return false;
}
if (!memberName) {
this.showTreasuryNotice(
"error",
"Select a member for the credit line.",
);
return false;
}
if (amount <= 0) {
this.showTreasuryNotice(
"error",
"Enter a valid credit line amount.",
);
return false;
}
store.setCreditLines((currentLines) => {
const existingIndex = currentLines.findIndex(
(line) => line.member === memberName,
);
if (existingIndex === -1) {
return [...currentLines, { member: memberName, amount }];
}
const updatedLines = [...currentLines];
updatedLines[existingIndex] = { member: memberName, amount };
return updatedLines;
});
this.showTreasuryNotice(
"success",
`Credit line of ${this.formatCurrency(amount)} assigned to ${memberName}.`,
);
return true;
}
}
OrgPortal.actions = new OrgPortalActions();
OrgPortal.actions = SharedLogic.createPortalActions({
portalData,
store,
permissions,
registryStore,
});
})();

View File

@ -1,32 +0,0 @@
.org-activity-row {
padding: 1rem;
border: 1px solid var(--border);
border-left: 3px solid #94a3b8;
border-radius: var(--radius);
background: #f8fafc;
&:nth-child(even) {
background: linear-gradient(
180deg,
rgb(248 250 252) 0%,
rgb(241 245 249) 100%
);
border-color: rgb(148 163 184 / 0.45);
border-left-color: #64748b;
}
p {
margin: 0;
color: var(--text-main);
}
}
.org-activity-time {
display: inline-block;
margin-bottom: 0.35rem;
color: var(--text-muted);
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}

View File

@ -1,44 +0,0 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.ActivityCard = function ActivityCard() {
return h(
"section",
{ className: "card org-panel org-scroll-panel org-span-6" },
h(
"div",
{ className: "org-panel-head" },
h(
"div",
null,
h("h2", { className: "org-panel-title" }, "Command Feed"),
h(
"p",
{ className: "org-panel-subtitle" },
"Recent organization-level actions and updates.",
),
),
),
h(
"div",
{ className: "org-activity-list" },
...portalData.activity.map((item) =>
h(
"article",
{ className: "org-activity-row" },
h(
"span",
{ className: "org-activity-time" },
item.time,
),
h("p", null, item.text),
),
),
),
);
};
})();

View File

@ -1,55 +0,0 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const actions = OrgPortal.actions;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.AssetsCard = function AssetsCard() {
const SimpleStat = OrgPortal.componentFns.SimpleStat;
return h(
"section",
{ className: "card org-panel org-scroll-panel org-span-7" },
h(
"div",
{ className: "org-panel-head" },
h(
"div",
null,
h("h2", { className: "org-panel-title" }, "Assets"),
h(
"p",
{ className: "org-panel-subtitle" },
"Inventory supplies and equipment with quantity totals.",
),
),
),
h(
"div",
{ className: "org-simple-list" },
...portalData.assets.map((asset) =>
h(
"article",
{ className: "org-simple-row" },
h(
"strong",
{ className: "org-simple-name" },
asset.name,
),
h(
"div",
{ className: "org-simple-meta" },
SimpleStat(
"Type",
actions.formatAssetType(asset.type),
),
SimpleStat("Quantity", asset.quantity),
),
),
),
),
);
};
})();

View File

@ -1,22 +0,0 @@
.org-danger-panel {
border-color: #fecaca;
background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%);
}
.org-danger-copy {
margin-bottom: 1rem;
strong,
p {
display: block;
}
p {
margin: 0.4rem 0 0;
color: var(--text-muted);
}
}
.org-empty-state {
text-align: left;
}

View File

@ -1,56 +0,0 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const permissions = OrgPortal.permissions;
const actions = OrgPortal.actions;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.DangerCard = function DangerCard() {
if (!permissions.canDisbandOrg()) {
return null;
}
return h(
"section",
{ className: "card org-panel org-span-12 org-danger-panel" },
h(
"div",
{ className: "org-panel-head" },
h(
"div",
null,
h(
"h2",
{ className: "org-panel-title" },
"Organization Controls",
),
h(
"p",
{ className: "org-panel-subtitle" },
"Leader-only actions for membership and permanent organization removal.",
),
),
),
h(
"div",
{ className: "org-danger-copy" },
h("strong", null, "Disband organization"),
h(
"p",
null,
"This removes the organization and revokes access to the portal for all members.",
),
),
h(
"button",
{
type: "button",
className: "org-danger-btn",
onClick: () => actions.openModal("disband"),
},
"Disband Organization",
),
);
};
})();

View File

@ -1,47 +0,0 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const registryStore = window.RegistryApp.store;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.DisbandedView = function DisbandedView() {
return h(
"section",
{ className: "card org-panel org-span-12 org-empty-state" },
h(
"div",
{ className: "org-panel-head" },
h(
"div",
null,
h(
"div",
{ className: "org-eyebrow" },
"Organization Removed",
),
h(
"h2",
{ className: "org-panel-title" },
portalData.org.name,
),
),
),
h(
"p",
{ className: "org-summary" },
"This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview.",
),
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => registryStore.setView("home"),
},
"Return to Registry",
),
);
};
})();

View File

@ -1,56 +0,0 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const actions = OrgPortal.actions;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.FleetCard = function FleetCard() {
const SimpleStat = OrgPortal.componentFns.SimpleStat;
return h(
"section",
{ className: "card org-panel org-scroll-panel org-span-7" },
h(
"div",
{ className: "org-panel-head" },
h(
"div",
null,
h("h2", { className: "org-panel-title" }, "Fleet"),
h(
"p",
{ className: "org-panel-subtitle" },
"Individual vehicles with type, status, and overall damage.",
),
),
),
h(
"div",
{ className: "org-simple-list" },
...portalData.fleet.map((unit) =>
h(
"article",
{ className: "org-simple-row" },
h(
"strong",
{ className: "org-simple-name" },
unit.name,
),
h(
"div",
{ className: "org-simple-meta" },
SimpleStat(
"Type",
actions.formatVehicleType(unit.type),
),
SimpleStat("Status", unit.status),
SimpleStat("Damage", unit.damage),
),
),
),
),
);
};
})();

View File

@ -1,43 +0,0 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.Footer = function Footer() {
return h(
"div",
{ className: "footer" },
h(
"div",
{ className: "wrapper" },
h(
"div",
null,
h("h3", null, "Organization Controls"),
h(
"ul",
{ style: { listStyleType: "none", padding: 0 } },
h("li", null, "Roster Management"),
h("li", null, "Fleet Assignment"),
h("li", null, "Treasury Permissions"),
h("li", null, "Asset Registry"),
),
),
h(
"div",
null,
h("h3", null, "Planned Extensions"),
h(
"ul",
{ style: { listStyleType: "none", padding: 0 } },
h("li", null, "Contracts Board"),
h("li", null, "Diplomacy Layer"),
h("li", null, "Procurement Queue"),
h("li", null, "Reputation History"),
),
),
),
);
};
})();

View File

@ -1,74 +0,0 @@
.org-roadmap-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
flex: 1;
min-height: 0;
overflow: auto;
padding-right: 0.35rem;
}
.org-roadmap-card {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.7rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
&:nth-child(4n + 2),
&:nth-child(4n + 3) {
background: linear-gradient(
180deg,
rgb(248 250 252) 0%,
rgb(241 245 249) 100%
);
border-color: rgb(100 116 139 / 0.4);
}
p {
margin: 0;
color: var(--text-main);
}
}
.org-list-tag {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.2rem 0.55rem;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: #e2e8f0;
color: var(--primary-hover);
.org-roadmap-card:nth-child(4n + 2) &,
.org-roadmap-card:nth-child(4n + 3) & {
background: #cbd5e1;
color: #1e293b;
}
}
@media (max-width: 960px) {
.org-roadmap-grid {
grid-template-columns: 1fr;
}
.org-roadmap-card {
&:nth-child(4n + 3) {
background: #f8fafc;
border-color: var(--border);
}
}
.org-list-tag {
.org-roadmap-card:nth-child(4n + 3) & {
background: #e2e8f0;
color: var(--primary-hover);
}
}
}

View File

@ -1,45 +0,0 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.FutureCard = function FutureCard() {
return h(
"section",
{ className: "card org-panel org-scroll-panel org-span-6" },
h(
"div",
{ className: "org-panel-head" },
h(
"div",
null,
h(
"h2",
{ className: "org-panel-title" },
"Expansion Slots",
),
h(
"p",
{ className: "org-panel-subtitle" },
"Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.",
),
),
),
h(
"div",
{ className: "org-roadmap-grid" },
...portalData.roadmap.map((item) =>
h(
"article",
{ className: "org-roadmap-card" },
h("span", { className: "org-list-tag" }, item.status),
h("strong", null, item.name),
h("p", null, item.detail),
),
),
),
);
};
})();

View File

@ -1,65 +0,0 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const store = OrgPortal.store;
OrgPortal.components = OrgPortal.components || {};
OrgPortal.components.App = function App() {
const PortalHeader = OrgPortal.componentFns.PortalHeader;
const OverviewCard = OrgPortal.componentFns.OverviewCard;
const FleetCard = OrgPortal.componentFns.FleetCard;
const TreasuryCard = OrgPortal.componentFns.TreasuryCard;
const MembersCard = OrgPortal.componentFns.MembersCard;
const AssetsCard = OrgPortal.componentFns.AssetsCard;
const ActivityCard = OrgPortal.componentFns.ActivityCard;
const FutureCard = OrgPortal.componentFns.FutureCard;
const DangerCard = OrgPortal.componentFns.DangerCard;
const ModalLayer = OrgPortal.componentFns.ModalLayer;
const DisbandedView = OrgPortal.componentFns.DisbandedView;
const Footer = OrgPortal.componentFns.Footer;
if (store.getOrgDisbanded()) {
return h(
"main",
null,
h(
"div",
{ className: "container" },
h(
"div",
{ className: "org-dashboard-grid" },
PortalHeader(),
DisbandedView(),
),
),
ModalLayer(),
Footer(),
);
}
return h(
"main",
null,
h(
"div",
{ className: "container" },
h(
"div",
{ className: "org-dashboard-grid" },
PortalHeader(),
OverviewCard(),
FleetCard(),
TreasuryCard(),
MembersCard(),
AssetsCard(),
ActivityCard(),
FutureCard(),
DangerCard(),
),
),
ModalLayer(),
Footer(),
);
};
})();

View File

@ -1,81 +0,0 @@
.org-dashboard-grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 1.5rem;
}
.org-panel {
margin-bottom: 0;
text-align: left;
}
.org-scroll-panel {
display: flex;
flex-direction: column;
max-height: 31rem;
overflow: hidden;
}
.org-span-12 {
grid-column: span 12;
}
.org-span-7 {
grid-column: span 7;
}
.org-span-6 {
grid-column: span 6;
}
.org-span-5 {
grid-column: span 5;
}
.org-panel-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.5rem;
&.org-panel-head-stack {
flex-direction: column;
align-items: stretch;
}
}
.org-eyebrow {
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 0.4rem;
}
.org-panel-title {
margin: 0;
color: var(--primary-hover);
font-size: 1.45rem;
}
.org-panel-subtitle {
margin: 0.35rem 0 0;
color: var(--text-muted);
font-size: 0.95rem;
}
@media (max-width: 960px) {
.org-span-12,
.org-span-7,
.org-span-6,
.org-span-5 {
grid-column: span 12;
}
.org-panel-head {
flex-direction: column;
align-items: flex-start;
}
}

View File

@ -1,69 +0,0 @@
.org-metric-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
.org-metric-card {
display: flex;
flex-direction: column;
gap: 0.45rem;
padding: 1rem;
border-radius: var(--radius);
border: 1px solid var(--border);
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
&:nth-child(4n + 2),
&:nth-child(4n + 3) {
background: linear-gradient(
180deg,
rgb(248 250 252) 0%,
rgb(226 232 240) 100%
);
border-color: rgb(100 116 139 / 0.35);
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);
}
}
.org-metric-label {
font-size: 0.76rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
}
.org-metric-value {
font-size: 1.8rem;
color: var(--primary-hover);
line-height: 1.1;
.org-metric-card:nth-child(4n + 2) &,
.org-metric-card:nth-child(4n + 3) & {
color: #334155;
}
}
.org-metric-note {
color: var(--text-muted);
font-size: 0.9rem;
}
@media (max-width: 960px) {
.org-metric-grid {
grid-template-columns: 1fr;
}
.org-metric-card {
&:nth-child(4n + 3) {
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
border-color: var(--border);
box-shadow: none;
}
}
.org-metric-value {
.org-metric-card:nth-child(4n + 3) & {
color: var(--primary-hover);
}
}
}

View File

@ -1,20 +0,0 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.MetricCard = function MetricCard(
label,
value,
note,
) {
return h(
"div",
{ className: "org-metric-card" },
h("span", { className: "org-metric-label" }, label),
h("strong", { className: "org-metric-value" }, value),
h("span", { className: "org-metric-note" }, note),
);
};
})();

View File

@ -1,131 +0,0 @@
.org-modal-backdrop {
position: fixed;
inset: 0;
background: rgb(15 23 42 / 0.38);
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
z-index: 20;
}
.org-modal-card {
width: min(100%, 30rem);
margin-bottom: 0;
text-align: left;
}
.org-modal-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
.org-modal-close {
width: 2.25rem;
height: 2.25rem;
padding: 0;
background: var(--bg-surface);
color: var(--text-main);
border: 1px solid var(--border);
box-shadow: none;
transform: none;
&:hover {
background: var(--bg-surface-hover);
color: var(--text-main);
box-shadow: none;
transform: none;
}
}
.org-modal-form {
display: flex;
flex-direction: column;
gap: 1rem;
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,
box-shadow 0.2s;
&:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12);
}
&:disabled {
background: #f1f5f9;
color: var(--text-muted);
cursor: not-allowed;
}
}
}
.org-modal-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 0.5rem;
button + button {
margin-left: 0;
}
}
.org-danger-confirm {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 1rem;
padding: 1rem;
border: 1px solid #fecaca;
border-radius: var(--radius);
background: #fff1f2;
align-items: flex-start;
p {
margin: 0;
color: var(--text-main);
}
}
.org-danger-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
button + button {
margin-left: 0;
}
}
@media (max-width: 960px) {
.org-modal-head,
.org-danger-confirm {
flex-direction: column;
align-items: flex-start;
}
}

View File

@ -1,58 +0,0 @@
.org-hero-grid {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 1.5rem;
align-items: start;
}
.org-summary {
margin: 0;
font-size: 1.05rem;
color: var(--text-main);
}
.org-meta-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
.org-meta-item {
display: flex;
flex-direction: column;
gap: 0.4rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
&:nth-child(even) {
background: linear-gradient(
180deg,
rgb(241 245 249) 0%,
rgb(226 232 240) 100%
);
border-color: rgb(148 163 184 / 0.45);
}
}
.org-meta-label {
font-size: 0.76rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
.org-meta-value {
font-size: 1rem;
font-weight: 600;
color: var(--primary-hover);
}
@media (max-width: 960px) {
.org-hero-grid,
.org-meta-row {
grid-template-columns: 1fr;
}
}

View File

@ -1,30 +0,0 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { portalData, session } = OrgPortal.data;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.PortalHeader = function PortalHeader() {
return h(
"section",
{ className: "card org-panel org-span-12 org-page-header" },
h(
"div",
{ className: "org-page-heading" },
h("span", { className: "org-page-kicker" }, portalData.org.tag),
h("h1", { className: "org-page-title" }, portalData.org.name),
h(
"p",
{ className: "org-page-subtitle" },
"Player organization command portal",
),
h(
"span",
{ className: "org-page-meta" },
`${session.actorName} - ${session.role}`,
),
),
);
};
})();

View File

@ -1,67 +0,0 @@
.org-simple-list,
.org-name-list,
.org-activity-list {
display: flex;
flex-direction: column;
flex: 1;
gap: 0.85rem;
min-height: 0;
overflow: auto;
padding-right: 0.35rem;
}
.org-simple-list,
.org-name-list,
.org-activity-list,
.org-roadmap-grid {
scrollbar-width: thin;
scrollbar-color: #94a3b8 #e2e8f0;
}
.org-simple-row,
.org-name-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
&:nth-child(even) {
background: linear-gradient(
180deg,
rgb(248 250 252) 0%,
rgb(241 245 249) 100%
);
border-color: rgb(148 163 184 / 0.45);
}
}
.org-simple-name {
color: var(--primary-hover);
}
.org-simple-meta {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 1rem;
}
.org-name-row {
justify-content: flex-start;
button {
margin-left: auto;
}
}
@media (max-width: 960px) {
.org-simple-row,
.org-name-row {
flex-direction: column;
align-items: flex-start;
}
}

View File

@ -1,18 +0,0 @@
.org-simple-stat {
display: flex;
flex-direction: column;
gap: 0.2rem;
min-width: 90px;
}
.org-simple-label {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
.org-simple-value {
font-size: 0.95rem;
color: var(--text-main);
}

View File

@ -1,15 +0,0 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.SimpleStat = function SimpleStat(label, value) {
return h(
"div",
{ className: "org-simple-stat" },
h("span", { className: "org-simple-label" }, label),
h("strong", { className: "org-simple-value" }, value),
);
};
})();

View File

@ -1,99 +0,0 @@
.org-finance-meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
> div {
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
}
.org-action-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1rem;
button + button {
margin-left: 0;
}
button {
width: 100%;
}
}
.org-treasury-notice {
margin-bottom: 1rem;
padding: 0.85rem 1rem;
border-radius: var(--radius);
font-size: 0.92rem;
&.is-success {
background: #ecfdf5;
border: 1px solid #bbf7d0;
color: #166534;
}
&.is-error {
background: #fef2f2;
border: 1px solid #fecaca;
color: #991b1b;
}
}
.org-credit-lines {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.org-access-note {
margin: 0 0 1rem;
color: var(--text-muted);
font-size: 0.95rem;
}
.org-credit-list {
display: flex;
flex-direction: column;
gap: 0.6rem;
}
.org-credit-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 0.9rem 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
&:nth-child(even) {
background: linear-gradient(
180deg,
rgb(248 250 252) 0%,
rgb(241 245 249) 100%
);
border-color: rgb(148 163 184 / 0.45);
}
}
@media (max-width: 960px) {
.org-finance-meta {
grid-template-columns: 1fr;
}
.org-credit-row {
flex-direction: column;
align-items: flex-start;
}
}

View File

@ -1,126 +0,0 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const store = OrgPortal.store;
const permissions = OrgPortal.permissions;
const actions = OrgPortal.actions;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.TreasuryCard = function TreasuryCard() {
const notice = store.getTreasuryNotice();
const creditLines = store.getCreditLines();
const allowTreasuryActions = permissions.canManageTreasury();
return h(
"section",
{ className: "card org-panel org-span-5" },
h(
"div",
{ className: "org-panel-head" },
h(
"div",
null,
h("h2", { className: "org-panel-title" }, "Treasury"),
h(
"p",
{ className: "org-panel-subtitle" },
"Organization funds, reputation, and member payouts.",
),
),
),
h(
"div",
{ className: "org-finance-meta" },
h(
"div",
null,
h("span", { className: "org-meta-label" }, "Funds"),
h("strong", null, actions.formatCurrency(store.getFunds())),
),
h(
"div",
null,
h("span", { className: "org-meta-label" }, "Reputation"),
h("strong", null, `${portalData.reputation}`),
),
),
allowTreasuryActions
? h(
"div",
{ className: "org-action-grid" },
h(
"button",
{
type: "button",
onClick: () => actions.openModal("payroll"),
},
"Run Payroll",
),
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.openModal("transfer"),
},
"Send Funds",
),
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.openModal("credit"),
},
"Credit Line",
),
)
: h(
"p",
{ className: "org-access-note" },
"Only the organization leader or CEO can manage treasury actions.",
),
notice.text
? h(
"div",
{
className:
notice.type === "error"
? "org-treasury-notice is-error"
: "org-treasury-notice is-success",
},
notice.text,
)
: null,
creditLines.length > 0
? h(
"div",
{ className: "org-credit-lines" },
h(
"span",
{ className: "org-meta-label" },
"Active Credit Lines",
),
h(
"div",
{ className: "org-credit-list" },
...creditLines.map((line) =>
h(
"div",
{ className: "org-credit-row" },
h("span", null, line.member),
h(
"strong",
null,
actions.formatCurrency(line.amount),
),
),
),
),
)
: null,
);
};
})();

View File

@ -1,71 +1,10 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { portalData, session } = OrgPortal.data;
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
class OrgPortalPermissions {
getNormalizedRole() {
return String(session.role || "")
.trim()
.toUpperCase();
}
isDefaultOrg() {
return (
portalData.org.isDefault === true ||
String(portalData.org.tag || "")
.trim()
.toUpperCase() === "DEFAULT"
);
}
isOrgOwner() {
const ownerUid = String(
portalData.org.ownerUid || portalData.org.owner || "",
)
.trim()
.toLowerCase();
const actorUid = String(session.actorUid || "")
.trim()
.toLowerCase();
if (ownerUid && actorUid) {
return actorUid === ownerUid;
}
return (
String(session.actorName || "")
.trim()
.toLowerCase() ===
String(portalData.org.owner || "")
.trim()
.toLowerCase()
);
}
isSessionCeo() {
return session.ceo === true;
}
isOrgLeaderOrCeo() {
return (
this.isOrgOwner() ||
this.getNormalizedRole() === "LEADER" ||
(this.isDefaultOrg() && this.isSessionCeo())
);
}
canManageMembers() {
return this.isOrgLeaderOrCeo();
}
canManageTreasury() {
return this.isOrgLeaderOrCeo();
}
canDisbandOrg() {
return this.isOrgLeaderOrCeo();
}
}
OrgPortal.permissions = new OrgPortalPermissions();
OrgPortal.permissions = SharedLogic.createPortalPermissions({
portalData,
session,
});
})();

View File

@ -1,98 +0,0 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const SVG_NS = "http://www.w3.org/2000/svg";
const SVG_TAGS = new Set([
"svg",
"path",
"circle",
"rect",
"line",
"polyline",
"polygon",
"g",
"defs",
"use",
"text",
"tspan",
"clipPath",
"mask",
]);
function h(tag, props = {}, ...children) {
const isSvg = SVG_TAGS.has(tag);
const el = isSvg
? document.createElementNS(SVG_NS, tag)
: 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") {
if (isSvg) {
el.setAttribute("class", value);
} else {
el.className = value;
}
} else if (key === "style" && typeof value === "object") {
Object.assign(el.style, value);
} else if (typeof value === "boolean") {
if (value) {
el.setAttribute(key, "");
} else {
el.removeAttribute(key);
}
} else if (value === null || value === undefined) {
el.removeAttribute(key);
} 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) => el.appendChild(c));
}
});
return el;
}
let rootContainer = null;
let rootComponent = null;
function render(component, container) {
rootContainer = container;
rootComponent = component;
rerender();
}
function rerender() {
rootContainer.innerHTML = "";
rootContainer.appendChild(rootComponent());
}
function createSignal(initialValue) {
let value = initialValue;
const getValue = () => value;
const setValue = (newValue) => {
value = typeof newValue === "function" ? newValue(value) : newValue;
rerender();
};
return [getValue, setValue];
}
OrgPortal.runtime = {
h,
render,
createSignal,
};
})();

View File

@ -2,31 +2,10 @@
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { createSignal } = window.RegistryApp.runtime;
const { portalData } = OrgPortal.data;
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
class OrgPortalStore {
constructor() {
[this.getFunds, this.setFunds] = createSignal(portalData.funds);
[this.getMembers, this.setMembers] = createSignal([
...portalData.members,
]);
[this.getCreditLines, this.setCreditLines] = createSignal([]);
[this.getTreasuryNotice, this.setTreasuryNotice] = createSignal({
type: "",
text: "",
});
[this.getModal, this.setModal] = createSignal(null);
[this.getOrgDisbanded, this.setOrgDisbanded] = createSignal(false);
}
hydrateFromPayload(payload) {
this.setFunds(payload.portalData.funds || 0);
this.setMembers([...(payload.portalData.members || [])]);
this.setCreditLines([]);
this.setTreasuryNotice({ type: "", text: "" });
this.setModal(null);
this.setOrgDisbanded(false);
}
}
OrgPortal.store = new OrgPortalStore();
OrgPortal.store = SharedLogic.createPortalStore({
createSignal,
portalData,
});
})();

View File

@ -1,5 +1,6 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const SVG_NS = "http://www.w3.org/2000/svg";
const SVG_TAGS = new Set([
@ -66,6 +67,7 @@
let rootContainer = null;
let rootComponent = null;
const injectedStyles = new Set();
function render(component, container) {
rootContainer = container;
@ -74,10 +76,26 @@
}
function rerender() {
if (!rootContainer || !rootComponent) {
return;
}
rootContainer.innerHTML = "";
rootContainer.appendChild(rootComponent());
}
function ensureScopedStyle(id, cssText) {
if (!id || !cssText || injectedStyles.has(id)) {
return;
}
const style = document.createElement("style");
style.setAttribute("data-ui-style", id);
style.textContent = cssText;
document.head.appendChild(style);
injectedStyles.add(id);
}
function createSignal(initialValue) {
let value = initialValue;
@ -90,9 +108,14 @@
return [getValue, setValue];
}
RegistryApp.runtime = {
const runtime = {
h,
render,
createSignal,
ensureScopedStyle,
};
RegistryApp.runtime = runtime;
OrgPortal.runtime = runtime;
window.AppRuntime = runtime;
})();

View File

@ -1,43 +1,23 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { createSignal } = RegistryApp.runtime;
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
class RegistryStore {
constructor() {
[this.getView, this.setView] = createSignal("home");
[this.getIsAuthenticating, this.setIsAuthenticating] =
createSignal(false);
[this.getLoginError, this.setLoginError] = createSignal("");
}
startLogin() {
this.setLoginError("");
this.setIsAuthenticating(true);
}
failLogin(message) {
this.setIsAuthenticating(false);
this.setLoginError(message || "Authentication failed.");
}
completeLogin(payload) {
RegistryApp.store = SharedLogic.createRegistryStore({
createSignal,
onHydratePortal(payload) {
const OrgPortal = window.OrgPortal;
const portalData = payload?.portalData;
const session = payload?.session;
if (!OrgPortal || !portalData || !session) {
this.failLogin("Login response was missing portal data.");
return;
return false;
}
OrgPortal.data.applyLoginPayload(payload);
OrgPortal.store.hydrateFromPayload(payload);
this.setLoginError("");
this.setIsAuthenticating(false);
this.setView("portal");
}
}
RegistryApp.store = new RegistryStore();
RegistryApp.store.setView("portal");
return true;
},
});
})();

View File

@ -0,0 +1,36 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const registryStore = window.RegistryApp.store;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.DisbandedView = function DisbandedView() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
return PanelCard({
className: "org-span-12 org-empty-state",
eyebrow: "Organization Removed",
title: portalData.org.name,
body: h(
"div",
null,
h(
"p",
{ className: "org-summary" },
"This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview.",
),
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => registryStore.setView("home"),
},
"Return to Registry",
),
),
});
};
})();

View File

@ -0,0 +1,89 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h, ensureScopedStyle } = RegistryApp.runtime;
const store = RegistryApp.store;
const bridge = RegistryApp.bridge;
const scopeAttr = "data-ui-home-view";
const scopeSelector = `[${scopeAttr}]`;
const homeViewCss = `
${scopeSelector} {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
${scopeSelector} .home-feedback {
padding: 0.85rem 1rem;
border-radius: var(--radius);
font-size: 0.92rem;
background: #fef2f2;
border: 1px solid #fecaca;
color: #991b1b;
}
@media (max-width: 960px) {
${scopeSelector} {
grid-template-columns: 1fr;
}
}
`;
RegistryApp.componentFns = RegistryApp.componentFns || {};
RegistryApp.componentFns.HomeView = function HomeView() {
const isAuthenticating = store.getIsAuthenticating();
const loginError = store.getLoginError();
ensureScopedStyle("main-home-view", homeViewCss);
return h(
"div",
{ className: "content", [scopeAttr]: "" },
h(
"div",
{ className: "card" },
h("h2", null, "Create Organization"),
h(
"p",
null,
"Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly.",
),
h(
"button",
{ onClick: () => store.setView("create") },
"Register",
),
),
h(
"div",
{ className: "card" },
h("h2", null, "Organization Portal"),
h(
"p",
null,
"Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink.",
),
loginError
? h("div", { className: "home-feedback" }, loginError)
: null,
h(
"button",
{
disabled: isAuthenticating,
onClick: () => {
if (!bridge) {
store.failLogin(
"Login bridge is not available.",
);
return;
}
bridge.requestLogin({});
},
},
isAuthenticating ? "Opening Portal..." : "Login",
),
),
);
};
})();

View File

@ -0,0 +1,206 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const { portalData, session } = OrgPortal.data;
const store = OrgPortal.store;
const portalViewScope = "[data-ui-portal-view]";
ensureScopedStyle(
"portal-view",
`
${portalViewScope} .org-toast-stack {
position: fixed;
top: 1.5rem;
right: 2rem;
z-index: 20;
display: flex;
flex-direction: column;
gap: 0.75rem;
pointer-events: none;
}
${portalViewScope} .org-toast {
max-width: 24rem;
padding: 0.9rem 1rem;
border-radius: var(--radius);
border: 1px solid var(--border);
background: #fff;
box-shadow: 0 12px 28px rgb(15 23 42 / 0.14);
font-size: 0.92rem;
pointer-events: auto;
}
${portalViewScope} .org-toast.is-success {
background: #ecfdf5;
border-color: #bbf7d0;
color: #166534;
}
${portalViewScope} .org-toast.is-error {
background: #fef2f2;
border-color: #fecaca;
color: #991b1b;
}
${portalViewScope} .org-dashboard-grid {
display: grid;
grid-template-columns: repeat(12, minmax(0, 1fr));
gap: 1.5rem;
}
${portalViewScope} .org-panel {
margin-bottom: 0;
text-align: left;
}
${portalViewScope} .org-scroll-panel {
display: flex;
flex-direction: column;
max-height: 31rem;
overflow: hidden;
}
${portalViewScope} .org-span-12 {
grid-column: span 12;
}
${portalViewScope} .org-span-7 {
grid-column: span 7;
}
${portalViewScope} .org-span-6 {
grid-column: span 6;
}
${portalViewScope} .org-span-5 {
grid-column: span 5;
}
@media (max-width: 960px) {
${portalViewScope} .org-toast-stack {
top: 1rem;
right: 1rem;
left: 1rem;
}
${portalViewScope} .org-toast {
max-width: none;
}
${portalViewScope} .org-span-12,
${portalViewScope} .org-span-7,
${portalViewScope} .org-span-6,
${portalViewScope} .org-span-5 {
grid-column: span 12;
}
}
`,
);
OrgPortal.components = OrgPortal.components || {};
OrgPortal.components.App = function App() {
const Hero = window.SharedUI.componentFns.Hero;
const Footer = window.SharedUI.componentFns.Footer;
const OverviewCard = OrgPortal.componentFns.OverviewCard;
const FleetCard = OrgPortal.componentFns.FleetCard;
const TreasuryCard = OrgPortal.componentFns.TreasuryCard;
const MembersCard = OrgPortal.componentFns.MembersCard;
const AssetsCard = OrgPortal.componentFns.AssetsCard;
const ActivityCard = OrgPortal.componentFns.ActivityCard;
const FutureCard = OrgPortal.componentFns.FutureCard;
const DangerCard = OrgPortal.componentFns.DangerCard;
const ModalLayer = OrgPortal.componentFns.ModalLayer;
const DisbandedView = OrgPortal.componentFns.DisbandedView;
const treasuryNotice = store.getTreasuryNotice();
const footerSections = [
{
title: "Organization Controls",
items: [
"Roster Management",
"Fleet Assignment",
"Treasury Permissions",
"Asset Registry",
],
},
{
title: "Planned Extensions",
items: [
"Contracts Board",
"Diplomacy Layer",
"Procurement Queue",
"Reputation History",
],
},
];
if (store.getOrgDisbanded()) {
return h(
"main",
{ "data-ui-portal-view": "" },
h(
"div",
{ className: "container" },
h(
"div",
{ className: "org-dashboard-grid" },
Hero({
kicker: portalData.org.tag,
title: portalData.org.name,
subtitle: "Player organization command portal",
meta: `${session.actorName} - ${session.role}`,
}),
DisbandedView(),
),
),
ModalLayer(),
Footer({ sections: footerSections }),
);
}
return h(
"main",
{ "data-ui-portal-view": "" },
treasuryNotice.text
? h(
"div",
{ className: "org-toast-stack" },
h(
"div",
{
className:
treasuryNotice.type === "error"
? "org-toast is-error"
: "org-toast is-success",
},
treasuryNotice.text,
),
)
: null,
h(
"div",
{ className: "container" },
h(
"div",
{ className: "org-dashboard-grid" },
Hero({
kicker: portalData.org.tag,
title: portalData.org.name,
subtitle: "Player organization command portal",
meta: `${session.actorName} - ${session.role}`,
}),
OverviewCard(),
FleetCard(),
TreasuryCard(),
MembersCard(),
AssetsCard(),
ActivityCard(),
FutureCard(),
DangerCard(),
),
),
ModalLayer(),
Footer({ sections: footerSections }),
);
};
})();

View File

@ -1,26 +1,175 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
const { h, ensureScopedStyle } = RegistryApp.runtime;
const store = RegistryApp.store;
const bridge = RegistryApp.bridge;
const scopeAttr = "data-ui-registration-view";
const scopeSelector = `[${scopeAttr}]`;
const registrationViewCss = `
${scopeSelector} {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
align-items: center;
width: 100%;
}
${scopeSelector} .info-panel {
text-align: left;
padding: 1rem;
}
${scopeSelector} .create-feature-list {
text-align: left;
margin-top: 1.5rem;
list-style-type: none;
padding: 0;
}
${scopeSelector} .create-feature-item {
margin-bottom: 0.5rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
${scopeSelector} .create-feature-icon {
width: 1.2rem;
height: 1.2rem;
flex-shrink: 0;
}
${scopeSelector} .price-tag {
margin-top: 2rem;
padding: 1rem;
background: var(--bg-app);
border-radius: var(--radius);
border: 1px solid var(--border);
}
${scopeSelector} .price-label {
display: block;
font-size: 0.9rem;
color: var(--text-muted);
}
${scopeSelector} .price-value {
display: block;
font-size: 2rem;
font-weight: 700;
color: var(--primary);
}
${scopeSelector} .form-panel {
margin: 0;
}
${scopeSelector} .app-form {
display: flex;
flex-direction: column;
gap: 1rem;
text-align: left;
}
${scopeSelector} .app-form label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-muted);
font-weight: 500;
font-size: 0.9rem;
}
${scopeSelector} .app-form input,
${scopeSelector} .app-form 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;
}
${scopeSelector} .app-form input:focus,
${scopeSelector} .app-form select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1);
}
${scopeSelector} .form-actions {
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
}
${scopeSelector} .submit-btn {
width: 100%;
}
${scopeSelector} .cancel-link {
font-size: 0.9rem;
color: var(--text-muted);
cursor: pointer;
text-decoration: underline;
}
${scopeSelector} .cancel-link:hover {
color: var(--primary);
}
${scopeSelector} .form-feedback {
padding: 0.85rem 1rem;
border-radius: var(--radius);
font-size: 0.92rem;
}
${scopeSelector} .form-feedback.is-error {
background: #fef2f2;
border: 1px solid #fecaca;
color: #991b1b;
}
@media (max-width: 960px) {
${scopeSelector} {
grid-template-columns: 1fr;
}
}
`;
RegistryApp.componentFns = RegistryApp.componentFns || {};
RegistryApp.componentFns.CreateOrgForm = function CreateOrgForm() {
RegistryApp.componentFns.RegistrationView = function RegistrationView() {
const isCreating = store.getIsCreating();
const createError = store.getCreateError();
ensureScopedStyle("main-registration-view", registrationViewCss);
const handleCreate = () => {
const data = {
orgName: String(
document.getElementById("org-create-name")?.value || "",
),
).trim(),
type: String(
document.getElementById("org-create-type")?.value || "",
),
};
console.log("Org Registration:", data);
if (!bridge || typeof bridge.requestCreateOrg !== "function") {
store.failCreate("Registration bridge is not available.");
return;
}
bridge.requestCreateOrg(data);
};
return h(
"div",
{ className: "split-container" },
{ className: "split-container", [scopeAttr]: "" },
h(
"div",
{ className: "info-panel" },
@ -32,24 +181,10 @@
),
h(
"ul",
{
style: {
textAlign: "left",
marginTop: "1.5rem",
listStyleType: "none",
padding: 0,
},
},
{ className: "create-feature-list" },
h(
"li",
{
style: {
marginBottom: "0.5rem",
display: "flex",
alignItems: "center",
gap: "0.5rem",
},
},
{ className: "create-feature-item" },
h(
"svg",
{
@ -59,11 +194,7 @@
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
style: {
width: "1.2rem",
height: "1.2rem",
flexShrink: "0",
},
className: "create-feature-icon",
},
h("path", { d: "M20 6L9 17l-5-5" }),
),
@ -71,14 +202,7 @@
),
h(
"li",
{
style: {
marginBottom: "0.5rem",
display: "flex",
alignItems: "center",
gap: "0.5rem",
},
},
{ className: "create-feature-item" },
h(
"svg",
{
@ -88,11 +212,7 @@
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
style: {
width: "1.2rem",
height: "1.2rem",
flexShrink: "0",
},
className: "create-feature-icon",
},
h("path", { d: "M20 6L9 17l-5-5" }),
),
@ -100,14 +220,7 @@
),
h(
"li",
{
style: {
marginBottom: "0.5rem",
display: "flex",
alignItems: "center",
gap: "0.5rem",
},
},
{ className: "create-feature-item" },
h(
"svg",
{
@ -117,11 +230,7 @@
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
style: {
width: "1.2rem",
height: "1.2rem",
flexShrink: "0",
},
className: "create-feature-icon",
},
h("path", { d: "M20 6L9 17l-5-5" }),
),
@ -129,14 +238,7 @@
),
h(
"li",
{
style: {
marginBottom: "0.5rem",
display: "flex",
alignItems: "center",
gap: "0.5rem",
},
},
{ className: "create-feature-item" },
h(
"svg",
{
@ -146,11 +248,7 @@
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
style: {
width: "1.2rem",
height: "1.2rem",
flexShrink: "0",
},
className: "create-feature-icon",
},
h("path", { d: "M20 6L9 17l-5-5" }),
),
@ -159,44 +257,14 @@
),
h(
"div",
{
className: "price-tag",
style: {
marginTop: "2rem",
padding: "1rem",
background: "var(--bg-app)",
borderRadius: "var(--radius)",
border: "1px solid var(--border)",
},
},
h(
"span",
{
style: {
display: "block",
fontSize: "0.9rem",
color: "var(--text-muted)",
},
},
"Registration Fee",
),
h(
"span",
{
style: {
display: "block",
fontSize: "2rem",
fontWeight: "700",
color: "var(--primary)",
},
},
"$50,000",
),
{ className: "price-tag" },
h("span", { className: "price-label" }, "Registration Fee"),
h("span", { className: "price-value" }, "$50,000"),
),
),
h(
"div",
{ className: "form-panel card", style: { margin: 0 } },
{ className: "form-panel card" },
h("h2", null, "Organization Registration"),
h(
"div",
@ -239,14 +307,24 @@
h(
"div",
{ className: "form-actions" },
createError
? h(
"div",
{ className: "form-feedback is-error" },
createError,
)
: null,
h(
"button",
{
type: "button",
style: { width: "100%" },
className: "submit-btn",
disabled: isCreating,
onClick: handleCreate,
},
"Submit Registration",
isCreating
? "Submitting Registration..."
: "Submit Registration",
),
h(
"span",

View File

@ -14,6 +14,29 @@ PREP_RECOMPILE_END;
GVAR(OrgStore) call ["init", [_uid]];
}] call CFUNC(addEventHandler);
[QGVAR(requestCreateOrg), {
params [["_uid", "", [""]], ["_orgName", "", [""]]];
if (_uid isEqualTo "" || { _orgName isEqualTo "" }) exitWith {
diag_log "[FORGE:Server:Org] Empty/Invalid UID or Organization Name!"
};
private _player = [_uid] call EFUNC(common,getPlayer);
private _result = GVAR(OrgStore) call ["register", [_uid, _orgName]];
if (_result getOrDefault ["success", false]) then {
private _org = _result getOrDefault ["org", createHashMap];
private _orgID = _org getOrDefault ["id", ""];
if (_orgID isNotEqualTo "") then {
private _actorPatch = EGVAR(actor,ActorStore) call ["set", [EGVAR(actor,Registry), "actor:update", _uid, "organization", _orgID, true]];
[CRPC(actor,responseSyncActor), [_actorPatch], _player] call CFUNC(targetEvent);
};
};
[CRPC(org,responseCreateOrg), [_result], _player] call CFUNC(targetEvent);
}] call CFUNC(addEventHandler);
[QGVAR(requestGetOrg), {
params [["_uid", "", [""]], ["_field", "", [""]]];

View File

@ -82,7 +82,7 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
if !(_isSuccess) exitWith {
["ERROR", "Failed to check for default org!"] call EFUNC(common,log);
private _fallbackDefaultOrg = createHashMapFromArray [
private _defaultOrg = createHashMapFromArray [
["id", "default"],
["owner", "server"],
["name", "Forge Dynamics"],
@ -90,9 +90,9 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["reputation", 0],
["members", createHashMap]
];
GVAR(Registry) set ["default", _fallbackDefaultOrg];
GVAR(Registry) set ["default", _defaultOrg];
_fallbackDefaultOrg
_defaultOrg
};
private _finalOrg = createHashMap;
@ -112,6 +112,109 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
GVAR(Registry) set ["default", _finalOrg];
}],
["verifyMember", compileFinal {
params [["_org", createHashMap, [createHashMap]], ["_orgID", "", [""]], ["_uid", "", [""]], ["_player", objNull, [objNull]], ["_actor", createHashMap, [createHashMap]]];
if (_orgID isEqualTo "" || { _uid isEqualTo "" }) exitWith { _org };
private _members = _org getOrDefault ["members", createHashMap];
if ((_members getOrDefault [_uid, objNull]) isNotEqualTo objNull) exitWith { _org };
["org:members:add", [_orgID, _uid]] call EFUNC(extension,extCall) params ["_memberResult", "_memberSuccess"];
if (!_memberSuccess) then {
["WARNING", format ["Failed to add %1 to org %2 members: %3", _uid, _orgID, _memberResult]] call EFUNC(common,log);
};
private _memberName = _actor getOrDefault ["name", ""];
if (_memberName isEqualTo "" && { _player isNotEqualTo objNull }) then { _memberName = name _player; };
if (_memberName isEqualTo "") then { _memberName = "Unknown"; };
private _finalMembers = +_members;
_finalMembers set [_uid, createHashMapFromArray [["uid", _uid], ["name", _memberName]]];
_org set ["members", _finalMembers];
_org
}],
["register", compileFinal {
params [["_uid", "", [""]], ["_orgName", "", [""]]];
private _result = createHashMapFromArray [
["success", false],
["message", ""],
["org", createHashMap]
];
if (_uid isEqualTo "" || { _orgName isEqualTo "" }) exitWith {
_result set ["message", "A valid player and organization name are required."];
_result
};
private _player = [_uid] call EFUNC(common,getPlayer);
private _actor = EGVAR(actor,Registry) getOrDefault [_uid, createHashMap];
private _existingOrgID = _actor getOrDefault ["organization", ""];
if (_existingOrgID isNotEqualTo "") exitWith {
_result set ["message", "Player already belongs to an organization."];
_result
};
private _orgID = _actor getOrDefault ["phone_number", ""];
if (_orgID isEqualTo "") exitWith {
_result set ["message", "Player phone number was not available for organization registration."];
_result
};
["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_existsResult", "_existsSuccess"];
if (!_existsSuccess) exitWith {
_result set ["message", "Unable to verify organization ID availability."];
_result
};
if (_existsResult isEqualTo "true") exitWith {
_result set ["message", "An organization already exists for this phone number."];
_result
};
private _org = createHashMapFromArray [
["id", _orgID],
["owner", _uid],
["name", _orgName],
["funds", 0],
["reputation", 0],
["members", createHashMap]
];
private _json = _self call ["toJSON", [_org]];
["org:create", [_orgID, _json]] call EFUNC(extension,extCall) params ["_createResult", "_createSuccess"];
if (!_createSuccess) exitWith {
_result set ["message", format ["Failed to create organization: %1", _createResult]];
_result
};
if (_createResult isNotEqualTo "") then {
_org = _self call ["toHashMap", [_createResult]];
};
_org set ["members", createHashMap];
_org = _self call ["verifyMember", [_org, _orgID, _uid, _player, _actor]];
["org:members:remove", ["default", _uid]] call EFUNC(extension,extCall);
private _defaultOrg = GVAR(Registry) getOrDefault ["default", createHashMap];
if (_defaultOrg isNotEqualTo createHashMap) then {
private _defaultMembers = _defaultOrg getOrDefault ["members", createHashMap];
_defaultMembers deleteAt _uid;
_defaultOrg set ["members", _defaultMembers];
GVAR(Registry) set ["default", _defaultOrg, true];
};
GVAR(IndexRegistry) set [_uid, createHashMapFromArray [["orgID", _orgID]]];
GVAR(Registry) set [_orgID, _org, true];
_result set ["success", true];
_result set ["org", _org];
_result
}],
["init", compileFinal {
params [["_uid", "", [""]]];
@ -121,24 +224,26 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
if (_orgID isEqualTo "") then { _orgID = "default" };
private _cached = GVAR(Registry) getOrDefault [_orgID, nil];
if !(isNil { _cached }) exitWith { [CRPC(org,responseInitOrg), [_cached], _player] call CFUNC(targetEvent); _cached };
if !(isNil { _cached }) exitWith {
private _cachedOwner = _cached getOrDefault ["owner", ""];
if (_orgID isEqualTo "default" || { _cachedOwner isEqualTo _uid }) then {
_cached = _self call ["verifyMember", [_cached, _orgID, _uid, _player, _actor]];
};
GVAR(Registry) set [_orgID, _cached, true];
[CRPC(org,responseInitOrg), [_cached], _player] call CFUNC(targetEvent);
_cached
};
["org:exists", [_orgID]] call EFUNC(extension,extCall) params ["_result", "_isSuccess"];
if !(_isSuccess) exitWith {
["ERROR", format ["Failed to check for org %1! Using fallback org.", _orgID]] call EFUNC(common,log);
private _fallbackOrg = GVAR(Registry) getOrDefault ["default", createHashMapFromArray [
["id", "default"],
["owner", "server"],
["name", "Forge Dynamics"],
["funds", 200000],
["reputation", 0],
["members", createHashMap]
]];
private _fallbackOrg = GVAR(Registry) getOrDefault ["default", createHashMap];
private _entry = createHashMapFromArray [["orgID", _orgID]];
GVAR(IndexRegistry) set [_uid, _entry];
if (_orgID isEqualTo "default") then { _fallbackOrg = _self call ["verifyMember", [_fallbackOrg, _orgID, _uid, _player, _actor]]; };
GVAR(Registry) set [_orgID, _fallbackOrg, true];
[CRPC(org,responseInitOrg), [_fallbackOrg], _player] call CFUNC(targetEvent);
@ -154,6 +259,8 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
["INFO", format ["Found org for %1", _orgID]] call EFUNC(common,log);
} else {
["WARNING", format ["No existing org found for %1, using default org.", _uid]] call EFUNC(common,log);
_finalOrg = GVAR(Registry) getOrDefault ["default", createHashMap];
_orgID = "default";
};
private _entry = createHashMapFromArray [["orgID", _orgID]];
@ -177,6 +284,10 @@ GVAR(OrgBaseStore) = compileFinal createHashMapFromArray [
_finalOrg set ["members", _finalMembers];
// _finalOrg set ["assets", _finalAssets];
private _finalOwner = _finalOrg getOrDefault ["owner", ""];
if (_orgID isEqualTo "default" || { _finalOwner isEqualTo _uid }) then {
_finalOrg = _self call ["verifyMember", [_finalOrg, _orgID, _uid, _player, _actor]];
};
GVAR(Registry) set [_orgID, _finalOrg, true];
[CRPC(org,responseInitOrg), [_finalOrg], _player] call CFUNC(targetEvent);

View File

@ -0,0 +1,107 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
const store = RegistryApp.store;
RegistryApp.components = RegistryApp.components || {};
RegistryApp.components.App = function App() {
const Navbar = window.SharedUI.componentFns.Navbar;
const Header = window.SharedUI.componentFns.Header;
const Footer = window.SharedUI.componentFns.Footer;
const HomeView = RegistryApp.componentFns.HomeView;
const RegistrationView = RegistryApp.componentFns.RegistrationView;
const PortalApp =
window.OrgPortal && window.OrgPortal.components
? window.OrgPortal.components.App
: null;
const view = store.getView();
const viewLabel =
view === "create"
? "Organization Registration"
: view === "portal"
? "Organization Portal"
: "Entry Hub";
const actionLabel = view === "portal" ? "Sign Out" : "Close";
const footerSections = [
{
title: "Registry Resources",
items: [
"Registration Guidelines",
"Tax & Fee Schedule",
"Legal Compliance",
"Trademark Database",
],
},
{
title: "Bureau Support",
items: [
"Office: Sector 7 Admin Block",
"Hours: 0800 - 1600 (GST)",
"Helpdesk: 555-01-REGISTRY",
"support@org-bureau.gov",
],
},
];
function closeRegistry() {
if (
typeof A3API !== "undefined" &&
typeof A3API.SendAlert === "function"
) {
A3API.SendAlert(
JSON.stringify({
event: "org::close",
data: {},
}),
);
return;
}
store.setView("home");
}
if (view === "portal" && PortalApp) {
return h(
"div",
null,
Navbar({
title: "Global Organization Network",
viewLabel,
actionLabel,
onAction: closeRegistry,
}),
PortalApp(),
);
}
let mainContent;
if (view === "home") {
mainContent = HomeView();
} else if (view === "create") {
mainContent = RegistrationView();
}
return h(
"main",
null,
Navbar({
title: "Global Organization Network",
viewLabel,
actionLabel,
onAction: closeRegistry,
}),
h(
"div",
{ className: "container" },
Header({
title: "Global Organization Network",
onTitleClick: () => store.setView("home"),
}),
mainContent,
),
Footer({ sections: footerSections }),
);
};
})();

View File

@ -0,0 +1,32 @@
(function () {
const SharedUI = (window.SharedUI = window.SharedUI || {});
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
SharedUI.componentFns = SharedUI.componentFns || {};
SharedUI.componentFns.Footer = function Footer({ sections = [] }) {
return h(
"div",
{ className: "footer" },
h(
"div",
{ className: "wrapper" },
...sections.map((section) =>
h(
"div",
null,
h("h3", null, section.title),
h(
"ul",
{ style: { listStyleType: "none", padding: 0 } },
...(section.items || []).map((item) =>
h("li", null, item),
),
),
),
),
),
);
};
})();

View File

@ -0,0 +1,27 @@
(function () {
const SharedUI = (window.SharedUI = window.SharedUI || {});
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
SharedUI.componentFns = SharedUI.componentFns || {};
SharedUI.componentFns.Header = function Header({
title,
subtitle = "Organization Registration & Management Portal",
onTitleClick = null,
}) {
return h(
"div",
{ className: "header" },
h(
"h1",
{
style: { cursor: onTitleClick ? "pointer" : "default" },
onClick: onTitleClick,
},
title,
),
h("p", null, subtitle),
);
};
})();

View File

@ -0,0 +1,35 @@
(function () {
const SharedUI = (window.SharedUI = window.SharedUI || {});
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
SharedUI.componentFns = SharedUI.componentFns || {};
SharedUI.componentFns.Hero = function Hero({
className = "",
kicker = "",
title = "",
subtitle = "",
meta = "",
}) {
const finalClassName = [
"card org-panel org-span-12 org-page-header",
className,
]
.filter(Boolean)
.join(" ");
return h(
"section",
{ className: finalClassName },
h(
"div",
{ className: "org-page-heading" },
h("span", { className: "org-page-kicker" }, kicker),
h("h1", { className: "org-page-title" }, title),
h("p", { className: "org-page-subtitle" }, subtitle),
h("span", { className: "org-page-meta" }, meta),
),
);
};
})();

View File

@ -0,0 +1,190 @@
(function () {
const SharedUI = (window.SharedUI = window.SharedUI || {});
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h, ensureScopedStyle } = RegistryApp.runtime;
const scopeAttr = "data-ui-modal";
const scopeSelector = `[${scopeAttr}]`;
const modalCss = `
${scopeSelector} {
position: fixed;
inset: 0;
background: rgb(15 23 42 / 0.38);
display: flex;
align-items: center;
justify-content: center;
padding: 1.5rem;
z-index: 20;
}
${scopeSelector} .app-modal-card {
width: min(100%, 30rem);
margin-bottom: 0;
text-align: left;
}
${scopeSelector} .app-modal-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1rem;
}
${scopeSelector} .app-modal-title {
margin: 0;
color: var(--primary-hover);
font-size: 1.45rem;
}
${scopeSelector} .app-modal-close {
width: 2.25rem;
height: 2.25rem;
padding: 0;
background: var(--bg-surface);
color: var(--text-main);
border: 1px solid var(--border);
box-shadow: none;
transform: none;
}
${scopeSelector} .app-modal-close:hover {
background: var(--bg-surface-hover);
color: var(--text-main);
box-shadow: none;
transform: none;
}
${scopeSelector} .app-modal-form {
display: flex;
flex-direction: column;
gap: 1rem;
}
${scopeSelector} .app-modal-form label {
display: block;
margin-bottom: 0.5rem;
color: var(--text-muted);
font-weight: 500;
font-size: 0.9rem;
}
${scopeSelector} .app-modal-form input,
${scopeSelector} .app-modal-form 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, box-shadow 0.2s;
}
${scopeSelector} .app-modal-form input:focus,
${scopeSelector} .app-modal-form select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12);
}
${scopeSelector} .app-modal-form input:disabled,
${scopeSelector} .app-modal-form select:disabled {
background: #f1f5f9;
color: var(--text-muted);
cursor: not-allowed;
}
${scopeSelector} .app-modal-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 0.75rem;
margin-top: 0.5rem;
}
${scopeSelector} .app-modal-actions button + button,
${scopeSelector} .app-modal-danger-actions button + button {
margin-left: 0;
}
${scopeSelector} .app-modal-danger {
display: flex;
flex-direction: column;
justify-content: space-between;
gap: 1rem;
padding: 1rem;
border: 1px solid #fecaca;
border-radius: var(--radius);
background: #fff1f2;
align-items: flex-start;
}
${scopeSelector} .app-modal-danger p {
margin: 0;
color: var(--text-main);
}
${scopeSelector} .app-modal-danger-actions {
display: flex;
flex-wrap: wrap;
gap: 0.75rem;
}
@media (max-width: 960px) {
${scopeSelector} .app-modal-head,
${scopeSelector} .app-modal-danger {
flex-direction: column;
align-items: flex-start;
}
}
`;
SharedUI.componentFns = SharedUI.componentFns || {};
SharedUI.componentFns.Modal = function Modal({
title = "",
body = null,
onClose = null,
}) {
ensureScopedStyle("shared-modal", modalCss);
return h(
"div",
{
className: "app-modal-backdrop",
[scopeAttr]: "",
onClick: (e) => {
if (e.target === e.currentTarget && onClose) {
onClose();
}
},
},
h(
"div",
{ className: "card app-modal-card" },
h(
"div",
{ className: "app-modal-head" },
h(
"div",
null,
h("h2", { className: "app-modal-title" }, title),
),
h(
"button",
{
type: "button",
className: "app-modal-close",
onClick: onClose,
"aria-label": "Close dialog",
},
"x",
),
),
body,
),
);
};
})();

View File

@ -0,0 +1,129 @@
(function () {
const SharedUI = (window.SharedUI = window.SharedUI || {});
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h, ensureScopedStyle } = RegistryApp.runtime;
const scopeAttr = "data-ui-navbar";
const scopeSelector = `[${scopeAttr}]`;
const navbarCss = `
${scopeSelector} {
background: var(--bg-surface);
border-bottom: 1px solid var(--border);
box-shadow: var(--shadow);
}
${scopeSelector} .app-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;
}
${scopeSelector} .app-navbar-brand {
display: flex;
flex-direction: column;
gap: 0.125rem;
}
${scopeSelector} .app-navbar-kicker {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
font-weight: 600;
}
${scopeSelector} .app-navbar-title {
font-size: 1.25rem;
font-weight: 700;
color: var(--primary-hover);
letter-spacing: -0.025em;
}
${scopeSelector} .app-navbar-actions {
display: flex;
align-items: center;
gap: 1.5rem;
}
${scopeSelector} .app-navbar-view {
font-size: 0.8rem;
text-transform: uppercase;
letter-spacing: 0.05em;
color: var(--text-muted);
font-weight: 600;
}
${scopeSelector} .app-close-btn {
background: transparent;
color: var(--text-muted);
border: 1px solid var(--border);
padding: 0.5rem 1rem;
font-size: 0.85rem;
}
${scopeSelector} .app-close-btn:hover {
background: var(--bg-surface-hover);
color: var(--primary-hover);
border-color: var(--primary);
transform: none;
box-shadow: none;
}
@media (max-width: 960px) {
${scopeSelector} .app-navbar-inner {
flex-direction: column;
align-items: flex-start;
padding: 1rem 1.5rem;
}
${scopeSelector} .app-navbar-actions {
align-items: flex-start;
}
}
`;
SharedUI.componentFns = SharedUI.componentFns || {};
SharedUI.componentFns.Navbar = function Navbar({
kicker = "ORBIS",
title = "",
viewLabel = "",
actionLabel = "",
onAction = null,
}) {
ensureScopedStyle("shared-navbar", navbarCss);
return h(
"nav",
{ className: "app-navbar", [scopeAttr]: "" },
h(
"div",
{ className: "app-navbar-inner" },
h(
"div",
{ className: "app-navbar-brand" },
h("span", { className: "app-navbar-kicker" }, kicker),
h("span", { className: "app-navbar-title" }, title),
),
h(
"div",
{ className: "app-navbar-actions" },
h("span", { className: "app-navbar-view" }, viewLabel),
h(
"button",
{
type: "button",
className: "app-close-btn",
onClick: onAction,
},
actionLabel,
),
),
),
);
};
})();

View File

@ -0,0 +1,83 @@
(function () {
const SharedUI = (window.SharedUI = window.SharedUI || {});
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h, ensureScopedStyle } = RegistryApp.runtime;
const scopeAttr = "data-ui-panel-card";
const scopeSelector = `[${scopeAttr}]`;
const panelCardCss = `
${scopeSelector} .org-panel-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 1rem;
margin-bottom: 1.5rem;
}
${scopeSelector} .org-eyebrow {
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--text-muted);
margin-bottom: 0.4rem;
}
${scopeSelector} .org-panel-title {
margin: 0;
color: var(--primary-hover);
font-size: 1.45rem;
}
${scopeSelector} .org-panel-subtitle {
margin: 0.35rem 0 0;
color: var(--text-muted);
font-size: 0.95rem;
}
@media (max-width: 960px) {
${scopeSelector} .org-panel-head {
flex-direction: column;
align-items: flex-start;
}
}
`;
SharedUI.componentFns = SharedUI.componentFns || {};
SharedUI.componentFns.PanelCard = function PanelCard({
className = "",
eyebrow = "",
title = "",
subtitle = "",
headerExtras = null,
body = null,
rootProps = {},
}) {
const finalClassName = ["card org-panel", className]
.filter(Boolean)
.join(" ");
ensureScopedStyle("shared-panel-card", panelCardCss);
return h(
"section",
{ className: finalClassName, [scopeAttr]: "", ...rootProps },
h(
"div",
{ className: "org-panel-head" },
h(
"div",
null,
eyebrow
? h("div", { className: "org-eyebrow" }, eyebrow)
: null,
h("h2", { className: "org-panel-title" }, title),
subtitle
? h("p", { className: "org-panel-subtitle" }, subtitle)
: null,
),
headerExtras,
),
body,
);
};
})();

View File

@ -0,0 +1,79 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const scopeAttr = "data-ui-activity-card";
const scopeSelector = `[${scopeAttr}]`;
const activityCardCss = `
${scopeSelector} .org-activity-list {
display: flex;
flex-direction: column;
flex: 1;
gap: 0.85rem;
min-height: 0;
overflow: auto;
padding-right: 0.35rem;
scrollbar-width: thin;
scrollbar-color: #94a3b8 #e2e8f0;
}
${scopeSelector} .org-activity-row {
padding: 1rem;
border: 1px solid var(--border);
border-left: 3px solid #94a3b8;
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-activity-row:nth-child(even) {
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
border-color: rgb(148 163 184 / 0.45);
border-left-color: #64748b;
}
${scopeSelector} .org-activity-row p {
margin: 0;
color: var(--text-main);
}
${scopeSelector} .org-activity-time {
display: inline-block;
margin-bottom: 0.35rem;
color: var(--text-muted);
font-size: 0.8rem;
font-weight: 700;
letter-spacing: 0.05em;
text-transform: uppercase;
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.ActivityCard = function ActivityCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
ensureScopedStyle("portal-activity-card", activityCardCss);
return PanelCard({
className: "org-scroll-panel org-span-6",
title: "Command Feed",
subtitle: "Recent organization-level actions and updates.",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
{ className: "org-activity-list" },
...portalData.activity.map((item) =>
h(
"article",
{ className: "org-activity-row" },
h(
"span",
{ className: "org-activity-time" },
item.time,
),
h("p", null, item.text),
),
),
),
});
};
})();

View File

@ -0,0 +1,94 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const actions = OrgPortal.actions;
const scopeAttr = "data-ui-assets-card";
const scopeSelector = `[${scopeAttr}]`;
const assetsCardCss = `
${scopeSelector} .org-simple-list {
display: flex;
flex-direction: column;
flex: 1;
gap: 0.85rem;
min-height: 0;
overflow: auto;
padding-right: 0.35rem;
scrollbar-width: thin;
scrollbar-color: #94a3b8 #e2e8f0;
}
${scopeSelector} .org-simple-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-simple-row:nth-child(even) {
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
border-color: rgb(148 163 184 / 0.45);
}
${scopeSelector} .org-simple-name {
color: var(--primary-hover);
}
${scopeSelector} .org-simple-meta {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 1rem;
}
@media (max-width: 960px) {
${scopeSelector} .org-simple-row {
flex-direction: column;
align-items: flex-start;
}
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.AssetsCard = function AssetsCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
const SimpleStat = OrgPortal.componentFns.SimpleStat;
ensureScopedStyle("portal-assets-card", assetsCardCss);
return PanelCard({
className: "org-scroll-panel org-span-7",
title: "Assets",
subtitle: "Inventory supplies and equipment with quantity totals.",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
{ className: "org-simple-list" },
...portalData.assets.map((asset) =>
h(
"article",
{ className: "org-simple-row" },
h(
"strong",
{ className: "org-simple-name" },
asset.name,
),
h(
"div",
{ className: "org-simple-meta" },
SimpleStat(
"Type",
actions.formatAssetType(asset.type),
),
SimpleStat("Quantity", asset.quantity),
),
),
),
),
});
};
})();

View File

@ -0,0 +1,70 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const permissions = OrgPortal.permissions;
const actions = OrgPortal.actions;
const scopeAttr = "data-ui-danger-card";
const scopeSelector = `[${scopeAttr}]`;
const dangerCardCss = `
${scopeSelector} {
border-color: #fecaca;
background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%);
}
${scopeSelector} .org-danger-copy {
margin-bottom: 1rem;
}
${scopeSelector} .org-danger-copy strong,
${scopeSelector} .org-danger-copy p {
display: block;
}
${scopeSelector} .org-danger-copy p {
margin: 0.4rem 0 0;
color: var(--text-muted);
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.DangerCard = function DangerCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
ensureScopedStyle("portal-danger-card", dangerCardCss);
if (!permissions.canDisbandOrg()) {
return null;
}
return PanelCard({
className: "org-span-12 org-danger-panel",
title: "Organization Controls",
subtitle:
"Leader-only actions for membership and permanent organization removal.",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
null,
h(
"div",
{ className: "org-danger-copy" },
h("strong", null, "Disband organization"),
h(
"p",
null,
"This removes the organization and revokes access to the portal for all members.",
),
),
h(
"button",
{
type: "button",
className: "org-danger-btn",
onClick: () => actions.openModal("disband"),
},
"Disband Organization",
),
),
});
};
})();

View File

@ -0,0 +1,101 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const actions = OrgPortal.actions;
const scopeAttr = "data-ui-fleet-card";
const scopeSelector = `[${scopeAttr}]`;
const fleetCardCss = `
${scopeSelector} .org-simple-list {
display: flex;
flex-direction: column;
flex: 1;
gap: 0.85rem;
min-height: 0;
overflow: auto;
padding-right: 0.35rem;
scrollbar-width: thin;
scrollbar-color: #94a3b8 #e2e8f0;
}
${scopeSelector} {
min-height: 32.5rem;
max-height: 32.5rem;
}
${scopeSelector} .org-simple-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-simple-row:nth-child(even) {
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
border-color: rgb(148 163 184 / 0.45);
}
${scopeSelector} .org-simple-name {
color: var(--primary-hover);
}
${scopeSelector} .org-simple-meta {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 1rem;
}
@media (max-width: 960px) {
${scopeSelector} .org-simple-row {
flex-direction: column;
align-items: flex-start;
}
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.FleetCard = function FleetCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
const SimpleStat = OrgPortal.componentFns.SimpleStat;
ensureScopedStyle("portal-fleet-card", fleetCardCss);
return PanelCard({
className: "org-scroll-panel org-span-7",
title: "Fleet",
subtitle:
"Individual vehicles with type, status, and overall damage.",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
{ className: "org-simple-list" },
...portalData.fleet.map((unit) =>
h(
"article",
{ className: "org-simple-row" },
h(
"strong",
{ className: "org-simple-name" },
unit.name,
),
h(
"div",
{ className: "org-simple-meta" },
SimpleStat(
"Type",
actions.formatVehicleType(unit.type),
),
SimpleStat("Status", unit.status),
SimpleStat("Damage", unit.damage),
),
),
),
),
});
};
})();

View File

@ -0,0 +1,105 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const scopeAttr = "data-ui-future-card";
const scopeSelector = `[${scopeAttr}]`;
const futureCardCss = `
${scopeSelector} .org-roadmap-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
flex: 1;
min-height: 0;
overflow: auto;
padding-right: 0.35rem;
scrollbar-width: thin;
scrollbar-color: #94a3b8 #e2e8f0;
}
${scopeSelector} .org-roadmap-card {
padding: 1rem;
display: flex;
flex-direction: column;
gap: 0.7rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-roadmap-card:nth-child(4n + 2),
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) {
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
border-color: rgb(100 116 139 / 0.4);
}
${scopeSelector} .org-roadmap-card p {
margin: 0;
color: var(--text-main);
}
${scopeSelector} .org-list-tag {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.2rem 0.55rem;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: #e2e8f0;
color: var(--primary-hover);
}
${scopeSelector} .org-roadmap-card:nth-child(4n + 2) .org-list-tag,
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {
background: #cbd5e1;
color: #1e293b;
}
@media (max-width: 960px) {
${scopeSelector} .org-roadmap-grid {
grid-template-columns: 1fr;
}
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) {
background: #f8fafc;
border-color: var(--border);
}
${scopeSelector} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {
background: #e2e8f0;
color: var(--primary-hover);
}
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.FutureCard = function FutureCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
ensureScopedStyle("portal-future-card", futureCardCss);
return PanelCard({
className: "org-scroll-panel org-span-6",
title: "Expansion Slots",
subtitle:
"Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
{ className: "org-roadmap-grid" },
...portalData.roadmap.map((item) =>
h(
"article",
{ className: "org-roadmap-card" },
h("span", { className: "org-list-tag" }, item.status),
h("strong", null, item.name),
h("p", null, item.detail),
),
),
),
});
};
})();

View File

@ -1,42 +1,83 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { h, ensureScopedStyle } = OrgPortal.runtime;
const store = OrgPortal.store;
const permissions = OrgPortal.permissions;
const actions = OrgPortal.actions;
const scopeAttr = "data-ui-members-card";
const scopeSelector = `[${scopeAttr}]`;
const membersCardCss = `
${scopeSelector} .org-name-list {
display: flex;
flex-direction: column;
flex: 1;
gap: 0.85rem;
min-height: 0;
overflow: auto;
padding-right: 0.35rem;
scrollbar-width: thin;
scrollbar-color: #94a3b8 #e2e8f0;
}
${scopeSelector} .org-name-row {
display: flex;
align-items: center;
justify-content: flex-start;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-name-row:nth-child(even) {
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
border-color: rgb(148 163 184 / 0.45);
}
${scopeSelector} .org-name-row button {
margin-left: auto;
}
@media (max-width: 960px) {
${scopeSelector} .org-name-row {
flex-direction: column;
align-items: flex-start;
}
${scopeSelector} .org-name-row button {
margin-left: 0;
}
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.MembersCard = function MembersCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
const members = store.getMembers();
const allowMemberManagement = permissions.canManageMembers();
ensureScopedStyle("portal-members-card", membersCardCss);
return h(
"section",
{ className: "card org-panel org-scroll-panel org-span-5" },
h(
"div",
{ className: "org-panel-head" },
h(
"div",
null,
h("h2", { className: "org-panel-title" }, "Members"),
h(
"p",
{ className: "org-panel-subtitle" },
"Current roster listing with member removal controls.",
),
),
),
h(
return PanelCard({
className: "org-scroll-panel org-span-5",
title: "Members",
subtitle:
"Current roster listing. The organization owner cannot be removed.",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
{ className: "org-name-list" },
...members.map((member) =>
h(
...members.map((member) => {
const canRemoveMember =
allowMemberManagement &&
!actions.isOwnerMember(member.name);
return h(
"article",
{ className: "org-name-row" },
h("strong", null, member.name),
allowMemberManagement
canRemoveMember
? h(
"button",
{
@ -67,9 +108,9 @@
),
)
: null,
),
),
);
}),
),
);
});
};
})();

View File

@ -0,0 +1,77 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const scopeAttr = "data-ui-metric-card";
const scopeSelector = `[${scopeAttr}]`;
const metricCardCss = `
${scopeSelector} {
display: flex;
flex-direction: column;
gap: 0.45rem;
padding: 1rem;
border-radius: var(--radius);
border: 1px solid var(--border);
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
}
${scopeSelector}:nth-child(4n + 2),
${scopeSelector}:nth-child(4n + 3) {
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(226 232 240) 100%);
border-color: rgb(100 116 139 / 0.35);
box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);
}
${scopeSelector} .org-metric-label {
font-size: 0.76rem;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--text-muted);
}
${scopeSelector} .org-metric-value {
font-size: 1.8rem;
color: var(--primary-hover);
line-height: 1.1;
}
${scopeSelector}:nth-child(4n + 2) .org-metric-value,
${scopeSelector}:nth-child(4n + 3) .org-metric-value {
color: #334155;
}
${scopeSelector} .org-metric-note {
color: var(--text-muted);
font-size: 0.9rem;
}
@media (max-width: 960px) {
${scopeSelector}:nth-child(4n + 3) {
background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);
border-color: var(--border);
box-shadow: none;
}
${scopeSelector}:nth-child(4n + 3) .org-metric-value {
color: var(--primary-hover);
}
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.MetricCard = function MetricCard(
label,
value,
note,
) {
ensureScopedStyle("portal-metric-card", metricCardCss);
return h(
"div",
{ className: "org-metric-card", [scopeAttr]: "" },
h("span", { className: "org-metric-label" }, label),
h("strong", { className: "org-metric-value" }, value),
h("span", { className: "org-metric-note" }, note),
);
};
})();

View File

@ -8,6 +8,7 @@
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.ModalLayer = function ModalLayer() {
const Modal = window.SharedUI.componentFns.Modal;
const modal = store.getModal();
if (!modal) {
return null;
@ -24,7 +25,7 @@
title = "Run Payroll";
body = h(
"div",
{ className: "org-modal-form" },
{ className: "app-modal-form" },
h(
"div",
null,
@ -39,7 +40,7 @@
),
h(
"div",
{ className: "org-modal-actions" },
{ className: "app-modal-actions" },
h(
"button",
{
@ -75,7 +76,7 @@
title = "Send Funds";
body = h(
"div",
{ className: "org-modal-form" },
{ className: "app-modal-form" },
h(
"div",
null,
@ -104,7 +105,7 @@
),
h(
"div",
{ className: "org-modal-actions" },
{ className: "app-modal-actions" },
h(
"button",
{
@ -146,7 +147,7 @@
title = "Assign Credit Line";
body = h(
"div",
{ className: "org-modal-form" },
{ className: "app-modal-form" },
h(
"div",
null,
@ -172,7 +173,7 @@
),
h(
"div",
{ className: "org-modal-actions" },
{ className: "app-modal-actions" },
h(
"button",
{
@ -214,7 +215,7 @@
title = "Disband Organization";
body = h(
"div",
{ className: "org-danger-confirm" },
{ className: "app-modal-danger" },
h(
"p",
null,
@ -224,7 +225,7 @@
),
h(
"div",
{ className: "org-danger-actions" },
{ className: "app-modal-danger-actions" },
h(
"button",
{
@ -247,40 +248,10 @@
);
}
return h(
"div",
{
className: "org-modal-backdrop",
onClick: (e) => {
if (e.target === e.currentTarget) {
actions.closeModal();
}
},
},
h(
"div",
{ className: "card org-modal-card" },
h(
"div",
{ className: "org-modal-head" },
h(
"div",
null,
h("h2", { className: "org-panel-title" }, title),
),
h(
"button",
{
type: "button",
className: "org-modal-close",
onClick: () => actions.closeModal(),
"aria-label": "Close dialog",
},
"x",
),
),
body,
),
);
return Modal({
title,
body,
onClose: () => actions.closeModal(),
});
};
})();

View File

@ -1,35 +1,90 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { h, ensureScopedStyle } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const store = OrgPortal.store;
const actions = OrgPortal.actions;
const scopeAttr = "data-ui-overview-card";
const scopeSelector = `[${scopeAttr}]`;
const overviewCardCss = `
${scopeSelector} .org-hero-grid {
display: grid;
grid-template-columns: 1.3fr 1fr;
gap: 1.5rem;
align-items: start;
}
${scopeSelector} .org-summary {
margin: 0;
font-size: 1.05rem;
color: var(--text-main);
}
${scopeSelector} .org-meta-row {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 1rem;
margin-top: 1.5rem;
}
${scopeSelector} .org-meta-item {
display: flex;
flex-direction: column;
gap: 0.4rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-meta-item:nth-child(even) {
background: linear-gradient(180deg, rgb(241 245 249) 0%, rgb(226 232 240) 100%);
border-color: rgb(148 163 184 / 0.45);
}
${scopeSelector} .org-meta-label {
font-size: 0.76rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
${scopeSelector} .org-meta-value {
font-size: 1rem;
font-weight: 600;
color: var(--primary-hover);
}
${scopeSelector} .org-metric-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
}
@media (max-width: 960px) {
${scopeSelector} .org-hero-grid,
${scopeSelector} .org-meta-row,
${scopeSelector} .org-metric-grid {
grid-template-columns: 1fr;
}
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.OverviewCard = function OverviewCard() {
const MetricCard = OrgPortal.componentFns.MetricCard;
const PanelCard = window.SharedUI.componentFns.PanelCard;
const readiness = actions.getAssetReadiness();
const headquarters = portalData.org.headquarters || "ArmA Verse";
ensureScopedStyle("portal-overview-card", overviewCardCss);
return h(
"section",
{ className: "card org-panel org-span-12" },
h(
"div",
{ className: "org-panel-head" },
h(
"div",
null,
h("div", { className: "org-eyebrow" }, portalData.org.tag),
h(
"h2",
{ className: "org-panel-title" },
"Organization Overview",
),
),
),
h(
return PanelCard({
className: "org-span-12",
eyebrow: portalData.org.tag,
title: "Organization Overview",
rootProps: { [scopeAttr]: "" },
body: h(
"div",
{ className: "org-hero-grid" },
h(
@ -115,6 +170,6 @@
),
),
),
);
});
};
})();

View File

@ -0,0 +1,39 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle } = OrgPortal.runtime;
const scopeAttr = "data-ui-simple-stat";
const scopeSelector = `[${scopeAttr}]`;
const simpleStatCss = `
${scopeSelector} {
display: flex;
flex-direction: column;
gap: 0.2rem;
min-width: 90px;
}
${scopeSelector} .org-simple-label {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
${scopeSelector} .org-simple-value {
font-size: 0.95rem;
color: var(--text-main);
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.SimpleStat = function SimpleStat(label, value) {
ensureScopedStyle("portal-simple-stat", simpleStatCss);
return h(
"div",
{ className: "org-simple-stat", [scopeAttr]: "" },
h("span", { className: "org-simple-label" }, label),
h("strong", { className: "org-simple-value" }, value),
);
};
})();

View File

@ -0,0 +1,430 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h, ensureScopedStyle, createSignal } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const store = OrgPortal.store;
const permissions = OrgPortal.permissions;
const actions = OrgPortal.actions;
const scopeAttr = "data-ui-treasury-card";
const scopeSelector = `[${scopeAttr}]`;
const [getTreasuryTab, setTreasuryTab] = createSignal("overview");
const [getTreasuryMenuOpen, setTreasuryMenuOpen] = createSignal(false);
const treasuryCardCss = `
${scopeSelector} .org-treasury-menu {
position: relative;
}
${scopeSelector} .org-menu-btn {
width: 2.75rem;
height: 2.75rem;
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0;
border: 1px solid var(--border);
background: #f8fafc;
color: var(--text-muted);
}
${scopeSelector} .org-menu-btn:hover {
color: var(--primary-hover);
border-color: rgb(148 163 184 / 0.65);
}
${scopeSelector} .org-menu-btn svg {
width: 1.1rem;
height: 1.1rem;
}
${scopeSelector} .org-menu-dropdown {
position: absolute;
top: calc(100% + 0.6rem);
right: 0;
min-width: 10.5rem;
padding: 0.45rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #fff;
box-shadow: 0 12px 28px rgb(15 23 42 / 0.12);
display: flex;
flex-direction: column;
gap: 0.35rem;
z-index: 5;
}
${scopeSelector} .org-menu-option + .org-menu-option {
margin-left: 0;
}
${scopeSelector} .org-menu-option {
width: 100%;
justify-content: flex-start;
background: transparent;
color: var(--text-main);
border: 1px solid transparent;
}
${scopeSelector} .org-menu-option:hover {
background: #f8fafc;
border-color: rgb(148 163 184 / 0.35);
}
${scopeSelector} .org-menu-option.is-active {
background: rgb(226 232 240 / 0.7);
color: var(--primary-hover);
border-color: rgb(148 163 184 / 0.35);
}
${scopeSelector} .org-finance-meta {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 1rem;
margin-bottom: 1.5rem;
}
${scopeSelector} .org-finance-meta > div {
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
display: flex;
flex-direction: column;
gap: 0.4rem;
}
${scopeSelector} .org-meta-label {
font-size: 0.76rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
${scopeSelector} .org-action-grid {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-bottom: 1rem;
}
${scopeSelector} .org-action-grid button + button {
margin-left: 0;
}
${scopeSelector} .org-action-grid button {
width: 100%;
}
${scopeSelector} .org-access-note {
margin: 0 0 1rem;
color: var(--text-muted);
font-size: 0.95rem;
}
${scopeSelector} .org-credit-summary {
display: flex;
flex-direction: column;
gap: 0.25rem;
padding: 0.85rem 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-credit-summary strong {
font-size: 1rem;
}
${scopeSelector} .org-credit-summary span:last-child {
font-size: 0.92rem;
line-height: 1.45;
}
${scopeSelector} .org-credit-lines-list {
display: flex;
flex-direction: column;
gap: 0.85rem;
}
${scopeSelector} .org-credit-line-row {
display: flex;
align-items: center;
justify-content: space-between;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
}
${scopeSelector} .org-credit-line-row:nth-child(even) {
background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);
border-color: rgb(148 163 184 / 0.45);
}
${scopeSelector} .org-credit-line-member {
display: flex;
flex-direction: column;
gap: 0.3rem;
}
${scopeSelector} .org-credit-line-label {
font-size: 0.76rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
}
${scopeSelector} .org-credit-line-empty {
padding: 1rem;
border: 1px solid var(--border);
border-radius: var(--radius);
background: #f8fafc;
color: var(--text-muted);
}
@media (max-width: 960px) {
${scopeSelector} .org-finance-meta {
grid-template-columns: 1fr;
}
${scopeSelector} .org-credit-line-row {
flex-direction: column;
align-items: flex-start;
}
}
`;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.TreasuryCard = function TreasuryCard() {
const PanelCard = window.SharedUI.componentFns.PanelCard;
const creditLines = store.getCreditLines();
const allowTreasuryActions = permissions.canManageTreasury();
const activeTab = getTreasuryTab();
const isMenuOpen = getTreasuryMenuOpen();
const activeCreditLabel =
creditLines.length === 1
? "1 active credit line"
: `${creditLines.length} active credit lines`;
ensureScopedStyle("portal-treasury-card", treasuryCardCss);
return PanelCard({
className: "org-span-5",
title: "Treasury",
subtitle: "Organization funds, reputation, and member payouts.",
headerExtras: h(
"div",
{ className: "org-treasury-menu" },
h(
"button",
{
type: "button",
className: "org-menu-btn",
title: "Treasury views",
"aria-label": "Treasury views",
onClick: () => setTreasuryMenuOpen((open) => !open),
},
h(
"svg",
{
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
"aria-hidden": "true",
},
h("line", { x1: "4", y1: "7", x2: "20", y2: "7" }),
h("line", { x1: "4", y1: "12", x2: "20", y2: "12" }),
h("line", { x1: "4", y1: "17", x2: "20", y2: "17" }),
),
),
isMenuOpen
? h(
"div",
{ className: "org-menu-dropdown" },
h(
"button",
{
type: "button",
className:
activeTab === "overview"
? "org-menu-option is-active"
: "org-menu-option",
onClick: () => {
setTreasuryTab("overview");
setTreasuryMenuOpen(false);
},
},
"Overview",
),
h(
"button",
{
type: "button",
className:
activeTab === "credit"
? "org-menu-option is-active"
: "org-menu-option",
onClick: () => {
setTreasuryTab("credit");
setTreasuryMenuOpen(false);
},
},
"Credit Lines",
),
)
: null,
),
rootProps: { [scopeAttr]: "" },
body: h(
"div",
null,
activeTab === "credit"
? creditLines.length > 0
? h(
"div",
{ className: "org-credit-lines-list" },
...creditLines.map((line) =>
h(
"article",
{ className: "org-credit-line-row" },
h(
"div",
{
className:
"org-credit-line-member",
},
h(
"span",
{
className:
"org-credit-line-label",
},
"Member",
),
h("strong", null, line.member),
),
h(
"div",
{
className:
"org-credit-line-member",
},
h(
"span",
{
className:
"org-credit-line-label",
},
"Amount",
),
h(
"strong",
null,
actions.formatCurrency(
line.amount,
),
),
),
),
),
)
: h(
"div",
{ className: "org-credit-line-empty" },
"No active credit lines.",
)
: h(
"div",
null,
h(
"div",
{ className: "org-finance-meta" },
h(
"div",
null,
h(
"span",
{ className: "org-meta-label" },
"Funds",
),
h(
"strong",
null,
actions.formatCurrency(store.getFunds()),
),
),
h(
"div",
null,
h(
"span",
{ className: "org-meta-label" },
"Reputation",
),
h("strong", null, `${portalData.reputation}`),
),
),
allowTreasuryActions
? h(
"div",
{ className: "org-action-grid" },
h(
"button",
{
type: "button",
onClick: () =>
actions.openModal("payroll"),
},
"Run Payroll",
),
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () =>
actions.openModal("transfer"),
},
"Send Funds",
),
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () =>
actions.openModal("credit"),
},
"Credit Line",
),
)
: h(
"p",
{ className: "org-access-note" },
"Only the organization leader or CEO can manage treasury actions.",
),
h(
"div",
{ className: "org-credit-summary" },
h(
"span",
{ className: "org-meta-label" },
"Credit Line Status",
),
h("strong", null, activeCreditLabel),
h(
"span",
null,
creditLines.length > 0
? "Open the Credit Lines tab to review assigned members and amounts."
: "Assign a credit line to create the first approved member limit.",
),
),
),
),
});
};
})();

View File

@ -0,0 +1,318 @@
(function () {
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
SharedLogic.createPortalActions = function createPortalActions({
portalData,
store,
permissions,
registryStore,
}) {
class OrgPortalActions {
constructor() {
this.treasuryNoticeTimer = null;
}
formatCurrency(value) {
return "$" + value.toLocaleString();
}
formatVehicleType(type) {
if (!type) {
return "";
}
return type.charAt(0).toUpperCase() + type.slice(1);
}
formatAssetType(type) {
if (!type) {
return "";
}
return type.charAt(0).toUpperCase() + type.slice(1);
}
formatDisplayName(value) {
if (!value) {
return "";
}
return String(value)
.trim()
.split(/\s+/)
.map((part) => {
if (!part) {
return "";
}
return (
part.charAt(0).toUpperCase() +
part.slice(1).toLowerCase()
);
})
.join(" ");
}
getAssetReadiness() {
if (portalData.fleet.length === 0) {
return null;
}
const total = portalData.fleet.reduce(
(sum, unit) => sum + (100 - parseInt(unit.damage, 10)),
0,
);
return Math.round(total / portalData.fleet.length);
}
showTreasuryNotice(type, text) {
store.setTreasuryNotice({ type, text });
if (this.treasuryNoticeTimer) {
clearTimeout(this.treasuryNoticeTimer);
}
this.treasuryNoticeTimer = setTimeout(() => {
store.setTreasuryNotice({ type: "", text: "" });
this.treasuryNoticeTimer = null;
}, 3500);
}
parseAmount(value) {
const amount = Number(value);
return Number.isFinite(amount) ? Math.round(amount) : 0;
}
getInputValue(id) {
const el = document.getElementById(id);
return el ? el.value : "";
}
isOwnerMember(memberName) {
return (
String(memberName || "")
.trim()
.toLowerCase() ===
String(portalData.org.owner || "")
.trim()
.toLowerCase()
);
}
closePortal() {
if (
typeof A3API !== "undefined" &&
typeof A3API.SendAlert === "function"
) {
A3API.SendAlert(
JSON.stringify({
event: "org::close",
data: {},
}),
);
return;
}
if (registryStore) {
registryStore.setView("home");
}
}
openModal(type) {
if (
(type === "payroll" ||
type === "transfer" ||
type === "credit") &&
!permissions.canManageTreasury()
) {
this.showTreasuryNotice(
"error",
"Only the organization leader or CEO can manage treasury actions.",
);
return;
}
if (type === "disband" && !permissions.canDisbandOrg()) {
return;
}
store.setModal({ type });
}
closeModal() {
store.setModal(null);
}
removeMember(memberName) {
if (!permissions.canManageMembers()) {
return false;
}
if (this.isOwnerMember(memberName)) {
return false;
}
store.setMembers((currentMembers) =>
currentMembers.filter(
(member) => member.name !== memberName,
),
);
store.setCreditLines((currentLines) =>
currentLines.filter((line) => line.member !== memberName),
);
return true;
}
disbandOrganization() {
if (!permissions.canDisbandOrg()) {
return false;
}
store.setOrgDisbanded(true);
this.closeModal();
return true;
}
runPayroll(amountPerMember) {
if (!permissions.canManageTreasury()) {
this.showTreasuryNotice(
"error",
"Only the organization leader or CEO can manage treasury actions.",
);
return false;
}
const members = store.getMembers();
const funds = store.getFunds();
if (members.length === 0) {
this.showTreasuryNotice(
"error",
"No members available for payroll.",
);
return false;
}
if (amountPerMember <= 0) {
this.showTreasuryNotice(
"error",
"Enter a valid payroll amount.",
);
return false;
}
const total = amountPerMember * members.length;
if (total > funds) {
this.showTreasuryNotice(
"error",
"Insufficient org funds for payroll.",
);
return false;
}
store.setFunds(funds - total);
this.showTreasuryNotice(
"success",
`Payroll sent to ${members.length} members for ${this.formatCurrency(total)}.`,
);
return true;
}
sendFundsToMember(memberName, amount) {
if (!permissions.canManageTreasury()) {
this.showTreasuryNotice(
"error",
"Only the organization leader or CEO can manage treasury actions.",
);
return false;
}
const funds = store.getFunds();
if (!memberName) {
this.showTreasuryNotice(
"error",
"Select a member to receive funds.",
);
return false;
}
if (amount <= 0) {
this.showTreasuryNotice(
"error",
"Enter a valid transfer amount.",
);
return false;
}
if (amount > funds) {
this.showTreasuryNotice(
"error",
"Insufficient org funds for this transfer.",
);
return false;
}
store.setFunds(funds - amount);
this.showTreasuryNotice(
"success",
`${this.formatCurrency(amount)} sent to ${memberName}.`,
);
return true;
}
grantCreditLine(memberName, amount) {
if (!permissions.canManageTreasury()) {
this.showTreasuryNotice(
"error",
"Only the organization leader or CEO can manage treasury actions.",
);
return false;
}
if (!memberName) {
this.showTreasuryNotice(
"error",
"Select a member for the credit line.",
);
return false;
}
if (amount <= 0) {
this.showTreasuryNotice(
"error",
"Enter a valid credit line amount.",
);
return false;
}
store.setCreditLines((currentLines) => {
const existingIndex = currentLines.findIndex(
(line) => line.member === memberName,
);
if (existingIndex === -1) {
return [
...currentLines,
{ member: memberName, amount },
];
}
const updatedLines = [...currentLines];
updatedLines[existingIndex] = {
member: memberName,
amount,
};
return updatedLines;
});
this.showTreasuryNotice(
"success",
`Credit line of ${this.formatCurrency(amount)} assigned to ${memberName}.`,
);
return true;
}
}
return new OrgPortalActions();
};
})();

View File

@ -0,0 +1,75 @@
(function () {
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
SharedLogic.createPortalPermissions = function createPortalPermissions({
portalData,
session,
}) {
class OrgPortalPermissions {
getNormalizedRole() {
return String(session.role || "")
.trim()
.toUpperCase();
}
isDefaultOrg() {
return (
portalData.org.isDefault === true ||
String(portalData.org.tag || "")
.trim()
.toUpperCase() === "DEFAULT"
);
}
isOrgOwner() {
const ownerUid = String(
portalData.org.ownerUid || portalData.org.owner || "",
)
.trim()
.toLowerCase();
const actorUid = String(session.actorUid || "")
.trim()
.toLowerCase();
if (ownerUid && actorUid) {
return actorUid === ownerUid;
}
return (
String(session.actorName || "")
.trim()
.toLowerCase() ===
String(portalData.org.owner || "")
.trim()
.toLowerCase()
);
}
isSessionCeo() {
return session.ceo === true;
}
isOrgLeaderOrCeo() {
return (
this.isOrgOwner() ||
this.getNormalizedRole() === "LEADER" ||
(this.isDefaultOrg() && this.isSessionCeo())
);
}
canManageMembers() {
return this.isOrgLeaderOrCeo();
}
canManageTreasury() {
return this.isOrgLeaderOrCeo();
}
canDisbandOrg() {
return this.isOrgLeaderOrCeo();
}
}
return new OrgPortalPermissions();
};
})();

View File

@ -0,0 +1,38 @@
(function () {
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
SharedLogic.createPortalStore = function createPortalStore({
createSignal,
portalData,
}) {
class OrgPortalStore {
constructor() {
[this.getFunds, this.setFunds] = createSignal(portalData.funds);
[this.getMembers, this.setMembers] = createSignal([
...portalData.members,
]);
[this.getCreditLines, this.setCreditLines] = createSignal([]);
[this.getTreasuryNotice, this.setTreasuryNotice] = createSignal(
{
type: "",
text: "",
},
);
[this.getModal, this.setModal] = createSignal(null);
[this.getOrgDisbanded, this.setOrgDisbanded] =
createSignal(false);
}
hydrateFromPayload(payload) {
this.setFunds(payload.portalData.funds || 0);
this.setMembers([...(payload.portalData.members || [])]);
this.setCreditLines([]);
this.setTreasuryNotice({ type: "", text: "" });
this.setModal(null);
this.setOrgDisbanded(false);
}
}
return new OrgPortalStore();
};
})();

View File

@ -0,0 +1,69 @@
(function () {
const SharedLogic = (window.SharedLogic = window.SharedLogic || {});
SharedLogic.createRegistryStore = function createRegistryStore({
createSignal,
onHydratePortal,
}) {
class RegistryStore {
constructor() {
[this.getView, this.setView] = createSignal("home");
[this.getIsAuthenticating, this.setIsAuthenticating] =
createSignal(false);
[this.getLoginError, this.setLoginError] = createSignal("");
[this.getIsCreating, this.setIsCreating] = createSignal(false);
[this.getCreateError, this.setCreateError] = createSignal("");
}
startLogin() {
this.setLoginError("");
this.setIsAuthenticating(true);
}
startCreate() {
this.setCreateError("");
this.setIsCreating(true);
}
failLogin(message) {
this.setIsAuthenticating(false);
this.setLoginError(message || "Authentication failed.");
}
failCreate(message) {
this.setIsCreating(false);
this.setCreateError(
message || "Organization registration failed.",
);
}
hydratePortal(payload) {
return Boolean(onHydratePortal && onHydratePortal(payload));
}
completeLogin(payload) {
if (!this.hydratePortal(payload)) {
this.failLogin("Login response was missing portal data.");
return;
}
this.setLoginError("");
this.setIsAuthenticating(false);
}
completeCreate(payload) {
if (!this.hydratePortal(payload)) {
this.failCreate(
"Organization registration response was missing portal data.",
);
return;
}
this.setCreateError("");
this.setIsCreating(false);
}
}
return new RegistryStore();
};
})();

View File

@ -36,12 +36,39 @@
}
window.setTimeout(() => {
if (!credentials.email || !credentials.password) {
store.failLogin("Enter both email and password.");
store.completeLogin(getMockPayload());
}, 350);
}
function requestCreateOrg(registration) {
store.startCreate();
const sent = sendEvent("org::create::request", registration);
if (sent) {
return;
}
window.setTimeout(() => {
const orgName = String(registration.orgName || "").trim();
if (!orgName) {
store.failCreate("Enter an organization name.");
return;
}
store.completeLogin(getMockPayload());
const payload = getMockPayload();
payload.portalData.org.name = orgName;
payload.portalData.org.tag = String(Date.now()).slice(-10);
payload.portalData.org.owner =
payload.session.actorName || "Unknown";
payload.portalData.org.ownerUid = payload.session.actorUid || "";
payload.portalData.org.isDefault = false;
payload.session.role = "Leader";
payload.session.ceo = false;
payload.portalData.members = [
{ name: payload.session.actorName || "Unknown" },
];
store.completeCreate(payload);
}, 350);
}
@ -64,18 +91,33 @@
store.failLogin(payloadData.message || "Authentication failed.");
return;
}
if (event === "org::create::success") {
store.completeCreate(payloadData);
return;
}
if (event === "org::create::failure") {
store.failCreate(
payloadData.message || "Organization registration failed.",
);
}
}
RegistryApp.bridge = {
requestLogin,
requestCreateOrg,
receive,
sendEvent,
};
window.OrgUIBridge = {
requestLogin,
requestCreateOrg,
receive,
receiveLoginSuccess: (data) => receive("org::login::success", data),
receiveLoginFailure: (data) => receive("org::login::failure", data),
receiveCreateSuccess: (data) => receive("org::create::success", data),
receiveCreateFailure: (data) => receive("org::create::failure", data),
};
})();

View File

@ -1,43 +0,0 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
RegistryApp.componentFns = RegistryApp.componentFns || {};
RegistryApp.componentFns.Footer = function Footer() {
return h(
"div",
{ className: "footer" },
h(
"div",
{ className: "wrapper" },
h(
"div",
null,
h("h3", null, "Registry Resources"),
h(
"ul",
{ style: { listStyleType: "none", padding: 0 } },
h("li", null, "Registration Guidelines"),
h("li", null, "Tax & Fee Schedule"),
h("li", null, "Legal Compliance"),
h("li", null, "Trademark Database"),
),
),
h(
"div",
null,
h("h3", null, "Bureau Support"),
h(
"ul",
{ style: { listStyleType: "none", padding: 0 } },
h("li", null, "Office: Sector 7 Admin Block"),
h("li", null, "Hours: 0800 - 1600 (GST)"),
h("li", null, "Helpdesk: 555-01-REGISTRY"),
h("li", null, "support@org-bureau.gov"),
),
),
),
);
};
})();

View File

@ -1,84 +0,0 @@
.split-container {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
align-items: center;
width: 100%;
}
.info-panel {
text-align: left;
padding: 1rem;
}
.app-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 rgb(59 130 246 / 0.1);
}
}
}
.form-actions {
margin-top: 1rem;
display: flex;
flex-direction: column;
gap: 1rem;
align-items: center;
}
.cancel-link {
font-size: 0.9rem;
color: var(--text-muted);
cursor: pointer;
text-decoration: underline;
&:hover {
color: var(--primary);
}
}
.form-feedback {
padding: 0.85rem 1rem;
border-radius: var(--radius);
font-size: 0.92rem;
&.is-error {
background: #fef2f2;
border: 1px solid #fecaca;
color: #991b1b;
}
}
@media (max-width: 960px) {
.split-container {
grid-template-columns: 1fr;
}
}

View File

@ -1,23 +0,0 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
const store = RegistryApp.store;
RegistryApp.componentFns = RegistryApp.componentFns || {};
RegistryApp.componentFns.Header = function Header({ title }) {
return h(
"div",
{ className: "header" },
h(
"h1",
{
style: { cursor: "pointer" },
onClick: () => store.setView("home"),
},
title,
),
h("p", null, "Organization Registration & Management Portal"),
);
};
})();

View File

@ -1,12 +0,0 @@
.content {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
@media (max-width: 960px) {
.content {
grid-template-columns: 1fr;
}
}

View File

@ -1,57 +0,0 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
const store = RegistryApp.store;
RegistryApp.componentFns = RegistryApp.componentFns || {};
RegistryApp.componentFns.HomeView = function HomeView() {
return h(
"div",
{ className: "content" },
h(
"div",
{ className: "card" },
h("h2", null, "Create Organization"),
h(
"p",
null,
"Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly.",
),
h(
"button",
{ onClick: () => store.setView("create") },
"Register",
),
),
h(
"div",
{ className: "card" },
h("h2", null, "Organization Portal"),
h(
"p",
null,
"Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink.",
),
h("button", { onClick: () => store.setView("login") }, "Login"),
),
h(
"div",
{ className: "card", style: { gridColumn: "span 2" } },
h("h2", null, "Organization Portal Preview"),
h(
"p",
null,
"Review the refactor direction for a player organization portal with fleet, assets, treasury, reputation, roster management, and reserved space for future modules.",
),
h(
"button",
{
onClick: () => store.setView("portal"),
},
"Open Portal Preview",
),
),
);
};
})();

Some files were not shown because too many files have changed in this diff Show More