Refactor org UI into single app and addon site

This commit is contained in:
Jacob Schmidt 2026-03-07 13:20:43 -06:00
parent a373943eca
commit aad0fc61c4
99 changed files with 6673 additions and 1248 deletions

View File

@ -0,0 +1,175 @@
:root {
--bg-app: #fdfcf8;
--bg-surface: #ffffff;
--bg-surface-hover: #f1f5f9;
--primary: #475569;
--primary-hover: #1e293b;
--text-main: #1f2937;
--text-muted: #64748b;
--text-inverse: #f8fafc;
--border: #e2e8f0;
--radius: 8px;
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--footer-bg: #1e293b;
}
body {
font-family:
"Inter",
system-ui,
-apple-system,
sans-serif;
margin: 0;
padding: 0;
background: var(--bg-app);
color: var(--text-main);
line-height: 1.6;
}
#app {
min-height: 100vh;
}
main {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.container {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 2rem;
flex: 1;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.header {
text-align: center;
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--border);
h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
letter-spacing: -0.025em;
color: var(--primary-hover);
}
p {
color: var(--text-muted);
font-size: 1.1rem;
}
}
.card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem;
box-shadow: var(--shadow);
text-align: center;
h2 {
margin-top: 0;
font-size: 1.8rem;
color: var(--primary-hover);
}
}
button {
background: var(--primary);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: var(--radius);
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: all 0.2s ease;
&:hover {
background: var(--primary-hover);
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
&:disabled {
cursor: not-allowed;
opacity: 0.65;
transform: none;
box-shadow: none;
}
& + & {
margin-left: 1rem;
}
}
.footer {
margin-top: auto;
background: var(--footer-bg);
color: var(--text-inverse);
display: block;
.wrapper {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 3rem 2rem;
box-sizing: border-box;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
}
h3 {
color: var(--text-inverse);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 700;
margin-bottom: 1.5rem;
border-bottom: 1px solid #475569;
padding-bottom: 0.5rem;
margin-right: 1rem;
}
ul {
li {
color: #cbd5e1;
font-size: 0.95rem;
margin-bottom: 0.75rem;
cursor: pointer;
transition: color 0.2s;
&:hover {
color: white;
}
}
}
}
@media (max-width: 960px) {
.container {
padding: 1.5rem;
}
.header {
margin-bottom: 2rem;
padding-bottom: 1.5rem;
h1 {
font-size: 2rem;
}
}
.footer .wrapper {
grid-template-columns: 1fr;
}
}

View File

@ -0,0 +1,6 @@
/**
* Registry app bootstrap
*/
const root = document.getElementById("app");
window.RegistryApp.runtime.render(window.RegistryApp.components.App, root);

View File

@ -0,0 +1,168 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
const store = RegistryApp.store;
RegistryApp.componentFns = RegistryApp.componentFns || {};
RegistryApp.componentFns.CreateOrgForm = function CreateOrgForm() {
const handleCreate = () => {
const data = {
orgName: String(
document.getElementById("org-create-name")?.value || "",
),
type: String(
document.getElementById("org-create-type")?.value || "",
),
};
console.log("Org Registration:", data);
};
return h(
"div",
{ className: "split-container" },
h(
"div",
{ className: "info-panel" },
h("h2", null, "Registration Details"),
h(
"p",
null,
"Complete the form to add your organization to the Global Organization Registry.",
),
h(
"ul",
{
style: {
textAlign: "left",
marginTop: "1.5rem",
listStyleType: "none",
padding: 0,
},
},
h(
"li",
{ style: { marginBottom: "0.5rem" } },
"✅ Official Organization Designator",
),
h(
"li",
{ style: { marginBottom: "0.5rem" } },
"✅ Secure Comms Channel",
),
h(
"li",
{ style: { marginBottom: "0.5rem" } },
"✅ Deployment Roster Access",
),
h(
"li",
{ style: { marginBottom: "0.5rem" } },
"✅ After-Action Report Tools",
),
),
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",
),
),
),
h(
"div",
{ className: "form-panel card", style: { margin: 0 } },
h("h2", null, "Organization Registration"),
h(
"div",
{ className: "app-form" },
h(
"div",
null,
h("label", null, "Organization Name"),
h("input", {
id: "org-create-name",
type: "text",
placeholder: "e.g. Task Force 141",
}),
),
h(
"div",
null,
h("label", null, "Organization Type"),
h(
"select",
{ id: "org-create-type" },
h(
"option",
{ value: "infantry" },
"Infantry / Milsim",
),
h("option", { value: "aviation" }, "Aviation Wing"),
h(
"option",
{ value: "pmc" },
"Private Military Company",
),
h(
"option",
{ value: "support" },
"Logistics & Support",
),
),
),
h(
"div",
{ className: "form-actions" },
h(
"button",
{
type: "button",
style: { width: "100%" },
onClick: handleCreate,
},
"Submit Registration",
),
h(
"span",
{
className: "cancel-link",
onClick: () => store.setView("home"),
},
"Cancel / Return to Main",
),
),
),
),
);
};
})();

View File

@ -0,0 +1,43 @@
(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

@ -0,0 +1,72 @@
.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);
}
}
@media (max-width: 960px) {
.split-container {
grid-template-columns: 1fr;
}
}

View File

@ -0,0 +1,23 @@
(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

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

View File

@ -0,0 +1,57 @@
(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",
),
),
);
};
})();

View File

@ -0,0 +1,48 @@
(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

@ -0,0 +1,76 @@
(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 handleLogin = () => {
const data = {
email: String(
document.getElementById("org-login-email")?.value || "",
),
password: String(
document.getElementById("org-login-password")?.value || "",
),
};
console.log("Login Attempt:", data);
store.setView("portal");
};
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: "********",
}),
),
h(
"div",
{ className: "form-actions" },
h(
"button",
{
type: "button",
style: { width: "100%" },
onClick: handleLogin,
},
"Access Authenticator",
),
h(
"span",
{
className: "cancel-link",
onClick: () => store.setView("home"),
},
"Cancel / Return to Main",
),
),
),
);
};
})();

View File

@ -0,0 +1,79 @@
.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

@ -0,0 +1,70 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
const store = RegistryApp.store;
RegistryApp.componentFns = RegistryApp.componentFns || {};
function closeRegistry() {
if (
typeof A3API !== "undefined" &&
typeof A3API.SendAlert === "function"
) {
A3API.SendAlert(
JSON.stringify({
event: "org::close",
data: {},
}),
);
return;
}
store.setView("home");
}
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";
return h(
"nav",
{ className: "app-navbar" },
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(
"div",
{ className: "app-navbar-actions" },
h("span", { className: "app-navbar-view" }, viewLabel),
h(
"button",
{
type: "button",
className: "app-close-btn",
onClick: closeRegistry,
},
actionLabel,
),
),
),
);
};
})();

View File

@ -1,243 +1,109 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<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 scriptFiles = [
"runtime.js",
"state.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\\footer.js",
"components\\index.js",
"bootstrap.js",
];
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Organization Dashboard</title>
<!-- <link rel="stylesheet" href="style.css" /> -->
<!--
Dynamic Resource Loading
The following script loads CSS and JavaScript files dynamically using the A3API
This approach is used instead of static HTML imports to work with Arma 3's file system
-->
<script>
Promise.all([
A3API.RequestFile(
"forge\\forge_client\\addons\\org\\ui\\_site\\style.css",
),
A3API.RequestFile(
"forge\\forge_client\\addons\\org\\ui\\_site\\script.js",
),
]).then(([css, js]) => {
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
function requestText(path) {
if (
typeof A3API !== "undefined" &&
typeof A3API.RequestFile === "function"
) {
return A3API.RequestFile(addonRoot + path);
}
const script = document.createElement("script");
script.text = js;
document.head.appendChild(script);
});
</script>
</head>
return fetch(path).then((response) => {
if (!response.ok) {
throw new Error("Failed to load " + path);
}
<body>
<div class="dashboard-container">
<!-- Header Section -->
<div class="dashboard-header">
<div class="org-logo">
<div class="logo-placeholder">ORG</div>
</div>
<div class="org-info">
<h1 class="org-name">Organization Name</h1>
<p class="org-tag">FACTION-001</p>
</div>
<div class="header-actions">
<button class="action-btn">Settings</button>
<button class="action-btn close-btn">Close</button>
</div>
</div>
return response.text();
});
}
<!-- Main Content Grid -->
<div class="dashboard-grid">
<!-- Overview Card -->
<div class="dashboard-card card-wide">
<div class="card-header">
<h2 class="card-title">Overview</h2>
<div class="card-status">Active</div>
</div>
<div class="card-content">
<div class="stat-grid">
<div class="stat-item">
<span class="stat-label">Total Members</span>
<span class="stat-value">24</span>
</div>
<div class="stat-item">
<span class="stat-label">Online Now</span>
<span class="stat-value">8</span>
</div>
<div class="stat-item">
<span class="stat-label">Org Balance</span>
<span class="stat-value">$125,000</span>
</div>
<div class="stat-item">
<span class="stat-label">Reputation</span>
<span class="stat-value">200</span>
</div>
</div>
</div>
</div>
function appendStyle(css) {
const style = document.createElement("style");
style.textContent = css;
document.head.appendChild(style);
}
<!-- Members Card -->
<div class="dashboard-card">
<div class="card-header">
<h2 class="card-title">Members Online</h2>
<span class="card-badge">8</span>
</div>
<div class="card-content">
<div class="member-list">
<div class="member-item">
<div class="member-status online"></div>
<div class="member-info">
<span class="member-name">John Doe</span>
<span class="member-rank">Leader</span>
</div>
</div>
<div class="member-item">
<div class="member-status online"></div>
<div class="member-info">
<span class="member-name">Jane Smith</span>
<span class="member-rank">Officer</span>
</div>
</div>
<div class="member-item">
<div class="member-status online"></div>
<div class="member-info">
<span class="member-name">Mike Johnson</span>
<span class="member-rank">Member</span>
</div>
</div>
<div class="member-item">
<div class="member-status online"></div>
<div class="member-info">
<span class="member-name">Sarah Wilson</span>
<span class="member-rank">Member</span>
</div>
</div>
</div>
</div>
</div>
function appendScript(js) {
const script = document.createElement("script");
script.text = js;
document.head.appendChild(script);
}
<!-- Recent Activity Card -->
<div class="dashboard-card">
<div class="card-header">
<h2 class="card-title">Recent Activity</h2>
</div>
<div class="card-content">
<div class="activity-list">
<div class="activity-item">
<div class="activity-time">2m ago</div>
<div class="activity-text">Mike Johnson completed mission "Alpha Strike"</div>
</div>
<div class="activity-item">
<div class="activity-time">15m ago</div>
<div class="activity-text">Jane Smith deposited $5,000 to org bank</div>
</div>
<div class="activity-item">
<div class="activity-time">1h ago</div>
<div class="activity-text">New member Alex Brown joined the organization</div>
</div>
<div class="activity-item">
<div class="activity-time">2h ago</div>
<div class="activity-text">Organization captured territory: Zone-7</div>
</div>
</div>
</div>
</div>
<!-- Assets Card -->
<div class="dashboard-card">
<div class="card-header">
<h2 class="card-title">Assets</h2>
</div>
<div class="card-content">
<div class="asset-list">
<div class="asset-item">
<span class="asset-icon">🏢</span>
<div class="asset-info">
<span class="asset-name">Headquarters</span>
<span class="asset-location">Downtown</span>
</div>
</div>
<div class="asset-item">
<span class="asset-icon">🚁</span>
<div class="asset-info">
<span class="asset-name">Helicopters</span>
<span class="asset-location">3 units</span>
</div>
</div>
<div class="asset-item">
<span class="asset-icon">🚗</span>
<div class="asset-info">
<span class="asset-name">Vehicles</span>
<span class="asset-location">12 units</span>
</div>
</div>
<div class="asset-item">
<span class="asset-icon">📦</span>
<div class="asset-info">
<span class="asset-name">Storage Units</span>
<span class="asset-location">5 locations</span>
</div>
</div>
</div>
</div>
</div>
<!-- Missions Card -->
<div class="dashboard-card card-wide">
<div class="card-header">
<h2 class="card-title">Active Missions</h2>
<span class="card-badge">3</span>
</div>
<div class="card-content">
<div class="mission-list">
<div class="mission-item">
<div class="mission-header">
<span class="mission-name">Supply Run</span>
<span class="mission-priority high">High Priority</span>
</div>
<div class="mission-description">Deliver supplies to northern outpost</div>
<div class="mission-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: 65%"></div>
</div>
<span class="progress-text">65%</span>
</div>
</div>
<div class="mission-item">
<div class="mission-header">
<span class="mission-name">Recon Operation</span>
<span class="mission-priority medium">Medium Priority</span>
</div>
<div class="mission-description">Scout enemy positions in Zone-4</div>
<div class="mission-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: 30%"></div>
</div>
<span class="progress-text">30%</span>
</div>
</div>
<div class="mission-item">
<div class="mission-header">
<span class="mission-name">Territory Defense</span>
<span class="mission-priority low">Low Priority</span>
</div>
<div class="mission-description">Maintain control of captured zones</div>
<div class="mission-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: 90%"></div>
</div>
<span class="progress-text">90%</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
<!-- <script src="script.js"></script> -->
</body>
Promise.all(styleFiles.map(requestText))
.then((styles) => {
styles.forEach(appendStyle);
return Promise.all(scriptFiles.map(requestText));
})
.then((scripts) => {
scripts.forEach(appendScript);
})
.catch((error) => {
console.error(
"[Org UI] Failed to load site assets.",
error,
);
});
</script>
</head>
<body>
<div id="app"></div>
</body>
</html>

View File

@ -0,0 +1,267 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { portalData } = OrgPortal.data;
const store = OrgPortal.store;
const permissions = OrgPortal.permissions;
const registryStore = window.RegistryApp.store;
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);
}
getAssetReadiness() {
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();
})();

View File

@ -0,0 +1,32 @@
.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

@ -0,0 +1,44 @@
(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

@ -0,0 +1,55 @@
(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

@ -0,0 +1,33 @@
.org-secondary-btn {
background: var(--bg-surface);
color: var(--text-main);
border: 1px solid var(--border);
&:hover {
background: var(--bg-surface-hover);
color: var(--text-main);
}
}
.org-danger-btn {
background: #7f1d1d;
color: #fef2f2;
&:hover {
background: #991b1b;
}
}
.org-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
}
.org-icon {
width: 1rem;
height: 1rem;
}

View File

@ -0,0 +1,22 @@
.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

@ -0,0 +1,56 @@
(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

@ -0,0 +1,47 @@
(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

@ -0,0 +1,56 @@
(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

@ -0,0 +1,43 @@
(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

@ -0,0 +1,74 @@
.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

@ -0,0 +1,45 @@
(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

@ -0,0 +1,65 @@
(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

@ -0,0 +1,81 @@
.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

@ -0,0 +1,75 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const store = OrgPortal.store;
const permissions = OrgPortal.permissions;
const actions = OrgPortal.actions;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.MembersCard = function MembersCard() {
const members = store.getMembers();
const allowMemberManagement = permissions.canManageMembers();
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(
"div",
{ className: "org-name-list" },
...members.map((member) =>
h(
"article",
{ className: "org-name-row" },
h("strong", null, member.name),
allowMemberManagement
? h(
"button",
{
type: "button",
className: "org-danger-btn org-icon-btn",
title: `Remove ${member.name}`,
"aria-label": `Remove ${member.name}`,
onClick: () =>
actions.removeMember(member.name),
},
h(
"svg",
{
className: "org-icon",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
"aria-hidden": "true",
},
h("path", { d: "M9 3h6" }),
h("path", { d: "M4 7h16" }),
h("path", { d: "M6 7l1 13h10l1-13" }),
h("path", { d: "M10 11v6" }),
h("path", { d: "M14 11v6" }),
),
)
: null,
),
),
),
);
};
})();

View File

@ -0,0 +1,69 @@
.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

@ -0,0 +1,20 @@
(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

@ -0,0 +1,131 @@
.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

@ -0,0 +1,286 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const store = OrgPortal.store;
const actions = OrgPortal.actions;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.ModalLayer = function ModalLayer() {
const modal = store.getModal();
if (!modal) {
return null;
}
const members = store.getMembers();
const memberSelectProps =
members.length === 0 ? { disabled: true } : {};
let title = "";
let body = null;
if (modal.type === "payroll") {
title = "Run Payroll";
body = h(
"div",
{ className: "org-modal-form" },
h(
"div",
null,
h("label", null, "Amount Per Member"),
h("input", {
id: "treasury-payroll-amount",
type: "number",
min: "1",
placeholder: "500",
autofocus: "true",
}),
),
h(
"div",
{ className: "org-modal-actions" },
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.closeModal(),
},
"Cancel",
),
h(
"button",
{
type: "button",
onClick: () => {
if (
actions.runPayroll(
actions.parseAmount(
actions.getInputValue(
"treasury-payroll-amount",
),
),
)
) {
actions.closeModal();
}
},
},
"Run Payroll",
),
),
);
} else if (modal.type === "transfer") {
title = "Send Funds";
body = h(
"div",
{ className: "org-modal-form" },
h(
"div",
null,
h("label", null, "Member"),
h(
"select",
{
id: "treasury-transfer-member",
...memberSelectProps,
},
...members.map((member) =>
h("option", { value: member.name }, member.name),
),
),
),
h(
"div",
null,
h("label", null, "Amount"),
h("input", {
id: "treasury-transfer-amount",
type: "number",
min: "1",
placeholder: "1500",
}),
),
h(
"div",
{ className: "org-modal-actions" },
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.closeModal(),
},
"Cancel",
),
h(
"button",
{
type: "button",
...memberSelectProps,
onClick: () => {
if (
actions.sendFundsToMember(
String(
actions.getInputValue(
"treasury-transfer-member",
) || "",
),
actions.parseAmount(
actions.getInputValue(
"treasury-transfer-amount",
),
),
)
) {
actions.closeModal();
}
},
},
"Send Funds",
),
),
);
} else if (modal.type === "credit") {
title = "Assign Credit Line";
body = h(
"div",
{ className: "org-modal-form" },
h(
"div",
null,
h("label", null, "Member"),
h(
"select",
{ id: "treasury-credit-member", ...memberSelectProps },
...members.map((member) =>
h("option", { value: member.name }, member.name),
),
),
),
h(
"div",
null,
h("label", null, "Credit Amount"),
h("input", {
id: "treasury-credit-amount",
type: "number",
min: "1",
placeholder: "5000",
}),
),
h(
"div",
{ className: "org-modal-actions" },
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.closeModal(),
},
"Cancel",
),
h(
"button",
{
type: "button",
...memberSelectProps,
onClick: () => {
if (
actions.grantCreditLine(
String(
actions.getInputValue(
"treasury-credit-member",
) || "",
),
actions.parseAmount(
actions.getInputValue(
"treasury-credit-amount",
),
),
)
) {
actions.closeModal();
}
},
},
"Assign Credit Line",
),
),
);
} else if (modal.type === "disband") {
title = "Disband Organization";
body = h(
"div",
{ className: "org-danger-confirm" },
h(
"p",
null,
"This action is permanent. Disband ",
portalData.org.name,
"?",
),
h(
"div",
{ className: "org-danger-actions" },
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.closeModal(),
},
"Cancel",
),
h(
"button",
{
type: "button",
className: "org-danger-btn",
onClick: () => actions.disbandOrganization(),
},
"Confirm Disband",
),
),
);
}
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,
),
);
};
})();

View File

@ -0,0 +1,58 @@
.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

@ -0,0 +1,118 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const store = OrgPortal.store;
const actions = OrgPortal.actions;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.OverviewCard = function OverviewCard() {
const MetricCard = OrgPortal.componentFns.MetricCard;
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(
"div",
{ className: "org-hero-grid" },
h(
"div",
{ className: "org-hero-copy" },
h(
"p",
{ className: "org-summary" },
portalData.org.type,
" operating from ",
portalData.org.headquarters,
". Treasury, fleet status, inventory, and roster management are surfaced here first.",
),
h(
"div",
{ className: "org-meta-row" },
h(
"div",
{ className: "org-meta-item" },
h(
"span",
{ className: "org-meta-label" },
"Director",
),
h(
"span",
{ className: "org-meta-value" },
portalData.org.owner,
),
),
h(
"div",
{ className: "org-meta-item" },
h(
"span",
{ className: "org-meta-label" },
"Active Members",
),
h(
"span",
{ className: "org-meta-value" },
`${store.getMembers().length} total`,
),
),
h(
"div",
{ className: "org-meta-item" },
h(
"span",
{ className: "org-meta-label" },
"Fleet Readiness",
),
h(
"span",
{ className: "org-meta-value" },
`${actions.getAssetReadiness()}%`,
),
),
),
),
h(
"div",
{ className: "org-metric-grid" },
MetricCard(
"Org Funds",
actions.formatCurrency(store.getFunds()),
"Organization treasury balance",
),
MetricCard(
"Reputation",
portalData.reputation,
"Organization standing",
),
MetricCard(
"Asset Lines",
portalData.assets.length,
"Tracked supply and equipment entries",
),
MetricCard(
"Fleet Vehicles",
portalData.fleet.length,
"Tracked air, ground, and naval vehicles",
),
),
),
);
};
})();

View File

@ -0,0 +1,41 @@
.org-page-header {
text-align: left;
margin-bottom: 0;
}
.org-page-heading {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.org-page-kicker {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
font-weight: 600;
}
.org-page-title {
margin: 0;
}
.org-page-subtitle {
font-size: 0.9rem;
color: var(--text-muted);
margin: 0;
}
.org-page-meta {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
@media (max-width: 960px) {
.org-page-heading {
gap: 0.3rem;
}
}

View File

@ -0,0 +1,30 @@
(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

@ -0,0 +1,67 @@
.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

@ -0,0 +1,18 @@
.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

@ -0,0 +1,15 @@
(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

@ -0,0 +1,99 @@
.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

@ -0,0 +1,130 @@
(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 noMembers = store.getMembers().length === 0;
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"),
disabled: noMembers,
},
"Run Payroll",
),
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.openModal("transfer"),
disabled: noMembers,
},
"Send Funds",
),
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.openModal("credit"),
disabled: noMembers,
},
"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

@ -0,0 +1,118 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
OrgPortal.data = {
portalData: {
org: {
name: "Black Rifle Company",
tag: "BRC-0160566824",
type: "Private Military Company",
status: "Operational",
headquarters: "Georgetown Command Annex",
owner: "Jacob Schmidt",
},
funds: 482750,
reputation: 72,
members: [
{ name: "Jacob Schmidt" },
{ name: "Mara Velez" },
{ name: "Rylan Cross" },
{ name: "Noah Briggs" },
{ name: "Elena Price" },
{ name: "Isaac Rowe" },
{ name: "Talia Boone" },
{ name: "Cade Mercer" },
],
fleet: [
{
name: "UH-80 Ghost Hawk",
type: "helicopter",
status: "Ready",
damage: "16%",
},
{
name: "MH-9 Hummingbird",
type: "helicopter",
status: "Ready",
damage: "8%",
},
{
name: "M-ATV Patrol 1",
type: "car",
status: "Fielded",
damage: "24%",
},
{
name: "M2A1 Slammer",
type: "armor",
status: "Ready",
damage: "11%",
},
{
name: "RHIB Patrol Boat",
type: "naval",
status: "Repairing",
damage: "32%",
},
],
assets: [
{ name: "First Aid Kits", type: "items", quantity: "36" },
{ name: "MX 6.5 mm Rifles", type: "weapons", quantity: "18" },
{
name: "6.5 mm Magazines",
type: "magazines",
quantity: "120",
},
{
name: "Carryall Backpacks",
type: "backpacks",
quantity: "24",
},
],
activity: [
{
time: "08:20",
text: "Treasury cleared contractor payment for northern route escort.",
},
{
time: "07:45",
text: "Viper Flight completed readiness checks on all rotary assets.",
},
{
time: "07:10",
text: "New recruit Cade Mercer accepted into ground training roster.",
},
{
time: "06:30",
text: "North Depot inventory count pushed reserve ratio above target.",
},
],
roadmap: [
{
name: "Contracts Board",
status: "Planned",
detail: "Track payouts, assignments, and claim approvals.",
},
{
name: "Diplomacy",
status: "Future Review",
detail: "Possible future module pending a full design and scope review.",
},
{
name: "Logistics Queue",
status: "Future Review",
detail: "Possible future module pending a full design and scope review.",
},
{
name: "Permissions",
status: "Future Review",
detail: "Possible future module pending a full design and scope review.",
},
],
},
session: {
actorName: "Jacob Schmidt",
role: "Leader",
},
};
})();

View File

@ -0,0 +1,28 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { portalData, session } = OrgPortal.data;
class OrgPortalPermissions {
isOrgLeaderOrCeo() {
return (
session.actorName === portalData.org.owner ||
session.role === "Leader" ||
session.role === "CEO"
);
}
canManageMembers() {
return this.isOrgLeaderOrCeo();
}
canManageTreasury() {
return this.isOrgLeaderOrCeo();
}
canDisbandOrg() {
return this.isOrgLeaderOrCeo();
}
}
OrgPortal.permissions = new OrgPortalPermissions();
})();

View File

@ -0,0 +1,98 @@
(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

@ -0,0 +1,23 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { createSignal } = window.RegistryApp.runtime;
const { portalData } = OrgPortal.data;
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);
}
}
OrgPortal.store = new OrgPortalStore();
})();

View File

@ -0,0 +1,73 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
function h(tag, props = {}, ...children) {
const el = document.createElement(tag);
if (props) {
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith("on") && typeof value === "function") {
el.addEventListener(key.substring(2).toLowerCase(), value);
} else if (key === "className") {
el.className = value;
} else if (key === "style" && typeof value === "object") {
Object.assign(el.style, value);
} else if (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];
}
RegistryApp.runtime = {
h,
render,
createSignal,
};
})();

View File

@ -1,294 +0,0 @@
/**
* Organization Dashboard
* Handles real-time updates and interactions
*/
// Mock data for demonstration
const mockData = {
org: {
name: "Black Phoenix Initiative",
tag: "BPI-001",
status: "Active"
},
stats: {
totalMembers: 24,
onlineMembers: 8,
balance: 125000,
reputation: 5
},
membersOnline: [
{ name: "John Doe", rank: "Leader", online: true },
{ name: "Jane Smith", rank: "Officer", online: true },
{ name: "Mike Johnson", rank: "Member", online: true },
{ name: "Sarah Wilson", rank: "Member", online: true },
{ name: "Alex Brown", rank: "Member", online: true },
{ name: "Chris Davis", rank: "Member", online: true },
{ name: "Pat Lee", rank: "Recruit", online: true },
{ name: "Sam Taylor", rank: "Recruit", online: true }
],
activities: [
{ time: "2m ago", text: "Mike Johnson completed mission \"Alpha Strike\"" },
{ time: "15m ago", text: "Jane Smith deposited $5,000 to org bank" },
{ time: "1h ago", text: "New member Alex Brown joined the organization" },
{ time: "2h ago", text: "Organization captured territory: Zone-7" }
],
assets: [
{ icon: "🏢", name: "Headquarters", location: "Downtown" },
{ icon: "🚁", name: "Helicopters", location: "3 units" },
{ icon: "🚗", name: "Vehicles", location: "12 units" },
{ icon: "📦", name: "Storage Units", location: "5 locations" }
],
missions: [
{
name: "Supply Run",
priority: "high",
description: "Deliver supplies to northern outpost",
progress: 65
},
{
name: "Recon Operation",
priority: "medium",
description: "Scout enemy positions in Zone-4",
progress: 30
},
{
name: "Territory Defense",
priority: "low",
description: "Maintain control of captured zones",
progress: 90
}
]
};
// Update dashboard with data
function updateDashboard(data) {
// Update header
if (data.org) {
const orgName = document.querySelector('.org-name');
const orgTag = document.querySelector('.org-tag');
if (orgName) orgName.textContent = data.org.name;
if (orgTag) orgTag.textContent = data.org.tag;
}
// Update stats
if (data.stats) {
const statValues = document.querySelectorAll('.stat-value');
if (statValues[0]) statValues[0].textContent = data.stats.totalMembers;
if (statValues[1]) statValues[1].textContent = data.stats.onlineMembers;
if (statValues[2]) statValues[2].textContent = `$${data.stats.balance.toLocaleString()}`;
if (statValues[3]) statValues[3].textContent = `${data.stats.reputation}`;
}
// Update Members List
if (data.membersOnline) {
const memberList = document.querySelector('.member-list');
if (memberList) {
memberList.innerHTML = '';
data.membersOnline.forEach(member => {
const item = document.createElement('div');
item.className = 'member-item';
item.innerHTML = `
<div class="member-status ${member.online ? 'online' : 'offline'}"></div>
<div class="member-info">
<span class="member-name">${member.name}</span>
<span class="member-rank">${member.rank}</span>
</div>
`;
memberList.appendChild(item);
});
// Update member count badge
const memberBadge = document.querySelector('.dashboard-card:nth-child(2) .card-badge');
if (memberBadge) memberBadge.textContent = data.membersOnline.length;
}
}
// Update Assets List
if (data.assets) {
const assetList = document.querySelector('.asset-list');
if (assetList) {
assetList.innerHTML = '';
data.assets.forEach(asset => {
const item = document.createElement('div');
item.className = 'asset-item';
item.innerHTML = `
<span class="asset-icon">${asset.icon || '📦'}</span>
<div class="asset-info">
<span class="asset-name">${asset.name}</span>
<span class="asset-location">${asset.location}</span>
</div>
`;
assetList.appendChild(item);
});
}
}
// Update Activities List
if (data.activities) {
const activityList = document.querySelector('.activity-list');
if (activityList) {
activityList.innerHTML = '';
data.activities.forEach(activity => {
const item = document.createElement('div');
item.className = 'activity-item';
item.innerHTML = `
<div class="activity-time">${activity.time}</div>
<div class="activity-text">${activity.text}</div>
`;
activityList.appendChild(item);
});
}
}
// Update Missions List
if (data.missions) {
const missionList = document.querySelector('.mission-list');
if (missionList) {
missionList.innerHTML = '';
data.missions.forEach(mission => {
const item = document.createElement('div');
item.className = 'mission-item';
item.innerHTML = `
<div class="mission-header">
<span class="mission-name">${mission.name}</span>
<span class="mission-priority ${mission.priority}">${mission.priority.charAt(0).toUpperCase() + mission.priority.slice(1)} Priority</span>
</div>
<div class="mission-description">${mission.description}</div>
<div class="mission-progress">
<div class="progress-bar">
<div class="progress-fill" style="width: ${mission.progress}%"></div>
</div>
<span class="progress-text">${mission.progress}%</span>
</div>
`;
missionList.appendChild(item);
});
// Update mission count badge
const missionBadge = document.querySelector('.dashboard-card:last-child .card-badge');
if (missionBadge) missionBadge.textContent = data.missions.length;
}
}
// Re-attach event handlers for new elements if needed
// Note: The original setupEventHandlers attached to querySelectorAll which only finds elements existing at that time.
// We should re-run or use delegation. For simplicity, we can re-attach listeners to the new items.
attachDynamicHandlers(data);
}
function attachDynamicHandlers(data) {
// Member item clicks
const memberItems = document.querySelectorAll('.member-item');
memberItems.forEach((item, index) => {
item.addEventListener('click', () => {
const memberData = data.membersOnline ? data.membersOnline[index] : null;
if (memberData) {
console.log('Member clicked:', memberData);
if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify({
event: 'org::member::view',
data: { member: memberData }
}));
}
}
});
});
// Asset item clicks
const assetItems = document.querySelectorAll('.asset-item');
assetItems.forEach((item, index) => {
item.addEventListener('click', () => {
const assetData = data.assets ? data.assets[index] : null;
if (assetData) {
console.log('Asset clicked:', assetData);
if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify({
event: 'org::asset::view',
data: { asset: assetData }
}));
}
}
});
});
// Mission item clicks
const missionItems = document.querySelectorAll('.mission-item');
missionItems.forEach((item, index) => {
item.addEventListener('click', () => {
const missionData = data.missions ? data.missions[index] : null;
if (missionData) {
console.log('Mission clicked:', missionData);
if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify({
event: 'org::mission::view',
data: { mission: missionData }
}));
}
}
});
});
}
// Static Event Handlers (Close, Settings, etc.)
function setupEventHandlers() {
// Close button
const closeBtn = document.querySelector('.close-btn');
if (closeBtn) {
closeBtn.addEventListener('click', () => {
console.log('Closing dashboard...');
// Send close event to Arma
if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify({
event: 'org::close',
data: {}
}));
}
});
}
// Settings button
const settingsBtn = document.querySelectorAll('.action-btn')[0];
if (settingsBtn && !settingsBtn.classList.contains('close-btn')) {
settingsBtn.addEventListener('click', () => {
console.log('Opening settings...');
// Send settings event to Arma
if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify({
event: 'org::settings',
data: {}
}));
}
});
}
}
// Initialize dashboard
function initDashboard() {
console.log('Organization Dashboard initializing...');
// Setup event handlers
setupEventHandlers();
// Request live data from Arma
if (typeof A3API !== 'undefined') {
A3API.SendAlert(JSON.stringify({
event: 'org::ready',
data: {}
}));
} else {
// Use mock data for browser testing/development
updateDashboard(mockData);
}
console.log('Organization Dashboard initialized');
}
// Auto-initialize when DOM is ready
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', initDashboard);
} else {
initDashboard();
}
// Expose functions globally for Arma integration
window.updateOrgDashboard = updateDashboard;

View File

@ -0,0 +1,12 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { createSignal } = RegistryApp.runtime;
class RegistryStore {
constructor() {
[this.getView, this.setView] = createSignal("home");
}
}
RegistryApp.store = new RegistryStore();
})();

View File

@ -1,469 +0,0 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
height: 100vh;
width: 100vw;
background: rgba(0, 0, 0, 0.6);
font-family: Arial, sans-serif;
color: rgba(200, 220, 240, 0.95);
overflow: hidden;
}
.dashboard-container {
height: 100vh;
width: 100vw;
padding: 2rem;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
/* Header Section */
.dashboard-header {
display: flex;
align-items: center;
gap: 1.5rem;
padding: 1.25rem 1.5rem;
background: rgba(15, 20, 30, 0.9);
border: 1px solid rgba(100, 150, 200, 0.4);
border-radius: 4px;
box-shadow:
0 0 20px rgba(100, 150, 200, 0.15),
0 4px 16px rgba(0, 0, 0, 0.8);
}
.org-logo {
width: 60px;
height: 60px;
background: rgba(20, 30, 45, 0.8);
border: 2px solid rgba(100, 150, 200, 0.5);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
}
.logo-placeholder {
font-size: 1.5rem;
font-weight: bold;
color: rgba(100, 150, 200, 0.9);
}
.org-info {
flex: 1;
}
.org-name {
font-size: 1.5rem;
font-weight: 600;
letter-spacing: 0.5px;
text-transform: uppercase;
color: rgba(200, 220, 255, 1);
margin-bottom: 0.25rem;
}
.org-tag {
font-size: 0.875rem;
color: rgba(140, 160, 180, 0.8);
letter-spacing: 1px;
}
.header-actions {
display: flex;
gap: 0.75rem;
}
.action-btn {
padding: 0.625rem 1.25rem;
background: rgba(20, 30, 45, 0.7);
border: 1px solid rgba(100, 150, 200, 0.4);
border-radius: 4px;
color: rgba(200, 220, 240, 0.95);
font-size: 0.875rem;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
transition: all 0.15s ease;
}
.action-btn:hover {
background: rgba(30, 45, 70, 0.9);
border-color: rgba(150, 200, 255, 0.7);
box-shadow:
0 0 15px rgba(100, 150, 200, 0.2),
inset 0 0 20px rgba(100, 150, 200, 0.05);
}
.close-btn {
border-color: rgba(200, 100, 100, 0.4);
}
.close-btn:hover {
border-color: rgba(255, 100, 100, 0.7);
box-shadow:
0 0 15px rgba(200, 100, 100, 0.2),
inset 0 0 20px rgba(200, 100, 100, 0.05);
}
/* Dashboard Grid */
.dashboard-grid {
flex: 1;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.25rem;
overflow-y: auto;
padding-right: 0.5rem;
}
/* Custom Scrollbar */
.dashboard-grid::-webkit-scrollbar {
width: 8px;
}
.dashboard-grid::-webkit-scrollbar-track {
background: rgba(15, 20, 30, 0.5);
border-radius: 4px;
}
.dashboard-grid::-webkit-scrollbar-thumb {
background: rgba(100, 150, 200, 0.3);
border-radius: 4px;
}
.dashboard-grid::-webkit-scrollbar-thumb:hover {
background: rgba(100, 150, 200, 0.5);
}
/* Dashboard Cards */
.dashboard-card {
background: rgba(15, 20, 30, 0.9);
border: 1px solid rgba(100, 150, 200, 0.4);
border-left: 3px solid rgba(100, 150, 200, 0.5);
border-radius: 4px;
padding: 1.25rem;
box-shadow:
0 0 20px rgba(100, 150, 200, 0.1),
0 4px 16px rgba(0, 0, 0, 0.6);
transition: all 0.2s ease;
}
.dashboard-card:hover {
border-left-color: rgba(150, 200, 255, 0.8);
box-shadow:
0 0 25px rgba(100, 150, 200, 0.2),
0 4px 20px rgba(0, 0, 0, 0.7);
}
.card-wide {
grid-column: span 2;
}
.card-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 1rem;
padding-bottom: 0.75rem;
border-bottom: 1px solid rgba(100, 150, 200, 0.2);
}
.card-title {
font-size: 1rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(200, 220, 255, 1);
}
.card-status {
padding: 0.25rem 0.75rem;
background: rgba(100, 200, 150, 0.2);
border: 1px solid rgba(100, 200, 150, 0.4);
border-radius: 3px;
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(150, 255, 200, 0.9);
}
.card-badge {
padding: 0.25rem 0.625rem;
background: rgba(100, 150, 200, 0.2);
border: 1px solid rgba(100, 150, 200, 0.4);
border-radius: 3px;
font-size: 0.75rem;
font-weight: 600;
color: rgba(150, 200, 255, 0.9);
}
.card-content {
color: rgba(180, 200, 220, 0.9);
}
/* Stat Grid */
.stat-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
gap: 1.25rem;
}
.stat-item {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 1rem;
background: rgba(20, 30, 45, 0.6);
border: 1px solid rgba(100, 150, 200, 0.3);
border-radius: 4px;
}
.stat-label {
font-size: 0.75rem;
text-transform: uppercase;
letter-spacing: 0.5px;
color: rgba(140, 160, 180, 0.85);
}
.stat-value {
font-size: 1.75rem;
font-weight: 600;
color: rgba(200, 220, 255, 1);
}
/* Member List */
.member-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.member-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.75rem;
background: rgba(20, 30, 45, 0.5);
border: 1px solid rgba(100, 150, 200, 0.2);
border-radius: 4px;
transition: all 0.15s ease;
}
.member-item:hover {
background: rgba(30, 45, 70, 0.7);
border-color: rgba(100, 150, 200, 0.4);
}
.member-status {
width: 10px;
height: 10px;
border-radius: 50%;
background: rgba(100, 100, 100, 0.5);
}
.member-status.online {
background: rgba(100, 200, 150, 0.9);
box-shadow: 0 0 8px rgba(100, 200, 150, 0.5);
}
.member-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.member-name {
font-size: 0.875rem;
font-weight: 500;
color: rgba(200, 220, 240, 0.95);
}
.member-rank {
font-size: 0.75rem;
color: rgba(140, 160, 180, 0.8);
}
/* Activity List */
.activity-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.activity-item {
padding: 0.75rem;
background: rgba(20, 30, 45, 0.5);
border-left: 2px solid rgba(100, 150, 200, 0.4);
border-radius: 2px;
}
.activity-time {
font-size: 0.7rem;
color: rgba(100, 150, 200, 0.7);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 0.375rem;
}
.activity-text {
font-size: 0.875rem;
color: rgba(180, 200, 220, 0.9);
line-height: 1.4;
}
/* Asset List */
.asset-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.asset-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.75rem;
background: rgba(20, 30, 45, 0.5);
border: 1px solid rgba(100, 150, 200, 0.2);
border-radius: 4px;
transition: all 0.15s ease;
}
.asset-item:hover {
background: rgba(30, 45, 70, 0.7);
border-color: rgba(100, 150, 200, 0.4);
}
.asset-icon {
font-size: 1.5rem;
}
.asset-info {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.asset-name {
font-size: 0.875rem;
font-weight: 500;
color: rgba(200, 220, 240, 0.95);
}
.asset-location {
font-size: 0.75rem;
color: rgba(140, 160, 180, 0.8);
}
/* Mission List */
.mission-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.mission-item {
padding: 1rem;
background: rgba(20, 30, 45, 0.6);
border: 1px solid rgba(100, 150, 200, 0.3);
border-left: 3px solid rgba(100, 150, 200, 0.5);
border-radius: 4px;
}
.mission-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 0.5rem;
}
.mission-name {
font-size: 0.95rem;
font-weight: 600;
color: rgba(200, 220, 255, 1);
}
.mission-priority {
padding: 0.25rem 0.625rem;
border-radius: 3px;
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.5px;
font-weight: 600;
}
.mission-priority.high {
background: rgba(200, 100, 100, 0.2);
border: 1px solid rgba(200, 100, 100, 0.4);
color: rgba(255, 150, 150, 0.9);
}
.mission-priority.medium {
background: rgba(200, 150, 100, 0.2);
border: 1px solid rgba(200, 150, 100, 0.4);
color: rgba(255, 200, 150, 0.9);
}
.mission-priority.low {
background: rgba(100, 150, 200, 0.2);
border: 1px solid rgba(100, 150, 200, 0.4);
color: rgba(150, 200, 255, 0.9);
}
.mission-description {
font-size: 0.85rem;
color: rgba(160, 180, 200, 0.85);
margin-bottom: 0.75rem;
line-height: 1.4;
}
.mission-progress {
display: flex;
align-items: center;
gap: 0.75rem;
}
.progress-bar {
flex: 1;
height: 6px;
background: rgba(20, 30, 45, 0.8);
border: 1px solid rgba(100, 150, 200, 0.3);
border-radius: 3px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg,
rgba(100, 150, 200, 0.6),
rgba(150, 200, 255, 0.8));
box-shadow: 0 0 10px rgba(100, 150, 200, 0.5);
transition: width 0.3s ease;
}
.progress-text {
font-size: 0.75rem;
font-weight: 600;
color: rgba(100, 150, 200, 0.9);
min-width: 40px;
}
/* Responsive adjustments */
@media (max-width: 1200px) {
.card-wide {
grid-column: span 1;
}
}
@media (max-width: 768px) {
.dashboard-container {
padding: 1rem;
}
.dashboard-grid {
grid-template-columns: 1fr;
}
}

View File

@ -1,235 +0,0 @@
/**
* Simple React-like Vanilla JS Implementation
*/
// --- 1. The "Library" Logic ---
// Helper to create DOM elements (like React.createElement)
function h(tag, props = {}, ...children) {
const el = document.createElement(tag);
// Handle props
if (props) {
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith('on') && typeof value === 'function') {
el.addEventListener(key.substring(2).toLowerCase(), value);
} else if (key === 'className') {
el.className = value;
} else if (key === 'style' && typeof value === 'object') {
Object.assign(el.style, value);
} else {
el.setAttribute(key, value);
}
});
}
// Handle children
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;
}
// Simple Rendering Logic
let _rootContainer = null;
let _rootComponent = null;
function render(component, container) {
_rootContainer = container;
_rootComponent = component;
_render();
}
function _render() {
_rootContainer.innerHTML = ''; // Clear previous tree (simple re-render)
_rootContainer.appendChild(_rootComponent());
}
// Simple State Hook (Global for simplicity, or localized using closures)
// Note: In a real app this would be more complex to handle multiple components.
// For this demo, we'll re-render the whole app on state change.
const createSignal = (initialValue) => {
let _val = initialValue;
const getValue = () => _val;
const setValue = (newValue) => {
_val = typeof newValue === 'function' ? newValue(_val) : newValue;
_render(); // Trigger re-render
};
return [getValue, setValue];
};
// --- 2. The Application Components ---
// Global View State: 'home', 'login', 'create'
const [getView, setView] = createSignal('home');
// Header Component
function Header({ title }) {
return h('div', { className: 'header' },
h('h1', {
style: { cursor: 'pointer' },
onClick: () => setView('home')
}, title),
h('p', null, 'Organization Registration & Management Portal')
);
}
// Login Form Component
function LoginForm() {
const handleSubmit = (e) => {
e.preventDefault(); // Critical for strict sandbox
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
console.log('Login Attempt:', data);
// TODO: Handle authentication logic here
};
return h('div', { className: 'card', style: { maxWidth: '400px', margin: '0 auto' } },
h('h2', null, 'Organization Login'),
h('form', { onSubmit: handleSubmit },
h('div', null,
h('label', null, 'Email'),
h('input', { name: 'email', type: 'text', placeholder: 'admin@spearnet.mil' })
),
h('div', null,
h('label', null, 'Password'),
h('input', { name: 'password', type: 'password', placeholder: '••••••••' })
),
h('div', { className: 'form-actions' },
h('button', { type: 'submit', style: { width: '100%' } }, 'Access Authenticator'),
h('span', {
className: 'cancel-link',
onClick: () => setView('home')
}, 'Cancel / Return to Main')
)
)
);
}
// Create Org Form Component
function CreateOrgForm() {
const handleSubmit = (e) => {
e.preventDefault(); // Critical for strict sandbox
const formData = new FormData(e.target);
const data = Object.fromEntries(formData.entries());
console.log('Org Registration:', data);
// TODO: Handle registration logic here
};
return h('div', { className: 'split-container' },
h('div', { className: 'info-panel' },
h('h2', null, 'Registration Details'),
h('p', null, 'Complete the form to add your organization to the Global Organization Registry.'),
h('ul', { style: { textAlign: 'left', marginTop: '1.5rem', listStyleType: 'none', padding: 0 } },
h('li', { style: { marginBottom: '0.5rem' } }, '✅ Official Organization Designator'),
h('li', { style: { marginBottom: '0.5rem' } }, '✅ Secure Comms Channel'),
h('li', { style: { marginBottom: '0.5rem' } }, '✅ Deployment Roster Access'),
h('li', { style: { marginBottom: '0.5rem' } }, '✅ After-Action Report Tools')
),
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')
)
),
h('div', { className: 'form-panel card', style: { margin: 0 } },
h('h2', null, 'Organization Registration'),
h('form', { onSubmit: handleSubmit },
h('div', null,
h('label', null, 'Organization Name'),
h('input', { name: 'orgName', type: 'text', placeholder: 'e.g. Task Force 141' })
),
h('div', null,
h('label', null, 'Organization Type'),
h('select', { name: 'type' },
h('option', { value: 'infantry' }, 'Infantry / Milsim'),
h('option', { value: 'aviation' }, 'Aviation Wing'),
h('option', { value: 'pmc' }, 'Private Military Company'),
h('option', { value: 'support' }, 'Logistics & Support')
)
),
h('div', { className: 'form-actions' },
h('button', { type: 'submit', style: { width: '100%' } }, 'Submit Registration'),
h('span', {
className: 'cancel-link',
onClick: () => setView('home')
}, 'Cancel / Return to Main')
)
)
)
);
}
// Home View Component
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: () => setView('create') }, 'Register')
),
h('div', { className: 'card' },
h('h2', null, 'Organization Dashboard'),
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: () => setView('login') }, 'Login')
)
);
}
// Footer Component (unchanged)
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')
)
)
)
);
}
// Main App Component
function App() {
const view = getView();
let mainContent;
if (view === 'home') {
mainContent = HomeView();
} else if (view === 'login') {
mainContent = LoginForm();
} else if (view === 'create') {
mainContent = CreateOrgForm();
}
return h('main', null,
h('div', { className: 'container' },
Header({ title: 'Global Organization Network' }),
mainContent
),
Footer()
);
}
// --- 3. Mount Application ---
const root = document.getElementById('app');
render(App, root);

175
arma/ui/apps/base.css Normal file
View File

@ -0,0 +1,175 @@
:root {
--bg-app: #fdfcf8;
--bg-surface: #ffffff;
--bg-surface-hover: #f1f5f9;
--primary: #475569;
--primary-hover: #1e293b;
--text-main: #1f2937;
--text-muted: #64748b;
--text-inverse: #f8fafc;
--border: #e2e8f0;
--radius: 8px;
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
--footer-bg: #1e293b;
}
body {
font-family:
"Inter",
system-ui,
-apple-system,
sans-serif;
margin: 0;
padding: 0;
background: var(--bg-app);
color: var(--text-main);
line-height: 1.6;
}
#app {
min-height: 100vh;
}
main {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.container {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 2rem;
flex: 1;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.header {
text-align: center;
margin-bottom: 3rem;
padding-bottom: 2rem;
border-bottom: 1px solid var(--border);
h1 {
font-size: 2.5rem;
font-weight: 700;
margin-bottom: 0.5rem;
letter-spacing: -0.025em;
color: var(--primary-hover);
}
p {
color: var(--text-muted);
font-size: 1.1rem;
}
}
.card {
background: var(--bg-surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem;
box-shadow: var(--shadow);
text-align: center;
h2 {
margin-top: 0;
font-size: 1.8rem;
color: var(--primary-hover);
}
}
button {
background: var(--primary);
color: white;
border: none;
padding: 0.75rem 1.5rem;
border-radius: var(--radius);
cursor: pointer;
font-size: 1rem;
font-weight: 500;
transition: all 0.2s ease;
&:hover {
background: var(--primary-hover);
transform: translateY(-1px);
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
}
&:disabled {
cursor: not-allowed;
opacity: 0.65;
transform: none;
box-shadow: none;
}
& + & {
margin-left: 1rem;
}
}
.footer {
margin-top: auto;
background: var(--footer-bg);
color: var(--text-inverse);
display: block;
.wrapper {
max-width: 1200px;
width: 100%;
margin: 0 auto;
padding: 3rem 2rem;
box-sizing: border-box;
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
}
h3 {
color: var(--text-inverse);
font-size: 0.85rem;
text-transform: uppercase;
letter-spacing: 0.1em;
font-weight: 700;
margin-bottom: 1.5rem;
border-bottom: 1px solid #475569;
padding-bottom: 0.5rem;
margin-right: 1rem;
}
ul {
li {
color: #cbd5e1;
font-size: 0.95rem;
margin-bottom: 0.75rem;
cursor: pointer;
transition: color 0.2s;
&:hover {
color: white;
}
}
}
}
@media (max-width: 960px) {
.container {
padding: 1.5rem;
}
.header {
margin-bottom: 2rem;
padding-bottom: 1.5rem;
h1 {
font-size: 2rem;
}
}
.footer .wrapper {
grid-template-columns: 1fr;
}
}

6
arma/ui/apps/main/bootstrap.js vendored Normal file
View File

@ -0,0 +1,6 @@
/**
* Registry app bootstrap
*/
const root = document.getElementById("app");
window.RegistryApp.runtime.render(window.RegistryApp.components.App, root);

View File

@ -0,0 +1,168 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
const store = RegistryApp.store;
RegistryApp.componentFns = RegistryApp.componentFns || {};
RegistryApp.componentFns.CreateOrgForm = function CreateOrgForm() {
const handleCreate = () => {
const data = {
orgName: String(
document.getElementById("org-create-name")?.value || "",
),
type: String(
document.getElementById("org-create-type")?.value || "",
),
};
console.log("Org Registration:", data);
};
return h(
"div",
{ className: "split-container" },
h(
"div",
{ className: "info-panel" },
h("h2", null, "Registration Details"),
h(
"p",
null,
"Complete the form to add your organization to the Global Organization Registry.",
),
h(
"ul",
{
style: {
textAlign: "left",
marginTop: "1.5rem",
listStyleType: "none",
padding: 0,
},
},
h(
"li",
{ style: { marginBottom: "0.5rem" } },
"✅ Official Organization Designator",
),
h(
"li",
{ style: { marginBottom: "0.5rem" } },
"✅ Secure Comms Channel",
),
h(
"li",
{ style: { marginBottom: "0.5rem" } },
"✅ Deployment Roster Access",
),
h(
"li",
{ style: { marginBottom: "0.5rem" } },
"✅ After-Action Report Tools",
),
),
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",
),
),
),
h(
"div",
{ className: "form-panel card", style: { margin: 0 } },
h("h2", null, "Organization Registration"),
h(
"div",
{ className: "app-form" },
h(
"div",
null,
h("label", null, "Organization Name"),
h("input", {
id: "org-create-name",
type: "text",
placeholder: "e.g. Task Force 141",
}),
),
h(
"div",
null,
h("label", null, "Organization Type"),
h(
"select",
{ id: "org-create-type" },
h(
"option",
{ value: "infantry" },
"Infantry / Milsim",
),
h("option", { value: "aviation" }, "Aviation Wing"),
h(
"option",
{ value: "pmc" },
"Private Military Company",
),
h(
"option",
{ value: "support" },
"Logistics & Support",
),
),
),
h(
"div",
{ className: "form-actions" },
h(
"button",
{
type: "button",
style: { width: "100%" },
onClick: handleCreate,
},
"Submit Registration",
),
h(
"span",
{
className: "cancel-link",
onClick: () => store.setView("home"),
},
"Cancel / Return to Main",
),
),
),
),
);
};
})();

View File

@ -0,0 +1,43 @@
(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

@ -0,0 +1,72 @@
.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);
}
}
@media (max-width: 960px) {
.split-container {
grid-template-columns: 1fr;
}
}

View File

@ -0,0 +1,23 @@
(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

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

View File

@ -0,0 +1,57 @@
(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",
),
),
);
};
})();

View File

@ -0,0 +1,48 @@
(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

@ -0,0 +1,76 @@
(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 handleLogin = () => {
const data = {
email: String(
document.getElementById("org-login-email")?.value || "",
),
password: String(
document.getElementById("org-login-password")?.value || "",
),
};
console.log("Login Attempt:", data);
store.setView("portal");
};
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: "********",
}),
),
h(
"div",
{ className: "form-actions" },
h(
"button",
{
type: "button",
style: { width: "100%" },
onClick: handleLogin,
},
"Access Authenticator",
),
h(
"span",
{
className: "cancel-link",
onClick: () => store.setView("home"),
},
"Cancel / Return to Main",
),
),
),
);
};
})();

View File

@ -0,0 +1,79 @@
.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

@ -0,0 +1,70 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { h } = RegistryApp.runtime;
const store = RegistryApp.store;
RegistryApp.componentFns = RegistryApp.componentFns || {};
function closeRegistry() {
if (
typeof A3API !== "undefined" &&
typeof A3API.SendAlert === "function"
) {
A3API.SendAlert(
JSON.stringify({
event: "org::close",
data: {},
}),
);
return;
}
store.setView("home");
}
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";
return h(
"nav",
{ className: "app-navbar" },
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(
"div",
{ className: "app-navbar-actions" },
h("span", { className: "app-navbar-view" }, viewLabel),
h(
"button",
{
type: "button",
className: "app-close-btn",
onClick: closeRegistry,
},
actionLabel,
),
),
),
);
};
})();

View File

@ -0,0 +1,58 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>ORBIS - Global Organization Network</title>
<link rel="stylesheet" href="../base.css" />
<link rel="stylesheet" href="components/navbar.css" />
<link rel="stylesheet" href="components/homeView.css" />
<link rel="stylesheet" href="components/forms.css" />
<link rel="stylesheet" href="../portal/components/controls.css" />
<link rel="stylesheet" href="../portal/components/layout.css" />
<link rel="stylesheet" href="../portal/components/portalHeader.css" />
<link rel="stylesheet" href="../portal/components/overviewCard.css" />
<link rel="stylesheet" href="../portal/components/metricCard.css" />
<link rel="stylesheet" href="../portal/components/simpleList.css" />
<link rel="stylesheet" href="../portal/components/simpleStat.css" />
<link rel="stylesheet" href="../portal/components/treasuryCard.css" />
<link rel="stylesheet" href="../portal/components/activityCard.css" />
<link rel="stylesheet" href="../portal/components/futureCard.css" />
<link rel="stylesheet" href="../portal/components/dangerCard.css" />
<link rel="stylesheet" href="../portal/components/modalLayer.css" />
</head>
<body>
<div id="app"></div>
<script src="runtime.js"></script>
<script src="state.js"></script>
<script src="../portal/runtime.js"></script>
<script src="../portal/data.js"></script>
<script src="../portal/store.js"></script>
<script src="../portal/permissions.js"></script>
<script src="../portal/actions.js"></script>
<script src="../portal/components/metricCard.js"></script>
<script src="../portal/components/simpleStat.js"></script>
<script src="../portal/components/portalHeader.js"></script>
<script src="../portal/components/overviewCard.js"></script>
<script src="../portal/components/fleetCard.js"></script>
<script src="../portal/components/treasuryCard.js"></script>
<script src="../portal/components/assetsCard.js"></script>
<script src="../portal/components/membersCard.js"></script>
<script src="../portal/components/activityCard.js"></script>
<script src="../portal/components/futureCard.js"></script>
<script src="../portal/components/dangerCard.js"></script>
<script src="../portal/components/modalLayer.js"></script>
<script src="../portal/components/disbandedView.js"></script>
<script src="../portal/components/footer.js"></script>
<script src="../portal/components/index.js"></script>
<script src="components/navbar.js"></script>
<script src="components/header.js"></script>
<script src="components/loginForm.js"></script>
<script src="components/createOrgForm.js"></script>
<script src="components/homeView.js"></script>
<script src="components/footer.js"></script>
<script src="components/index.js"></script>
<script src="bootstrap.js"></script>
</body>
</html>

View File

@ -0,0 +1,73 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
function h(tag, props = {}, ...children) {
const el = document.createElement(tag);
if (props) {
Object.entries(props).forEach(([key, value]) => {
if (key.startsWith("on") && typeof value === "function") {
el.addEventListener(key.substring(2).toLowerCase(), value);
} else if (key === "className") {
el.className = value;
} else if (key === "style" && typeof value === "object") {
Object.assign(el.style, value);
} else if (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];
}
RegistryApp.runtime = {
h,
render,
createSignal,
};
})();

View File

@ -0,0 +1,12 @@
(function () {
const RegistryApp = (window.RegistryApp = window.RegistryApp || {});
const { createSignal } = RegistryApp.runtime;
class RegistryStore {
constructor() {
[this.getView, this.setView] = createSignal("home");
}
}
RegistryApp.store = new RegistryStore();
})();

View File

@ -0,0 +1,267 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { portalData } = OrgPortal.data;
const store = OrgPortal.store;
const permissions = OrgPortal.permissions;
const registryStore = window.RegistryApp.store;
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);
}
getAssetReadiness() {
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();
})();

View File

@ -0,0 +1,32 @@
.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

@ -0,0 +1,44 @@
(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

@ -0,0 +1,55 @@
(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

@ -0,0 +1,33 @@
.org-secondary-btn {
background: var(--bg-surface);
color: var(--text-main);
border: 1px solid var(--border);
&:hover {
background: var(--bg-surface-hover);
color: var(--text-main);
}
}
.org-danger-btn {
background: #7f1d1d;
color: #fef2f2;
&:hover {
background: #991b1b;
}
}
.org-icon-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 2.5rem;
height: 2.5rem;
padding: 0;
}
.org-icon {
width: 1rem;
height: 1rem;
}

View File

@ -0,0 +1,22 @@
.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

@ -0,0 +1,56 @@
(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

@ -0,0 +1,47 @@
(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

@ -0,0 +1,56 @@
(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

@ -0,0 +1,43 @@
(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

@ -0,0 +1,74 @@
.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

@ -0,0 +1,45 @@
(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

@ -0,0 +1,65 @@
(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

@ -0,0 +1,81 @@
.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

@ -0,0 +1,75 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const store = OrgPortal.store;
const permissions = OrgPortal.permissions;
const actions = OrgPortal.actions;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.MembersCard = function MembersCard() {
const members = store.getMembers();
const allowMemberManagement = permissions.canManageMembers();
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(
"div",
{ className: "org-name-list" },
...members.map((member) =>
h(
"article",
{ className: "org-name-row" },
h("strong", null, member.name),
allowMemberManagement
? h(
"button",
{
type: "button",
className: "org-danger-btn org-icon-btn",
title: `Remove ${member.name}`,
"aria-label": `Remove ${member.name}`,
onClick: () =>
actions.removeMember(member.name),
},
h(
"svg",
{
className: "org-icon",
viewBox: "0 0 24 24",
fill: "none",
stroke: "currentColor",
"stroke-width": "2",
"stroke-linecap": "round",
"stroke-linejoin": "round",
"aria-hidden": "true",
},
h("path", { d: "M9 3h6" }),
h("path", { d: "M4 7h16" }),
h("path", { d: "M6 7l1 13h10l1-13" }),
h("path", { d: "M10 11v6" }),
h("path", { d: "M14 11v6" }),
),
)
: null,
),
),
),
);
};
})();

View File

@ -0,0 +1,69 @@
.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

@ -0,0 +1,20 @@
(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

@ -0,0 +1,131 @@
.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

@ -0,0 +1,286 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const store = OrgPortal.store;
const actions = OrgPortal.actions;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.ModalLayer = function ModalLayer() {
const modal = store.getModal();
if (!modal) {
return null;
}
const members = store.getMembers();
const memberSelectProps =
members.length === 0 ? { disabled: true } : {};
let title = "";
let body = null;
if (modal.type === "payroll") {
title = "Run Payroll";
body = h(
"div",
{ className: "org-modal-form" },
h(
"div",
null,
h("label", null, "Amount Per Member"),
h("input", {
id: "treasury-payroll-amount",
type: "number",
min: "1",
placeholder: "500",
autofocus: "true",
}),
),
h(
"div",
{ className: "org-modal-actions" },
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.closeModal(),
},
"Cancel",
),
h(
"button",
{
type: "button",
onClick: () => {
if (
actions.runPayroll(
actions.parseAmount(
actions.getInputValue(
"treasury-payroll-amount",
),
),
)
) {
actions.closeModal();
}
},
},
"Run Payroll",
),
),
);
} else if (modal.type === "transfer") {
title = "Send Funds";
body = h(
"div",
{ className: "org-modal-form" },
h(
"div",
null,
h("label", null, "Member"),
h(
"select",
{
id: "treasury-transfer-member",
...memberSelectProps,
},
...members.map((member) =>
h("option", { value: member.name }, member.name),
),
),
),
h(
"div",
null,
h("label", null, "Amount"),
h("input", {
id: "treasury-transfer-amount",
type: "number",
min: "1",
placeholder: "1500",
}),
),
h(
"div",
{ className: "org-modal-actions" },
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.closeModal(),
},
"Cancel",
),
h(
"button",
{
type: "button",
...memberSelectProps,
onClick: () => {
if (
actions.sendFundsToMember(
String(
actions.getInputValue(
"treasury-transfer-member",
) || "",
),
actions.parseAmount(
actions.getInputValue(
"treasury-transfer-amount",
),
),
)
) {
actions.closeModal();
}
},
},
"Send Funds",
),
),
);
} else if (modal.type === "credit") {
title = "Assign Credit Line";
body = h(
"div",
{ className: "org-modal-form" },
h(
"div",
null,
h("label", null, "Member"),
h(
"select",
{ id: "treasury-credit-member", ...memberSelectProps },
...members.map((member) =>
h("option", { value: member.name }, member.name),
),
),
),
h(
"div",
null,
h("label", null, "Credit Amount"),
h("input", {
id: "treasury-credit-amount",
type: "number",
min: "1",
placeholder: "5000",
}),
),
h(
"div",
{ className: "org-modal-actions" },
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.closeModal(),
},
"Cancel",
),
h(
"button",
{
type: "button",
...memberSelectProps,
onClick: () => {
if (
actions.grantCreditLine(
String(
actions.getInputValue(
"treasury-credit-member",
) || "",
),
actions.parseAmount(
actions.getInputValue(
"treasury-credit-amount",
),
),
)
) {
actions.closeModal();
}
},
},
"Assign Credit Line",
),
),
);
} else if (modal.type === "disband") {
title = "Disband Organization";
body = h(
"div",
{ className: "org-danger-confirm" },
h(
"p",
null,
"This action is permanent. Disband ",
portalData.org.name,
"?",
),
h(
"div",
{ className: "org-danger-actions" },
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.closeModal(),
},
"Cancel",
),
h(
"button",
{
type: "button",
className: "org-danger-btn",
onClick: () => actions.disbandOrganization(),
},
"Confirm Disband",
),
),
);
}
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,
),
);
};
})();

View File

@ -0,0 +1,58 @@
.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

@ -0,0 +1,118 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { h } = OrgPortal.runtime;
const { portalData } = OrgPortal.data;
const store = OrgPortal.store;
const actions = OrgPortal.actions;
OrgPortal.componentFns = OrgPortal.componentFns || {};
OrgPortal.componentFns.OverviewCard = function OverviewCard() {
const MetricCard = OrgPortal.componentFns.MetricCard;
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(
"div",
{ className: "org-hero-grid" },
h(
"div",
{ className: "org-hero-copy" },
h(
"p",
{ className: "org-summary" },
portalData.org.type,
" operating from ",
portalData.org.headquarters,
". Treasury, fleet status, inventory, and roster management are surfaced here first.",
),
h(
"div",
{ className: "org-meta-row" },
h(
"div",
{ className: "org-meta-item" },
h(
"span",
{ className: "org-meta-label" },
"Director",
),
h(
"span",
{ className: "org-meta-value" },
portalData.org.owner,
),
),
h(
"div",
{ className: "org-meta-item" },
h(
"span",
{ className: "org-meta-label" },
"Active Members",
),
h(
"span",
{ className: "org-meta-value" },
`${store.getMembers().length} total`,
),
),
h(
"div",
{ className: "org-meta-item" },
h(
"span",
{ className: "org-meta-label" },
"Fleet Readiness",
),
h(
"span",
{ className: "org-meta-value" },
`${actions.getAssetReadiness()}%`,
),
),
),
),
h(
"div",
{ className: "org-metric-grid" },
MetricCard(
"Org Funds",
actions.formatCurrency(store.getFunds()),
"Organization treasury balance",
),
MetricCard(
"Reputation",
portalData.reputation,
"Organization standing",
),
MetricCard(
"Asset Lines",
portalData.assets.length,
"Tracked supply and equipment entries",
),
MetricCard(
"Fleet Vehicles",
portalData.fleet.length,
"Tracked air, ground, and naval vehicles",
),
),
),
);
};
})();

View File

@ -0,0 +1,41 @@
.org-page-header {
text-align: left;
margin-bottom: 0;
}
.org-page-heading {
display: flex;
flex-direction: column;
gap: 0.35rem;
}
.org-page-kicker {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--text-muted);
font-weight: 600;
}
.org-page-title {
margin: 0;
}
.org-page-subtitle {
font-size: 0.9rem;
color: var(--text-muted);
margin: 0;
}
.org-page-meta {
font-size: 0.75rem;
color: var(--text-muted);
text-transform: uppercase;
letter-spacing: 0.05em;
}
@media (max-width: 960px) {
.org-page-heading {
gap: 0.3rem;
}
}

View File

@ -0,0 +1,30 @@
(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

@ -0,0 +1,67 @@
.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

@ -0,0 +1,18 @@
.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

@ -0,0 +1,15 @@
(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

@ -0,0 +1,99 @@
.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

@ -0,0 +1,130 @@
(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 noMembers = store.getMembers().length === 0;
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"),
disabled: noMembers,
},
"Run Payroll",
),
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.openModal("transfer"),
disabled: noMembers,
},
"Send Funds",
),
h(
"button",
{
type: "button",
className: "org-secondary-btn",
onClick: () => actions.openModal("credit"),
disabled: noMembers,
},
"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,
);
};
})();

118
arma/ui/apps/portal/data.js Normal file
View File

@ -0,0 +1,118 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
OrgPortal.data = {
portalData: {
org: {
name: "Black Rifle Company",
tag: "BRC-0160566824",
type: "Private Military Company",
status: "Operational",
headquarters: "Georgetown Command Annex",
owner: "Jacob Schmidt",
},
funds: 482750,
reputation: 72,
members: [
{ name: "Jacob Schmidt" },
{ name: "Mara Velez" },
{ name: "Rylan Cross" },
{ name: "Noah Briggs" },
{ name: "Elena Price" },
{ name: "Isaac Rowe" },
{ name: "Talia Boone" },
{ name: "Cade Mercer" },
],
fleet: [
{
name: "UH-80 Ghost Hawk",
type: "helicopter",
status: "Ready",
damage: "16%",
},
{
name: "MH-9 Hummingbird",
type: "helicopter",
status: "Ready",
damage: "8%",
},
{
name: "M-ATV Patrol 1",
type: "car",
status: "Fielded",
damage: "24%",
},
{
name: "M2A1 Slammer",
type: "armor",
status: "Ready",
damage: "11%",
},
{
name: "RHIB Patrol Boat",
type: "naval",
status: "Repairing",
damage: "32%",
},
],
assets: [
{ name: "First Aid Kits", type: "items", quantity: "36" },
{ name: "MX 6.5 mm Rifles", type: "weapons", quantity: "18" },
{
name: "6.5 mm Magazines",
type: "magazines",
quantity: "120",
},
{
name: "Carryall Backpacks",
type: "backpacks",
quantity: "24",
},
],
activity: [
{
time: "08:20",
text: "Treasury cleared contractor payment for northern route escort.",
},
{
time: "07:45",
text: "Viper Flight completed readiness checks on all rotary assets.",
},
{
time: "07:10",
text: "New recruit Cade Mercer accepted into ground training roster.",
},
{
time: "06:30",
text: "North Depot inventory count pushed reserve ratio above target.",
},
],
roadmap: [
{
name: "Contracts Board",
status: "Planned",
detail: "Track payouts, assignments, and claim approvals.",
},
{
name: "Diplomacy",
status: "Future Review",
detail: "Possible future module pending a full design and scope review.",
},
{
name: "Logistics Queue",
status: "Future Review",
detail: "Possible future module pending a full design and scope review.",
},
{
name: "Permissions",
status: "Future Review",
detail: "Possible future module pending a full design and scope review.",
},
],
},
session: {
actorName: "Jacob Schmidt",
role: "Leader",
},
};
})();

View File

@ -0,0 +1,28 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { portalData, session } = OrgPortal.data;
class OrgPortalPermissions {
isOrgLeaderOrCeo() {
return (
session.actorName === portalData.org.owner ||
session.role === "Leader" ||
session.role === "CEO"
);
}
canManageMembers() {
return this.isOrgLeaderOrCeo();
}
canManageTreasury() {
return this.isOrgLeaderOrCeo();
}
canDisbandOrg() {
return this.isOrgLeaderOrCeo();
}
}
OrgPortal.permissions = new OrgPortalPermissions();
})();

View File

@ -0,0 +1,98 @@
(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

@ -0,0 +1,23 @@
(function () {
const OrgPortal = (window.OrgPortal = window.OrgPortal || {});
const { createSignal } = window.RegistryApp.runtime;
const { portalData } = OrgPortal.data;
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);
}
}
OrgPortal.store = new OrgPortalStore();
})();

View File

@ -1,16 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ORBIS - Global Organization Network</title>
<link rel="stylesheet" href="style.css">
</head>
<body>
<div id="app"></div>
<script src="app.js"></script>
</body>
</html>

View File

@ -80,7 +80,6 @@ main {
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 2rem;
margin-bottom: 2rem;
box-shadow: var(--shadow);
text-align: center;