diff --git a/.gitignore b/.gitignore index db4bdb4..65107c9 100644 --- a/.gitignore +++ b/.gitignore @@ -21,6 +21,9 @@ target/ *.swo *~ +# Misc +node_modules/ + # OS .DS_Store Thumbs.db diff --git a/arma/client/addons/bank/ui/_site/bank-ui.css b/arma/client/addons/bank/ui/_site/bank-ui.css index e8d03f2..cea6a3b 100644 --- a/arma/client/addons/bank/ui/_site/bank-ui.css +++ b/arma/client/addons/bank/ui/_site/bank-ui.css @@ -1,591 +1 @@ -/* Generated by tools/build-webui.mjs for Bank UI styles. Do not edit directly. */ -:root { - --bank-shell-bg: #f6f4ee; - --bank-surface: linear-gradient(180deg, #ffffff 0%, #f4f8fd 100%); - --bank-border: rgba(18, 54, 93, 0.12); - --bank-border-strong: rgba(18, 54, 93, 0.18); - --bank-text-main: #142f52; - --bank-text-muted: #6f86a3; - --bank-text-subtle: #8ea2bb; - --bank-accent: #275a8c; - --bank-accent-soft: #dfeaf9; - --bank-accent-line: rgba(39, 90, 140, 0.12); - --bank-shadow: 0 16px 30px rgba(18, 36, 57, 0.08); -} - -*, -*::before, -*::after { - box-sizing: border-box; -} - -html, -body, -#app { - width: 100%; - height: 100%; - margin: 0; -} - -body { - overflow: hidden; - background: transparent; - color: var(--bank-text-main); - font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif; -} - -button, -input, -select { - font: inherit; -} - -.bank-shell { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - background: var(--bank-shell-bg); -} - -.bank-scroll-shell { - flex: 1; - min-height: 0; - overflow: auto; - display: flex; - flex-direction: column; -} - -.bank-layout { - min-height: 100%; - width: min(100%, 1600px); - margin: 0 auto; - display: grid; - grid-template-columns: 320px minmax(0, 1fr); - gap: 1.25rem; - padding: 1.25rem; - flex: 1 0 auto; -} - -.bank-sidebar, -.bank-main { - min-height: 0; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.bank-main { - overflow: visible; -} - -.bank-module, -.bank-card, -.bank-atm-panel { - background: var(--bank-surface); - border: 1px solid var(--bank-border); - border-radius: 1.3rem; - box-shadow: var(--bank-shadow); -} - -.bank-module, -.bank-card, -.bank-atm-panel { - padding: 1rem; - display: flex; - flex-direction: column; -} - -.bank-module-header, -.bank-card-header, -.bank-section-header, -.bank-page-header { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; -} - -.bank-module-header, -.bank-card-header { - margin-bottom: 0.9rem; -} - -.bank-page { - display: grid; - gap: 1.35rem; - padding: 0.1rem 0 0; -} - -.bank-page-header { - padding-top: 0.4rem; -} - -.bank-page-copy { - margin: 0; - color: var(--bank-text-muted); - line-height: 1.5; - max-width: 48rem; -} - -.bank-page-divider { - border-top: 1px solid var(--bank-accent-line); -} - -.bank-page-body { - display: grid; - gap: 1.25rem; - padding-bottom: 1.25rem; -} - -.bank-page-section { - display: grid; - gap: 1rem; - padding: 1.15rem 1.2rem 1.25rem; - border: 1px solid var(--bank-border); - border-radius: 1.3rem; - background: rgba(255, 255, 255, 0.72); - box-shadow: none; -} - -.bank-title, -.bank-section-title { - margin: 0; - color: var(--bank-text-main); - letter-spacing: -0.02em; -} - -.bank-title { - font-size: 1.7rem; -} - -.bank-section-title { - font-size: 1.1rem; -} - -.bank-eyebrow, -.bank-footer-title, -.bank-stat-label { - display: block; - font-size: 0.68rem; - letter-spacing: 0.16em; - text-transform: uppercase; - font-weight: 700; - color: var(--bank-text-subtle); -} - -.bank-pill { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.48rem 0.8rem; - border-radius: 999px; - background: var(--bank-accent-soft); - color: var(--bank-accent); - font-size: 0.74rem; - font-weight: 700; - letter-spacing: 0.1em; - text-transform: uppercase; - white-space: nowrap; -} - -.bank-summary-grid, -.bank-profile-stack { - display: grid; - gap: 0.8rem; -} - -.bank-summary-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.bank-stat-card, -.bank-metric-card { - min-width: 0; - display: flex; - flex-direction: column; - gap: 0.25rem; - padding: 0.9rem; - border-radius: 0.95rem; - border: 1px solid var(--bank-border); - background: rgba(255, 255, 255, 0.6); -} - -.bank-stat-card.is-accent, -.bank-metric-card.is-accent { - background: linear-gradient(180deg, #edf4fe 0%, #dfeaf9 100%); -} - -.bank-stat-card.is-success, -.bank-metric-card.is-success { - background: linear-gradient(180deg, #edf9f4 0%, #dff4ea 100%); -} - -.bank-stat-card.is-warning, -.bank-metric-card.is-warning { - background: linear-gradient(180deg, #fdf7ea 0%, #f7edd4 100%); -} - -.bank-stat-value, -.bank-metric-value { - min-width: 0; - color: var(--bank-text-main); - font-weight: 700; - overflow-wrap: anywhere; -} - -.bank-stat-value { - font-size: 1rem; -} - -.bank-metric-value { - font-size: 1.8rem; - letter-spacing: -0.03em; -} - -.bank-metric-copy, -.bank-card-copy, -.bank-empty-copy, -.bank-footer-copy, -.bank-history-meta { - color: var(--bank-text-muted); - line-height: 1.45; -} - -.bank-card-copy { - margin: 0 0 0.9rem; -} - -.bank-summary-band { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0.85rem; -} - -.bank-action-sections { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; -} - -.bank-support-sections { - display: grid; - grid-template-columns: minmax(0, 1fr); - gap: 1rem; -} - -.bank-form-stack { - display: grid; - gap: 0.75rem; -} - -.bank-input, -.bank-select { - width: 100%; - min-width: 0; - height: 2.9rem; - padding: 0 0.95rem; - border-radius: 0.8rem; - border: 1px solid var(--bank-border); - background: rgba(255, 255, 255, 0.82); - color: var(--bank-text-main); -} - -.bank-action-row { - display: flex; - gap: 0.75rem; -} - -.bank-btn { - display: inline-flex; - align-items: center; - justify-content: center; - min-height: 2.85rem; - padding: 0.75rem 1rem; - border-radius: 0.8rem; - border: 1px solid var(--bank-border); - font-size: 0.82rem; - font-weight: 700; - letter-spacing: 0.12em; - text-transform: uppercase; - cursor: pointer; - transition: - background-color 160ms ease, - color 160ms ease, - border-color 160ms ease; -} - -.bank-btn:disabled { - opacity: 0.55; - cursor: default; -} - -.bank-btn-primary { - background: #455a77; - border-color: #455a77; - color: #fff; -} - -.bank-btn-primary:hover:not(:disabled) { - background: #354863; - border-color: #354863; -} - -.bank-btn-secondary { - background: rgba(255, 255, 255, 0.82); - color: var(--bank-accent); -} - -.bank-btn-secondary:hover:not(:disabled) { - background: #eef4fd; -} - -.bank-history-list { - display: grid; - gap: 0.75rem; -} - -.bank-history-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 0.85rem 0.95rem; - border-radius: 0.9rem; - border: 1px solid var(--bank-border); - background: rgba(255, 255, 255, 0.6); -} - -.bank-history-copy { - min-width: 0; - display: grid; - gap: 0.18rem; -} - -.bank-history-title, -.bank-empty-title { - color: var(--bank-text-main); - font-weight: 700; -} - -.bank-history-value { - white-space: nowrap; - font-weight: 700; - color: var(--bank-accent); -} - -.bank-empty-state { - display: grid; - gap: 0.35rem; - padding: 1rem 0; -} - -.bank-notice-stack { - position: fixed; - top: 1.2rem; - right: 1.5rem; - z-index: 12; - display: grid; - gap: 0.65rem; -} - -.bank-notice { - max-width: 24rem; - padding: 0.85rem 1rem; - border-radius: 0.9rem; - border: 1px solid var(--bank-border); - background: #fff; - box-shadow: 0 14px 28px rgba(16, 34, 56, 0.14); - font-size: 0.92rem; -} - -.bank-notice.is-success { - background: #ecfdf5; - border-color: #bbf7d0; - color: #166534; -} - -.bank-notice.is-error { - background: #fef2f2; - border-color: #fecaca; - color: #991b1b; -} - -.bank-footer-bar { - width: 100%; - margin-top: auto; - background: #1e293b; - color: #f8fafc; -} - -.bank-footer { - width: min(100%, 1600px); - margin: 0 auto; - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 4rem; - padding: 3rem 1.25rem; -} - -.bank-footer-block { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -.bank-footer-title { - margin: 0; - color: #f8fafc; - font-size: 0.85rem; - text-transform: uppercase; - letter-spacing: 0.1em; - font-weight: 700; - padding-bottom: 0.5rem; - border-bottom: 1px solid #475569; -} - -.bank-footer-list { - margin: 0; - padding: 0; - list-style: none; -} - -.bank-atm-shell { - flex: 1; - min-height: 0; - display: flex; - align-items: center; - justify-content: center; - padding: 2rem 1rem; -} - -.bank-atm-panel { - width: min(100%, 560px); - display: grid; - gap: 1rem; -} - -.bank-atm-stack { - display: grid; - gap: 1rem; -} - -.bank-pin-display, -.bank-balance-display { - display: flex; - align-items: center; - justify-content: center; - min-height: 5rem; - padding: 1rem; - border-radius: 1rem; - border: 1px solid var(--bank-border-strong); - background: rgba(255, 255, 255, 0.68); - color: var(--bank-text-main); - text-align: center; -} - -.bank-pin-display { - font-size: 2rem; -} - -.bank-balance-display { - font-size: 2.5rem; - font-weight: 800; - letter-spacing: -0.03em; -} - -.bank-pin-indicators { - display: flex; - align-items: center; - justify-content: center; - gap: 0.9rem; -} - -.bank-pin-indicator { - width: 1rem; - height: 1rem; - border-radius: 999px; - border: 2px solid var(--bank-accent); - background: transparent; -} - -.bank-pin-indicator.is-filled { - background: var(--bank-accent); -} - -.bank-keypad { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 0.75rem; -} - -.bank-key { - min-height: 3.2rem; - padding: 0.9rem; - border-radius: 0.9rem; - border: 1px solid var(--bank-border); - background: rgba(255, 255, 255, 0.82); - color: var(--bank-text-main); - font-weight: 700; -} - -.bank-key.is-muted { - background: #eef2f8; - color: var(--bank-text-muted); -} - -.bank-key.is-accent { - background: #455a77; - border-color: #455a77; - color: #fff; -} - -.bank-key.is-wide { - grid-column: span 3; -} - -.bank-atm-action-grid { - display: grid; - gap: 0.75rem; -} - -.bank-shell.is-atm { - background: transparent; - min-height: 100%; - justify-content: center; -} - -.bank-shell.is-atm .bank-atm-shell { - flex: 1; - width: 100%; - min-height: 100%; - max-width: 100%; -} - -.bank-footer-copy { - color: #cbd5e1; - line-height: 1.5; - margin: 0 0 0.75rem; -} - -@media (max-width: 1200px) { - .bank-layout { - grid-template-columns: 1fr; - } - - .bank-main { - overflow: visible; - } -} - -@media (max-width: 900px) { - .bank-summary-band, - .bank-action-sections, - .bank-footer { - grid-template-columns: 1fr; - } - - .bank-summary-grid { - grid-template-columns: 1fr; - } -} +:root{--bank-shell-bg:#f6f4ee;--bank-surface:linear-gradient(180deg, #fff 0%, #f4f8fd 100%);--bank-border:#12365d1f;--bank-border-strong:#12365d2e;--bank-text-main:#142f52;--bank-text-muted:#6f86a3;--bank-text-subtle:#8ea2bb;--bank-accent:#275a8c;--bank-accent-soft:#dfeaf9;--bank-accent-line:#275a8c1f;--bank-shadow:0 16px 30px #12243914}*,:before,:after{box-sizing:border-box}html,body,#app{width:100%;height:100%;margin:0}body{color:var(--bank-text-main);background:0 0;font-family:Segoe UI,Tahoma,Geneva,Verdana,sans-serif;overflow:hidden}button,input,select{font:inherit}.bank-shell{background:var(--bank-shell-bg);flex-direction:column;width:100%;height:100%;display:flex}.bank-scroll-shell{flex-direction:column;flex:1;min-height:0;display:flex;overflow:auto}.bank-layout{flex:1 0 auto;grid-template-columns:320px minmax(0,1fr);gap:1.25rem;width:min(100%,1600px);min-height:100%;margin:0 auto;padding:1.25rem;display:grid}.bank-sidebar,.bank-main{flex-direction:column;gap:1rem;min-height:0;display:flex}.bank-main{overflow:visible}.bank-module,.bank-card,.bank-atm-panel{background:var(--bank-surface);border:1px solid var(--bank-border);box-shadow:var(--bank-shadow);border-radius:1.3rem;flex-direction:column;padding:1rem;display:flex}.bank-module-header,.bank-card-header,.bank-section-header,.bank-page-header{justify-content:space-between;align-items:flex-start;gap:1rem;display:flex}.bank-module-header,.bank-card-header{margin-bottom:.9rem}.bank-page{gap:1.35rem;padding:.1rem 0 0;display:grid}.bank-page-header{padding-top:.4rem}.bank-page-copy{color:var(--bank-text-muted);max-width:48rem;margin:0;line-height:1.5}.bank-page-divider{border-top:1px solid var(--bank-accent-line)}.bank-page-body{gap:1.25rem;padding-bottom:1.25rem;display:grid}.bank-page-section{border:1px solid var(--bank-border);box-shadow:none;background:#ffffffb8;border-radius:1.3rem;gap:1rem;padding:1.15rem 1.2rem 1.25rem;display:grid}.bank-title,.bank-section-title{color:var(--bank-text-main);letter-spacing:-.02em;margin:0}.bank-title{font-size:1.7rem}.bank-section-title{font-size:1.1rem}.bank-eyebrow,.bank-footer-title,.bank-stat-label{letter-spacing:.16em;text-transform:uppercase;color:var(--bank-text-subtle);font-size:.68rem;font-weight:700;display:block}.bank-pill{background:var(--bank-accent-soft);color:var(--bank-accent);letter-spacing:.1em;text-transform:uppercase;white-space:nowrap;border-radius:999px;justify-content:center;align-items:center;padding:.48rem .8rem;font-size:.74rem;font-weight:700;display:inline-flex}.bank-summary-grid,.bank-profile-stack{gap:.8rem;display:grid}.bank-summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.bank-stat-card,.bank-metric-card{border:1px solid var(--bank-border);background:#fff9;border-radius:.95rem;flex-direction:column;gap:.25rem;min-width:0;padding:.9rem;display:flex}.bank-stat-card.is-accent,.bank-metric-card.is-accent{background:linear-gradient(#edf4fe 0%,#dfeaf9 100%)}.bank-stat-card.is-success,.bank-metric-card.is-success{background:linear-gradient(#edf9f4 0%,#dff4ea 100%)}.bank-stat-card.is-warning,.bank-metric-card.is-warning{background:linear-gradient(#fdf7ea 0%,#f7edd4 100%)}.bank-stat-value,.bank-metric-value{min-width:0;color:var(--bank-text-main);overflow-wrap:anywhere;font-weight:700}.bank-stat-value{font-size:1rem}.bank-metric-value{letter-spacing:-.03em;font-size:1.8rem}.bank-metric-copy,.bank-card-copy,.bank-empty-copy,.bank-footer-copy,.bank-history-meta{color:var(--bank-text-muted);line-height:1.45}.bank-card-copy{margin:0 0 .9rem}.bank-summary-band{grid-template-columns:repeat(2,minmax(0,1fr));gap:.85rem;display:grid}.bank-action-sections{grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem;display:grid}.bank-support-sections{grid-template-columns:minmax(0,1fr);gap:1rem;display:grid}.bank-form-stack{gap:.75rem;display:grid}.bank-input,.bank-select{border:1px solid var(--bank-border);width:100%;min-width:0;height:2.9rem;color:var(--bank-text-main);background:#ffffffd1;border-radius:.8rem;padding:0 .95rem}.bank-action-row{gap:.75rem;display:flex}.bank-btn{border:1px solid var(--bank-border);letter-spacing:.12em;text-transform:uppercase;cursor:pointer;border-radius:.8rem;justify-content:center;align-items:center;min-height:2.85rem;padding:.75rem 1rem;font-size:.82rem;font-weight:700;transition:background-color .16s,color .16s,border-color .16s;display:inline-flex}.bank-btn:disabled{opacity:.55;cursor:default}.bank-btn-primary{color:#fff;background:#455a77;border-color:#455a77}.bank-btn-primary:hover:not(:disabled){background:#354863;border-color:#354863}.bank-btn-secondary{color:var(--bank-accent);background:#ffffffd1}.bank-btn-secondary:hover:not(:disabled){background:#eef4fd}.bank-history-list{gap:.75rem;display:grid}.bank-history-row{border:1px solid var(--bank-border);background:#fff9;border-radius:.9rem;justify-content:space-between;align-items:center;gap:1rem;padding:.85rem .95rem;display:flex}.bank-history-copy{gap:.18rem;min-width:0;display:grid}.bank-history-title,.bank-empty-title{color:var(--bank-text-main);font-weight:700}.bank-history-value{white-space:nowrap;color:var(--bank-accent);font-weight:700}.bank-empty-state{gap:.35rem;padding:1rem 0;display:grid}.bank-notice-stack{z-index:12;gap:.65rem;display:grid;position:fixed;top:1.2rem;right:1.5rem}.bank-notice{border:1px solid var(--bank-border);background:#fff;border-radius:.9rem;max-width:24rem;padding:.85rem 1rem;font-size:.92rem;box-shadow:0 14px 28px #10223824}.bank-notice.is-success{color:#166534;background:#ecfdf5;border-color:#bbf7d0}.bank-notice.is-error{color:#991b1b;background:#fef2f2;border-color:#fecaca}.bank-footer-bar{color:#f8fafc;background:#1e293b;width:100%;margin-top:auto}.bank-footer{grid-template-columns:repeat(2,minmax(0,1fr));gap:4rem;width:min(100%,1600px);margin:0 auto;padding:3rem 1.25rem;display:grid}.bank-footer-block{flex-direction:column;gap:.75rem;display:flex}.bank-footer-title{color:#f8fafc;text-transform:uppercase;letter-spacing:.1em;border-bottom:1px solid #475569;margin:0;padding-bottom:.5rem;font-size:.85rem;font-weight:700}.bank-footer-list{margin:0;padding:0;list-style:none}.bank-atm-shell{flex:1;justify-content:center;align-items:center;min-height:0;padding:2rem 1rem;display:flex}.bank-atm-panel{gap:1rem;width:min(100%,560px);display:grid}.bank-atm-stack{gap:1rem;display:grid}.bank-pin-display,.bank-balance-display{border:1px solid var(--bank-border-strong);min-height:5rem;color:var(--bank-text-main);text-align:center;background:#ffffffad;border-radius:1rem;justify-content:center;align-items:center;padding:1rem;display:flex}.bank-pin-display{font-size:2rem}.bank-balance-display{letter-spacing:-.03em;font-size:2.5rem;font-weight:800}.bank-pin-indicators{justify-content:center;align-items:center;gap:.9rem;display:flex}.bank-pin-indicator{border:2px solid var(--bank-accent);background:0 0;border-radius:999px;width:1rem;height:1rem}.bank-pin-indicator.is-filled{background:var(--bank-accent)}.bank-keypad{grid-template-columns:repeat(3,minmax(0,1fr));gap:.75rem;display:grid}.bank-key{border:1px solid var(--bank-border);min-height:3.2rem;color:var(--bank-text-main);background:#ffffffd1;border-radius:.9rem;padding:.9rem;font-weight:700}.bank-key.is-muted{color:var(--bank-text-muted);background:#eef2f8}.bank-key.is-accent{color:#fff;background:#455a77;border-color:#455a77}.bank-key.is-wide{grid-column:span 3}.bank-atm-action-grid{gap:.75rem;display:grid}.bank-shell.is-atm{background:0 0;justify-content:center;min-height:100%}.bank-shell.is-atm .bank-atm-shell{flex:1;width:100%;max-width:100%;min-height:100%}.bank-footer-copy{color:#cbd5e1;margin:0 0 .75rem;line-height:1.5}@media (width<=1200px){.bank-layout{grid-template-columns:1fr}.bank-main{overflow:visible}}@media (width<=900px){.bank-summary-band,.bank-action-sections,.bank-footer,.bank-summary-grid{grid-template-columns:1fr}} \ No newline at end of file diff --git a/arma/client/addons/bank/ui/_site/bank-ui.js b/arma/client/addons/bank/ui/_site/bank-ui.js index 5203cfd..cf05616 100644 --- a/arma/client/addons/bank/ui/_site/bank-ui.js +++ b/arma/client/addons/bank/ui/_site/bank-ui.js @@ -1,1650 +1 @@ -/* Generated by tools/build-webui.mjs for Bank UI app. Do not edit directly. */ -(function () { - const runtime = window.ForgeWebUI; - const BankApp = (window.BankApp = window.BankApp || {}); - BankApp.runtime = runtime; - window.AppRuntime = runtime; -})(); - -(function () { - const BankApp = (window.BankApp = window.BankApp || {}); - - const defaultSession = { - mode: "bank", - orgFunds: 0, - orgName: "", - playerName: "", - transferTargets: [], - uid: "", - }; - - const defaultAccount = { - bank: 0, - cash: 0, - earnings: 0, - pin: "1234", - transactions: [], - }; - - function cloneValue(value) { - return JSON.parse(JSON.stringify(value)); - } - - function replaceObject(target, source) { - Object.keys(target).forEach((key) => delete target[key]); - Object.assign(target, cloneValue(source)); - } - - BankApp.data = { - account: Object.assign({}, defaultAccount), - session: Object.assign({}, defaultSession), - applyHydratePayload(payload) { - replaceObject( - this.session, - Object.assign({}, defaultSession, payload?.session || {}), - ); - replaceObject( - this.account, - Object.assign({}, defaultAccount, payload?.account || {}), - ); - }, - }; -})(); - -(function () { - const BankApp = (window.BankApp = window.BankApp || {}); - const { createSignal } = BankApp.runtime; - - class BankStore { - constructor() { - [this.getMode, this.setMode] = createSignal("bank"); - [this.getNotice, this.setNotice] = createSignal({ - text: "", - type: "", - }); - [this.getPendingAction, this.setPendingAction] = createSignal(""); - [this.getAtmView, this.setAtmView] = createSignal("pin"); - [this.getEnteredPin, this.setEnteredPin] = createSignal(""); - [this.getCustomAmount, this.setCustomAmount] = createSignal(""); - [this.getAccountVersion, this.setAccountVersion] = createSignal(0); - [this.getSessionVersion, this.setSessionVersion] = createSignal(0); - } - - finishAction() { - this.setPendingAction(""); - } - - hydrateFromPayload(payload) { - const mode = String(payload?.session?.mode || "bank") - .trim() - .toLowerCase(); - const currentMode = this.getMode(); - const currentAtmView = this.getAtmView(); - - this.setMode(mode === "atm" ? "atm" : "bank"); - this.setPendingAction(""); - this.setNotice({ text: "", type: "" }); - this.setEnteredPin(""); - this.setCustomAmount(""); - this.setAccountVersion(this.getAccountVersion() + 1); - this.setSessionVersion(this.getSessionVersion() + 1); - - if (mode === "atm") { - this.setAtmView(currentMode === "atm" ? currentAtmView : "pin"); - return; - } - - this.setAtmView("dashboard"); - } - - resetAtm() { - this.setEnteredPin(""); - this.setCustomAmount(""); - this.setAtmView("pin"); - } - - startAction(action) { - this.setPendingAction( - String(action || "") - .trim() - .toLowerCase(), - ); - } - } - - BankApp.store = new BankStore(); -})(); - -(function () { - const BankApp = (window.BankApp = window.BankApp || {}); - const store = BankApp.store; - const bridge = window.ForgeWebUI.createBridge({ - closeEvent: "bank::close", - globalName: "ForgeBridge", - readyEvent: "bank::ready", - }); - - function hydrate(payloadData) { - BankApp.data.applyHydratePayload(payloadData); - store.hydrateFromPayload(payloadData); - } - - bridge.on("bank::hydrate", hydrate); - bridge.on("bank::sync", hydrate); - bridge.on("bank::notice", (payloadData) => { - if (BankApp.actions) { - BankApp.actions.showNotice( - payloadData.type || "error", - payloadData.message || "Bank notice received.", - ); - } - }); - - BankApp.bridge = { - notifyReady() { - return bridge.ready({ loaded: true }); - }, - receive: bridge.receive, - requestClose() { - return bridge.close({}); - }, - requestDeposit(payload) { - return bridge.send("bank::deposit::request", payload); - }, - requestDepositEarnings(payload) { - return bridge.send("bank::depositEarnings::request", payload); - }, - requestRefresh() { - return bridge.send("bank::refresh", {}); - }, - requestTransfer(payload) { - return bridge.send("bank::transfer::request", payload); - }, - requestWithdraw(payload) { - return bridge.send("bank::withdraw::request", payload); - }, - sendEvent: bridge.send, - }; -})(); - -(function () { - const BankApp = (window.BankApp = window.BankApp || {}); - const store = BankApp.store; - - let noticeTimer = null; - - function getAccount() { - return BankApp.data?.account || {}; - } - - function getSession() { - return BankApp.data?.session || {}; - } - - function normalizeAmount(value) { - const amount = Math.floor(Number(value || 0)); - return Number.isFinite(amount) ? amount : 0; - } - - function showNotice(type, text) { - store.setNotice({ type, text }); - - if (noticeTimer) { - clearTimeout(noticeTimer); - } - - noticeTimer = setTimeout(() => { - store.setNotice({ text: "", type: "" }); - noticeTimer = null; - }, 3200); - } - - function closeBank() { - const bridge = BankApp.bridge; - if (bridge && typeof bridge.requestClose === "function") { - const sent = bridge.requestClose(); - if (sent) { - return true; - } - } - - showNotice("error", "Bank bridge is unavailable."); - return false; - } - - function refreshBank() { - const bridge = BankApp.bridge; - if (bridge && typeof bridge.requestRefresh === "function") { - const sent = bridge.requestRefresh(); - if (sent) { - return true; - } - } - - showNotice("error", "Bank refresh bridge is unavailable."); - return false; - } - - function requestDeposit(amountValue) { - const amount = normalizeAmount(amountValue); - const account = getAccount(); - - if (amount <= 0) { - showNotice("error", "Enter a valid deposit amount."); - return false; - } - - if (amount > Number(account.cash || 0)) { - showNotice("error", "Cash on hand cannot cover that deposit."); - return false; - } - - const bridge = BankApp.bridge; - if (!bridge || typeof bridge.requestDeposit !== "function") { - showNotice("error", "Deposit bridge is unavailable."); - return false; - } - - store.startAction("deposit"); - const sent = bridge.requestDeposit({ amount }); - if (!sent) { - store.finishAction(); - showNotice("error", "Deposit bridge is unavailable."); - return false; - } - - return true; - } - - function requestWithdraw(amountValue) { - const amount = normalizeAmount(amountValue); - const account = getAccount(); - - if (amount <= 0) { - showNotice("error", "Enter a valid withdrawal amount."); - return false; - } - - if (amount > Number(account.bank || 0)) { - showNotice("error", "Bank balance cannot cover that withdrawal."); - return false; - } - - const bridge = BankApp.bridge; - if (!bridge || typeof bridge.requestWithdraw !== "function") { - showNotice("error", "Withdraw bridge is unavailable."); - return false; - } - - store.startAction("withdraw"); - const sent = bridge.requestWithdraw({ amount }); - if (!sent) { - store.finishAction(); - showNotice("error", "Withdraw bridge is unavailable."); - return false; - } - - return true; - } - - function requestTransfer(targetUid, amountValue) { - const amount = normalizeAmount(amountValue); - const session = getSession(); - const account = getAccount(); - const targetId = String(targetUid || "").trim(); - - if (!targetId) { - showNotice("error", "Select a transfer recipient."); - return false; - } - - if (targetId === String(session.uid || "")) { - showNotice("error", "You cannot transfer funds to yourself."); - return false; - } - - if (amount <= 0) { - showNotice("error", "Enter a valid transfer amount."); - return false; - } - - if (amount > Number(account.bank || 0)) { - showNotice("error", "Bank balance cannot cover that transfer."); - return false; - } - - const bridge = BankApp.bridge; - if (!bridge || typeof bridge.requestTransfer !== "function") { - showNotice("error", "Transfer bridge is unavailable."); - return false; - } - - store.startAction("transfer"); - const sent = bridge.requestTransfer({ - amount, - from: "bank", - target: targetId, - }); - if (!sent) { - store.finishAction(); - showNotice("error", "Transfer bridge is unavailable."); - return false; - } - - return true; - } - - function requestDepositEarnings(amountValue) { - const amount = normalizeAmount(amountValue); - const account = getAccount(); - - if (amount <= 0) { - showNotice("error", "No earnings are available to deposit."); - return false; - } - - if (amount > Number(account.earnings || 0)) { - showNotice( - "error", - "Pending earnings cannot cover that deposit request.", - ); - return false; - } - - const bridge = BankApp.bridge; - if (!bridge || typeof bridge.requestDepositEarnings !== "function") { - showNotice("error", "Earnings bridge is unavailable."); - return false; - } - - store.startAction("depositearnings"); - const sent = bridge.requestDepositEarnings({ amount }); - if (!sent) { - store.finishAction(); - showNotice("error", "Earnings bridge is unavailable."); - return false; - } - - return true; - } - - function appendPinDigit(digit) { - const nextDigit = String(digit || "").trim(); - if (!nextDigit) { - return; - } - - const currentPin = String(store.getEnteredPin() || ""); - if (currentPin.length >= 4) { - return; - } - - store.setEnteredPin(currentPin + nextDigit); - } - - function backspacePin() { - const currentPin = String(store.getEnteredPin() || ""); - store.setEnteredPin(currentPin.slice(0, -1)); - } - - function clearPin() { - store.setEnteredPin(""); - } - - function submitPin() { - const enteredPin = String(store.getEnteredPin() || ""); - const actualPin = String(getAccount().pin || "1234"); - - if (enteredPin.length !== 4) { - showNotice("error", "Enter your four-digit access PIN."); - return false; - } - - if (enteredPin !== actualPin) { - clearPin(); - showNotice("error", "Incorrect PIN."); - return false; - } - - clearPin(); - store.setAtmView("menu"); - return true; - } - - function selectAtmView(view) { - const nextView = String(view || "").trim(); - if (!nextView) { - return false; - } - - if (nextView === "pin") { - store.resetAtm(); - return true; - } - - store.setCustomAmount(""); - store.setAtmView(nextView); - return true; - } - - function appendCustomAmountDigit(digit) { - const nextDigit = String(digit || "").trim(); - if (!nextDigit) { - return; - } - - const currentValue = String(store.getCustomAmount() || ""); - if (currentValue.length >= 7) { - return; - } - - store.setCustomAmount(currentValue + nextDigit); - } - - function backspaceCustomAmount() { - const currentValue = String(store.getCustomAmount() || ""); - store.setCustomAmount(currentValue.slice(0, -1)); - } - - function clearCustomAmount() { - store.setCustomAmount(""); - } - - function submitCustomAmount(kind) { - const amount = normalizeAmount(store.getCustomAmount()); - const nextKind = String(kind || "") - .trim() - .toLowerCase(); - - if (amount <= 0) { - showNotice("error", "Enter a valid transaction amount."); - return false; - } - - const success = - nextKind === "deposit" - ? requestDeposit(amount) - : requestWithdraw(amount); - - if (success) { - store.setCustomAmount(""); - store.setAtmView("menu"); - } - - return success; - } - - function requestAtmAmount(kind, amount) { - const nextKind = String(kind || "") - .trim() - .toLowerCase(); - const success = - nextKind === "deposit" - ? requestDeposit(amount) - : requestWithdraw(amount); - - if (success) { - store.setAtmView("menu"); - } - - return success; - } - - BankApp.actions = { - appendCustomAmountDigit, - appendPinDigit, - backspaceCustomAmount, - backspacePin, - clearCustomAmount, - clearPin, - closeBank, - refreshBank, - requestAtmAmount, - requestDeposit, - requestDepositEarnings, - requestTransfer, - requestWithdraw, - selectAtmView, - showNotice, - submitCustomAmount, - submitPin, - }; -})(); - -(function () { - const BankApp = (window.BankApp = window.BankApp || {}); - const { h } = BankApp.runtime; - const store = BankApp.store; - const { account } = BankApp.data; - - function formatCurrency(value) { - return `$${Math.round(Number(value || 0)).toLocaleString()}`; - } - - function pending(actionName) { - return store.getPendingAction() === actionName; - } - - function statCard(label, value, tone = "") { - return h( - "div", - { - className: tone - ? `bank-stat-card is-${tone}` - : "bank-stat-card", - }, - h("span", { className: "bank-stat-label" }, label), - h("span", { className: "bank-stat-value" }, value), - ); - } - - function metricCard(label, value, copy, tone = "") { - return h( - "div", - { - className: tone - ? `bank-metric-card is-${tone}` - : "bank-metric-card", - }, - h("span", { className: "bank-eyebrow" }, label), - h("span", { className: "bank-metric-value" }, value), - h("span", { className: "bank-metric-copy" }, copy), - ); - } - - function pinIndicators(value) { - const pin = String(value || ""); - - return h( - "div", - { className: "bank-pin-indicators" }, - [0, 1, 2, 3].map((index) => - h("span", { - className: - index < pin.length - ? "bank-pin-indicator is-filled" - : "bank-pin-indicator", - }), - ), - ); - } - - function readInputValue(id) { - return document.getElementById(id)?.value || ""; - } - - function clearInputValue(id) { - const input = document.getElementById(id); - if (input) { - input.value = ""; - } - } - - function keypad(onDigit, onBackspace, onClear, onEnter) { - const keys = ["1", "2", "3", "4", "5", "6", "7", "8", "9"]; - - return h( - "div", - { className: "bank-keypad" }, - keys.map((digit) => - h( - "button", - { - type: "button", - className: "bank-key", - onClick: () => onDigit(digit), - }, - digit, - ), - ), - h( - "button", - { - type: "button", - className: "bank-key is-muted", - onClick: onClear, - }, - "C", - ), - h( - "button", - { - type: "button", - className: "bank-key", - onClick: () => onDigit("0"), - }, - "0", - ), - h( - "button", - { - type: "button", - className: "bank-key is-accent", - onClick: onEnter, - }, - "Enter", - ), - h( - "button", - { - type: "button", - className: "bank-key is-wide", - onClick: onBackspace, - }, - "Backspace", - ), - ); - } - - function transactionRows() { - const transactions = Array.isArray(account.transactions) - ? account.transactions - : []; - - if (transactions.length === 0) { - return h( - "div", - { className: "bank-empty-state" }, - h("h3", { className: "bank-empty-title" }, "No transactions"), - h( - "p", - { className: "bank-empty-copy" }, - "Deposits, withdrawals, and transfers will appear here after the account begins moving funds.", - ), - ); - } - - return h( - "div", - { className: "bank-history-list" }, - transactions - .slice(0, 8) - .map((entry) => - h( - "div", - { className: "bank-history-row" }, - h( - "div", - { className: "bank-history-copy" }, - h( - "span", - { className: "bank-history-title" }, - entry.type || "Transaction", - ), - h( - "span", - { className: "bank-history-meta" }, - entry.date || "Pending timestamp", - ), - ), - h( - "span", - { className: "bank-history-value" }, - formatCurrency(entry.amount || 0), - ), - ), - ), - ); - } - - BankApp.componentFns = BankApp.componentFns || {}; - Object.assign(BankApp.componentFns, { - clearInputValue, - formatCurrency, - keypad, - metricCard, - pending, - pinIndicators, - readInputValue, - statCard, - transactionRows, - }); -})(); - -(function () { - const BankApp = (window.BankApp = window.BankApp || {}); - const { h } = BankApp.runtime; - const store = BankApp.store; - const actions = BankApp.actions; - const { account, session } = BankApp.data; - const { formatCurrency, statCard } = BankApp.componentFns; - - BankApp.componentFns = BankApp.componentFns || {}; - BankApp.componentFns.BankSidebar = function BankSidebar() { - store.getAccountVersion(); - store.getSessionVersion(); - - return h( - "aside", - { className: "bank-sidebar" }, - h( - "section", - { className: "bank-module" }, - h( - "div", - { className: "bank-module-header" }, - h( - "div", - null, - h("span", { className: "bank-eyebrow" }, "Account"), - h( - "h2", - { className: "bank-section-title" }, - "Balances", - ), - ), - h("span", { className: "bank-pill" }, "Live"), - ), - h( - "div", - { className: "bank-summary-grid" }, - statCard("Bank", formatCurrency(account.bank), "accent"), - statCard("Cash", formatCurrency(account.cash)), - statCard( - "Earnings", - formatCurrency(account.earnings), - account.earnings > 0 ? "warning" : "", - ), - statCard( - "Org Funds", - formatCurrency(session.orgFunds), - session.orgFunds > 0 ? "success" : "", - ), - ), - ), - h( - "section", - { className: "bank-module" }, - h( - "div", - { className: "bank-module-header" }, - h( - "div", - null, - h("span", { className: "bank-eyebrow" }, "Profile"), - h( - "h2", - { className: "bank-section-title" }, - "Account Holder", - ), - ), - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-secondary", - onClick: () => actions.refreshBank(), - }, - "Refresh", - ), - ), - h( - "div", - { className: "bank-profile-stack" }, - statCard("Name", session.playerName || "Unknown"), - statCard("UID", session.uid || "-"), - statCard( - "Organization", - session.orgName || "No active organization", - ), - ), - ), - ); - }; -})(); - -(function () { - const BankApp = (window.BankApp = window.BankApp || {}); - const { h } = BankApp.runtime; - const store = BankApp.store; - const { account, session } = BankApp.data; - const { formatCurrency } = BankApp.componentFns; - - BankApp.componentFns = BankApp.componentFns || {}; - BankApp.componentFns.BankFooter = function BankFooter() { - store.getAccountVersion(); - store.getSessionVersion(); - - const sections = [ - { - title: "Banking Resources", - items: [ - "Account Access Policy", - "Transfer & Wire Guidelines", - "Cash Handling Schedule", - "Terminal Security Notice", - ], - }, - { - title: "Bank Support", - items: session.orgName - ? [ - `Organization: ${session.orgName}`, - `Treasury Reference: ${formatCurrency(session.orgFunds)}`, - `${session.transferTargets.length} transfer recipient(s) currently visible.`, - `Primary Ledger: ${formatCurrency(account.bank)}`, - ] - : [ - "Organization: No active treasury link", - `${session.transferTargets.length} transfer recipient(s) currently visible.`, - `Primary Ledger: ${formatCurrency(account.bank)}`, - `Cash On Hand: ${formatCurrency(account.cash)}`, - ], - }, - ]; - - return h( - "footer", - { className: "bank-footer-bar" }, - h( - "div", - { className: "bank-footer" }, - ...sections.map((section) => - h( - "div", - { className: "bank-footer-block" }, - h( - "h3", - { className: "bank-footer-title" }, - section.title, - ), - h( - "ul", - { className: "bank-footer-list" }, - ...(section.items || []).map((item) => - h( - "li", - { className: "bank-footer-copy" }, - item, - ), - ), - ), - ), - ), - ), - ); - }; -})(); - -(function () { - const BankApp = (window.BankApp = window.BankApp || {}); - const { h } = BankApp.runtime; - const store = BankApp.store; - const actions = BankApp.actions; - const { account, session } = BankApp.data; - const { - clearInputValue, - formatCurrency, - metricCard, - pending, - readInputValue, - transactionRows, - } = BankApp.componentFns; - - function trackAccount() { - store.getAccountVersion(); - } - - function trackSession() { - store.getSessionVersion(); - } - - function pageHeader() { - trackSession(); - - return h( - "div", - { className: "bank-page-header" }, - h( - "div", - null, - h("span", { className: "bank-eyebrow" }, "Treasury Desk"), - h("h1", { className: "bank-title" }, "Personal Banking"), - ), - h( - "span", - { className: "bank-pill" }, - session.playerName || "Account Holder", - ), - ); - } - - function summarySection() { - trackAccount(); - trackSession(); - - return h( - "section", - { className: "bank-page-section bank-summary-section" }, - h( - "div", - { className: "bank-section-header" }, - h( - "div", - null, - h("span", { className: "bank-eyebrow" }, "Overview"), - h( - "h2", - { className: "bank-section-title" }, - "Financial Position", - ), - ), - h("span", { className: "bank-pill" }, "Banking Desk"), - ), - h( - "div", - { className: "bank-summary-band" }, - metricCard( - "Primary Balance", - formatCurrency(account.bank), - "Available for transfers and withdrawals.", - "accent", - ), - metricCard( - "Cash On Hand", - formatCurrency(account.cash), - "Funds currently carried by the player.", - ), - metricCard( - "Pending Earnings", - formatCurrency(account.earnings), - "Ready to sweep into the main account ledger.", - account.earnings > 0 ? "warning" : "", - ), - metricCard( - "Org Snapshot", - formatCurrency(session.orgFunds), - "Reference value pulled from the organization treasury.", - session.orgFunds > 0 ? "success" : "", - ), - ), - ); - } - - function actionSections() { - trackSession(); - - return h( - "div", - { className: "bank-action-sections" }, - h( - "section", - { className: "bank-page-section" }, - h( - "div", - { className: "bank-section-header" }, - h( - "div", - null, - h("span", { className: "bank-eyebrow" }, "Movement"), - h( - "h2", - { className: "bank-section-title" }, - "Deposit / Withdraw", - ), - ), - ), - h( - "div", - { className: "bank-form-stack" }, - h("input", { - id: "bank-amount-input", - className: "bank-input", - type: "number", - min: "1", - placeholder: "Enter amount", - }), - h( - "div", - { className: "bank-action-row" }, - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-primary", - disabled: pending("deposit"), - onClick: () => { - const sent = actions.requestDeposit( - readInputValue("bank-amount-input"), - ); - if (sent) { - clearInputValue("bank-amount-input"); - } - }, - }, - pending("deposit") ? "Depositing..." : "Deposit", - ), - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-secondary", - disabled: pending("withdraw"), - onClick: () => { - const sent = actions.requestWithdraw( - readInputValue("bank-amount-input"), - ); - if (sent) { - clearInputValue("bank-amount-input"); - } - }, - }, - pending("withdraw") ? "Withdrawing..." : "Withdraw", - ), - ), - ), - ), - h( - "section", - { className: "bank-page-section" }, - h( - "div", - { className: "bank-section-header" }, - h( - "div", - null, - h("span", { className: "bank-eyebrow" }, "Transfer"), - h( - "h2", - { className: "bank-section-title" }, - "Wire Funds", - ), - ), - ), - h( - "div", - { className: "bank-form-stack" }, - h( - "select", - { - id: "bank-transfer-target", - className: "bank-select", - }, - h( - "option", - { value: "" }, - session.transferTargets.length > 0 - ? "Select recipient" - : "No available recipients", - ), - session.transferTargets.map((entry) => - h( - "option", - { value: entry.uid }, - entry.name || entry.uid, - ), - ), - ), - h("input", { - id: "bank-transfer-amount", - className: "bank-input", - type: "number", - min: "1", - placeholder: "Enter transfer amount", - }), - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-primary", - disabled: - pending("transfer") || - session.transferTargets.length === 0, - onClick: () => { - const sent = actions.requestTransfer( - readInputValue("bank-transfer-target"), - readInputValue("bank-transfer-amount"), - ); - if (sent) { - clearInputValue("bank-transfer-amount"); - } - }, - }, - pending("transfer") - ? "Transferring..." - : "Transfer Funds", - ), - ), - ), - ); - } - - function supportSection() { - trackAccount(); - - return h( - "div", - { className: "bank-support-sections" }, - h( - "section", - { className: "bank-page-section" }, - h( - "div", - { className: "bank-section-header" }, - h( - "div", - null, - h("span", { className: "bank-eyebrow" }, "Sweep"), - h( - "h2", - { className: "bank-section-title" }, - "Deposit Earnings", - ), - ), - ), - h( - "p", - { className: "bank-card-copy" }, - "Sweep pending earnings into the primary account when you want them reflected in the main balance.", - ), - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-primary", - disabled: - pending("depositearnings") || - Number(account.earnings || 0) <= 0, - onClick: () => - actions.requestDepositEarnings(account.earnings), - }, - pending("depositearnings") - ? "Depositing..." - : "Deposit Earnings", - ), - ), - ); - } - - function historySection() { - trackAccount(); - - return h( - "section", - { className: "bank-page-section bank-history-section" }, - h( - "div", - { className: "bank-section-header" }, - h( - "div", - null, - h("span", { className: "bank-eyebrow" }, "History"), - h( - "h2", - { className: "bank-section-title" }, - "Recent Transactions", - ), - ), - ), - transactionRows(), - ); - } - - BankApp.componentFns = BankApp.componentFns || {}; - BankApp.componentFns.BankPageHeader = pageHeader; - BankApp.componentFns.BankSummarySection = summarySection; - BankApp.componentFns.BankActionSections = actionSections; - BankApp.componentFns.BankSupportSection = supportSection; - BankApp.componentFns.BankHistorySection = historySection; -})(); - -(function () { - const BankApp = (window.BankApp = window.BankApp || {}); - const { h } = BankApp.runtime; - const store = BankApp.store; - const actions = BankApp.actions; - const { account } = BankApp.data; - const { formatCurrency, keypad, pinIndicators } = BankApp.componentFns; - - function atmMenuCard() { - return h( - "div", - { className: "bank-atm-action-grid" }, - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-primary", - onClick: () => actions.selectAtmView("withdraw"), - }, - "Withdraw Cash", - ), - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-primary", - onClick: () => actions.selectAtmView("deposit"), - }, - "Deposit Cash", - ), - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-secondary", - onClick: () => actions.selectAtmView("balance"), - }, - "Check Balance", - ), - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-secondary", - onClick: () => actions.closeBank(), - }, - "Exit Terminal", - ), - ); - } - - function atmAmountMenu(kind) { - const label = kind === "deposit" ? "Deposit" : "Withdraw"; - const amounts = [20, 50, 100, 500]; - - return h( - "div", - { className: "bank-atm-action-grid" }, - amounts.map((amount) => - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-primary", - onClick: () => actions.requestAtmAmount(kind, amount), - }, - `${label} ${formatCurrency(amount)}`, - ), - ), - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-secondary", - onClick: () => - actions.selectAtmView( - kind === "deposit" - ? "customDeposit" - : "customWithdraw", - ), - }, - "Custom Amount", - ), - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-secondary", - onClick: () => actions.selectAtmView("menu"), - }, - "Back", - ), - ); - } - - function atmCustomAmount(kind) { - const label = kind === "deposit" ? "Deposit" : "Withdraw"; - - return h( - "div", - { className: "bank-atm-stack" }, - h( - "div", - { className: "bank-pin-display" }, - store.getCustomAmount() - ? formatCurrency(store.getCustomAmount()) - : "$0", - ), - keypad( - actions.appendCustomAmountDigit, - actions.backspaceCustomAmount, - actions.clearCustomAmount, - () => actions.submitCustomAmount(kind), - ), - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-secondary", - onClick: () => actions.selectAtmView("menu"), - }, - `Cancel ${label}`, - ), - ); - } - - BankApp.componentFns = BankApp.componentFns || {}; - BankApp.componentFns.ATMView = function ATMView() { - store.getAccountVersion(); - const atmViewName = store.getAtmView(); - const enteredPin = String(store.getEnteredPin() || ""); - let title = "Terminal Access"; - let copy = - "Authenticate with the four-digit account PIN before using the terminal."; - let content = null; - - switch (atmViewName) { - case "menu": - title = "ATM Menu"; - copy = - "Select a banking action. The ATM can deposit, withdraw, and show the live account balance."; - content = atmMenuCard(); - break; - case "withdraw": - title = "Withdraw Cash"; - copy = - "Choose a preset amount or enter a custom amount for withdrawal."; - content = atmAmountMenu("withdraw"); - break; - case "deposit": - title = "Deposit Cash"; - copy = - "Move cash on hand back into the main bank balance from the terminal."; - content = atmAmountMenu("deposit"); - break; - case "customWithdraw": - title = "Custom Withdraw"; - copy = "Enter the exact withdrawal amount."; - content = atmCustomAmount("withdraw"); - break; - case "customDeposit": - title = "Custom Deposit"; - copy = "Enter the exact deposit amount."; - content = atmCustomAmount("deposit"); - break; - case "balance": - title = "Available Balance"; - copy = "Current bank balance available at this terminal."; - content = h( - "div", - { className: "bank-atm-stack" }, - h( - "div", - { className: "bank-balance-display" }, - formatCurrency(account.bank), - ), - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-primary", - onClick: () => actions.selectAtmView("menu"), - }, - "Return to Menu", - ), - ); - break; - default: - content = h( - "div", - { className: "bank-atm-stack" }, - h( - "div", - { className: "bank-pin-display" }, - pinIndicators(enteredPin), - ), - keypad( - actions.appendPinDigit, - actions.backspacePin, - actions.clearPin, - actions.submitPin, - ), - h( - "button", - { - type: "button", - className: "bank-btn bank-btn-secondary", - onClick: () => actions.closeBank(), - }, - "Exit Terminal", - ), - ); - break; - } - - return h( - "div", - { className: "bank-atm-shell" }, - h( - "section", - { className: "bank-atm-panel" }, - h( - "div", - { className: "bank-panel-header" }, - h( - "div", - null, - h("span", { className: "bank-eyebrow" }, "ATM"), - h("h1", { className: "bank-title" }, title), - ), - h("span", { className: "bank-pill" }, "Secure Terminal"), - ), - h("p", { className: "bank-panel-copy" }, copy), - content, - ), - ); - }; -})(); - -(function () { - const BankApp = (window.BankApp = window.BankApp || {}); - const { h } = BankApp.runtime; - const WindowTitleBar = window.SharedUI.componentFns.WindowTitleBar; - const store = BankApp.store; - const actions = BankApp.actions; - - BankApp.componentFns = BankApp.componentFns || {}; - BankApp.componentFns.NoticeLayer = function NoticeLayer() { - const notice = store.getNotice(); - - if (!notice.text) { - return null; - } - - return h( - "div", - { className: "bank-notice-stack" }, - h( - "div", - { - className: - notice.type === "error" - ? "bank-notice is-error" - : "bank-notice is-success", - }, - notice.text, - ), - ); - }; - - BankApp.components = BankApp.components || {}; - BankApp.components.App = function App() { - const mode = store.getMode(); - - return h( - "div", - { className: mode === "atm" ? "bank-shell is-atm" : "bank-shell" }, - mode === "atm" - ? null - : WindowTitleBar({ - kicker: "FORGE Finance", - title: "Global Banking Network", - onClose: () => actions.closeBank(), - closeLabel: "Close banking interface", - }), - h("div", { id: "bank-notice-root" }), - mode === "atm" - ? h("div", { id: "bank-atm-root" }) - : [ - h( - "div", - { - className: "bank-scroll-shell", - "data-preserve-scroll-id": "bank-page-scroll", - }, - [ - h( - "div", - { className: "bank-layout" }, - h("div", { id: "bank-sidebar-root" }), - h( - "main", - { className: "bank-main" }, - h( - "div", - { className: "bank-page" }, - h("div", { - id: "bank-page-header-root", - }), - h( - "p", - { className: "bank-page-copy" }, - "Manage deposits, withdrawals, transfers, and earnings sweeps from the same shared financial console.", - ), - h("div", { - className: "bank-page-divider", - }), - h( - "div", - { className: "bank-page-body" }, - h("div", { - id: "bank-summary-section-root", - }), - h("div", { - id: "bank-action-sections-root", - }), - h("div", { - id: "bank-support-section-root", - }), - h("div", { - id: "bank-history-section-root", - }), - ), - ), - ), - ), - h("div", { id: "bank-footer-root" }), - ], - ), - ], - ); - }; -})(); - -(function () { - const ForgeWebUI = window.ForgeWebUI; - const BankApp = window.BankApp; - const islandDefinitions = [ - { - id: "bank-notice-root", - preserveScroll: false, - render: () => BankApp.componentFns.NoticeLayer(), - }, - { - id: "bank-sidebar-root", - preserveScroll: false, - render: () => BankApp.componentFns.BankSidebar(), - }, - { - id: "bank-page-header-root", - preserveScroll: false, - render: () => BankApp.componentFns.BankPageHeader(), - }, - { - id: "bank-summary-section-root", - preserveScroll: false, - render: () => BankApp.componentFns.BankSummarySection(), - }, - { - id: "bank-action-sections-root", - preserveScroll: false, - render: () => BankApp.componentFns.BankActionSections(), - }, - { - id: "bank-support-section-root", - preserveScroll: false, - render: () => BankApp.componentFns.BankSupportSection(), - }, - { - id: "bank-history-section-root", - preserveScroll: false, - render: () => BankApp.componentFns.BankHistorySection(), - }, - { - id: "bank-atm-root", - preserveScroll: false, - render: () => BankApp.componentFns.ATMView(), - }, - { - id: "bank-footer-root", - preserveScroll: false, - render: () => BankApp.componentFns.BankFooter(), - }, - ]; - - function createIslandManager() { - const mounts = new Map(); - - function sync() { - islandDefinitions.forEach((definition) => { - const container = document.getElementById(definition.id); - const current = mounts.get(definition.id); - - if (!container) { - if (current) { - current.handle.dispose(); - mounts.delete(definition.id); - } - return; - } - - if (current && current.container === container) { - return; - } - - if (current) { - current.handle.dispose(); - } - - const handle = ForgeWebUI.mount(container, definition.render, { - preserveScroll: definition.preserveScroll, - }); - mounts.set(definition.id, { - container, - handle, - }); - }); - } - - return { - sync, - }; - } - - const app = ForgeWebUI.createApp({ - name: "bank", - root: "#app", - setup({ root }) { - const islandManager = createIslandManager(); - - ForgeWebUI.mount(root, () => BankApp.components.App(), { - preserveScroll: false, - }); - - if (BankApp.bridge) { - BankApp.bridge.notifyReady(); - } - - ForgeWebUI.effect(() => { - BankApp.store.getMode(); - - requestAnimationFrame(() => { - islandManager.sync(); - }); - }); - }, - }); - - app.start(); -})(); +!function(){const n=window.ForgeWebUI;(window.BankApp=window.BankApp||{}).runtime=n,window.AppRuntime=n}(),function(){const n=window.BankApp=window.BankApp||{},e={mode:"bank",orgFunds:0,orgName:"",playerName:"",transferTargets:[],uid:""},t={bank:0,cash:0,earnings:0,pin:"1234",transactions:[]};function a(n,e){var t;Object.keys(n).forEach(e=>delete n[e]),Object.assign(n,(t=e,JSON.parse(JSON.stringify(t))))}n.data={account:Object.assign({},t),session:Object.assign({},e),applyHydratePayload(n){a(this.session,Object.assign({},e,n?.session||{})),a(this.account,Object.assign({},t,n?.account||{}))}}}(),function(){const n=window.BankApp=window.BankApp||{},{createSignal:e}=n.runtime;n.store=new class{constructor(){[this.getMode,this.setMode]=e("bank"),[this.getNotice,this.setNotice]=e({text:"",type:""}),[this.getPendingAction,this.setPendingAction]=e(""),[this.getAtmView,this.setAtmView]=e("pin"),[this.getEnteredPin,this.setEnteredPin]=e(""),[this.getCustomAmount,this.setCustomAmount]=e(""),[this.getAccountVersion,this.setAccountVersion]=e(0),[this.getSessionVersion,this.setSessionVersion]=e(0)}finishAction(){this.setPendingAction("")}hydrateFromPayload(n){const e=String(n?.session?.mode||"bank").trim().toLowerCase(),t=this.getMode(),a=this.getAtmView();this.setMode("atm"===e?"atm":"bank"),this.setPendingAction(""),this.setNotice({text:"",type:""}),this.setEnteredPin(""),this.setCustomAmount(""),this.setAccountVersion(this.getAccountVersion()+1),this.setSessionVersion(this.getSessionVersion()+1),"atm"!==e?this.setAtmView("dashboard"):this.setAtmView("atm"===t?a:"pin")}resetAtm(){this.setEnteredPin(""),this.setCustomAmount(""),this.setAtmView("pin")}startAction(n){this.setPendingAction(String(n||"").trim().toLowerCase())}}}(),function(){const n=window.BankApp=window.BankApp||{},e=n.store,t=window.ForgeWebUI.createBridge({closeEvent:"bank::close",globalName:"ForgeBridge",readyEvent:"bank::ready"});function a(t){n.data.applyHydratePayload(t),e.hydrateFromPayload(t)}t.on("bank::hydrate",a),t.on("bank::sync",a),t.on("bank::notice",e=>{n.actions&&n.actions.showNotice(e.type||"error",e.message||"Bank notice received.")}),n.bridge={notifyReady:()=>t.ready({loaded:!0}),receive:t.receive,requestClose:()=>t.close({}),requestDeposit:n=>t.send("bank::deposit::request",n),requestDepositEarnings:n=>t.send("bank::depositEarnings::request",n),requestRefresh:()=>t.send("bank::refresh",{}),requestTransfer:n=>t.send("bank::transfer::request",n),requestWithdraw:n=>t.send("bank::withdraw::request",n),sendEvent:t.send}}(),function(){const n=window.BankApp=window.BankApp||{},e=n.store;let t=null;function a(){return n.data?.account||{}}function s(n){const e=Math.floor(Number(n||0));return Number.isFinite(e)?e:0}function i(n,a){e.setNotice({type:n,text:a}),t&&clearTimeout(t),t=setTimeout(()=>{e.setNotice({text:"",type:""}),t=null},3200)}function o(t){const o=s(t),r=a();if(o<=0)return i("error","Enter a valid deposit amount."),!1;if(o>Number(r.cash||0))return i("error","Cash on hand cannot cover that deposit."),!1;const c=n.bridge;if(!c||"function"!=typeof c.requestDeposit)return i("error","Deposit bridge is unavailable."),!1;e.startAction("deposit");return!!c.requestDeposit({amount:o})||(e.finishAction(),i("error","Deposit bridge is unavailable."),!1)}function r(t){const o=s(t),r=a();if(o<=0)return i("error","Enter a valid withdrawal amount."),!1;if(o>Number(r.bank||0))return i("error","Bank balance cannot cover that withdrawal."),!1;const c=n.bridge;if(!c||"function"!=typeof c.requestWithdraw)return i("error","Withdraw bridge is unavailable."),!1;e.startAction("withdraw");return!!c.requestWithdraw({amount:o})||(e.finishAction(),i("error","Withdraw bridge is unavailable."),!1)}function c(){e.setEnteredPin("")}n.actions={appendCustomAmountDigit:function(n){const t=String(n||"").trim();if(!t)return;const a=String(e.getCustomAmount()||"");a.length>=7||e.setCustomAmount(a+t)},appendPinDigit:function(n){const t=String(n||"").trim();if(!t)return;const a=String(e.getEnteredPin()||"");a.length>=4||e.setEnteredPin(a+t)},backspaceCustomAmount:function(){const n=String(e.getCustomAmount()||"");e.setCustomAmount(n.slice(0,-1))},backspacePin:function(){const n=String(e.getEnteredPin()||"");e.setEnteredPin(n.slice(0,-1))},clearCustomAmount:function(){e.setCustomAmount("")},clearPin:c,closeBank:function(){const e=n.bridge;if(e&&"function"==typeof e.requestClose){if(e.requestClose())return!0}return i("error","Bank bridge is unavailable."),!1},refreshBank:function(){const e=n.bridge;if(e&&"function"==typeof e.requestRefresh){if(e.requestRefresh())return!0}return i("error","Bank refresh bridge is unavailable."),!1},requestAtmAmount:function(n,t){const a="deposit"===String(n||"").trim().toLowerCase()?o(t):r(t);return a&&e.setAtmView("menu"),a},requestDeposit:o,requestDepositEarnings:function(t){const o=s(t),r=a();if(o<=0)return i("error","No earnings are available to deposit."),!1;if(o>Number(r.earnings||0))return i("error","Pending earnings cannot cover that deposit request."),!1;const c=n.bridge;return c&&"function"==typeof c.requestDepositEarnings?(e.startAction("depositearnings"),!!c.requestDepositEarnings({amount:o})||(e.finishAction(),i("error","Earnings bridge is unavailable."),!1)):(i("error","Earnings bridge is unavailable."),!1)},requestTransfer:function(t,o){const r=s(o),c=n.data?.session||{},u=a(),l=String(t||"").trim();if(!l)return i("error","Select a transfer recipient."),!1;if(l===String(c.uid||""))return i("error","You cannot transfer funds to yourself."),!1;if(r<=0)return i("error","Enter a valid transfer amount."),!1;if(r>Number(u.bank||0))return i("error","Bank balance cannot cover that transfer."),!1;const m=n.bridge;return m&&"function"==typeof m.requestTransfer?(e.startAction("transfer"),!!m.requestTransfer({amount:r,from:"bank",target:l})||(e.finishAction(),i("error","Transfer bridge is unavailable."),!1)):(i("error","Transfer bridge is unavailable."),!1)},requestWithdraw:r,selectAtmView:function(n){const t=String(n||"").trim();return!!t&&("pin"===t?(e.resetAtm(),!0):(e.setCustomAmount(""),e.setAtmView(t),!0))},showNotice:i,submitCustomAmount:function(n){const t=s(e.getCustomAmount()),a=String(n||"").trim().toLowerCase();if(t<=0)return i("error","Enter a valid transaction amount."),!1;const c="deposit"===a?o(t):r(t);return c&&(e.setCustomAmount(""),e.setAtmView("menu")),c},submitPin:function(){const n=String(e.getEnteredPin()||""),t=String(a().pin||"1234");return 4!==n.length?(i("error","Enter your four-digit access PIN."),!1):n!==t?(c(),i("error","Incorrect PIN."),!1):(c(),e.setAtmView("menu"),!0)}}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,{account:a}=n.data;function s(n){return`$${Math.round(Number(n||0)).toLocaleString()}`}n.componentFns=n.componentFns||{},Object.assign(n.componentFns,{clearInputValue:function(n){const e=document.getElementById(n);e&&(e.value="")},formatCurrency:s,keypad:function(n,t,a,s){return e("div",{className:"bank-keypad"},["1","2","3","4","5","6","7","8","9"].map(t=>e("button",{type:"button",className:"bank-key",onClick:()=>n(t)},t)),e("button",{type:"button",className:"bank-key is-muted",onClick:a},"C"),e("button",{type:"button",className:"bank-key",onClick:()=>n("0")},"0"),e("button",{type:"button",className:"bank-key is-accent",onClick:s},"Enter"),e("button",{type:"button",className:"bank-key is-wide",onClick:t},"Backspace"))},metricCard:function(n,t,a,s=""){return e("div",{className:s?`bank-metric-card is-${s}`:"bank-metric-card"},e("span",{className:"bank-eyebrow"},n),e("span",{className:"bank-metric-value"},t),e("span",{className:"bank-metric-copy"},a))},pending:function(n){return t.getPendingAction()===n},pinIndicators:function(n){const t=String(n||"");return e("div",{className:"bank-pin-indicators"},[0,1,2,3].map(n=>e("span",{className:ne("div",{className:"bank-history-row"},e("div",{className:"bank-history-copy"},e("span",{className:"bank-history-title"},n.type||"Transaction"),e("span",{className:"bank-history-meta"},n.date||"Pending timestamp")),e("span",{className:"bank-history-value"},s(n.amount||0)))))}})}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,a=n.actions,{account:s,session:i}=n.data,{formatCurrency:o,statCard:r}=n.componentFns;n.componentFns=n.componentFns||{},n.componentFns.BankSidebar=function(){return t.getAccountVersion(),t.getSessionVersion(),e("aside",{className:"bank-sidebar"},e("section",{className:"bank-module"},e("div",{className:"bank-module-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Account"),e("h2",{className:"bank-section-title"},"Balances")),e("span",{className:"bank-pill"},"Live")),e("div",{className:"bank-summary-grid"},r("Bank",o(s.bank),"accent"),r("Cash",o(s.cash)),r("Earnings",o(s.earnings),s.earnings>0?"warning":""),r("Org Funds",o(i.orgFunds),i.orgFunds>0?"success":""))),e("section",{className:"bank-module"},e("div",{className:"bank-module-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Profile"),e("h2",{className:"bank-section-title"},"Account Holder")),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.refreshBank()},"Refresh")),e("div",{className:"bank-profile-stack"},r("Name",i.playerName||"Unknown"),r("UID",i.uid||"-"),r("Organization",i.orgName||"No active organization"))))}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,{account:a,session:s}=n.data,{formatCurrency:i}=n.componentFns;n.componentFns=n.componentFns||{},n.componentFns.BankFooter=function(){t.getAccountVersion(),t.getSessionVersion();const n=[{title:"Banking Resources",items:["Account Access Policy","Transfer & Wire Guidelines","Cash Handling Schedule","Terminal Security Notice"]},{title:"Bank Support",items:s.orgName?[`Organization: ${s.orgName}`,`Treasury Reference: ${i(s.orgFunds)}`,`${s.transferTargets.length} transfer recipient(s) currently visible.`,`Primary Ledger: ${i(a.bank)}`]:["Organization: No active treasury link",`${s.transferTargets.length} transfer recipient(s) currently visible.`,`Primary Ledger: ${i(a.bank)}`,`Cash On Hand: ${i(a.cash)}`]}];return e("footer",{className:"bank-footer-bar"},e("div",{className:"bank-footer"},...n.map(n=>e("div",{className:"bank-footer-block"},e("h3",{className:"bank-footer-title"},n.title),e("ul",{className:"bank-footer-list"},...(n.items||[]).map(n=>e("li",{className:"bank-footer-copy"},n)))))))}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,a=n.actions,{account:s,session:i}=n.data,{clearInputValue:o,formatCurrency:r,metricCard:c,pending:u,readInputValue:l,transactionRows:m}=n.componentFns;function d(){t.getAccountVersion()}function b(){t.getSessionVersion()}n.componentFns=n.componentFns||{},n.componentFns.BankPageHeader=function(){return b(),e("div",{className:"bank-page-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Treasury Desk"),e("h1",{className:"bank-title"},"Personal Banking")),e("span",{className:"bank-pill"},i.playerName||"Account Holder"))},n.componentFns.BankSummarySection=function(){return d(),b(),e("section",{className:"bank-page-section bank-summary-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Overview"),e("h2",{className:"bank-section-title"},"Financial Position")),e("span",{className:"bank-pill"},"Banking Desk")),e("div",{className:"bank-summary-band"},c("Primary Balance",r(s.bank),"Available for transfers and withdrawals.","accent"),c("Cash On Hand",r(s.cash),"Funds currently carried by the player."),c("Pending Earnings",r(s.earnings),"Ready to sweep into the main account ledger.",s.earnings>0?"warning":""),c("Org Snapshot",r(i.orgFunds),"Reference value pulled from the organization treasury.",i.orgFunds>0?"success":"")))},n.componentFns.BankActionSections=function(){return b(),e("div",{className:"bank-action-sections"},e("section",{className:"bank-page-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Movement"),e("h2",{className:"bank-section-title"},"Deposit / Withdraw"))),e("div",{className:"bank-form-stack"},e("input",{id:"bank-amount-input",className:"bank-input",type:"number",min:"1",placeholder:"Enter amount"}),e("div",{className:"bank-action-row"},e("button",{type:"button",className:"bank-btn bank-btn-primary",disabled:u("deposit"),onClick:()=>{a.requestDeposit(l("bank-amount-input"))&&o("bank-amount-input")}},u("deposit")?"Depositing...":"Deposit"),e("button",{type:"button",className:"bank-btn bank-btn-secondary",disabled:u("withdraw"),onClick:()=>{a.requestWithdraw(l("bank-amount-input"))&&o("bank-amount-input")}},u("withdraw")?"Withdrawing...":"Withdraw")))),e("section",{className:"bank-page-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Transfer"),e("h2",{className:"bank-section-title"},"Wire Funds"))),e("div",{className:"bank-form-stack"},e("select",{id:"bank-transfer-target",className:"bank-select"},e("option",{value:""},i.transferTargets.length>0?"Select recipient":"No available recipients"),i.transferTargets.map(n=>e("option",{value:n.uid},n.name||n.uid))),e("input",{id:"bank-transfer-amount",className:"bank-input",type:"number",min:"1",placeholder:"Enter transfer amount"}),e("button",{type:"button",className:"bank-btn bank-btn-primary",disabled:u("transfer")||0===i.transferTargets.length,onClick:()=>{a.requestTransfer(l("bank-transfer-target"),l("bank-transfer-amount"))&&o("bank-transfer-amount")}},u("transfer")?"Transferring...":"Transfer Funds"))))},n.componentFns.BankSupportSection=function(){return d(),e("div",{className:"bank-support-sections"},e("section",{className:"bank-page-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"Sweep"),e("h2",{className:"bank-section-title"},"Deposit Earnings"))),e("p",{className:"bank-card-copy"},"Sweep pending earnings into the primary account when you want them reflected in the main balance."),e("button",{type:"button",className:"bank-btn bank-btn-primary",disabled:u("depositearnings")||Number(s.earnings||0)<=0,onClick:()=>a.requestDepositEarnings(s.earnings)},u("depositearnings")?"Depositing...":"Deposit Earnings")))},n.componentFns.BankHistorySection=function(){return d(),e("section",{className:"bank-page-section bank-history-section"},e("div",{className:"bank-section-header"},e("div",null,e("span",{className:"bank-eyebrow"},"History"),e("h2",{className:"bank-section-title"},"Recent Transactions"))),m())}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=n.store,a=n.actions,{account:s}=n.data,{formatCurrency:i,keypad:o,pinIndicators:r}=n.componentFns;function c(n){const t="deposit"===n?"Deposit":"Withdraw";return e("div",{className:"bank-atm-action-grid"},[20,50,100,500].map(s=>e("button",{type:"button",className:"bank-btn bank-btn-primary",onClick:()=>a.requestAtmAmount(n,s)},`${t} ${i(s)}`)),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.selectAtmView("deposit"===n?"customDeposit":"customWithdraw")},"Custom Amount"),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.selectAtmView("menu")},"Back"))}function u(n){const s="deposit"===n?"Deposit":"Withdraw";return e("div",{className:"bank-atm-stack"},e("div",{className:"bank-pin-display"},t.getCustomAmount()?i(t.getCustomAmount()):"$0"),o(a.appendCustomAmountDigit,a.backspaceCustomAmount,a.clearCustomAmount,()=>a.submitCustomAmount(n)),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.selectAtmView("menu")},`Cancel ${s}`))}n.componentFns=n.componentFns||{},n.componentFns.ATMView=function(){t.getAccountVersion();const n=t.getAtmView(),l=String(t.getEnteredPin()||"");let m="Terminal Access",d="Authenticate with the four-digit account PIN before using the terminal.",b=null;switch(n){case"menu":m="ATM Menu",d="Select a banking action. The ATM can deposit, withdraw, and show the live account balance.",b=e("div",{className:"bank-atm-action-grid"},e("button",{type:"button",className:"bank-btn bank-btn-primary",onClick:()=>a.selectAtmView("withdraw")},"Withdraw Cash"),e("button",{type:"button",className:"bank-btn bank-btn-primary",onClick:()=>a.selectAtmView("deposit")},"Deposit Cash"),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.selectAtmView("balance")},"Check Balance"),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.closeBank()},"Exit Terminal"));break;case"withdraw":m="Withdraw Cash",d="Choose a preset amount or enter a custom amount for withdrawal.",b=c("withdraw");break;case"deposit":m="Deposit Cash",d="Move cash on hand back into the main bank balance from the terminal.",b=c("deposit");break;case"customWithdraw":m="Custom Withdraw",d="Enter the exact withdrawal amount.",b=u("withdraw");break;case"customDeposit":m="Custom Deposit",d="Enter the exact deposit amount.",b=u("deposit");break;case"balance":m="Available Balance",d="Current bank balance available at this terminal.",b=e("div",{className:"bank-atm-stack"},e("div",{className:"bank-balance-display"},i(s.bank)),e("button",{type:"button",className:"bank-btn bank-btn-primary",onClick:()=>a.selectAtmView("menu")},"Return to Menu"));break;default:b=e("div",{className:"bank-atm-stack"},e("div",{className:"bank-pin-display"},r(l)),o(a.appendPinDigit,a.backspacePin,a.clearPin,a.submitPin),e("button",{type:"button",className:"bank-btn bank-btn-secondary",onClick:()=>a.closeBank()},"Exit Terminal"))}return e("div",{className:"bank-atm-shell"},e("section",{className:"bank-atm-panel"},e("div",{className:"bank-panel-header"},e("div",null,e("span",{className:"bank-eyebrow"},"ATM"),e("h1",{className:"bank-title"},m)),e("span",{className:"bank-pill"},"Secure Terminal")),e("p",{className:"bank-panel-copy"},d),b))}}(),function(){const n=window.BankApp=window.BankApp||{},{h:e}=n.runtime,t=window.SharedUI.componentFns.WindowTitleBar,a=n.store,s=n.actions;n.componentFns=n.componentFns||{},n.componentFns.NoticeLayer=function(){const n=a.getNotice();return n.text?e("div",{className:"bank-notice-stack"},e("div",{className:"error"===n.type?"bank-notice is-error":"bank-notice is-success"},n.text)):null},n.components=n.components||{},n.components.App=function(){const n=a.getMode();return e("div",{className:"atm"===n?"bank-shell is-atm":"bank-shell"},"atm"===n?null:t({kicker:"FORGE Finance",title:"Global Banking Network",onClose:()=>s.closeBank(),closeLabel:"Close banking interface"}),e("div",{id:"bank-notice-root"}),"atm"===n?e("div",{id:"bank-atm-root"}):[e("div",{className:"bank-scroll-shell","data-preserve-scroll-id":"bank-page-scroll"},[e("div",{className:"bank-layout"},e("div",{id:"bank-sidebar-root"}),e("main",{className:"bank-main"},e("div",{className:"bank-page"},e("div",{id:"bank-page-header-root"}),e("p",{className:"bank-page-copy"},"Manage deposits, withdrawals, transfers, and earnings sweeps from the same shared financial console."),e("div",{className:"bank-page-divider"}),e("div",{className:"bank-page-body"},e("div",{id:"bank-summary-section-root"}),e("div",{id:"bank-action-sections-root"}),e("div",{id:"bank-support-section-root"}),e("div",{id:"bank-history-section-root"}))))),e("div",{id:"bank-footer-root"})])])}}(),function(){const n=window.ForgeWebUI,e=window.BankApp,t=[{id:"bank-notice-root",preserveScroll:!1,render:()=>e.componentFns.NoticeLayer()},{id:"bank-sidebar-root",preserveScroll:!1,render:()=>e.componentFns.BankSidebar()},{id:"bank-page-header-root",preserveScroll:!1,render:()=>e.componentFns.BankPageHeader()},{id:"bank-summary-section-root",preserveScroll:!1,render:()=>e.componentFns.BankSummarySection()},{id:"bank-action-sections-root",preserveScroll:!1,render:()=>e.componentFns.BankActionSections()},{id:"bank-support-section-root",preserveScroll:!1,render:()=>e.componentFns.BankSupportSection()},{id:"bank-history-section-root",preserveScroll:!1,render:()=>e.componentFns.BankHistorySection()},{id:"bank-atm-root",preserveScroll:!1,render:()=>e.componentFns.ATMView()},{id:"bank-footer-root",preserveScroll:!1,render:()=>e.componentFns.BankFooter()}];n.createApp({name:"bank",root:"#app",setup({root:a}){const s=function(){const e=new Map;return{sync:function(){t.forEach(t=>{const a=document.getElementById(t.id),s=e.get(t.id);if(!a)return void(s&&(s.handle.dispose(),e.delete(t.id)));if(s&&s.container===a)return;s&&s.handle.dispose();const i=n.mount(a,t.render,{preserveScroll:t.preserveScroll});e.set(t.id,{container:a,handle:i})})}}}();n.mount(a,()=>e.components.App(),{preserveScroll:!1}),e.bridge&&e.bridge.notifyReady(),n.effect(()=>{e.store.getMode(),requestAnimationFrame(()=>{s.sync()})})}}).start()}(); \ No newline at end of file diff --git a/arma/client/addons/bank/ui/_site/index.html b/arma/client/addons/bank/ui/_site/index.html index 002ba11..e15a999 100644 --- a/arma/client/addons/bank/ui/_site/index.html +++ b/arma/client/addons/bank/ui/_site/index.html @@ -1,64 +1 @@ - - - - - - - FORGE Banking Console - - - - -
- - +FORGE Banking Console
\ No newline at end of file diff --git a/arma/client/addons/common/ui/_site/forge-site-loader.js b/arma/client/addons/common/ui/_site/forge-site-loader.js index 8590bd8..14d7756 100644 --- a/arma/client/addons/common/ui/_site/forge-site-loader.js +++ b/arma/client/addons/common/ui/_site/forge-site-loader.js @@ -1,127 +1 @@ -/* Generated by tools/build-webui.mjs for Forge Web UI site loader. Do not edit directly. */ -(function (global) { - const ForgeSiteLoader = (global.ForgeSiteLoader = - global.ForgeSiteLoader || {}); - const commonAddonRoot = "forge\\forge_client\\addons\\common\\ui\\_site\\"; - const defaultBrowserCommonBase = "../../../common/ui/_site/"; - - function isArmaAvailable() { - return ( - typeof A3API !== "undefined" && - A3API && - typeof A3API.RequestFile === "function" - ); - } - - function isAbsoluteAddonPath(path) { - return typeof path === "string" && path.startsWith("forge\\"); - } - - function normalizeAddonRoot(addonName) { - return `forge\\forge_client\\addons\\${addonName}\\ui\\_site\\`; - } - - function normalizeBrowserPath(basePath, assetPath) { - const normalizedBase = String(basePath || "./").replace(/\\/g, "/"); - const normalizedAssetPath = String(assetPath || "").replace(/\\/g, "/"); - return `${normalizedBase}${normalizedAssetPath}`; - } - - function requestText({ addonRoot, browserBase, assetPath }) { - if (isArmaAvailable()) { - const resolvedPath = isAbsoluteAddonPath(assetPath) - ? assetPath - : addonRoot + String(assetPath || "").replace(/\//g, "\\"); - return A3API.RequestFile(resolvedPath); - } - - const browserPath = isAbsoluteAddonPath(assetPath) - ? assetPath - : normalizeBrowserPath(browserBase, assetPath); - - return fetch(browserPath).then((response) => { - if (!response.ok) { - throw new Error(`Failed to load ${browserPath}`); - } - - return response.text(); - }); - } - - function appendStyle(cssText) { - const style = document.createElement("style"); - style.textContent = cssText; - document.head.appendChild(style); - } - - function appendScript(jsText) { - const script = document.createElement("script"); - script.text = jsText; - document.head.appendChild(script); - } - - async function boot(config) { - const addonName = config && config.addonName ? config.addonName : ""; - - if (!addonName) { - throw new Error( - "ForgeSiteLoader requires a config.addonName value.", - ); - } - - const addonRoot = normalizeAddonRoot(addonName); - const browserAddonBase = config.browserAddonBase || "./"; - const browserCommonBase = - config.browserCommonBase || defaultBrowserCommonBase; - const styles = Array.isArray(config.styles) ? config.styles : []; - const commonScripts = Array.isArray(config.commonScripts) - ? config.commonScripts - : []; - const scripts = Array.isArray(config.scripts) ? config.scripts : []; - - const styleChunks = await Promise.all( - styles.map((assetPath) => - requestText({ - addonRoot, - browserBase: browserAddonBase, - assetPath, - }), - ), - ); - styleChunks.forEach(appendStyle); - - const commonScriptChunks = await Promise.all( - commonScripts.map((assetPath) => - requestText({ - addonRoot: commonAddonRoot, - browserBase: browserCommonBase, - assetPath, - }), - ), - ); - commonScriptChunks.forEach(appendScript); - - const scriptChunks = await Promise.all( - scripts.map((assetPath) => - requestText({ - addonRoot, - browserBase: browserAddonBase, - assetPath, - }), - ), - ); - scriptChunks.forEach(appendScript); - } - - ForgeSiteLoader.boot = boot; - - if (global.ForgeSiteConfig && global.ForgeSiteConfig.autoBoot !== false) { - boot(global.ForgeSiteConfig).catch((error) => { - const logLabel = - global.ForgeSiteConfig.logLabel || - global.ForgeSiteConfig.addonName || - "Forge UI"; - console.error(`[${logLabel}] Failed to load site assets.`, error); - }); - } -})(window); +!function(e){const o=e.ForgeSiteLoader=e.ForgeSiteLoader||{};function t(e){return"string"==typeof e&&e.startsWith("forge\\")}function r({addonRoot:e,browserBase:o,assetPath:r}){if("undefined"!=typeof A3API&&A3API&&"function"==typeof A3API.RequestFile){const o=t(r)?r:e+String(r||"").replace(/\//g,"\\");return A3API.RequestFile(o)}const n=t(r)?r:function(e,o){return`${String(e||"./").replace(/\\/g,"/")}${String(o||"").replace(/\\/g,"/")}`}(o,r);return fetch(n).then(e=>{if(!e.ok)throw new Error(`Failed to load ${n}`);return e.text()})}function n(e){const o=document.createElement("style");o.textContent=e,document.head.appendChild(o)}function a(e){const o=document.createElement("script");o.text=e,document.head.appendChild(o)}async function i(e){const o=e&&e.addonName?e.addonName:"";if(!o)throw new Error("ForgeSiteLoader requires a config.addonName value.");const t=function(e){return`forge\\forge_client\\addons\\${e}\\ui\\_site\\`}(o),i=e.browserAddonBase||"./",s=e.browserCommonBase||"../../../common/ui/_site/",c=Array.isArray(e.styles)?e.styles:[],d=Array.isArray(e.commonScripts)?e.commonScripts:[],f=Array.isArray(e.scripts)?e.scripts:[];(await Promise.all(c.map(e=>r({addonRoot:t,browserBase:i,assetPath:e})))).forEach(n);(await Promise.all(d.map(e=>r({addonRoot:"forge\\forge_client\\addons\\common\\ui\\_site\\",browserBase:s,assetPath:e})))).forEach(a);(await Promise.all(f.map(e=>r({addonRoot:t,browserBase:i,assetPath:e})))).forEach(a)}o.boot=i,e.ForgeSiteConfig&&!1!==e.ForgeSiteConfig.autoBoot&&i(e.ForgeSiteConfig).catch(o=>{const t=e.ForgeSiteConfig.logLabel||e.ForgeSiteConfig.addonName||"Forge UI";console.error(`[${t}] Failed to load site assets.`,o)})}(window); \ No newline at end of file diff --git a/arma/client/addons/common/ui/_site/forge-webui.js b/arma/client/addons/common/ui/_site/forge-webui.js index 1587b65..66fb106 100644 --- a/arma/client/addons/common/ui/_site/forge-webui.js +++ b/arma/client/addons/common/ui/_site/forge-webui.js @@ -1,933 +1 @@ -/* Generated by tools/build-webui.mjs for Forge Web UI runtime. Do not edit directly. */ -(function (global) { - const ForgeWebUI = (global.ForgeWebUI = global.ForgeWebUI || {}); - - 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", - ]); - - const injectedStyles = new Set(); - const scheduledObservers = new Set(); - let activeObserver = null; - let batchDepth = 0; - let flushQueued = false; - - function queueFlush() { - if (flushQueued || batchDepth > 0) { - return; - } - - flushQueued = true; - queueMicrotask(() => { - flushQueued = false; - flushObservers(); - }); - } - - function flushObservers() { - while (scheduledObservers.size > 0) { - const queue = Array.from(scheduledObservers); - scheduledObservers.clear(); - queue.forEach((observer) => runObserver(observer)); - } - } - - function cleanupObserver(observer) { - if (typeof observer.cleanup === "function") { - try { - observer.cleanup(); - } catch (error) { - console.error("[ForgeWebUI] Observer cleanup failed.", error); - } - } - - observer.cleanup = null; - observer.dependencies.forEach((dependency) => { - dependency.delete(observer); - }); - observer.dependencies.clear(); - } - - function runObserver(observer) { - if (!observer || observer.disposed) { - return; - } - - cleanupObserver(observer); - - const previousObserver = activeObserver; - activeObserver = observer; - - try { - const cleanup = observer.fn(); - if (typeof cleanup === "function") { - observer.cleanup = cleanup; - } - } catch (error) { - console.error("[ForgeWebUI] Observer execution failed.", error); - } finally { - activeObserver = previousObserver; - } - } - - function scheduleObserver(observer) { - if (!observer || observer.disposed) { - return; - } - - scheduledObservers.add(observer); - queueFlush(); - } - - function trackDependency(dependency) { - if (!activeObserver) { - return; - } - - dependency.add(activeObserver); - activeObserver.dependencies.add(dependency); - } - - function createSignalValue(initialValue) { - let value = initialValue; - const subscribers = new Set(); - - function read() { - trackDependency(subscribers); - return value; - } - - read.peek = () => value; - read.set = (nextValue) => { - const resolvedValue = - typeof nextValue === "function" ? nextValue(value) : nextValue; - - if (Object.is(resolvedValue, value)) { - return value; - } - - value = resolvedValue; - subscribers.forEach((observer) => scheduleObserver(observer)); - return value; - }; - read.update = (updater) => read.set(updater); - read.subscribe = (listener) => - effect(() => { - listener(read()); - }); - - return read; - } - - function createSignal(initialValue) { - const signal = createSignalValue(initialValue); - return [signal, signal.set]; - } - - function computed(factory) { - const valueSignal = createSignalValue(undefined); - let initialized = false; - - effect(() => { - const nextValue = factory(); - if (!initialized || !Object.is(nextValue, valueSignal.peek())) { - initialized = true; - valueSignal.set(nextValue); - } - }); - - return valueSignal; - } - - function effect(fn) { - const observer = { - cleanup: null, - dependencies: new Set(), - disposed: false, - fn, - }; - - observer.dispose = () => { - if (observer.disposed) { - return; - } - - observer.disposed = true; - scheduledObservers.delete(observer); - cleanupObserver(observer); - }; - - runObserver(observer); - return observer.dispose; - } - - function batch(fn) { - batchDepth += 1; - - try { - return fn(); - } finally { - batchDepth = Math.max(0, batchDepth - 1); - if (batchDepth === 0) { - flushObservers(); - } - } - } - - function appendChild(node, child) { - if (child === null || child === undefined || child === false) { - return; - } - - if (Array.isArray(child)) { - child.forEach((entry) => appendChild(node, entry)); - return; - } - - if ( - typeof child === "string" || - typeof child === "number" || - typeof child === "bigint" - ) { - node.appendChild(document.createTextNode(String(child))); - return; - } - - if (child instanceof Node) { - node.appendChild(child); - } - } - - function fragment(...children) { - const node = document.createDocumentFragment(); - children.forEach((child) => appendChild(node, child)); - return node; - } - - function text(value) { - return document.createTextNode(String(value ?? "")); - } - - function applyProp(node, key, value, isSvg) { - if (key === "key") { - return; - } - - if (key === "ref" && typeof value === "function") { - value(node); - return; - } - - if (key === "className") { - if (isSvg) { - node.setAttribute("class", value || ""); - } else { - node.className = value || ""; - } - return; - } - - if (key === "style" && value && typeof value === "object") { - Object.assign(node.style, value); - return; - } - - if (key === "dataset" && value && typeof value === "object") { - Object.entries(value).forEach(([name, datasetValue]) => { - node.dataset[name] = datasetValue; - }); - return; - } - - if (key.startsWith("on") && typeof value === "function") { - node.addEventListener(key.slice(2).toLowerCase(), value); - return; - } - - if (key === "value" && "value" in node) { - node.value = value ?? ""; - return; - } - - if (key === "checked" && "checked" in node) { - node.checked = Boolean(value); - return; - } - - if (key === "selected" && "selected" in node) { - node.selected = Boolean(value); - return; - } - - if (typeof value === "boolean") { - if (value) { - node.setAttribute(key, ""); - } else { - node.removeAttribute(key); - } - return; - } - - if (value === null || value === undefined) { - node.removeAttribute(key); - return; - } - - node.setAttribute(key, value); - } - - function h(tag, props = {}, ...children) { - const isSvg = SVG_TAGS.has(tag); - const node = isSvg - ? document.createElementNS(SVG_NS, tag) - : document.createElement(tag); - - if (props && typeof props === "object") { - Object.entries(props).forEach(([key, value]) => { - applyProp(node, key, value, isSvg); - }); - } - - children.forEach((child) => appendChild(node, child)); - return node; - } - - function normalizeNode(node) { - if (node === null || node === undefined || node === false) { - return document.createDocumentFragment(); - } - - if (Array.isArray(node)) { - return fragment(...node); - } - - if ( - typeof node === "string" || - typeof node === "number" || - typeof node === "bigint" - ) { - return text(node); - } - - if (node instanceof Node) { - return node; - } - - return document.createDocumentFragment(); - } - - function captureScrollState(container) { - return Array.from( - container.querySelectorAll("[data-preserve-scroll-id]"), - ).map((node) => ({ - id: node.getAttribute("data-preserve-scroll-id"), - scrollLeft: node.scrollLeft, - scrollTop: node.scrollTop, - })); - } - - function restoreScrollState(container, scrollState) { - if (!Array.isArray(scrollState) || scrollState.length === 0) { - return; - } - - scrollState.forEach((entry) => { - if (!entry || !entry.id) { - return; - } - - const target = container.querySelector( - `[data-preserve-scroll-id="${entry.id}"]`, - ); - - if (!target) { - return; - } - - target.scrollTop = Number(entry.scrollTop || 0); - target.scrollLeft = Number(entry.scrollLeft || 0); - }); - } - - function mount(container, render, options = {}) { - const preserveScroll = options.preserveScroll !== false; - - const dispose = effect(() => { - const scrollState = preserveScroll - ? captureScrollState(container) - : []; - const nextNode = normalizeNode(render()); - - container.replaceChildren(nextNode); - - if (preserveScroll && scrollState.length > 0) { - requestAnimationFrame(() => { - restoreScrollState(container, scrollState); - }); - } - }); - - return { - container, - dispose, - rerender() { - container.replaceChildren(normalizeNode(render())); - }, - }; - } - - function render(component, container, options = {}) { - return mount(container, component, options); - } - - function unmount(mountHandle) { - if (!mountHandle || typeof mountHandle.dispose !== "function") { - return; - } - - mountHandle.dispose(); - } - - function ensureScopedStyle(id, cssText) { - if (!id || !cssText || injectedStyles.has(id)) { - return; - } - - const style = document.createElement("style"); - style.setAttribute("data-ui-style", id); - style.textContent = cssText; - document.head.appendChild(style); - injectedStyles.add(id); - } - - ForgeWebUI.batch = batch; - ForgeWebUI.computed = computed; - ForgeWebUI.createSignal = createSignal; - ForgeWebUI.effect = effect; - ForgeWebUI.ensureScopedStyle = ensureScopedStyle; - ForgeWebUI.fragment = fragment; - ForgeWebUI.h = h; - ForgeWebUI.mount = mount; - ForgeWebUI.render = render; - ForgeWebUI.signal = createSignalValue; - ForgeWebUI.text = text; - ForgeWebUI.unmount = unmount; -})(window); - -(function (global) { - const ForgeWebUI = (global.ForgeWebUI = global.ForgeWebUI || {}); - - function createHost() { - const api = global.A3API; - - return { - isArma: Boolean(api), - close(event = "ui::close", data = {}) { - return this.send(event, data); - }, - exec(statement) { - if ( - !api || - typeof api.Exec !== "function" || - typeof statement !== "string" - ) { - return false; - } - - api.Exec(statement); - return true; - }, - requestFile(path) { - if (api && typeof api.RequestFile === "function") { - return api.RequestFile(path); - } - - return fetch(path).then((response) => { - if (!response.ok) { - throw new Error(`Failed to load ${path}`); - } - - return response.text(); - }); - }, - requestTexture(path, size = 512) { - if (api && typeof api.RequestTexture === "function") { - return api.RequestTexture(path, size); - } - - return Promise.reject( - new Error("Texture requests are unavailable outside Arma."), - ); - }, - send(event, data = {}) { - if ( - !api || - typeof api.SendAlert !== "function" || - typeof event !== "string" || - event === "" - ) { - return false; - } - - api.SendAlert( - JSON.stringify({ - event, - data, - }), - ); - return true; - }, - }; - } - - ForgeWebUI.createHost = createHost; -})(window); - -(function (global) { - const ForgeWebUI = (global.ForgeWebUI = global.ForgeWebUI || {}); - - function createBridge(options = {}) { - const host = - options.host && typeof options.host === "object" - ? options.host - : ForgeWebUI.createHost(); - const globalName = options.globalName || "ForgeBridge"; - const readyEvent = options.readyEvent || "ui::ready"; - const closeEvent = options.closeEvent || "ui::close"; - const listeners = new Map(); - - function getListeners(eventName) { - if (!listeners.has(eventName)) { - listeners.set(eventName, new Set()); - } - - return listeners.get(eventName); - } - - function emit(eventName, payload) { - const eventListeners = listeners.get(eventName); - if (!eventListeners || eventListeners.size === 0) { - return; - } - - eventListeners.forEach((listener) => { - try { - listener(payload); - } catch (error) { - console.error( - `[ForgeWebUI] Bridge listener failed for ${eventName}.`, - error, - ); - } - }); - } - - function receive(eventOrPayload, data = {}) { - const eventName = - typeof eventOrPayload === "object" && eventOrPayload !== null - ? String(eventOrPayload.event || "") - : String(eventOrPayload || ""); - const payload = - typeof eventOrPayload === "object" && eventOrPayload !== null - ? eventOrPayload.data || {} - : data; - - if (!eventName) { - return false; - } - - emit(eventName, payload); - emit("*", { data: payload, event: eventName }); - return true; - } - - function receiveMany(events) { - if (!Array.isArray(events)) { - return false; - } - - events.forEach((payload) => receive(payload)); - return true; - } - - const globalBridge = { - ping() { - return true; - }, - receive, - receiveMany, - reset() { - listeners.clear(); - return true; - }, - }; - - const api = { - close(data = {}) { - return host.send(closeEvent, data); - }, - emit, - host, - installCompatibility(name) { - if (name) { - global[name] = globalBridge; - } - - return api; - }, - off(eventName, listener) { - const eventListeners = listeners.get(eventName); - if (!eventListeners) { - return false; - } - - eventListeners.delete(listener); - if (eventListeners.size === 0) { - listeners.delete(eventName); - } - - return true; - }, - on(eventName, listener) { - getListeners(eventName).add(listener); - return () => api.off(eventName, listener); - }, - ready(data = { loaded: true }) { - return host.send(readyEvent, data); - }, - receive, - receiveMany, - request(eventName, payload = {}) { - return host.send(eventName, payload); - }, - send(eventName, payload = {}) { - return host.send(eventName, payload); - }, - }; - - global[globalName] = globalBridge; - return api; - } - - ForgeWebUI.createBridge = createBridge; -})(window); - -(function (global) { - const ForgeWebUI = (global.ForgeWebUI = global.ForgeWebUI || {}); - - function resolveRoot(root) { - if (!root) { - return null; - } - - if (typeof root === "string") { - return document.querySelector(root); - } - - return root instanceof Element ? root : null; - } - - function createApp(options = {}) { - const name = options.name || "app"; - const root = options.root || "#app"; - const setup = - typeof options.setup === "function" ? options.setup : () => {}; - let started = false; - - function start() { - if (started) { - return; - } - - started = true; - - const boot = () => { - const rootNode = resolveRoot(root); - if (!rootNode) { - console.error( - `[ForgeWebUI] Root node not found for ${name}.`, - ); - return; - } - - setup({ - name, - root: rootNode, - runtime: ForgeWebUI, - }); - }; - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", boot, { - once: true, - }); - return; - } - - boot(); - } - - return { start }; - } - - ForgeWebUI.createApp = createApp; -})(window); - -(function (global) { - const ForgeWebUI = global.ForgeWebUI; - const SharedUI = (global.SharedUI = global.SharedUI || {}); - const { h, ensureScopedStyle } = ForgeWebUI; - const titleBarCss = ` -.ui-window-titlebar { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - min-height: var(--ui-titlebar-min-height, 3.5rem); - padding: var(--ui-titlebar-padding, 0.65rem 0.8rem 0.7rem 0.95rem); - background: var( - --ui-titlebar-bg, - linear-gradient(180deg, #12325b 0%, #0d2643 100%) - ); - color: var(--ui-titlebar-text, #f4f8fd); - border-bottom: 1px solid var(--ui-titlebar-border, rgb(33 73 120 / 1)); - box-shadow: var(--ui-titlebar-shadow, 0 8px 18px rgb(18 50 91 / 0.18)); - position: var(--ui-titlebar-position, relative); - top: var(--ui-titlebar-top, auto); - z-index: var(--ui-titlebar-z-index, 5); - flex-shrink: 0; -} - -.ui-window-titlebar-brand { - display: flex; - flex-direction: column; - justify-content: center; - gap: 0.1rem; - min-width: 0; -} - -.ui-window-titlebar-kicker { - font-size: 0.64rem; - font-weight: 700; - line-height: 1; - letter-spacing: 0.18em; - text-transform: uppercase; - color: var(--ui-titlebar-kicker-color, rgb(214 227 241 / 0.72)); -} - -.ui-window-titlebar-title { - font-size: var(--ui-titlebar-title-size, 1rem); - font-weight: 700; - line-height: 1.1; - letter-spacing: var(--ui-titlebar-title-spacing, -0.03em); - color: inherit; -} - -.ui-window-titlebar-controls { - display: flex; - align-items: center; - gap: 0.12rem; -} - -.ui-window-control-btn { - min-width: 2rem; - height: 2rem; - margin: 0; - padding: 0; - border-radius: 0.38rem; - border: 1px solid var(--ui-window-control-border, rgb(197 220 243 / 0.16)); - background: var(--ui-window-control-bg, rgb(255 255 255 / 0.04)); - color: var(--ui-window-control-text, rgb(237 244 251 / 0.88)); - line-height: 1; - font-size: 0.82rem; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; - box-shadow: none; - transform: none; - display: inline-flex; - align-items: center; - justify-content: center; -} - -.ui-window-control-btn + .ui-window-control-btn { - margin-left: 0; -} - -.ui-window-control-btn:hover { - background: var(--ui-window-control-hover-bg, rgb(255 255 255 / 0.04)); - box-shadow: none; - transform: none; -} - -.ui-window-control-btn:disabled { - opacity: 0.5; - cursor: not-allowed; -} - -.ui-window-control-btn.is-close { - cursor: pointer; - opacity: 1; - background: var(--ui-window-control-close-bg, rgb(255 255 255 / 0.1)); -} - -.ui-window-control-btn.is-close:hover { - background: var( - --ui-window-control-close-hover-bg, - rgb(185 67 67 / 0.9) - ); - border-color: var( - --ui-window-control-close-hover-border, - rgb(255 222 222 / 0.45) - ); -} - -.ui-window-control-icon { - width: 0.78rem; - height: 0.78rem; - stroke: currentColor; - fill: none; - stroke-width: 1.5; - stroke-linecap: round; - stroke-linejoin: round; - pointer-events: none; -} - -@media (max-width: 960px) { - .ui-window-titlebar { - flex-direction: column; - align-items: flex-start; - } - - .ui-window-titlebar-controls { - width: 100%; - justify-content: flex-end; - } -} -`; - - SharedUI.componentFns = SharedUI.componentFns || {}; - - function WindowControlIcon({ type }) { - if (type === "minimize") { - return h( - "svg", - { - className: "ui-window-control-icon", - viewBox: "0 0 16 16", - "aria-hidden": "true", - }, - h("line", { x1: "3", y1: "8", x2: "13", y2: "8" }), - ); - } - - if (type === "maximize") { - return h( - "svg", - { - className: "ui-window-control-icon", - viewBox: "0 0 16 16", - "aria-hidden": "true", - }, - h("rect", { x: "3.5", y: "3.5", width: "9", height: "9" }), - ); - } - - return h( - "svg", - { - className: "ui-window-control-icon", - viewBox: "0 0 16 16", - "aria-hidden": "true", - }, - h("line", { x1: "4", y1: "4", x2: "12", y2: "12" }), - h("line", { x1: "12", y1: "4", x2: "4", y2: "12" }), - ); - } - - SharedUI.componentFns.WindowTitleBar = function WindowTitleBar({ - kicker = "", - title = "", - onClose = null, - closeLabel = "Close interface", - minimizeLabel = "Minimize unavailable", - maximizeLabel = "Maximize unavailable", - } = {}) { - ensureScopedStyle("shared-window-titlebar", titleBarCss); - - return h( - "div", - { className: "ui-window-titlebar" }, - h( - "div", - { className: "ui-window-titlebar-brand" }, - kicker - ? h( - "span", - { className: "ui-window-titlebar-kicker" }, - kicker, - ) - : null, - h("span", { className: "ui-window-titlebar-title" }, title), - ), - h( - "div", - { className: "ui-window-titlebar-controls" }, - h( - "button", - { - type: "button", - className: "ui-window-control-btn", - disabled: true, - title: minimizeLabel, - "aria-label": minimizeLabel, - }, - WindowControlIcon({ type: "minimize" }), - ), - h( - "button", - { - type: "button", - className: "ui-window-control-btn", - disabled: true, - title: maximizeLabel, - "aria-label": maximizeLabel, - }, - WindowControlIcon({ type: "maximize" }), - ), - h( - "button", - { - type: "button", - className: "ui-window-control-btn is-close", - title: "Close", - "aria-label": closeLabel, - onClick: - typeof onClose === "function" ? onClose : () => {}, - }, - WindowControlIcon({ type: "close" }), - ), - ), - ); - }; -})(window); - -(function (global) { - const ForgeWebUI = (global.ForgeWebUI = global.ForgeWebUI || {}); - - ForgeWebUI.version = "0.1.0"; -})(window); +!function(e){const n=e.ForgeWebUI=e.ForgeWebUI||{},t=new Set(["svg","path","circle","rect","line","polyline","polygon","g","defs","use","text","tspan","clipPath","mask"]),o=new Set,r=new Set;let i=null,c=0,a=!1;function l(){for(;r.size>0;){const e=Array.from(r);r.clear(),e.forEach(e=>u(e))}}function s(e){if("function"==typeof e.cleanup)try{e.cleanup()}catch(e){console.error("[ForgeWebUI] Observer cleanup failed.",e)}e.cleanup=null,e.dependencies.forEach(n=>{n.delete(e)}),e.dependencies.clear()}function u(e){if(!e||e.disposed)return;s(e);const n=i;i=e;try{const n=e.fn();"function"==typeof n&&(e.cleanup=n)}catch(e){console.error("[ForgeWebUI] Observer execution failed.",e)}finally{i=n}}function d(e){e&&!e.disposed&&(r.add(e),a||c>0||(a=!0,queueMicrotask(()=>{a=!1,l()})))}function f(e){let n=e;const t=new Set;function o(){var e;return e=t,i&&(e.add(i),i.dependencies.add(e)),n}return o.peek=()=>n,o.set=e=>{const o="function"==typeof e?e(n):e;return Object.is(o,n)||(n=o,t.forEach(e=>d(e))),n},o.update=e=>o.set(e),o.subscribe=e=>b(()=>{e(o())}),o}function b(e){const n={cleanup:null,dependencies:new Set,disposed:!1,fn:e,dispose:()=>{n.disposed||(n.disposed=!0,r.delete(n),s(n))}};return u(n),n.dispose}function p(e,n){null!=n&&!1!==n&&(Array.isArray(n)?n.forEach(n=>p(e,n)):"string"!=typeof n&&"number"!=typeof n&&"bigint"!=typeof n?n instanceof Node&&e.appendChild(n):e.appendChild(document.createTextNode(String(n))))}function m(...e){const n=document.createDocumentFragment();return e.forEach(e=>p(n,e)),n}function w(e){return document.createTextNode(String(e??""))}function g(e){return null==e||!1===e?document.createDocumentFragment():Array.isArray(e)?m(...e):"string"==typeof e||"number"==typeof e||"bigint"==typeof e?w(e):e instanceof Node?e:document.createDocumentFragment()}function y(e,n,t={}){const o=!1!==t.preserveScroll,r=b(()=>{const t=o?function(e){return Array.from(e.querySelectorAll("[data-preserve-scroll-id]")).map(e=>({id:e.getAttribute("data-preserve-scroll-id"),scrollLeft:e.scrollLeft,scrollTop:e.scrollTop}))}(e):[],r=g(n());e.replaceChildren(r),o&&t.length>0&&requestAnimationFrame(()=>{!function(e,n){Array.isArray(n)&&0!==n.length&&n.forEach(n=>{if(!n||!n.id)return;const t=e.querySelector(`[data-preserve-scroll-id="${n.id}"]`);t&&(t.scrollTop=Number(n.scrollTop||0),t.scrollLeft=Number(n.scrollLeft||0))})}(e,t)})});return{container:e,dispose:r,rerender(){e.replaceChildren(g(n()))}}}n.batch=function(e){c+=1;try{return e()}finally{c=Math.max(0,c-1),0===c&&l()}},n.computed=function(e){const n=f(void 0);let t=!1;return b(()=>{const o=e();t&&Object.is(o,n.peek())||(t=!0,n.set(o))}),n},n.createSignal=function(e){const n=f(e);return[n,n.set]},n.effect=b,n.ensureScopedStyle=function(e,n){if(!e||!n||o.has(e))return;const t=document.createElement("style");t.setAttribute("data-ui-style",e),t.textContent=n,document.head.appendChild(t),o.add(e)},n.fragment=m,n.h=function(e,n={},...o){const r=t.has(e),i=r?document.createElementNS("http://www.w3.org/2000/svg",e):document.createElement(e);return n&&"object"==typeof n&&Object.entries(n).forEach(([e,n])=>{!function(e,n,t,o){"key"!==n&&("ref"!==n||"function"!=typeof t?"className"!==n?"style"===n&&t&&"object"==typeof t?Object.assign(e.style,t):"dataset"===n&&t&&"object"==typeof t?Object.entries(t).forEach(([n,t])=>{e.dataset[n]=t}):n.startsWith("on")&&"function"==typeof t?e.addEventListener(n.slice(2).toLowerCase(),t):"value"===n&&"value"in e?e.value=t??"":"checked"===n&&"checked"in e?e.checked=Boolean(t):"selected"===n&&"selected"in e?e.selected=Boolean(t):"boolean"!=typeof t?null!=t?e.setAttribute(n,t):e.removeAttribute(n):t?e.setAttribute(n,""):e.removeAttribute(n):o?e.setAttribute("class",t||""):e.className=t||"":t(e))}(i,e,n,r)}),o.forEach(e=>p(i,e)),i},n.mount=y,n.render=function(e,n,t={}){return y(n,e,t)},n.signal=f,n.text=w,n.unmount=function(e){e&&"function"==typeof e.dispose&&e.dispose()}}(window),function(e){(e.ForgeWebUI=e.ForgeWebUI||{}).createHost=function(){const n=e.A3API;return{isArma:Boolean(n),close(e="ui::close",n={}){return this.send(e,n)},exec:e=>!(!n||"function"!=typeof n.Exec||"string"!=typeof e)&&(n.Exec(e),!0),requestFile:e=>n&&"function"==typeof n.RequestFile?n.RequestFile(e):fetch(e).then(n=>{if(!n.ok)throw new Error(`Failed to load ${e}`);return n.text()}),requestTexture:(e,t=512)=>n&&"function"==typeof n.RequestTexture?n.RequestTexture(e,t):Promise.reject(new Error("Texture requests are unavailable outside Arma.")),send:(e,t={})=>!(!n||"function"!=typeof n.SendAlert||"string"!=typeof e||""===e)&&(n.SendAlert(JSON.stringify({event:e,data:t})),!0)}}}(window),function(e){const n=e.ForgeWebUI=e.ForgeWebUI||{};n.createBridge=function(t={}){const o=t.host&&"object"==typeof t.host?t.host:n.createHost(),r=t.globalName||"ForgeBridge",i=t.readyEvent||"ui::ready",c=t.closeEvent||"ui::close",a=new Map;function l(e,n){const t=a.get(e);t&&0!==t.size&&t.forEach(t=>{try{t(n)}catch(n){console.error(`[ForgeWebUI] Bridge listener failed for ${e}.`,n)}})}function s(e,n={}){const t=String("object"==typeof e&&null!==e?e.event||"":e||""),o="object"==typeof e&&null!==e?e.data||{}:n;return!!t&&(l(t,o),l("*",{data:o,event:t}),!0)}function u(e){return!!Array.isArray(e)&&(e.forEach(e=>s(e)),!0)}const d={ping:()=>!0,receive:s,receiveMany:u,reset:()=>(a.clear(),!0)},f={close:(e={})=>o.send(c,e),emit:l,host:o,installCompatibility:n=>(n&&(e[n]=d),f),off(e,n){const t=a.get(e);return!!t&&(t.delete(n),0===t.size&&a.delete(e),!0)},on:(e,n)=>(function(e){return a.has(e)||a.set(e,new Set),a.get(e)}(e).add(n),()=>f.off(e,n)),ready:(e={loaded:!0})=>o.send(i,e),receive:s,receiveMany:u,request:(e,n={})=>o.send(e,n),send:(e,n={})=>o.send(e,n)};return e[r]=d,f}}(window),function(e){const n=e.ForgeWebUI=e.ForgeWebUI||{};n.createApp=function(e={}){const t=e.name||"app",o=e.root||"#app",r="function"==typeof e.setup?e.setup:()=>{};let i=!1;return{start:function(){if(i)return;i=!0;const e=()=>{const e=function(e){return e?"string"==typeof e?document.querySelector(e):e instanceof Element?e:null:null}(o);e?r({name:t,root:e,runtime:n}):console.error(`[ForgeWebUI] Root node not found for ${t}.`)};"loading"!==document.readyState?e():document.addEventListener("DOMContentLoaded",e,{once:!0})}}}}(window),function(e){const n=e.ForgeWebUI,t=e.SharedUI=e.SharedUI||{},{h:o,ensureScopedStyle:r}=n;function i({type:e}){return"minimize"===e?o("svg",{className:"ui-window-control-icon",viewBox:"0 0 16 16","aria-hidden":"true"},o("line",{x1:"3",y1:"8",x2:"13",y2:"8"})):"maximize"===e?o("svg",{className:"ui-window-control-icon",viewBox:"0 0 16 16","aria-hidden":"true"},o("rect",{x:"3.5",y:"3.5",width:"9",height:"9"})):o("svg",{className:"ui-window-control-icon",viewBox:"0 0 16 16","aria-hidden":"true"},o("line",{x1:"4",y1:"4",x2:"12",y2:"12"}),o("line",{x1:"12",y1:"4",x2:"4",y2:"12"}))}t.componentFns=t.componentFns||{},t.componentFns.WindowTitleBar=function({kicker:e="",title:n="",onClose:t=null,closeLabel:c="Close interface",minimizeLabel:a="Minimize unavailable",maximizeLabel:l="Maximize unavailable"}={}){return r("shared-window-titlebar","\n.ui-window-titlebar {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n min-height: var(--ui-titlebar-min-height, 3.5rem);\n padding: var(--ui-titlebar-padding, 0.65rem 0.8rem 0.7rem 0.95rem);\n background: var(\n --ui-titlebar-bg,\n linear-gradient(180deg, #12325b 0%, #0d2643 100%)\n );\n color: var(--ui-titlebar-text, #f4f8fd);\n border-bottom: 1px solid var(--ui-titlebar-border, rgb(33 73 120 / 1));\n box-shadow: var(--ui-titlebar-shadow, 0 8px 18px rgb(18 50 91 / 0.18));\n position: var(--ui-titlebar-position, relative);\n top: var(--ui-titlebar-top, auto);\n z-index: var(--ui-titlebar-z-index, 5);\n flex-shrink: 0;\n}\n\n.ui-window-titlebar-brand {\n display: flex;\n flex-direction: column;\n justify-content: center;\n gap: 0.1rem;\n min-width: 0;\n}\n\n.ui-window-titlebar-kicker {\n font-size: 0.64rem;\n font-weight: 700;\n line-height: 1;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--ui-titlebar-kicker-color, rgb(214 227 241 / 0.72));\n}\n\n.ui-window-titlebar-title {\n font-size: var(--ui-titlebar-title-size, 1rem);\n font-weight: 700;\n line-height: 1.1;\n letter-spacing: var(--ui-titlebar-title-spacing, -0.03em);\n color: inherit;\n}\n\n.ui-window-titlebar-controls {\n display: flex;\n align-items: center;\n gap: 0.12rem;\n}\n\n.ui-window-control-btn {\n min-width: 2rem;\n height: 2rem;\n margin: 0;\n padding: 0;\n border-radius: 0.38rem;\n border: 1px solid var(--ui-window-control-border, rgb(197 220 243 / 0.16));\n background: var(--ui-window-control-bg, rgb(255 255 255 / 0.04));\n color: var(--ui-window-control-text, rgb(237 244 251 / 0.88));\n line-height: 1;\n font-size: 0.82rem;\n font-weight: 700;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n box-shadow: none;\n transform: none;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n}\n\n.ui-window-control-btn + .ui-window-control-btn {\n margin-left: 0;\n}\n\n.ui-window-control-btn:hover {\n background: var(--ui-window-control-hover-bg, rgb(255 255 255 / 0.04));\n box-shadow: none;\n transform: none;\n}\n\n.ui-window-control-btn:disabled {\n opacity: 0.5;\n cursor: not-allowed;\n}\n\n.ui-window-control-btn.is-close {\n cursor: pointer;\n opacity: 1;\n background: var(--ui-window-control-close-bg, rgb(255 255 255 / 0.1));\n}\n\n.ui-window-control-btn.is-close:hover {\n background: var(\n --ui-window-control-close-hover-bg,\n rgb(185 67 67 / 0.9)\n );\n border-color: var(\n --ui-window-control-close-hover-border,\n rgb(255 222 222 / 0.45)\n );\n}\n\n.ui-window-control-icon {\n width: 0.78rem;\n height: 0.78rem;\n stroke: currentColor;\n fill: none;\n stroke-width: 1.5;\n stroke-linecap: round;\n stroke-linejoin: round;\n pointer-events: none;\n}\n\n@media (max-width: 960px) {\n .ui-window-titlebar {\n flex-direction: column;\n align-items: flex-start;\n }\n\n .ui-window-titlebar-controls {\n width: 100%;\n justify-content: flex-end;\n }\n}\n"),o("div",{className:"ui-window-titlebar"},o("div",{className:"ui-window-titlebar-brand"},e?o("span",{className:"ui-window-titlebar-kicker"},e):null,o("span",{className:"ui-window-titlebar-title"},n)),o("div",{className:"ui-window-titlebar-controls"},o("button",{type:"button",className:"ui-window-control-btn",disabled:!0,title:a,"aria-label":a},i({type:"minimize"})),o("button",{type:"button",className:"ui-window-control-btn",disabled:!0,title:l,"aria-label":l},i({type:"maximize"})),o("button",{type:"button",className:"ui-window-control-btn is-close",title:"Close","aria-label":c,onClick:"function"==typeof t?t:()=>{}},i({type:"close"}))))}}(window),function(e){(e.ForgeWebUI=e.ForgeWebUI||{}).version="0.1.0"}(window); \ No newline at end of file diff --git a/arma/client/addons/garage/ui/_site/garage-ui.css b/arma/client/addons/garage/ui/_site/garage-ui.css index 47bd2ea..0f5a3cb 100644 --- a/arma/client/addons/garage/ui/_site/garage-ui.css +++ b/arma/client/addons/garage/ui/_site/garage-ui.css @@ -1,580 +1 @@ -/* Generated by tools/build-webui.mjs for Garage UI styles. Do not edit directly. */ -:root { - --garage-shell-bg: #e4e3df; - --garage-surface: #f5f3ef; - --garage-surface-alt: #ece8e2; - --garage-border: rgba(74, 91, 110, 0.2); - --garage-border-strong: rgba(20, 46, 79, 0.18); - --garage-text-main: #1f2d3d; - --garage-text-muted: #6a7787; - --garage-text-subtle: #8792a0; - --garage-accent: #12365d; - --garage-accent-soft: #dbe7f3; - --garage-accent-line: rgba(18, 54, 93, 0.12); - --garage-warning: #8f5f26; -} - -* { - box-sizing: border-box; -} - -html, -body { - width: 100%; - height: 100%; - margin: 0; - overflow: hidden; -} - -body { - font-family: "Segoe UI", "Trebuchet MS", sans-serif; - color: var(--garage-text-main); - background: var(--garage-shell-bg); -} - -button, -input { - font: inherit; -} - -button { - cursor: pointer; -} - -button:disabled { - cursor: not-allowed; - opacity: 0.72; -} - -:focus-visible { - outline: 2px solid rgb(18 54 93 / 0.35); - outline-offset: 2px; -} - -#app { - width: 100%; - height: 100%; -} - -.garage-shell { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - overflow: hidden; - background: var(--garage-shell-bg); -} - -.garage-layout { - flex: 1; - min-height: 0; - width: min(100%, 1613px); - margin: 0 auto; - padding: 1.25rem; - display: grid; - grid-template-columns: 308px minmax(0, 1fr); - gap: 1.25rem; -} - -.garage-sidebar, -.garage-main { - min-height: 0; - display: flex; - flex-direction: column; - gap: 1rem; -} - -.garage-main { - overflow: hidden; -} - -.garage-module, -.garage-panel, -.garage-card { - background: linear-gradient( - 180deg, - var(--garage-surface) 0%, - var(--garage-surface-alt) 100% - ); - border: 1px solid var(--garage-border); - border-radius: 1.35rem; -} - -.garage-module, -.garage-card { - padding: 1rem; -} - -.garage-module { - display: grid; - gap: 0.85rem; - align-content: start; -} - -.garage-panel { - flex: 1 1 auto; - min-height: 0; - display: flex; - flex-direction: column; - overflow: hidden; -} - -.garage-panel-header, -.garage-module-header, -.garage-card-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; -} - -.garage-panel-header { - padding: 1rem 1rem 0; -} - -.garage-module-header { - align-items: flex-start; -} - -.garage-panel-intro { - padding: 0 1rem 1rem; - border-bottom: 1px solid var(--garage-accent-line); -} - -.garage-dashboard { - flex: 1; - min-height: 0; - padding: 1rem; - display: grid; - grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); - gap: 1rem; - align-items: stretch; -} - -.garage-list-card, -.garage-detail-card { - min-height: 0; - display: flex; - flex-direction: column; -} - -.garage-detail-card { - grid-column: 1 / -1; -} - -.garage-scroll-body { - flex: 1; - min-height: 20rem; - max-height: 24rem; - overflow: auto; - display: grid; - gap: 0.8rem; - padding-right: 0.2rem; -} - -.garage-detail-body { - padding-top: 0.95rem; -} - -.garage-detail-grid { - display: grid; - grid-template-columns: minmax(0, 1.25fr) minmax(280px, 0.85fr); - gap: 1rem; -} - -.garage-detail-meta, -.garage-summary-grid, -.garage-search-actions, -.garage-category-grid, -.garage-action-row, -.garage-inline-meters, -.garage-hitpoint-grid, -.garage-footer { - display: grid; - gap: 0.75rem; -} - -.garage-detail-meta { - grid-template-columns: repeat(3, minmax(0, 1fr)); - margin-bottom: 1rem; -} - -.garage-summary-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -.garage-summary-grid > :last-child { - grid-column: 1 / -1; -} - -.garage-search-actions, -.garage-action-row { - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0.65rem; -} - -.garage-category-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0.65rem; -} - -.garage-footer-bar { - width: 100%; - border-top: 1px solid rgb(18 54 93 / 0.1); -} - -.garage-footer { - width: min(100%, 1613px); - margin: 0 auto; - grid-template-columns: repeat(3, minmax(0, 1fr)); - padding: 0.95rem 1.25rem 1.15rem; -} - -.garage-meter-stack { - display: grid; - gap: 0.75rem; - margin-bottom: 1rem; -} - -.garage-eyebrow, -.garage-footer-title, -.garage-stat-label, -.garage-meter-label, -.garage-hitpoint-selection { - font-size: 0.68rem; - font-weight: 700; - letter-spacing: 0.18em; - text-transform: uppercase; - color: var(--garage-text-subtle); -} - -.garage-title, -.garage-section-title { - margin: 0.16rem 0 0; - font-weight: 700; - letter-spacing: -0.02em; - color: var(--garage-text-main); -} - -.garage-title { - font-size: 1.1rem; -} - -.garage-section-title { - font-size: 1.05rem; -} - -.garage-copy, -.garage-detail-note, -.garage-empty-copy, -.garage-footer-copy, -.garage-vehicle-meta, -.garage-detail-caption { - margin: 0; - font-size: 0.92rem; - line-height: 1.48; - color: var(--garage-text-muted); -} - -.garage-pill, -.garage-badge { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.48rem 0.8rem; - border-radius: 999px; - font-size: 0.74rem; - font-weight: 700; - letter-spacing: 0.1em; - text-transform: uppercase; - background: var(--garage-accent-soft); - color: var(--garage-accent); -} - -.garage-badge.is-warning { - background: rgb(246 226 193 / 0.88); - color: var(--garage-warning); -} - -.garage-search-form { - display: grid; - gap: 0.75rem; -} - -.garage-search-input { - width: 100%; - height: 2.9rem; - padding: 0 0.95rem; - border-radius: 0.8rem; - border: 1px solid var(--garage-border); - background: rgb(255 255 255 / 0.75); - color: var(--garage-text-main); -} - -.garage-stat-card { - min-width: 0; - padding: 0.85rem; - border-radius: 0.85rem; - border: 1px solid var(--garage-border); - background: rgb(255 255 255 / 0.48); - display: flex; - flex-direction: column; - gap: 0.3rem; -} - -.garage-stat-card.is-accent { - background: linear-gradient( - 180deg, - rgb(237 243 249 / 0.92) 0%, - rgb(223 232 242 / 0.72) 100% - ); -} - -.garage-stat-card.is-danger { - background: linear-gradient( - 180deg, - rgb(254 242 242 / 0.95) 0%, - rgb(252 225 225 / 0.82) 100% - ); - border-color: rgb(220 151 151 / 0.38); -} - -.garage-stat-value { - font-size: 1rem; - font-weight: 700; - color: var(--garage-text-main); - line-height: 1.3; - overflow-wrap: anywhere; - word-break: break-word; -} - -.garage-chip { - min-height: 2.6rem; - padding: 0.68rem 0.9rem; - border-radius: 0.85rem; - border: 1px solid var(--garage-border); - background: rgb(255 255 255 / 0.52); - color: var(--garage-text-muted); - font-size: 0.8rem; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.garage-chip.is-active { - background: var(--garage-accent-soft); - color: var(--garage-accent); - border-color: rgb(18 54 93 / 0.2); -} - -.garage-vehicle-item { - width: 100%; - padding: 0.9rem; - border-radius: 0.95rem; - border: 1px solid var(--garage-border); - background: rgb(255 255 255 / 0.48); - color: inherit; - text-align: left; -} - -.garage-vehicle-item.is-selected { - border-color: rgb(18 54 93 / 0.24); - background: linear-gradient( - 180deg, - rgb(237 243 249 / 0.96) 0%, - rgb(223 232 242 / 0.74) 100% - ); - box-shadow: 0 16px 26px rgb(18 54 93 / 0.08); -} - -.garage-vehicle-item-head, -.garage-meter-label-row, -.garage-subsystem-header, -.garage-hitpoint-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; -} - -.garage-vehicle-copy, -.garage-hitpoint-copy, -.garage-footer-block { - min-width: 0; - display: flex; - flex-direction: column; - gap: 0.18rem; -} - -.garage-vehicle-title, -.garage-hitpoint-name, -.garage-hitpoint-value { - font-size: 0.9rem; - font-weight: 700; - color: var(--garage-text-main); -} - -.garage-meter { - display: grid; - gap: 0.32rem; -} - -.garage-meter-track { - width: 100%; - height: 0.45rem; - overflow: hidden; - border-radius: 999px; - background: rgb(18 54 93 / 0.08); -} - -.garage-meter-value { - font-size: 0.78rem; - font-weight: 700; - color: var(--garage-text-main); -} - -.garage-meter-fill { - display: block; - height: 100%; - border-radius: inherit; -} - -.garage-meter-fill.is-health { - background: linear-gradient(90deg, #2f7d5b 0%, #4eaa82 100%); -} - -.garage-meter-fill.is-fuel { - background: linear-gradient(90deg, #12365d 0%, #3c6792 100%); -} - -.garage-btn { - min-height: 2.75rem; - padding: 0.72rem 1rem; - border-radius: 0.8rem; - border: 1px solid var(--garage-border-strong); - font-size: 0.82rem; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.garage-btn-primary { - background: rgb(255 255 255 / 0.68); - color: var(--garage-accent); -} - -.garage-btn-primary:hover { - background: rgb(219 231 243 / 0.88); -} - -.garage-btn-secondary { - background: rgb(255 255 255 / 0.42); - color: var(--garage-text-muted); -} - -.garage-btn-secondary:hover { - background: rgb(255 255 255 / 0.6); - color: var(--garage-text-main); -} - -.garage-hitpoint-row { - padding: 0.72rem 0.78rem; - border-radius: 0.85rem; - border: 1px solid var(--garage-border); - background: rgb(255 255 255 / 0.52); -} - -.garage-detail-empty, -.garage-empty-state { - display: flex; - flex-direction: column; - align-items: flex-start; - justify-content: center; - min-height: 100%; -} - -.garage-empty-title { - margin: 0 0 0.35rem; - font-size: 1rem; - font-weight: 700; - color: var(--garage-text-main); -} - -.garage-empty-inline { - padding: 0.9rem; - border-radius: 0.85rem; - border: 1px dashed var(--garage-border); - color: var(--garage-text-muted); - background: rgb(255 255 255 / 0.36); -} - -.garage-toast-stack { - position: fixed; - top: 1.2rem; - right: 1.5rem; - z-index: 10; - display: flex; - flex-direction: column; - gap: 0.65rem; -} - -.garage-toast { - max-width: 24rem; - padding: 0.85rem 1rem; - border-radius: 0.9rem; - border: 1px solid var(--garage-border); - background: #fff; - box-shadow: 0 14px 28px rgb(16 34 56 / 0.14); - font-size: 0.92rem; -} - -.garage-toast.is-success { - background: #ecfdf5; - border-color: #bbf7d0; - color: #166534; -} - -.garage-toast.is-error { - background: #fef2f2; - border-color: #fecaca; - color: #991b1b; -} - -@media (max-width: 1440px) { - .garage-layout { - grid-template-columns: 288px minmax(0, 1fr); - } - - .garage-detail-grid { - grid-template-columns: 1fr; - } -} - -@media (max-width: 1120px) { - .garage-layout { - grid-template-columns: 1fr; - overflow: auto; - } - - .garage-main, - .garage-sidebar { - min-height: auto; - } - - .garage-dashboard { - grid-template-columns: 1fr; - } - - .garage-detail-card { - grid-column: auto; - } - - .garage-scroll-body { - max-height: none; - min-height: 16rem; - } - - .garage-footer { - grid-template-columns: 1fr; - } -} +:root{--garage-shell-bg:#e4e3df;--garage-surface:#f5f3ef;--garage-surface-alt:#ece8e2;--garage-border:#4a5b6e33;--garage-border-strong:#142e4f2e;--garage-text-main:#1f2d3d;--garage-text-muted:#6a7787;--garage-text-subtle:#8792a0;--garage-accent:#12365d;--garage-accent-soft:#dbe7f3;--garage-accent-line:#12365d1f;--garage-warning:#8f5f26}*{box-sizing:border-box}html,body{width:100%;height:100%;margin:0;overflow:hidden}body{color:var(--garage-text-main);background:var(--garage-shell-bg);font-family:Segoe UI,Trebuchet MS,sans-serif}button,input{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.72}:focus-visible{outline-offset:2px;outline:2px solid #12365d59}#app{width:100%;height:100%}.garage-shell{background:var(--garage-shell-bg);flex-direction:column;width:100%;height:100%;display:flex;overflow:hidden}.garage-layout{flex:1;grid-template-columns:308px minmax(0,1fr);gap:1.25rem;width:min(100%,1613px);min-height:0;margin:0 auto;padding:1.25rem;display:grid}.garage-sidebar,.garage-main{flex-direction:column;gap:1rem;min-height:0;display:flex}.garage-main{overflow:hidden}.garage-module,.garage-panel,.garage-card{background:linear-gradient(180deg, var(--garage-surface) 0%, var(--garage-surface-alt) 100%);border:1px solid var(--garage-border);border-radius:1.35rem}.garage-module,.garage-card{padding:1rem}.garage-module{align-content:start;gap:.85rem;display:grid}.garage-panel{flex-direction:column;flex:auto;min-height:0;display:flex;overflow:hidden}.garage-panel-header,.garage-module-header,.garage-card-header{justify-content:space-between;align-items:center;gap:1rem;display:flex}.garage-panel-header{padding:1rem 1rem 0}.garage-module-header{align-items:flex-start}.garage-panel-intro{border-bottom:1px solid var(--garage-accent-line);padding:0 1rem 1rem}.garage-dashboard{flex:1;grid-template-columns:minmax(0,1fr) minmax(0,1fr);align-items:stretch;gap:1rem;min-height:0;padding:1rem;display:grid}.garage-list-card,.garage-detail-card{flex-direction:column;min-height:0;display:flex}.garage-detail-card{grid-column:1/-1}.garage-scroll-body{flex:1;gap:.8rem;min-height:20rem;max-height:24rem;padding-right:.2rem;display:grid;overflow:auto}.garage-detail-body{padding-top:.95rem}.garage-detail-grid{grid-template-columns:minmax(0,1.25fr) minmax(280px,.85fr);gap:1rem;display:grid}.garage-detail-meta,.garage-summary-grid,.garage-search-actions,.garage-category-grid,.garage-action-row,.garage-inline-meters,.garage-hitpoint-grid,.garage-footer{gap:.75rem;display:grid}.garage-detail-meta{grid-template-columns:repeat(3,minmax(0,1fr));margin-bottom:1rem}.garage-summary-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.garage-summary-grid>:last-child{grid-column:1/-1}.garage-search-actions,.garage-action-row,.garage-category-grid{grid-template-columns:repeat(2,minmax(0,1fr));gap:.65rem}.garage-footer-bar{border-top:1px solid #12365d1a;width:100%}.garage-footer{grid-template-columns:repeat(3,minmax(0,1fr));width:min(100%,1613px);margin:0 auto;padding:.95rem 1.25rem 1.15rem}.garage-meter-stack{gap:.75rem;margin-bottom:1rem;display:grid}.garage-eyebrow,.garage-footer-title,.garage-stat-label,.garage-meter-label,.garage-hitpoint-selection{letter-spacing:.18em;text-transform:uppercase;color:var(--garage-text-subtle);font-size:.68rem;font-weight:700}.garage-title,.garage-section-title{letter-spacing:-.02em;color:var(--garage-text-main);margin:.16rem 0 0;font-weight:700}.garage-title{font-size:1.1rem}.garage-section-title{font-size:1.05rem}.garage-copy,.garage-detail-note,.garage-empty-copy,.garage-footer-copy,.garage-vehicle-meta,.garage-detail-caption{color:var(--garage-text-muted);margin:0;font-size:.92rem;line-height:1.48}.garage-pill,.garage-badge{letter-spacing:.1em;text-transform:uppercase;background:var(--garage-accent-soft);color:var(--garage-accent);border-radius:999px;justify-content:center;align-items:center;padding:.48rem .8rem;font-size:.74rem;font-weight:700;display:inline-flex}.garage-badge.is-warning{color:var(--garage-warning);background:#f6e2c1e0}.garage-search-form{gap:.75rem;display:grid}.garage-search-input{border:1px solid var(--garage-border);width:100%;height:2.9rem;color:var(--garage-text-main);background:#ffffffbf;border-radius:.8rem;padding:0 .95rem}.garage-stat-card{border:1px solid var(--garage-border);background:#ffffff7a;border-radius:.85rem;flex-direction:column;gap:.3rem;min-width:0;padding:.85rem;display:flex}.garage-stat-card.is-accent{background:linear-gradient(#edf3f9eb 0%,#dfe8f2b8 100%)}.garage-stat-card.is-danger{background:linear-gradient(#fef2f2f2 0%,#fce1e1d1 100%);border-color:#dc979761}.garage-stat-value{color:var(--garage-text-main);overflow-wrap:anywhere;word-break:break-word;font-size:1rem;font-weight:700;line-height:1.3}.garage-chip{border:1px solid var(--garage-border);min-height:2.6rem;color:var(--garage-text-muted);letter-spacing:.08em;text-transform:uppercase;background:#ffffff85;border-radius:.85rem;padding:.68rem .9rem;font-size:.8rem;font-weight:700}.garage-chip.is-active{background:var(--garage-accent-soft);color:var(--garage-accent);border-color:#12365d33}.garage-vehicle-item{border:1px solid var(--garage-border);width:100%;color:inherit;text-align:left;background:#ffffff7a;border-radius:.95rem;padding:.9rem}.garage-vehicle-item.is-selected{background:linear-gradient(#edf3f9f5 0%,#dfe8f2bd 100%);border-color:#12365d3d;box-shadow:0 16px 26px #12365d14}.garage-vehicle-item-head,.garage-meter-label-row,.garage-subsystem-header,.garage-hitpoint-row{justify-content:space-between;align-items:center;gap:.75rem;display:flex}.garage-vehicle-copy,.garage-hitpoint-copy,.garage-footer-block{flex-direction:column;gap:.18rem;min-width:0;display:flex}.garage-vehicle-title,.garage-hitpoint-name,.garage-hitpoint-value{color:var(--garage-text-main);font-size:.9rem;font-weight:700}.garage-meter{gap:.32rem;display:grid}.garage-meter-track{background:#12365d14;border-radius:999px;width:100%;height:.45rem;overflow:hidden}.garage-meter-value{color:var(--garage-text-main);font-size:.78rem;font-weight:700}.garage-meter-fill{border-radius:inherit;height:100%;display:block}.garage-meter-fill.is-health{background:linear-gradient(90deg,#2f7d5b 0%,#4eaa82 100%)}.garage-meter-fill.is-fuel{background:linear-gradient(90deg,#12365d 0%,#3c6792 100%)}.garage-btn{border:1px solid var(--garage-border-strong);letter-spacing:.08em;text-transform:uppercase;border-radius:.8rem;min-height:2.75rem;padding:.72rem 1rem;font-size:.82rem;font-weight:700}.garage-btn-primary{color:var(--garage-accent);background:#ffffffad}.garage-btn-primary:hover{background:#dbe7f3e0}.garage-btn-secondary{color:var(--garage-text-muted);background:#ffffff6b}.garage-btn-secondary:hover{color:var(--garage-text-main);background:#fff9}.garage-hitpoint-row{border:1px solid var(--garage-border);background:#ffffff85;border-radius:.85rem;padding:.72rem .78rem}.garage-detail-empty,.garage-empty-state{flex-direction:column;justify-content:center;align-items:flex-start;min-height:100%;display:flex}.garage-empty-title{color:var(--garage-text-main);margin:0 0 .35rem;font-size:1rem;font-weight:700}.garage-empty-inline{border:1px dashed var(--garage-border);color:var(--garage-text-muted);background:#ffffff5c;border-radius:.85rem;padding:.9rem}.garage-toast-stack{z-index:10;flex-direction:column;gap:.65rem;display:flex;position:fixed;top:1.2rem;right:1.5rem}.garage-toast{border:1px solid var(--garage-border);background:#fff;border-radius:.9rem;max-width:24rem;padding:.85rem 1rem;font-size:.92rem;box-shadow:0 14px 28px #10223824}.garage-toast.is-success{color:#166534;background:#ecfdf5;border-color:#bbf7d0}.garage-toast.is-error{color:#991b1b;background:#fef2f2;border-color:#fecaca}@media (width<=1440px){.garage-layout{grid-template-columns:288px minmax(0,1fr)}.garage-detail-grid{grid-template-columns:1fr}}@media (width<=1120px){.garage-layout{grid-template-columns:1fr;overflow:auto}.garage-main,.garage-sidebar{min-height:auto}.garage-dashboard{grid-template-columns:1fr}.garage-detail-card{grid-column:auto}.garage-scroll-body{min-height:16rem;max-height:none}.garage-footer{grid-template-columns:1fr}} \ No newline at end of file diff --git a/arma/client/addons/garage/ui/_site/garage-ui.js b/arma/client/addons/garage/ui/_site/garage-ui.js index 118a535..2f7fb77 100644 --- a/arma/client/addons/garage/ui/_site/garage-ui.js +++ b/arma/client/addons/garage/ui/_site/garage-ui.js @@ -1,1295 +1 @@ -/* Generated by tools/build-webui.mjs for Garage UI app. Do not edit directly. */ -(function () { - const runtime = window.ForgeWebUI; - const GarageApp = (window.GarageApp = window.GarageApp || {}); - - GarageApp.runtime = runtime; - window.AppRuntime = runtime; -})(); - -(function () { - const GarageApp = (window.GarageApp = window.GarageApp || {}); - - const defaultSession = { - garageName: "Vehicle Garage", - capacityUsed: 0, - capacityMax: 5, - nearbyCount: 0, - spawnBlocked: false, - spawnStatus: "Ready", - }; - - const defaultGarage = { - vehicles: [], - }; - - const defaultNearby = { - vehicles: [], - }; - - function cloneValue(value) { - return JSON.parse(JSON.stringify(value)); - } - - function replaceObject(target, source) { - Object.keys(target).forEach((key) => delete target[key]); - Object.assign(target, cloneValue(source)); - } - - GarageApp.data = { - categories: [ - { id: "all", label: "All" }, - { id: "car", label: "Cars" }, - { id: "armor", label: "Armor" }, - { id: "air", label: "Air" }, - { id: "naval", label: "Naval" }, - { id: "other", label: "Other" }, - ], - session: Object.assign({}, defaultSession), - garage: Object.assign({}, defaultGarage), - nearby: Object.assign({}, defaultNearby), - applyHydratePayload(payload) { - replaceObject( - this.session, - Object.assign({}, defaultSession, payload?.session || {}), - ); - replaceObject( - this.garage, - Object.assign({}, defaultGarage, payload?.garage || {}), - ); - replaceObject( - this.nearby, - Object.assign({}, defaultNearby, payload?.nearby || {}), - ); - }, - }; -})(); - -(function () { - const GarageApp = (window.GarageApp = window.GarageApp || {}); - const { createSignal } = GarageApp.runtime; - - class GarageStore { - constructor() { - [this.getSelectedKind, this.setSelectedKind] = createSignal(""); - [this.getSelectedId, this.setSelectedId] = createSignal(""); - [this.getSearchQuery, this.setSearchQuery] = createSignal(""); - [this.getCategoryFilter, this.setCategoryFilter] = - createSignal("all"); - [this.getPendingAction, this.setPendingAction] = createSignal(""); - [this.getNotice, this.setNotice] = createSignal({ - type: "", - text: "", - }); - } - - getSelection() { - return { - id: this.getSelectedId(), - kind: this.getSelectedKind(), - }; - } - - clearSelection() { - this.setSelectedKind(""); - this.setSelectedId(""); - } - - select(kind, id) { - this.setSelectedKind(String(kind || "")); - this.setSelectedId(String(id || "")); - } - - startAction(action) { - this.setPendingAction(String(action || "")); - } - - finishAction() { - this.setPendingAction(""); - } - - matchesSelection(entry) { - if (!entry || typeof entry !== "object") { - return false; - } - - const selection = this.getSelection(); - if (!selection.kind || !selection.id) { - return false; - } - - if (selection.kind === "stored") { - return ( - entry.entryKind === "stored" && - String(entry.plate || "") === selection.id - ); - } - - if (selection.kind === "nearby") { - return ( - entry.entryKind === "nearby" && - String(entry.netId || "") === selection.id - ); - } - - return false; - } - - ensureSelection() { - const garageVehicles = Array.isArray( - GarageApp.data?.garage?.vehicles, - ) - ? GarageApp.data.garage.vehicles - : []; - const nearbyVehicles = Array.isArray( - GarageApp.data?.nearby?.vehicles, - ) - ? GarageApp.data.nearby.vehicles - : []; - const hasCurrentSelection = [ - ...garageVehicles, - ...nearbyVehicles, - ].some((entry) => this.matchesSelection(entry)); - - if (hasCurrentSelection) { - return; - } - - const firstStored = garageVehicles[0] || null; - if (firstStored) { - this.select("stored", firstStored.plate || ""); - return; - } - - const firstNearby = nearbyVehicles[0] || null; - if (firstNearby) { - this.select("nearby", firstNearby.netId || ""); - return; - } - - this.clearSelection(); - } - - hydrateFromPayload() { - this.finishAction(); - this.ensureSelection(); - } - } - - GarageApp.store = new GarageStore(); -})(); - -(function () { - const GarageApp = (window.GarageApp = window.GarageApp || {}); - const store = GarageApp.store; - const bridge = window.ForgeWebUI.createBridge({ - closeEvent: "garage::close", - globalName: "ForgeBridge", - readyEvent: "garage::ready", - }); - - function requestClose() { - return bridge.close({}); - } - - function requestRefresh() { - return bridge.send("garage::refresh", {}); - } - - function requestRetrieve(payload) { - return bridge.send("garage::vehicle::retrieve::request", payload); - } - - function requestStore(payload) { - return bridge.send("garage::vehicle::store::request", payload); - } - - function notifyReady() { - return bridge.ready({ loaded: true }); - } - - function hydrate(payloadData) { - GarageApp.data.applyHydratePayload(payloadData); - store.hydrateFromPayload(payloadData); - } - - bridge.on("garage::hydrate", hydrate); - bridge.on("garage::sync", hydrate); - - bridge.on("garage::retrieve::success", (payloadData) => { - store.finishAction(); - if (GarageApp.actions) { - GarageApp.actions.showNotice( - "success", - payloadData.message || "Vehicle retrieved from the garage.", - ); - } - }); - - bridge.on("garage::retrieve::failure", (payloadData) => { - store.finishAction(); - if (GarageApp.actions) { - GarageApp.actions.showNotice( - "error", - payloadData.message || "Unable to retrieve vehicle.", - ); - } - }); - - bridge.on("garage::store::success", (payloadData) => { - store.finishAction(); - if (GarageApp.actions) { - GarageApp.actions.showNotice( - "success", - payloadData.message || "Vehicle stored in the garage.", - ); - } - }); - - bridge.on("garage::store::failure", (payloadData) => { - store.finishAction(); - if (GarageApp.actions) { - GarageApp.actions.showNotice( - "error", - payloadData.message || "Unable to store vehicle.", - ); - } - }); - - GarageApp.bridge = { - notifyReady, - receive: bridge.receive, - requestClose, - requestRefresh, - requestRetrieve, - requestStore, - sendEvent: bridge.send, - }; -})(); - -(function () { - const GarageApp = (window.GarageApp = window.GarageApp || {}); - const store = GarageApp.store; - - let noticeTimer = null; - - function getStoredVehicles() { - return Array.isArray(GarageApp.data?.garage?.vehicles) - ? GarageApp.data.garage.vehicles - : []; - } - - function getNearbyVehicles() { - return Array.isArray(GarageApp.data?.nearby?.vehicles) - ? GarageApp.data.nearby.vehicles - : []; - } - - function getSelectedEntry() { - const selection = store.getSelection(); - if (selection.kind === "stored") { - return ( - getStoredVehicles().find( - (vehicle) => String(vehicle.plate || "") === selection.id, - ) || null - ); - } - - if (selection.kind === "nearby") { - return ( - getNearbyVehicles().find( - (vehicle) => String(vehicle.netId || "") === selection.id, - ) || null - ); - } - - return null; - } - - function showNotice(type, text) { - store.setNotice({ type, text }); - - if (noticeTimer) { - clearTimeout(noticeTimer); - } - - noticeTimer = setTimeout(() => { - store.setNotice({ type: "", text: "" }); - noticeTimer = null; - }, 3200); - } - - function closeGarage() { - const bridge = GarageApp.bridge; - if (bridge && typeof bridge.requestClose === "function") { - const sent = bridge.requestClose(); - if (sent) { - return true; - } - } - - showNotice("error", "Garage bridge is unavailable."); - return false; - } - - function refreshGarage() { - const bridge = GarageApp.bridge; - if (bridge && typeof bridge.requestRefresh === "function") { - const sent = bridge.requestRefresh(); - if (sent) { - return true; - } - } - - showNotice("error", "Garage refresh bridge is unavailable."); - return false; - } - - function applySearchQuery(value) { - store.setSearchQuery(String(value || "").trim()); - } - - function clearSearch() { - store.setSearchQuery(""); - } - - function selectCategory(categoryId) { - store.setCategoryFilter(String(categoryId || "all").trim() || "all"); - } - - function selectEntry(kind, id) { - store.select(kind, id); - } - - function requestRetrieveSelected() { - const selectedEntry = getSelectedEntry(); - if (!selectedEntry || selectedEntry.entryKind !== "stored") { - showNotice("error", "Select a stored vehicle to retrieve."); - return false; - } - - if (GarageApp.data?.session?.spawnBlocked) { - showNotice("error", "The garage spawn area is blocked."); - return false; - } - - const bridge = GarageApp.bridge; - if (!bridge || typeof bridge.requestRetrieve !== "function") { - showNotice("error", "Garage retrieve bridge is unavailable."); - return false; - } - - store.startAction("retrieve"); - const sent = bridge.requestRetrieve({ - plate: selectedEntry.plate || "", - }); - - if (!sent) { - store.finishAction(); - showNotice("error", "Garage retrieve bridge is unavailable."); - return false; - } - - return true; - } - - function requestStoreSelected() { - const selectedEntry = getSelectedEntry(); - if (!selectedEntry || selectedEntry.entryKind !== "nearby") { - showNotice("error", "Select a nearby vehicle to store."); - return false; - } - - if (selectedEntry.isEmpty === false) { - showNotice( - "error", - "All crew must exit the vehicle before storing it.", - ); - return false; - } - - const bridge = GarageApp.bridge; - if (!bridge || typeof bridge.requestStore !== "function") { - showNotice("error", "Garage store bridge is unavailable."); - return false; - } - - store.startAction("store"); - const sent = bridge.requestStore({ - netId: selectedEntry.netId || "", - }); - - if (!sent) { - store.finishAction(); - showNotice("error", "Garage store bridge is unavailable."); - return false; - } - - return true; - } - - GarageApp.actions = { - showNotice, - closeGarage, - refreshGarage, - applySearchQuery, - clearSearch, - selectCategory, - selectEntry, - getSelectedEntry, - requestRetrieveSelected, - requestStoreSelected, - }; -})(); - -(function () { - const GarageApp = (window.GarageApp = window.GarageApp || {}); - const { h } = GarageApp.runtime; - const WindowTitleBar = window.SharedUI.componentFns.WindowTitleBar; - const store = GarageApp.store; - const actions = GarageApp.actions; - const { categories, garage, nearby, session } = GarageApp.data; - - function q(query, values) { - const needle = String(query || "") - .trim() - .toLowerCase(); - if (!needle) { - return true; - } - - return values.some((value) => - String(value || "") - .toLowerCase() - .includes(needle), - ); - } - - function pct(value) { - return Math.max(0, Math.min(100, Math.round(Number(value || 0) * 100))); - } - - function categoryLabel(category) { - const match = categories.find( - (entry) => entry.id === String(category || "other").toLowerCase(), - ); - return match ? match.label : "Other"; - } - - function distanceLabel(value) { - return `${Math.round(Number(value || 0))} m`; - } - - function plateLabel(value) { - return String(value || "").trim() || "Untracked"; - } - - function statusLabel(vehicle) { - if (!vehicle) { - return "-"; - } - - if (vehicle.entryKind === "stored") { - return "Stored"; - } - - return vehicle.isEmpty === false ? "Crewed" : "Ready"; - } - - function normalizeHitPointLabel(value) { - return String(value || "") - .replace(/^Hit/i, "") - .replace(/([a-z])([A-Z])/g, "$1 $2") - .replace(/_/g, " ") - .trim(); - } - - function sameEntry(left, right) { - if (!left || !right) { - return false; - } - - return ( - String(left.entryKind || "") === String(right.entryKind || "") && - String(left.plate || "") === String(right.plate || "") && - String(left.netId || "") === String(right.netId || "") - ); - } - - function selectedEntry(state) { - if (state.selectedKind === "stored") { - return ( - (garage.vehicles || []).find( - (vehicle) => - String(vehicle.plate || "") === state.selectedId, - ) || null - ); - } - - if (state.selectedKind === "nearby") { - return ( - (nearby.vehicles || []).find( - (vehicle) => - String(vehicle.netId || "") === state.selectedId, - ) || null - ); - } - - return null; - } - - function visibleVehicles(vehicles, state) { - return (vehicles || []).filter((vehicle) => { - if ( - state.categoryFilter !== "all" && - String(vehicle.category || "").toLowerCase() !== - state.categoryFilter - ) { - return false; - } - - return q(state.searchQuery, [ - vehicle.displayName, - vehicle.classname, - vehicle.plate, - vehicle.netId, - vehicle.category, - ]); - }); - } - - function stat(label, value, tone = "") { - return h( - "div", - { - className: tone - ? `garage-stat-card is-${tone}` - : "garage-stat-card", - }, - h("span", { className: "garage-stat-label" }, label), - h("span", { className: "garage-stat-value" }, value), - ); - } - - function meter(label, percent, tone) { - return h( - "div", - { className: "garage-meter" }, - h( - "div", - { className: "garage-meter-label-row" }, - h("span", { className: "garage-meter-label" }, label), - h("span", { className: "garage-meter-value" }, `${percent}%`), - ), - h( - "div", - { className: "garage-meter-track" }, - h("span", { - className: `garage-meter-fill is-${tone}`, - style: { width: `${percent}%` }, - }), - ), - ); - } - - function vehicleItem(vehicle, currentSelection) { - const id = - vehicle.entryKind === "stored" - ? String(vehicle.plate || "") - : String(vehicle.netId || ""); - const isNearby = vehicle.entryKind === "nearby"; - - return h( - "button", - { - type: "button", - className: sameEntry(vehicle, currentSelection) - ? "garage-vehicle-item is-selected" - : "garage-vehicle-item", - onClick: () => actions.selectEntry(vehicle.entryKind, id), - }, - h( - "div", - { className: "garage-vehicle-item-head" }, - h( - "div", - { className: "garage-vehicle-copy" }, - h( - "span", - { className: "garage-vehicle-title" }, - vehicle.displayName || vehicle.classname || "Vehicle", - ), - h( - "span", - { className: "garage-vehicle-meta" }, - isNearby - ? `Nearby ${distanceLabel(vehicle.distance)}` - : `Plate ${plateLabel(vehicle.plate)}`, - ), - ), - h( - "span", - { - className: - isNearby && vehicle.isEmpty === false - ? "garage-badge is-warning" - : "garage-badge", - }, - isNearby - ? vehicle.isEmpty === false - ? "Crewed" - : "Empty" - : categoryLabel(vehicle.category), - ), - ), - h( - "div", - { className: "garage-inline-meters" }, - meter("Health", pct(vehicle.health), "health"), - meter("Fuel", pct(vehicle.fuel), "fuel"), - ), - ); - } - - function vehicleList(title, eyebrow, scrollId, vehicles, currentSelection) { - return h( - "section", - { className: "garage-card garage-list-card" }, - h( - "div", - { className: "garage-card-header" }, - h( - "div", - null, - h("span", { className: "garage-eyebrow" }, eyebrow), - h("h2", { className: "garage-section-title" }, title), - ), - h( - "span", - { className: "garage-pill" }, - `${vehicles.length} ${vehicles.length === 1 ? "Vehicle" : "Vehicles"}`, - ), - ), - h( - "div", - { - className: "garage-card-body garage-scroll-body", - "data-preserve-scroll-id": scrollId, - }, - vehicles.length > 0 - ? vehicles.map((vehicle) => - vehicleItem(vehicle, currentSelection), - ) - : h( - "div", - { className: "garage-empty-state" }, - h( - "h3", - { className: "garage-empty-title" }, - "No matching vehicles", - ), - h( - "p", - { className: "garage-empty-copy" }, - "Adjust the current search or category filter to view more records.", - ), - ), - ), - ); - } - - function hitPointRows(hitPoints) { - const rows = (Array.isArray(hitPoints) ? hitPoints : []) - .slice() - .sort( - (left, right) => - Number(right.value || 0) - Number(left.value || 0), - ) - .slice(0, 6) - .filter((row) => Number(row.value || 0) > 0); - - if (rows.length === 0) { - return h( - "div", - { className: "garage-empty-inline" }, - "No subsystem damage reported.", - ); - } - - return h( - "div", - { className: "garage-hitpoint-grid" }, - rows.map((row) => - h( - "div", - { className: "garage-hitpoint-row" }, - h( - "div", - { className: "garage-hitpoint-copy" }, - h( - "span", - { className: "garage-hitpoint-name" }, - normalizeHitPointLabel(row.name) || "Subsystem", - ), - row.selection - ? h( - "span", - { className: "garage-hitpoint-selection" }, - row.selection, - ) - : null, - ), - h( - "span", - { className: "garage-hitpoint-value" }, - `${Math.round(Number(row.value || 0) * 100)}%`, - ), - ), - ), - ); - } - - function detailPanel(currentSelection, state) { - if (!currentSelection) { - return h( - "section", - { className: "garage-card garage-detail-card" }, - h( - "div", - { className: "garage-card-header" }, - h( - "div", - null, - h("span", { className: "garage-eyebrow" }, "Selection"), - h( - "h2", - { className: "garage-section-title" }, - "Vehicle Detail", - ), - ), - ), - h( - "div", - { className: "garage-card-body garage-detail-empty" }, - h( - "h3", - { className: "garage-empty-title" }, - "Select a vehicle", - ), - h( - "p", - { className: "garage-empty-copy" }, - "Choose a stored record to retrieve or a nearby vehicle to store.", - ), - ), - ); - } - - const isStored = currentSelection.entryKind === "stored"; - const pendingAction = String(state.pendingAction || ""); - const isBusy = - pendingAction === "retrieve" || pendingAction === "store"; - const canRetrieve = isStored && !session.spawnBlocked && !isBusy; - const canStore = - !isStored && currentSelection.isEmpty !== false && !isBusy; - - return h( - "section", - { className: "garage-card garage-detail-card" }, - h( - "div", - { className: "garage-card-header" }, - h( - "div", - null, - h( - "span", - { className: "garage-eyebrow" }, - isStored ? "Stored Record" : "Nearby Vehicle", - ), - h( - "h2", - { className: "garage-section-title" }, - currentSelection.displayName || - currentSelection.classname || - "Vehicle", - ), - ), - h( - "span", - { - className: - currentSelection.entryKind === "nearby" && - currentSelection.isEmpty === false - ? "garage-badge is-warning" - : "garage-badge", - }, - isStored - ? `Plate ${plateLabel(currentSelection.plate)}` - : currentSelection.isEmpty === false - ? "Crewed" - : "Ready", - ), - ), - h( - "div", - { className: "garage-card-body garage-detail-body" }, - h( - "div", - { className: "garage-detail-grid" }, - h( - "div", - { className: "garage-detail-copy" }, - h( - "div", - { className: "garage-detail-meta" }, - stat( - "Category", - categoryLabel(currentSelection.category), - ), - stat( - "Status", - statusLabel(currentSelection), - currentSelection.entryKind === "nearby" && - currentSelection.isEmpty === false - ? "danger" - : "", - ), - stat( - isStored ? "Record" : "Distance", - isStored - ? plateLabel(currentSelection.plate) - : distanceLabel(currentSelection.distance), - isStored ? "" : "accent", - ), - ), - h( - "div", - { className: "garage-meter-stack" }, - meter( - "Health", - pct(currentSelection.health), - "health", - ), - meter("Fuel", pct(currentSelection.fuel), "fuel"), - ), - h( - "div", - { className: "garage-action-row" }, - isStored - ? h( - "button", - { - type: "button", - className: - "garage-btn garage-btn-primary", - disabled: !canRetrieve, - onClick: () => - actions.requestRetrieveSelected(), - }, - pendingAction === "retrieve" - ? "Retrieving..." - : "Retrieve Vehicle", - ) - : h( - "button", - { - type: "button", - className: - "garage-btn garage-btn-primary", - disabled: !canStore, - onClick: () => - actions.requestStoreSelected(), - }, - pendingAction === "store" - ? "Storing..." - : "Store Vehicle", - ), - h( - "button", - { - type: "button", - className: - "garage-btn garage-btn-secondary", - disabled: isBusy, - onClick: () => actions.refreshGarage(), - }, - "Refresh", - ), - ), - h( - "p", - { className: "garage-detail-note" }, - isStored - ? session.spawnBlocked - ? "The garage spawn lane is currently blocked." - : "Retrieve this stored vehicle into the active spawn lane." - : currentSelection.isEmpty === false - ? "Only empty nearby vehicles can be stored." - : "Store this nearby vehicle back into persistent garage storage.", - ), - ), - h( - "div", - { className: "garage-detail-subsystems" }, - h( - "div", - { className: "garage-subsystem-header" }, - h( - "span", - { className: "garage-eyebrow" }, - "Subsystems", - ), - h( - "span", - { className: "garage-detail-caption" }, - "Highest damage first", - ), - ), - hitPointRows(currentSelection.hitPoints), - ), - ), - ), - ); - } - - GarageApp.components = GarageApp.components || {}; - GarageApp.components.App = function App() { - const state = { - categoryFilter: store.getCategoryFilter(), - notice: store.getNotice(), - pendingAction: store.getPendingAction(), - searchQuery: store.getSearchQuery(), - selectedId: store.getSelectedId(), - selectedKind: store.getSelectedKind(), - }; - const currentSelection = selectedEntry(state); - const storedVehicles = visibleVehicles(garage.vehicles || [], state); - const nearbyVehicles = visibleVehicles(nearby.vehicles || [], state); - const searchLabel = state.searchQuery - ? `Search: ${state.searchQuery}` - : "Live"; - - return h( - "div", - { className: "garage-shell" }, - WindowTitleBar({ - kicker: "FORGE Logistics", - title: "Vehicle Garage", - onClose: () => actions.closeGarage(), - closeLabel: "Close garage interface", - }), - state.notice.text - ? h( - "div", - { className: "garage-toast-stack" }, - h( - "div", - { - className: - state.notice.type === "error" - ? "garage-toast is-error" - : "garage-toast is-success", - }, - state.notice.text, - ), - ) - : null, - h( - "div", - { className: "garage-layout" }, - h( - "aside", - { className: "garage-sidebar" }, - h( - "section", - { className: "garage-module" }, - h( - "div", - { className: "garage-module-header" }, - h( - "div", - null, - h( - "span", - { className: "garage-eyebrow" }, - "Search", - ), - h( - "h2", - { className: "garage-section-title" }, - "Vehicle Records", - ), - ), - h( - "span", - { className: "garage-pill" }, - searchLabel, - ), - ), - h( - "div", - { className: "garage-search-form" }, - h("input", { - id: "garage-search-input", - type: "text", - className: "garage-search-input", - placeholder: - "Search by name, plate, or category", - value: state.searchQuery, - }), - h( - "div", - { className: "garage-search-actions" }, - h( - "button", - { - type: "button", - className: - "garage-btn garage-btn-primary", - onClick: () => - actions.applySearchQuery( - document.getElementById( - "garage-search-input", - )?.value || "", - ), - }, - "Apply Search", - ), - h( - "button", - { - type: "button", - className: - "garage-btn garage-btn-secondary", - onClick: () => actions.clearSearch(), - }, - "Clear", - ), - ), - ), - ), - h( - "section", - { className: "garage-module" }, - h( - "div", - { className: "garage-module-header" }, - h( - "div", - null, - h( - "span", - { className: "garage-eyebrow" }, - "Filter", - ), - h( - "h2", - { className: "garage-section-title" }, - "Vehicle Categories", - ), - ), - ), - h( - "div", - { className: "garage-category-grid" }, - categories.map((category) => - h( - "button", - { - type: "button", - className: - state.categoryFilter === category.id - ? "garage-chip is-active" - : "garage-chip", - onClick: () => - actions.selectCategory(category.id), - }, - category.label, - ), - ), - ), - ), - h( - "section", - { className: "garage-module" }, - h( - "div", - { className: "garage-module-header" }, - h( - "div", - null, - h( - "span", - { className: "garage-eyebrow" }, - "Status", - ), - h( - "h2", - { className: "garage-section-title" }, - "Garage Summary", - ), - ), - h( - "button", - { - type: "button", - className: - "garage-btn garage-btn-secondary", - disabled: Boolean(state.pendingAction), - onClick: () => actions.refreshGarage(), - }, - "Refresh", - ), - ), - h( - "div", - { className: "garage-summary-grid" }, - stat( - "Stored", - `${session.capacityUsed}/${session.capacityMax}`, - ), - stat("Nearby", session.nearbyCount, "accent"), - stat( - "Spawn Lane", - session.spawnStatus, - session.spawnBlocked ? "danger" : "", - ), - ), - ), - ), - h( - "main", - { className: "garage-main" }, - h( - "section", - { className: "garage-panel" }, - h( - "div", - { className: "garage-panel-header" }, - h( - "div", - null, - h( - "span", - { className: "garage-eyebrow" }, - "Operations Bay", - ), - h( - "h1", - { className: "garage-title" }, - session.garageName || "Vehicle Garage", - ), - ), - h( - "span", - { className: "garage-pill" }, - `${session.capacityUsed}/${session.capacityMax} Stored`, - ), - ), - h( - "div", - { className: "garage-panel-intro" }, - h( - "p", - { className: "garage-copy" }, - "Retrieve stored vehicles into the active spawn lane or store nearby empty vehicles back into persistent ownership records.", - ), - ), - h( - "div", - { className: "garage-dashboard" }, - vehicleList( - "Stored Vehicles", - "Persistent Records", - "garage-stored-list", - storedVehicles, - currentSelection, - ), - vehicleList( - "Nearby Vehicles", - "Store Window", - "garage-nearby-list", - nearbyVehicles, - currentSelection, - ), - detailPanel(currentSelection, state), - ), - ), - ), - ), - h( - "footer", - { className: "garage-footer-bar" }, - h( - "div", - { className: "garage-footer" }, - h( - "div", - { className: "garage-footer-block" }, - h( - "span", - { className: "garage-footer-title" }, - "Storage Capacity", - ), - h( - "span", - { className: "garage-footer-copy" }, - `${session.capacityUsed} of ${session.capacityMax} vehicle slot(s) are currently occupied.`, - ), - ), - h( - "div", - { className: "garage-footer-block" }, - h( - "span", - { className: "garage-footer-title" }, - "Retrieval Window", - ), - h( - "span", - { className: "garage-footer-copy" }, - session.spawnBlocked - ? "Spawn lane is blocked. Clear the bay before retrieving another vehicle." - : "Spawn lane is clear. Stored vehicles can be retrieved immediately.", - ), - ), - h( - "div", - { className: "garage-footer-block" }, - h( - "span", - { className: "garage-footer-title" }, - "Store Rules", - ), - h( - "span", - { className: "garage-footer-copy" }, - "Only nearby empty vehicles can be stored. Nearby count updates from the live world state.", - ), - ), - ), - ), - ); - }; -})(); - -(function () { - const ForgeWebUI = window.ForgeWebUI; - const GarageApp = window.GarageApp; - const app = ForgeWebUI.createApp({ - name: "garage", - root: "#app", - setup({ root }) { - ForgeWebUI.mount(root, () => GarageApp.components.App(), { - preserveScroll: true, - }); - - if (GarageApp.bridge) { - GarageApp.bridge.notifyReady(); - } - }, - }); - - app.start(); -})(); +!function(){const e=window.ForgeWebUI;(window.GarageApp=window.GarageApp||{}).runtime=e,window.AppRuntime=e}(),function(){const e=window.GarageApp=window.GarageApp||{},a={garageName:"Vehicle Garage",capacityUsed:0,capacityMax:5,nearbyCount:0,spawnBlocked:!1,spawnStatus:"Ready"},t={vehicles:[]},r={vehicles:[]};function s(e,a){var t;Object.keys(e).forEach(a=>delete e[a]),Object.assign(e,(t=a,JSON.parse(JSON.stringify(t))))}e.data={categories:[{id:"all",label:"All"},{id:"car",label:"Cars"},{id:"armor",label:"Armor"},{id:"air",label:"Air"},{id:"naval",label:"Naval"},{id:"other",label:"Other"}],session:Object.assign({},a),garage:Object.assign({},t),nearby:Object.assign({},r),applyHydratePayload(e){s(this.session,Object.assign({},a,e?.session||{})),s(this.garage,Object.assign({},t,e?.garage||{})),s(this.nearby,Object.assign({},r,e?.nearby||{}))}}}(),function(){const e=window.GarageApp=window.GarageApp||{},{createSignal:a}=e.runtime;e.store=new class{constructor(){[this.getSelectedKind,this.setSelectedKind]=a(""),[this.getSelectedId,this.setSelectedId]=a(""),[this.getSearchQuery,this.setSearchQuery]=a(""),[this.getCategoryFilter,this.setCategoryFilter]=a("all"),[this.getPendingAction,this.setPendingAction]=a(""),[this.getNotice,this.setNotice]=a({type:"",text:""})}getSelection(){return{id:this.getSelectedId(),kind:this.getSelectedKind()}}clearSelection(){this.setSelectedKind(""),this.setSelectedId("")}select(e,a){this.setSelectedKind(String(e||"")),this.setSelectedId(String(a||""))}startAction(e){this.setPendingAction(String(e||""))}finishAction(){this.setPendingAction("")}matchesSelection(e){if(!e||"object"!=typeof e)return!1;const a=this.getSelection();return!(!a.kind||!a.id)&&("stored"===a.kind?"stored"===e.entryKind&&String(e.plate||"")===a.id:"nearby"===a.kind&&("nearby"===e.entryKind&&String(e.netId||"")===a.id))}ensureSelection(){const a=Array.isArray(e.data?.garage?.vehicles)?e.data.garage.vehicles:[],t=Array.isArray(e.data?.nearby?.vehicles)?e.data.nearby.vehicles:[];if([...a,...t].some(e=>this.matchesSelection(e)))return;const r=a[0]||null;if(r)return void this.select("stored",r.plate||"");const s=t[0]||null;s?this.select("nearby",s.netId||""):this.clearSelection()}hydrateFromPayload(){this.finishAction(),this.ensureSelection()}}}(),function(){const e=window.GarageApp=window.GarageApp||{},a=e.store,t=window.ForgeWebUI.createBridge({closeEvent:"garage::close",globalName:"ForgeBridge",readyEvent:"garage::ready"});function r(t){e.data.applyHydratePayload(t),a.hydrateFromPayload(t)}t.on("garage::hydrate",r),t.on("garage::sync",r),t.on("garage::retrieve::success",t=>{a.finishAction(),e.actions&&e.actions.showNotice("success",t.message||"Vehicle retrieved from the garage.")}),t.on("garage::retrieve::failure",t=>{a.finishAction(),e.actions&&e.actions.showNotice("error",t.message||"Unable to retrieve vehicle.")}),t.on("garage::store::success",t=>{a.finishAction(),e.actions&&e.actions.showNotice("success",t.message||"Vehicle stored in the garage.")}),t.on("garage::store::failure",t=>{a.finishAction(),e.actions&&e.actions.showNotice("error",t.message||"Unable to store vehicle.")}),e.bridge={notifyReady:function(){return t.ready({loaded:!0})},receive:t.receive,requestClose:function(){return t.close({})},requestRefresh:function(){return t.send("garage::refresh",{})},requestRetrieve:function(e){return t.send("garage::vehicle::retrieve::request",e)},requestStore:function(e){return t.send("garage::vehicle::store::request",e)},sendEvent:t.send}}(),function(){const e=window.GarageApp=window.GarageApp||{},a=e.store;let t=null;function r(){const t=a.getSelection();return"stored"===t.kind?(Array.isArray(e.data?.garage?.vehicles)?e.data.garage.vehicles:[]).find(e=>String(e.plate||"")===t.id)||null:"nearby"===t.kind&&(Array.isArray(e.data?.nearby?.vehicles)?e.data.nearby.vehicles:[]).find(e=>String(e.netId||"")===t.id)||null}function s(e,r){a.setNotice({type:e,text:r}),t&&clearTimeout(t),t=setTimeout(()=>{a.setNotice({type:"",text:""}),t=null},3200)}e.actions={showNotice:s,closeGarage:function(){const a=e.bridge;if(a&&"function"==typeof a.requestClose){if(a.requestClose())return!0}return s("error","Garage bridge is unavailable."),!1},refreshGarage:function(){const a=e.bridge;if(a&&"function"==typeof a.requestRefresh){if(a.requestRefresh())return!0}return s("error","Garage refresh bridge is unavailable."),!1},applySearchQuery:function(e){a.setSearchQuery(String(e||"").trim())},clearSearch:function(){a.setSearchQuery("")},selectCategory:function(e){a.setCategoryFilter(String(e||"all").trim()||"all")},selectEntry:function(e,t){a.select(e,t)},getSelectedEntry:r,requestRetrieveSelected:function(){const t=r();if(!t||"stored"!==t.entryKind)return s("error","Select a stored vehicle to retrieve."),!1;if(e.data?.session?.spawnBlocked)return s("error","The garage spawn area is blocked."),!1;const i=e.bridge;return i&&"function"==typeof i.requestRetrieve?(a.startAction("retrieve"),!!i.requestRetrieve({plate:t.plate||""})||(a.finishAction(),s("error","Garage retrieve bridge is unavailable."),!1)):(s("error","Garage retrieve bridge is unavailable."),!1)},requestStoreSelected:function(){const t=r();if(!t||"nearby"!==t.entryKind)return s("error","Select a nearby vehicle to store."),!1;if(!1===t.isEmpty)return s("error","All crew must exit the vehicle before storing it."),!1;const i=e.bridge;return i&&"function"==typeof i.requestStore?(a.startAction("store"),!!i.requestStore({netId:t.netId||""})||(a.finishAction(),s("error","Garage store bridge is unavailable."),!1)):(s("error","Garage store bridge is unavailable."),!1)}}}(),function(){const e=window.GarageApp=window.GarageApp||{},{h:a}=e.runtime,t=window.SharedUI.componentFns.WindowTitleBar,r=e.store,s=e.actions,{categories:i,garage:n,nearby:c,session:l}=e.data;function o(e){return Math.max(0,Math.min(100,Math.round(100*Number(e||0))))}function g(e){const a=i.find(a=>a.id===String(e||"other").toLowerCase());return a?a.label:"Other"}function d(e){return`${Math.round(Number(e||0))} m`}function u(e){return String(e||"").trim()||"Untracked"}function m(e,a){return(e||[]).filter(e=>("all"===a.categoryFilter||String(e.category||"").toLowerCase()===a.categoryFilter)&&function(e,a){const t=String(e||"").trim().toLowerCase();return!t||a.some(e=>String(e||"").toLowerCase().includes(t))}(a.searchQuery,[e.displayName,e.classname,e.plate,e.netId,e.category]))}function p(e,t,r=""){return a("div",{className:r?`garage-stat-card is-${r}`:"garage-stat-card"},a("span",{className:"garage-stat-label"},e),a("span",{className:"garage-stat-value"},t))}function h(e,t,r){return a("div",{className:"garage-meter"},a("div",{className:"garage-meter-label-row"},a("span",{className:"garage-meter-label"},e),a("span",{className:"garage-meter-value"},`${t}%`)),a("div",{className:"garage-meter-track"},a("span",{className:`garage-meter-fill is-${r}`,style:{width:`${t}%`}})))}function y(e,t,r,i,n){return a("section",{className:"garage-card garage-list-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},t),a("h2",{className:"garage-section-title"},e)),a("span",{className:"garage-pill"},`${i.length} ${1===i.length?"Vehicle":"Vehicles"}`)),a("div",{className:"garage-card-body garage-scroll-body","data-preserve-scroll-id":r},i.length>0?i.map(e=>function(e,t){const r="stored"===e.entryKind?String(e.plate||""):String(e.netId||""),i="nearby"===e.entryKind;return a("button",{type:"button",className:(n=e,c=t,n&&c&&String(n.entryKind||"")===String(c.entryKind||"")&&String(n.plate||"")===String(c.plate||"")&&String(n.netId||"")===String(c.netId||"")?"garage-vehicle-item is-selected":"garage-vehicle-item"),onClick:()=>s.selectEntry(e.entryKind,r)},a("div",{className:"garage-vehicle-item-head"},a("div",{className:"garage-vehicle-copy"},a("span",{className:"garage-vehicle-title"},e.displayName||e.classname||"Vehicle"),a("span",{className:"garage-vehicle-meta"},i?`Nearby ${d(e.distance)}`:`Plate ${u(e.plate)}`)),a("span",{className:i&&!1===e.isEmpty?"garage-badge is-warning":"garage-badge"},i?!1===e.isEmpty?"Crewed":"Empty":g(e.category))),a("div",{className:"garage-inline-meters"},h("Health",o(e.health),"health"),h("Fuel",o(e.fuel),"fuel")));var n,c}(e,n)):a("div",{className:"garage-empty-state"},a("h3",{className:"garage-empty-title"},"No matching vehicles"),a("p",{className:"garage-empty-copy"},"Adjust the current search or category filter to view more records."))))}function b(e){const t=(Array.isArray(e)?e:[]).slice().sort((e,a)=>Number(a.value||0)-Number(e.value||0)).slice(0,6).filter(e=>Number(e.value||0)>0);return 0===t.length?a("div",{className:"garage-empty-inline"},"No subsystem damage reported."):a("div",{className:"garage-hitpoint-grid"},t.map(e=>{return a("div",{className:"garage-hitpoint-row"},a("div",{className:"garage-hitpoint-copy"},a("span",{className:"garage-hitpoint-name"},(t=e.name,String(t||"").replace(/^Hit/i,"").replace(/([a-z])([A-Z])/g,"$1 $2").replace(/_/g," ").trim()||"Subsystem")),e.selection?a("span",{className:"garage-hitpoint-selection"},e.selection):null),a("span",{className:"garage-hitpoint-value"},`${Math.round(100*Number(e.value||0))}%`));var t}))}e.components=e.components||{},e.components.App=function(){const e={categoryFilter:r.getCategoryFilter(),notice:r.getNotice(),pendingAction:r.getPendingAction(),searchQuery:r.getSearchQuery(),selectedId:r.getSelectedId(),selectedKind:r.getSelectedKind()},v=function(e){return"stored"===e.selectedKind?(n.vehicles||[]).find(a=>String(a.plate||"")===e.selectedId)||null:"nearby"===e.selectedKind&&(c.vehicles||[]).find(a=>String(a.netId||"")===e.selectedId)||null}(e),N=m(n.vehicles||[],e),f=m(c.vehicles||[],e),S=e.searchQuery?`Search: ${e.searchQuery}`:"Live";return a("div",{className:"garage-shell"},t({kicker:"FORGE Logistics",title:"Vehicle Garage",onClose:()=>s.closeGarage(),closeLabel:"Close garage interface"}),e.notice.text?a("div",{className:"garage-toast-stack"},a("div",{className:"error"===e.notice.type?"garage-toast is-error":"garage-toast is-success"},e.notice.text)):null,a("div",{className:"garage-layout"},a("aside",{className:"garage-sidebar"},a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Search"),a("h2",{className:"garage-section-title"},"Vehicle Records")),a("span",{className:"garage-pill"},S)),a("div",{className:"garage-search-form"},a("input",{id:"garage-search-input",type:"text",className:"garage-search-input",placeholder:"Search by name, plate, or category",value:e.searchQuery}),a("div",{className:"garage-search-actions"},a("button",{type:"button",className:"garage-btn garage-btn-primary",onClick:()=>s.applySearchQuery(document.getElementById("garage-search-input")?.value||"")},"Apply Search"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",onClick:()=>s.clearSearch()},"Clear")))),a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Filter"),a("h2",{className:"garage-section-title"},"Vehicle Categories"))),a("div",{className:"garage-category-grid"},i.map(t=>a("button",{type:"button",className:e.categoryFilter===t.id?"garage-chip is-active":"garage-chip",onClick:()=>s.selectCategory(t.id)},t.label)))),a("section",{className:"garage-module"},a("div",{className:"garage-module-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Status"),a("h2",{className:"garage-section-title"},"Garage Summary")),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:Boolean(e.pendingAction),onClick:()=>s.refreshGarage()},"Refresh")),a("div",{className:"garage-summary-grid"},p("Stored",`${l.capacityUsed}/${l.capacityMax}`),p("Nearby",l.nearbyCount,"accent"),p("Spawn Lane",l.spawnStatus,l.spawnBlocked?"danger":"")))),a("main",{className:"garage-main"},a("section",{className:"garage-panel"},a("div",{className:"garage-panel-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Operations Bay"),a("h1",{className:"garage-title"},l.garageName||"Vehicle Garage")),a("span",{className:"garage-pill"},`${l.capacityUsed}/${l.capacityMax} Stored`)),a("div",{className:"garage-panel-intro"},a("p",{className:"garage-copy"},"Retrieve stored vehicles into the active spawn lane or store nearby empty vehicles back into persistent ownership records.")),a("div",{className:"garage-dashboard"},y("Stored Vehicles","Persistent Records","garage-stored-list",N,v),y("Nearby Vehicles","Store Window","garage-nearby-list",f,v),function(e,t){if(!e)return a("section",{className:"garage-card garage-detail-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},"Selection"),a("h2",{className:"garage-section-title"},"Vehicle Detail"))),a("div",{className:"garage-card-body garage-detail-empty"},a("h3",{className:"garage-empty-title"},"Select a vehicle"),a("p",{className:"garage-empty-copy"},"Choose a stored record to retrieve or a nearby vehicle to store.")));const r="stored"===e.entryKind,i=String(t.pendingAction||""),n="retrieve"===i||"store"===i,c=r&&!l.spawnBlocked&&!n,m=!r&&!1!==e.isEmpty&&!n;return a("section",{className:"garage-card garage-detail-card"},a("div",{className:"garage-card-header"},a("div",null,a("span",{className:"garage-eyebrow"},r?"Stored Record":"Nearby Vehicle"),a("h2",{className:"garage-section-title"},e.displayName||e.classname||"Vehicle")),a("span",{className:"nearby"===e.entryKind&&!1===e.isEmpty?"garage-badge is-warning":"garage-badge"},r?`Plate ${u(e.plate)}`:!1===e.isEmpty?"Crewed":"Ready")),a("div",{className:"garage-card-body garage-detail-body"},a("div",{className:"garage-detail-grid"},a("div",{className:"garage-detail-copy"},a("div",{className:"garage-detail-meta"},p("Category",g(e.category)),p("Status",(y=e)?"stored"===y.entryKind?"Stored":!1===y.isEmpty?"Crewed":"Ready":"-","nearby"===e.entryKind&&!1===e.isEmpty?"danger":""),p(r?"Record":"Distance",r?u(e.plate):d(e.distance),r?"":"accent")),a("div",{className:"garage-meter-stack"},h("Health",o(e.health),"health"),h("Fuel",o(e.fuel),"fuel")),a("div",{className:"garage-action-row"},r?a("button",{type:"button",className:"garage-btn garage-btn-primary",disabled:!c,onClick:()=>s.requestRetrieveSelected()},"retrieve"===i?"Retrieving...":"Retrieve Vehicle"):a("button",{type:"button",className:"garage-btn garage-btn-primary",disabled:!m,onClick:()=>s.requestStoreSelected()},"store"===i?"Storing...":"Store Vehicle"),a("button",{type:"button",className:"garage-btn garage-btn-secondary",disabled:n,onClick:()=>s.refreshGarage()},"Refresh")),a("p",{className:"garage-detail-note"},r?l.spawnBlocked?"The garage spawn lane is currently blocked.":"Retrieve this stored vehicle into the active spawn lane.":!1===e.isEmpty?"Only empty nearby vehicles can be stored.":"Store this nearby vehicle back into persistent garage storage.")),a("div",{className:"garage-detail-subsystems"},a("div",{className:"garage-subsystem-header"},a("span",{className:"garage-eyebrow"},"Subsystems"),a("span",{className:"garage-detail-caption"},"Highest damage first")),b(e.hitPoints)))));var y}(v,e))))),a("footer",{className:"garage-footer-bar"},a("div",{className:"garage-footer"},a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Storage Capacity"),a("span",{className:"garage-footer-copy"},`${l.capacityUsed} of ${l.capacityMax} vehicle slot(s) are currently occupied.`)),a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Retrieval Window"),a("span",{className:"garage-footer-copy"},l.spawnBlocked?"Spawn lane is blocked. Clear the bay before retrieving another vehicle.":"Spawn lane is clear. Stored vehicles can be retrieved immediately.")),a("div",{className:"garage-footer-block"},a("span",{className:"garage-footer-title"},"Store Rules"),a("span",{className:"garage-footer-copy"},"Only nearby empty vehicles can be stored. Nearby count updates from the live world state.")))))}}(),function(){const e=window.ForgeWebUI,a=window.GarageApp;e.createApp({name:"garage",root:"#app",setup({root:t}){e.mount(t,()=>a.components.App(),{preserveScroll:!0}),a.bridge&&a.bridge.notifyReady()}}).start()}(); \ No newline at end of file diff --git a/arma/client/addons/garage/ui/_site/index.html b/arma/client/addons/garage/ui/_site/index.html index 187d47a..02cdff9 100644 --- a/arma/client/addons/garage/ui/_site/index.html +++ b/arma/client/addons/garage/ui/_site/index.html @@ -1,64 +1 @@ - - - - - - - FORGE Vehicle Garage - - - - -
- - +FORGE Vehicle Garage
\ No newline at end of file diff --git a/arma/client/addons/org/ui/_site/index.html b/arma/client/addons/org/ui/_site/index.html index 5aaf38e..05d3f52 100644 --- a/arma/client/addons/org/ui/_site/index.html +++ b/arma/client/addons/org/ui/_site/index.html @@ -1,64 +1 @@ - - - - - - - ORBIS - Global Organization Network - - - - -
- - +ORBIS - Global Organization Network
\ No newline at end of file diff --git a/arma/client/addons/org/ui/_site/org-ui.css b/arma/client/addons/org/ui/_site/org-ui.css index d74b453..2972aa6 100644 --- a/arma/client/addons/org/ui/_site/org-ui.css +++ b/arma/client/addons/org/ui/_site/org-ui.css @@ -1,281 +1 @@ -/* Generated by tools/build-webui.mjs for Org UI styles. Do not edit directly. */ -: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; -} - -html, -body { - height: 100%; -} - -*, -*::before, -*::after { - box-sizing: border-box; -} - -body { - font-family: - "Inter", - system-ui, - -apple-system, - sans-serif; - margin: 0; - padding: 0; - background: var(--bg-app); - color: var(--text-main); - line-height: 1.6; - overflow: hidden; -} - -#app { - height: 100vh; - overflow: hidden; -} - -.app-shell { - height: 100vh; - display: flex; - flex-direction: column; - overflow: hidden; -} - -#org-portal-frame-root { - display: flex; - flex: 1 1 auto; - flex-direction: column; - min-height: 0; - overflow: hidden; -} - -main { - display: flex; - flex-direction: column; - flex: 1 1 auto; - min-height: 0; - overflow: auto; - overscroll-behavior: contain; -} - -.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; - } - } - } -} - -.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; -} - -.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) { - .container { - padding: 1.5rem; - } - - .header { - margin-bottom: 2rem; - padding-bottom: 1.5rem; - - h1 { - font-size: 2rem; - } - } - - .footer .wrapper { - grid-template-columns: 1fr; - } - - .org-page-heading { - gap: 0.3rem; - } -} +:root{--bg-app:#fdfcf8;--bg-surface:#fff;--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 #0000001a, 0 1px 2px -1px #0000001a;--footer-bg:#1e293b}html,body{height:100%}*,:before,:after{box-sizing:border-box}body{background:var(--bg-app);color:var(--text-main);margin:0;padding:0;font-family:Inter,system-ui,-apple-system,sans-serif;line-height:1.6;overflow:hidden}#app{height:100vh;overflow:hidden}.app-shell{flex-direction:column;height:100vh;display:flex;overflow:hidden}#org-portal-frame-root{flex-direction:column;flex:auto;min-height:0;display:flex;overflow:hidden}main{overscroll-behavior:contain;flex-direction:column;flex:auto;min-height:0;display:flex;overflow:auto}.container{box-sizing:border-box;flex-direction:column;flex:1;width:100%;max-width:1200px;margin:0 auto;padding:2rem;display:flex}.header{text-align:center;border-bottom:1px solid var(--border);margin-bottom:3rem;padding-bottom:2rem}.header h1{letter-spacing:-.025em;color:var(--primary-hover);margin-bottom:.5rem;font-size:2.5rem;font-weight:700}.header p{color:var(--text-muted);font-size:1.1rem}.card{background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);text-align:center;padding:2rem}.card h2{color:var(--primary-hover);margin-top:0;font-size:1.8rem}button{background:var(--primary);color:#fff;border-radius:var(--radius);cursor:pointer;border:none;padding:.75rem 1.5rem;font-size:1rem;font-weight:500;transition:all .2s}button:hover{background:var(--primary-hover);transform:translateY(-1px);box-shadow:0 4px 6px -1px #0000001a}button:disabled{cursor:not-allowed;opacity:.65;box-shadow:none;transform:none}button+button{margin-left:1rem}.footer{background:var(--footer-bg);color:var(--text-inverse);margin-top:auto;display:block}.footer .wrapper{box-sizing:border-box;grid-template-columns:1fr 1fr;gap:4rem;width:100%;max-width:1200px;margin:0 auto;padding:3rem 2rem;display:grid}.footer h3{color:var(--text-inverse);text-transform:uppercase;letter-spacing:.1em;border-bottom:1px solid #475569;margin-bottom:1.5rem;margin-right:1rem;padding-bottom:.5rem;font-size:.85rem;font-weight:700}.footer ul li{color:#cbd5e1;cursor:pointer;margin-bottom:.75rem;font-size:.95rem;transition:color .2s}.footer ul li:hover{color:#fff}.org-secondary-btn{background:var(--bg-surface);color:var(--text-main);border:1px solid var(--border)}.org-secondary-btn:hover{background:var(--bg-surface-hover);color:var(--text-main)}.org-danger-btn{color:#fef2f2;background:#7f1d1d}.org-danger-btn:hover{background:#991b1b}.org-icon-btn{justify-content:center;align-items:center;width:2.5rem;height:2.5rem;padding:0;display:inline-flex}.org-icon{width:1rem;height:1rem}.org-page-header{text-align:left;margin-bottom:0}.org-page-heading{flex-direction:column;gap:.35rem;display:flex}.org-page-kicker{text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);font-size:.7rem;font-weight:600}.org-page-title{margin:0}.org-page-subtitle{color:var(--text-muted);margin:0;font-size:.9rem}.org-page-meta{color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;font-size:.75rem}@media (width<=960px){.container{padding:1.5rem}.header{margin-bottom:2rem;padding-bottom:1.5rem}.header h1{font-size:2rem}.footer .wrapper{grid-template-columns:1fr}.org-page-heading{gap:.3rem}} \ No newline at end of file diff --git a/arma/client/addons/org/ui/_site/org-ui.js b/arma/client/addons/org/ui/_site/org-ui.js index ee1321b..375a7ff 100644 --- a/arma/client/addons/org/ui/_site/org-ui.js +++ b/arma/client/addons/org/ui/_site/org-ui.js @@ -1,4082 +1 @@ -/* Generated by tools/build-webui.mjs for Org UI app. Do not edit directly. */ -(function () { - const runtime = window.ForgeWebUI; - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - - RegistryApp.runtime = runtime; - OrgPortal.runtime = runtime; - window.AppRuntime = runtime; -})(); - -(function () { - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { createSignal } = RegistryApp.runtime; - - class RegistryStore { - constructor() { - [this.getView, this.setView] = createSignal("home"); - [this.getIsAuthenticating, this.setIsAuthenticating] = - createSignal(false); - [this.getLoginError, this.setLoginError] = createSignal(""); - [this.getIsCreating, this.setIsCreating] = createSignal(false); - [this.getCreateError, this.setCreateError] = createSignal(""); - } - - startLogin() { - this.setLoginError(""); - this.setIsAuthenticating(true); - } - - startCreate() { - this.setCreateError(""); - this.setIsCreating(true); - } - - failLogin(message) { - this.setIsAuthenticating(false); - this.setLoginError(message || "Authentication failed."); - } - - failCreate(message) { - this.setIsCreating(false); - this.setCreateError(message || "Organization registration failed."); - } - - hydratePortal(payload) { - const portalApi = - window.OrgPortal && window.OrgPortal.data - ? window.OrgPortal.data - : null; - const portalStore = - window.OrgPortal && window.OrgPortal.store - ? window.OrgPortal.store - : null; - const portalData = - payload && payload.portalData ? payload.portalData : null; - const sessionData = - payload && payload.session ? payload.session : null; - - if ( - !portalApi || - typeof portalApi.applyLoginPayload !== "function" || - !portalStore || - typeof portalStore.hydrateFromPayload !== "function" || - !portalData || - !sessionData - ) { - return false; - } - - portalApi.applyLoginPayload(payload); - portalStore.hydrateFromPayload(payload); - return true; - } - - completeLogin(payload) { - if (!this.hydratePortal(payload)) { - this.failLogin("Login response was missing portal data."); - return; - } - - this.setLoginError(""); - this.setIsAuthenticating(false); - this.setView("portal"); - } - - completeCreate(payload) { - if (!this.hydratePortal(payload)) { - this.failCreate( - "Organization registration response was missing portal data.", - ); - return; - } - - this.setCreateError(""); - this.setIsCreating(false); - this.setView("portal"); - } - } - - RegistryApp.store = new RegistryStore(); -})(); - -(function () { - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const store = RegistryApp.store; - const bridge = window.ForgeWebUI.createBridge({ - closeEvent: "org::close", - globalName: "ForgeBridge", - readyEvent: "org::ready", - }); - - function sendEvent(event, data) { - return bridge.send(event, data); - } - - function requestLogin(credentials) { - store.startLogin(); - - const sent = sendEvent("org::login::request", credentials); - if (sent) { - return; - } - - store.failLogin("Arma login bridge is unavailable."); - } - - function requestCreateOrg(registration) { - store.startCreate(); - - const sent = sendEvent("org::create::request", registration); - if (sent) { - return; - } - - store.failCreate("Arma registration bridge is unavailable."); - } - - function requestDisbandOrg() { - const sent = sendEvent("org::disband::request", {}); - if (sent) { - return; - } - - const OrgPortal = window.OrgPortal; - if (OrgPortal && OrgPortal.actions) { - OrgPortal.actions.showTreasuryNotice( - "error", - "Arma disband bridge is unavailable.", - ); - } - } - - function requestLeaveOrg() { - const sent = sendEvent("org::leave::request", {}); - if (sent) { - return; - } - - const OrgPortal = window.OrgPortal; - if (OrgPortal && OrgPortal.actions) { - OrgPortal.actions.showTreasuryNotice( - "error", - "Arma leave bridge is unavailable.", - ); - } - } - - function requestCreditLine(payload) { - const sent = sendEvent("org::credit::request", payload); - if (sent) { - return true; - } - - const OrgPortal = window.OrgPortal; - if (OrgPortal && OrgPortal.actions) { - OrgPortal.actions.showTreasuryNotice( - "error", - "Arma credit line bridge is unavailable.", - ); - } - - return false; - } - - bridge.on("org::login::success", (payloadData) => { - store.completeLogin(payloadData); - }); - - bridge.on("org::login::failure", (payloadData) => { - store.failLogin(payloadData.message || "Authentication failed."); - }); - - bridge.on("org::create::success", (payloadData) => { - store.completeCreate(payloadData); - }); - - bridge.on("org::create::failure", (payloadData) => { - store.failCreate( - payloadData.message || "Organization registration failed.", - ); - }); - - bridge.on("org::sync", (payloadData) => { - if (store && typeof store.hydratePortal === "function") { - store.hydratePortal(payloadData); - } - }); - - bridge.on("org::credit::success", (payloadData) => { - const OrgPortal = window.OrgPortal; - if (OrgPortal && OrgPortal.store) { - OrgPortal.store.setModal(null); - } - - if (OrgPortal && OrgPortal.actions) { - OrgPortal.actions.showTreasuryNotice( - "success", - payloadData.message || "Credit line assigned.", - ); - } - }); - - bridge.on("org::credit::failure", (payloadData) => { - const OrgPortal = window.OrgPortal; - if (OrgPortal && OrgPortal.actions) { - OrgPortal.actions.showTreasuryNotice( - "error", - payloadData.message || "Unable to assign credit line.", - ); - } - }); - - bridge.on("org::member::creditUpdated", (payloadData) => { - const OrgPortal = window.OrgPortal; - if (!OrgPortal || !OrgPortal.store) { - return; - } - - OrgPortal.store.setCreditLines((currentLines) => { - const nextLine = { - amount: payloadData.amount || 0, - member: payloadData.memberName || "", - uid: payloadData.memberUid || "", - }; - const matchIndex = currentLines.findIndex( - (line) => line.uid === nextLine.uid, - ); - - if (matchIndex === -1) { - return [...currentLines, nextLine]; - } - - return currentLines.map((line, index) => - index === matchIndex ? nextLine : line, - ); - }); - }); - - bridge.on("org::disband::success", () => { - const OrgPortal = window.OrgPortal; - if (OrgPortal && OrgPortal.store) { - OrgPortal.store.setModal(null); - OrgPortal.store.setOrgDisbanded(true); - } - }); - - bridge.on("org::disband::failure", (payloadData) => { - const OrgPortal = window.OrgPortal; - if (OrgPortal && OrgPortal.store) { - OrgPortal.store.setModal(null); - } - - if (OrgPortal && OrgPortal.actions) { - OrgPortal.actions.showTreasuryNotice( - "error", - payloadData.message || "Organization disbanding failed.", - ); - } - }); - - bridge.on("org::leave::success", (payloadData) => { - const OrgPortal = window.OrgPortal; - if (OrgPortal && OrgPortal.store) { - OrgPortal.store.setModal(null); - } - - store.failLogin( - payloadData.message || "You have left the organization.", - ); - store.setView("home"); - }); - - bridge.on("org::leave::failure", (payloadData) => { - const OrgPortal = window.OrgPortal; - if (OrgPortal && OrgPortal.store) { - OrgPortal.store.setModal(null); - } - - if (OrgPortal && OrgPortal.actions) { - OrgPortal.actions.showTreasuryNotice( - "error", - payloadData.message || "Unable to leave the organization.", - ); - } - }); - - bridge.on("org::portal::revoked", (payloadData) => { - const OrgPortal = window.OrgPortal; - if (OrgPortal && OrgPortal.store) { - OrgPortal.store.setModal(null); - } - - store.failLogin( - payloadData.message || - "Organization access is no longer available.", - ); - store.setView("home"); - }); - - RegistryApp.bridge = { - close: bridge.close, - ready: bridge.ready, - receive: bridge.receive, - requestLogin, - requestCreateOrg, - requestDisbandOrg, - requestLeaveOrg, - requestCreditLine, - sendEvent, - }; -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const staticOrgProfile = { - type: "Organization", - status: "Operational", - headquarters: "ArmA Verse", - }; - - function cloneValue(value) { - return JSON.parse(JSON.stringify(value)); - } - - function replaceObject(target, source) { - Object.keys(target).forEach((key) => delete target[key]); - Object.assign(target, cloneValue(source)); - } - - function replaceArray(target, source) { - target.splice(0, target.length, ...cloneValue(source)); - } - - OrgPortal.data = { - portalData: { - org: Object.assign( - { - name: "", - tag: "", - owner: "", - ownerUid: "", - isDefault: false, - }, - staticOrgProfile, - ), - funds: 0, - reputation: 0, - creditLines: [], - members: [], - fleet: [], - assets: [], - activity: [], - 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: "", - actorUid: "", - role: "", - ceo: false, - }, - applyLoginPayload(payload) { - replaceObject( - this.portalData.org, - Object.assign( - {}, - payload.portalData.org || {}, - staticOrgProfile, - ), - ); - this.portalData.funds = payload.portalData.funds || 0; - this.portalData.reputation = payload.portalData.reputation || 0; - replaceArray( - this.portalData.creditLines, - payload.portalData.creditLines || [], - ); - - replaceArray( - this.portalData.members, - payload.portalData.members || [], - ); - replaceArray(this.portalData.fleet, payload.portalData.fleet || []); - replaceArray( - this.portalData.assets, - payload.portalData.assets || [], - ); - replaceArray( - this.portalData.activity, - payload.portalData.activity || [], - ); - replaceArray( - this.portalData.roadmap, - payload.portalData.roadmap || [], - ); - - replaceObject(this.session, payload.session || {}); - }, - }; -})(); - -(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.getReputation, this.setReputation] = createSignal( - portalData.reputation, - ); - [this.getMembers, this.setMembers] = createSignal([ - ...portalData.members, - ]); - [this.getCreditLines, this.setCreditLines] = createSignal([ - ...portalData.creditLines, - ]); - [this.getFleet, this.setFleet] = createSignal([ - ...portalData.fleet, - ]); - [this.getAssets, this.setAssets] = createSignal([ - ...portalData.assets, - ]); - [this.getActivity, this.setActivity] = createSignal([ - ...portalData.activity, - ]); - [this.getTreasuryNotice, this.setTreasuryNotice] = createSignal({ - type: "", - text: "", - }); - [this.getModal, this.setModal] = createSignal(null); - [this.getOrgDisbanded, this.setOrgDisbanded] = createSignal(false); - } - - hydrateFromPayload(payload) { - const nextPortalData = payload.portalData || {}; - - this.setFunds(nextPortalData.funds || 0); - this.setReputation(nextPortalData.reputation || 0); - this.setMembers([...(nextPortalData.members || [])]); - this.setCreditLines([...(nextPortalData.creditLines || [])]); - this.setFleet([...(nextPortalData.fleet || [])]); - this.setAssets([...(nextPortalData.assets || [])]); - this.setActivity([...(nextPortalData.activity || [])]); - } - } - - OrgPortal.store = new OrgPortalStore(); -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { portalData, session } = OrgPortal.data; - - class OrgPortalGetters { - formatCurrency(value) { - return "$" + Number(value || 0).toLocaleString(); - } - - formatVehicleType(type) { - if (!type) { - return ""; - } - - return type.charAt(0).toUpperCase() + type.slice(1); - } - - formatAssetType(type) { - if (!type) { - return ""; - } - - return type.charAt(0).toUpperCase() + type.slice(1); - } - - formatDisplayName(value) { - if (!value) { - return ""; - } - - return String(value) - .trim() - .split(/\s+/) - .map((part) => { - if (!part) { - return ""; - } - - return ( - part.charAt(0).toUpperCase() + - part.slice(1).toLowerCase() - ); - }) - .join(" "); - } - - getAssetReadiness() { - const fleet = OrgPortal.store - ? OrgPortal.store.getFleet() - : portalData.fleet; - if (fleet.length === 0) { - return null; - } - - const total = fleet.reduce( - (sum, unit) => sum + (100 - parseInt(unit.damage, 10)), - 0, - ); - return Math.round(total / fleet.length); - } - - getNormalizedRole() { - return String(session.role || "") - .trim() - .toUpperCase(); - } - - isDefaultOrg() { - return ( - portalData.org.isDefault === true || - String(portalData.org.tag || "") - .trim() - .toUpperCase() === "DEFAULT" - ); - } - - isOrgOwner() { - const ownerUid = String( - portalData.org.ownerUid || portalData.org.owner || "", - ) - .trim() - .toLowerCase(); - const actorUid = String(session.actorUid || "") - .trim() - .toLowerCase(); - - if (ownerUid && actorUid) { - return actorUid === ownerUid; - } - - return ( - String(session.actorName || "") - .trim() - .toLowerCase() === - String(portalData.org.owner || "") - .trim() - .toLowerCase() - ); - } - - isSessionCeo() { - return session.ceo === true; - } - - isOrgLeaderOrCeo() { - return ( - this.isOrgOwner() || - this.getNormalizedRole() === "LEADER" || - (this.isDefaultOrg() && this.isSessionCeo()) - ); - } - - canManageMembers() { - return this.isOrgLeaderOrCeo(); - } - - canManageTreasury() { - return this.isOrgLeaderOrCeo(); - } - - canDisbandOrg() { - return this.isOrgOwner() && !this.isDefaultOrg(); - } - - canLeaveOrg() { - return !this.isDefaultOrg() && !this.isOrgOwner(); - } - - getMemberName(member) { - if (member && typeof member === "object") { - return String(member.name || ""); - } - - return String(member || ""); - } - - getMemberUid(member) { - if (member && typeof member === "object") { - return String(member.uid || ""); - } - - return ""; - } - - isOwnerMember(member) { - return ( - this.getMemberName(member).trim().toLowerCase() === - String(portalData.org.owner || "") - .trim() - .toLowerCase() - ); - } - - isCurrentMember(member) { - const memberUid = this.getMemberUid(member).trim().toLowerCase(); - const actorUid = String(session.actorUid || "") - .trim() - .toLowerCase(); - - if (memberUid && actorUid) { - return memberUid === actorUid; - } - - return ( - this.getMemberName(member).trim().toLowerCase() === - String(session.actorName || "") - .trim() - .toLowerCase() - ); - } - - isProtectedMember(member) { - return this.isOwnerMember(member) || this.isCurrentMember(member); - } - } - - OrgPortal.getters = new OrgPortalGetters(); -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { portalData } = OrgPortal.data; - const store = OrgPortal.store; - const getters = OrgPortal.getters; - const registryStore = window.RegistryApp.store; - - class OrgPortalActions { - constructor() { - this.treasuryNoticeTimer = null; - } - - 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() { - const bridge = window.RegistryApp - ? window.RegistryApp.bridge - : null; - - if (bridge && typeof bridge.close === "function") { - bridge.close({}); - return; - } - - if (registryStore) { - registryStore.setView("home"); - } - } - - openModal(type) { - if ( - (type === "payroll" || - type === "transfer" || - type === "credit") && - !getters.canManageTreasury() - ) { - this.showTreasuryNotice( - "error", - "Only the organization leader or CEO can manage treasury actions.", - ); - return; - } - - if (type === "disband" && !getters.canDisbandOrg()) { - return; - } - - if (type === "leave" && !getters.canLeaveOrg()) { - return; - } - - store.setModal({ type }); - } - - closeModal() { - store.setModal(null); - } - - removeMember(member) { - if (!getters.canManageMembers()) { - return false; - } - - if (getters.isProtectedMember(member)) { - return false; - } - - const memberUid = getters.getMemberUid(member); - const memberName = getters.getMemberName(member); - - store.setMembers((currentMembers) => - currentMembers.filter((entry) => - memberUid - ? entry.uid !== memberUid - : entry.name !== memberName, - ), - ); - store.setCreditLines((currentLines) => - currentLines.filter((line) => - memberUid - ? line.uid !== memberUid - : line.member !== memberName, - ), - ); - return true; - } - - disbandOrganization() { - if (!getters.canDisbandOrg()) { - return false; - } - - const bridge = window.RegistryApp - ? window.RegistryApp.bridge - : null; - - if (!bridge || typeof bridge.requestDisbandOrg !== "function") { - this.showTreasuryNotice( - "error", - "Disband bridge is unavailable.", - ); - return false; - } - - this.closeModal(); - bridge.requestDisbandOrg(); - return true; - } - - leaveOrganization() { - if (!getters.canLeaveOrg()) { - return false; - } - - const bridge = window.RegistryApp - ? window.RegistryApp.bridge - : null; - - if (!bridge || typeof bridge.requestLeaveOrg !== "function") { - this.showTreasuryNotice( - "error", - "Leave bridge is unavailable.", - ); - return false; - } - - this.closeModal(); - bridge.requestLeaveOrg(); - return true; - } - - runPayroll(amountPerMember) { - if (!getters.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 ${getters.formatCurrency(total)}.`, - ); - return true; - } - - sendFundsToMember(memberName, amount) { - if (!getters.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", - `${getters.formatCurrency(amount)} sent to ${memberName}.`, - ); - return true; - } - - grantCreditLine(memberUid, amount) { - if (!getters.canManageTreasury()) { - this.showTreasuryNotice( - "error", - "Only the organization leader or CEO can manage treasury actions.", - ); - return false; - } - - if (!memberUid) { - 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; - } - - const member = store - .getMembers() - .find((entry) => getters.getMemberUid(entry) === memberUid); - const memberName = member ? getters.getMemberName(member) : ""; - - if (!memberName) { - this.showTreasuryNotice( - "error", - "Selected member was not found in the organization roster.", - ); - return false; - } - - const bridge = window.RegistryApp - ? window.RegistryApp.bridge - : null; - - if (!bridge || typeof bridge.requestCreditLine !== "function") { - this.showTreasuryNotice( - "error", - "Credit line bridge is unavailable.", - ); - return false; - } - - return bridge.requestCreditLine({ - memberUid, - memberName, - amount, - }); - } - } - - OrgPortal.actions = new OrgPortalActions(); -})(); - -(function () { - const SharedUI = (window.SharedUI = window.SharedUI || {}); - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h, ensureScopedStyle } = RegistryApp.runtime; - const scopeAttr = "data-ui-navbar"; - const scopeSelector = `[${scopeAttr}]`; - const navbarCss = ` -${scopeSelector} { - background: var(--bg-surface); - border-bottom: 1px solid var(--border); - box-shadow: var(--shadow); -} - -${scopeSelector} .app-navbar-inner { - display: flex; - justify-content: space-between; - align-items: center; - max-width: 1200px; - width: 100%; - margin: 0 auto; - padding: 1rem 2rem; - box-sizing: border-box; -} - -${scopeSelector} .app-navbar-brand { - display: flex; - flex-direction: column; - gap: 0.125rem; -} - -${scopeSelector} .app-navbar-kicker { - font-size: 0.7rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); - font-weight: 600; -} - -${scopeSelector} .app-navbar-title { - font-size: 1.25rem; - font-weight: 700; - color: var(--primary-hover); - letter-spacing: -0.025em; -} - -${scopeSelector} .app-navbar-actions { - display: flex; - align-items: center; - gap: 1.5rem; -} - -${scopeSelector} .app-navbar-view { - font-size: 0.8rem; - text-transform: uppercase; - letter-spacing: 0.05em; - color: var(--text-muted); - font-weight: 600; -} - -${scopeSelector} .app-close-btn { - background: transparent; - color: var(--text-muted); - border: 1px solid var(--border); - padding: 0.5rem 1rem; - font-size: 0.85rem; -} - -${scopeSelector} .app-close-btn:hover { - background: var(--bg-surface-hover); - color: var(--primary-hover); - border-color: var(--primary); - transform: none; - box-shadow: none; -} - -@media (max-width: 960px) { - ${scopeSelector} .app-navbar-inner { - flex-direction: column; - align-items: flex-start; - padding: 1rem 1.5rem; - } - - ${scopeSelector} .app-navbar-actions { - align-items: flex-start; - } -} -`; - - SharedUI.componentFns = SharedUI.componentFns || {}; - - SharedUI.componentFns.Navbar = function Navbar({ - kicker = "ORBIS", - title = "", - viewLabel = "", - actionLabel = "", - onAction = null, - }) { - ensureScopedStyle("shared-navbar", navbarCss); - - return h( - "nav", - { className: "app-navbar", [scopeAttr]: "" }, - h( - "div", - { className: "app-navbar-inner" }, - h( - "div", - { className: "app-navbar-brand" }, - h("span", { className: "app-navbar-kicker" }, kicker), - h("span", { className: "app-navbar-title" }, title), - ), - h( - "div", - { className: "app-navbar-actions" }, - h("span", { className: "app-navbar-view" }, viewLabel), - actionLabel && typeof onAction === "function" - ? h( - "button", - { - type: "button", - className: "app-close-btn", - onClick: onAction, - }, - actionLabel, - ) - : null, - ), - ), - ); - }; -})(); - -(function () { - const SharedUI = (window.SharedUI = window.SharedUI || {}); - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h } = RegistryApp.runtime; - - SharedUI.componentFns = SharedUI.componentFns || {}; - - SharedUI.componentFns.Header = function Header({ - title, - subtitle = "Organization Registration & Management Portal", - onTitleClick = null, - }) { - return h( - "div", - { className: "header" }, - h( - "h1", - { - style: { cursor: onTitleClick ? "pointer" : "default" }, - onClick: onTitleClick, - }, - title, - ), - h("p", null, subtitle), - ); - }; -})(); - -(function () { - const SharedUI = (window.SharedUI = window.SharedUI || {}); - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - - SharedUI.componentFns = SharedUI.componentFns || {}; - - SharedUI.componentFns.Hero = function Hero({ - className = "", - kicker = "", - title = "", - subtitle = "", - meta = "", - }) { - const finalClassName = [ - "card org-panel org-span-12 org-page-header", - className, - ] - .filter(Boolean) - .join(" "); - - return h( - "section", - { className: finalClassName }, - h( - "div", - { className: "org-page-heading" }, - h("span", { className: "org-page-kicker" }, kicker), - h("h1", { className: "org-page-title" }, title), - h("p", { className: "org-page-subtitle" }, subtitle), - h("span", { className: "org-page-meta" }, meta), - ), - ); - }; -})(); - -(function () { - const SharedUI = (window.SharedUI = window.SharedUI || {}); - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h } = RegistryApp.runtime; - - SharedUI.componentFns = SharedUI.componentFns || {}; - - SharedUI.componentFns.Footer = function Footer({ sections = [] }) { - return h( - "div", - { className: "footer" }, - h( - "div", - { className: "wrapper" }, - ...sections.map((section) => - h( - "div", - null, - h("h3", null, section.title), - h( - "ul", - { style: { listStyleType: "none", padding: 0 } }, - ...(section.items || []).map((item) => - h("li", null, item), - ), - ), - ), - ), - ), - ); - }; -})(); - -(function () { - const SharedUI = (window.SharedUI = window.SharedUI || {}); - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h, ensureScopedStyle } = RegistryApp.runtime; - const scopeAttr = "data-ui-modal"; - const scopeSelector = `[${scopeAttr}]`; - const modalCss = ` -${scopeSelector} { - position: fixed; - inset: 0; - background: rgb(15 23 42 / 0.38); - display: flex; - align-items: center; - justify-content: center; - padding: 1.5rem; - z-index: 20; -} - -${scopeSelector} .app-modal-card { - width: min(100%, 30rem); - margin-bottom: 0; - text-align: left; -} - -${scopeSelector} .app-modal-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; - margin-bottom: 1rem; -} - -${scopeSelector} .app-modal-title { - margin: 0; - color: var(--primary-hover); - font-size: 1.45rem; -} - -${scopeSelector} .app-modal-close { - width: 2.25rem; - height: 2.25rem; - padding: 0; - background: var(--bg-surface); - color: var(--text-main); - border: 1px solid var(--border); - box-shadow: none; - transform: none; -} - -${scopeSelector} .app-modal-close:hover { - background: var(--bg-surface-hover); - color: var(--text-main); - box-shadow: none; - transform: none; -} - -${scopeSelector} .app-modal-form { - display: flex; - flex-direction: column; - gap: 1rem; -} - -${scopeSelector} .app-modal-form label { - display: block; - margin-bottom: 0.5rem; - color: var(--text-muted); - font-weight: 500; - font-size: 0.9rem; -} - -${scopeSelector} .app-modal-form input, -${scopeSelector} .app-modal-form select { - width: 100%; - padding: 0.75rem; - border-radius: var(--radius); - border: 1px solid var(--border); - background: var(--bg-app); - color: var(--text-main); - font-family: inherit; - font-size: 1rem; - box-sizing: border-box; - transition: border-color 0.2s, box-shadow 0.2s; -} - -${scopeSelector} .app-modal-form input:focus, -${scopeSelector} .app-modal-form select:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12); -} - -${scopeSelector} .app-modal-form input:disabled, -${scopeSelector} .app-modal-form select:disabled { - background: #f1f5f9; - color: var(--text-muted); - cursor: not-allowed; -} - -${scopeSelector} .app-modal-actions { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 0.75rem; - margin-top: 0.5rem; -} - -${scopeSelector} .app-modal-actions button + button, -${scopeSelector} .app-modal-danger-actions button + button { - margin-left: 0; -} - -${scopeSelector} .app-modal-danger { - display: flex; - flex-direction: column; - justify-content: space-between; - gap: 1rem; - padding: 1rem; - border: 1px solid #fecaca; - border-radius: var(--radius); - background: #fff1f2; - align-items: flex-start; -} - -${scopeSelector} .app-modal-danger p { - margin: 0; - color: var(--text-main); -} - -${scopeSelector} .app-modal-danger-actions { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; -} - -@media (max-width: 960px) { - ${scopeSelector} .app-modal-head, - ${scopeSelector} .app-modal-danger { - flex-direction: column; - align-items: flex-start; - } -} -`; - - SharedUI.componentFns = SharedUI.componentFns || {}; - - SharedUI.componentFns.Modal = function Modal({ - title = "", - body = null, - onClose = null, - }) { - ensureScopedStyle("shared-modal", modalCss); - - return h( - "div", - { - className: "app-modal-backdrop", - [scopeAttr]: "", - onClick: (e) => { - if (e.target === e.currentTarget && onClose) { - onClose(); - } - }, - }, - h( - "div", - { className: "card app-modal-card" }, - h( - "div", - { className: "app-modal-head" }, - h( - "div", - null, - h("h2", { className: "app-modal-title" }, title), - ), - h( - "button", - { - type: "button", - className: "app-modal-close", - onClick: onClose, - "aria-label": "Close dialog", - }, - "x", - ), - ), - body, - ), - ); - }; -})(); - -(function () { - const SharedUI = (window.SharedUI = window.SharedUI || {}); - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h, ensureScopedStyle } = RegistryApp.runtime; - const scopeAttr = "data-ui-panel-card"; - const scopeSelector = `[${scopeAttr}]`; - const panelCardCss = ` -${scopeSelector} { - display: flex; - flex-direction: column; - height: 100%; - min-height: 0; -} - -${scopeSelector} .org-panel-head { - display: flex; - align-items: flex-start; - justify-content: space-between; - gap: 1rem; - margin-bottom: 1.5rem; -} - -${scopeSelector} .org-panel-body { - display: flex; - flex: 1 1 auto; - flex-direction: column; - min-height: 0; -} - -${scopeSelector} .org-eyebrow { - font-size: 0.8rem; - font-weight: 700; - letter-spacing: 0.12em; - text-transform: uppercase; - color: var(--text-muted); - margin-bottom: 0.4rem; -} - -${scopeSelector} .org-panel-title { - margin: 0; - color: var(--primary-hover); - font-size: 1.45rem; -} - -${scopeSelector} .org-panel-subtitle { - margin: 0.35rem 0 0; - color: var(--text-muted); - font-size: 0.95rem; -} - -@media (max-width: 960px) { - ${scopeSelector} .org-panel-head { - flex-direction: column; - align-items: flex-start; - } -} -`; - - SharedUI.componentFns = SharedUI.componentFns || {}; - - SharedUI.componentFns.PanelCard = function PanelCard({ - className = "", - eyebrow = "", - title = "", - subtitle = "", - headerExtras = null, - body = null, - rootProps = {}, - }) { - const finalClassName = ["card org-panel", className] - .filter(Boolean) - .join(" "); - ensureScopedStyle("shared-panel-card", panelCardCss); - - return h( - "section", - { className: finalClassName, [scopeAttr]: "", ...rootProps }, - h( - "div", - { className: "org-panel-head" }, - h( - "div", - null, - eyebrow - ? h("div", { className: "org-eyebrow" }, eyebrow) - : null, - h("h2", { className: "org-panel-title" }, title), - subtitle - ? h("p", { className: "org-panel-subtitle" }, subtitle) - : null, - ), - headerExtras, - ), - h("div", { className: "org-panel-body" }, body), - ); - }; -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const scopeAttr = "data-ui-metric-card"; - const scopeSelector = `[${scopeAttr}]`; - const metricCardCss = ` -${scopeSelector} { - display: flex; - flex-direction: column; - gap: 0.45rem; - padding: 1rem; - border-radius: var(--radius); - border: 1px solid var(--border); - background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); -} - -${scopeSelector}:nth-child(4n + 2), -${scopeSelector}:nth-child(4n + 3) { - background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(226 232 240) 100%); - border-color: rgb(100 116 139 / 0.35); - box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6); -} - -${scopeSelector} .org-metric-label { - font-size: 0.76rem; - letter-spacing: 0.08em; - text-transform: uppercase; - color: var(--text-muted); -} - -${scopeSelector} .org-metric-value { - font-size: 1.8rem; - color: var(--primary-hover); - line-height: 1.1; -} - -${scopeSelector}:nth-child(4n + 2) .org-metric-value, -${scopeSelector}:nth-child(4n + 3) .org-metric-value { - color: #334155; -} - -${scopeSelector} .org-metric-note { - color: var(--text-muted); - font-size: 0.9rem; -} - -@media (max-width: 960px) { - ${scopeSelector}:nth-child(4n + 3) { - background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%); - border-color: var(--border); - box-shadow: none; - } - - ${scopeSelector}:nth-child(4n + 3) .org-metric-value { - color: var(--primary-hover); - } -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.MetricCard = function MetricCard( - label, - value, - note, - ) { - ensureScopedStyle("portal-metric-card", metricCardCss); - - return h( - "div", - { className: "org-metric-card", [scopeAttr]: "" }, - h("span", { className: "org-metric-label" }, label), - h("strong", { className: "org-metric-value" }, value), - h("span", { className: "org-metric-note" }, note), - ); - }; -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const scopeAttr = "data-ui-simple-stat"; - const scopeSelector = `[${scopeAttr}]`; - const simpleStatCss = ` -${scopeSelector} { - display: flex; - flex-direction: column; - gap: 0.2rem; - min-width: 90px; -} - -${scopeSelector} .org-simple-label { - font-size: 0.72rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); -} - -${scopeSelector} .org-simple-value { - font-size: 0.95rem; - color: var(--text-main); -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.SimpleStat = function SimpleStat(label, value) { - ensureScopedStyle("portal-simple-stat", simpleStatCss); - - return h( - "div", - { className: "org-simple-stat", [scopeAttr]: "" }, - h("span", { className: "org-simple-label" }, label), - h("strong", { className: "org-simple-value" }, value), - ); - }; -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const store = OrgPortal.store; - const getters = OrgPortal.getters; - const scopeAttr = "data-ui-overview-card"; - const scopeSelector = `[${scopeAttr}]`; - const overviewCardCss = ` -${scopeSelector} .org-hero-grid { - display: grid; - grid-template-columns: 1.3fr 1fr; - gap: 1.5rem; - align-items: start; -} - -${scopeSelector} .org-summary { - margin: 0; - font-size: 1.05rem; - color: var(--text-main); -} - -${scopeSelector} .org-meta-row { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 1rem; - margin-top: 1.5rem; -} - -${scopeSelector} .org-meta-item { - display: flex; - flex-direction: column; - gap: 0.4rem; - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-meta-item:nth-child(even) { - background: linear-gradient(180deg, rgb(241 245 249) 0%, rgb(226 232 240) 100%); - border-color: rgb(148 163 184 / 0.45); -} - -${scopeSelector} .org-meta-label { - font-size: 0.76rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); -} - -${scopeSelector} .org-meta-value { - font-size: 1rem; - font-weight: 600; - color: var(--primary-hover); -} - -${scopeSelector} .org-metric-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; -} - -@media (max-width: 960px) { - ${scopeSelector} .org-hero-grid, - ${scopeSelector} .org-meta-row, - ${scopeSelector} .org-metric-grid { - grid-template-columns: 1fr; - } -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.OverviewCard = function OverviewCard() { - const MetricCard = OrgPortal.componentFns.MetricCard; - const PanelCard = window.SharedUI.componentFns.PanelCard; - const readiness = getters.getAssetReadiness(); - const headquarters = portalData.org.headquarters || "ArmA Verse"; - const assetCount = store.getAssets().length; - const fleetCount = store.getFleet().length; - const funds = store.getFunds(); - const memberCount = store.getMembers().length; - const reputation = store.getReputation(); - ensureScopedStyle("portal-overview-card", overviewCardCss); - - return PanelCard({ - className: "org-span-12", - eyebrow: portalData.org.tag, - title: "Organization Overview", - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - { className: "org-hero-grid" }, - h( - "div", - { className: "org-hero-copy" }, - h( - "p", - { className: "org-summary" }, - portalData.org.type, - " operating from ", - 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" }, - getters.formatDisplayName(portalData.org.owner), - ), - ), - h( - "div", - { className: "org-meta-item" }, - h( - "span", - { className: "org-meta-label" }, - "Active Members", - ), - h( - "span", - { className: "org-meta-value" }, - `${memberCount} total`, - ), - ), - h( - "div", - { className: "org-meta-item" }, - h( - "span", - { className: "org-meta-label" }, - "Fleet Readiness", - ), - h( - "span", - { className: "org-meta-value" }, - readiness === null ? "N/A" : `${readiness}%`, - ), - ), - ), - ), - h( - "div", - { className: "org-metric-grid" }, - MetricCard( - "Org Funds", - getters.formatCurrency(funds), - "Organization treasury balance", - ), - MetricCard( - "Reputation", - reputation, - "Organization standing", - ), - MetricCard( - "Asset Lines", - assetCount, - "Tracked supply and equipment entries", - ), - MetricCard( - "Fleet Vehicles", - fleetCount, - "Tracked air, ground, and naval vehicles", - ), - ), - ), - }); - }; -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const getters = OrgPortal.getters; - const scopeAttr = "data-ui-fleet-card"; - const scopeSelector = `[${scopeAttr}]`; - const fleetCardCss = ` -${scopeSelector} .org-simple-list { - display: flex; - flex-direction: column; - flex: 1; - gap: 0.85rem; - min-height: 0; - overflow: auto; - padding-right: 0.35rem; - scrollbar-width: thin; - scrollbar-color: #94a3b8 #e2e8f0; -} - -${scopeSelector} .org-simple-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-simple-row:nth-child(even) { - background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); - border-color: rgb(148 163 184 / 0.45); -} - -${scopeSelector} .org-simple-name { - color: var(--primary-hover); -} - -${scopeSelector} .org-simple-meta { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 1rem; -} - -@media (max-width: 960px) { - ${scopeSelector} .org-simple-row { - flex-direction: column; - align-items: flex-start; - } -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.FleetCard = function FleetCard() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - const SimpleStat = OrgPortal.componentFns.SimpleStat; - const fleet = OrgPortal.store.getFleet(); - ensureScopedStyle("portal-fleet-card", fleetCardCss); - - return PanelCard({ - className: "org-scroll-panel org-span-7", - title: "Fleet", - subtitle: - "Individual vehicles with type, status, and overall damage.", - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - { className: "org-simple-list" }, - ...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", - getters.formatVehicleType(unit.type), - ), - SimpleStat("Status", unit.status), - SimpleStat("Damage", unit.damage), - ), - ), - ), - ), - }); - }; -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle, createSignal } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const store = OrgPortal.store; - const getters = OrgPortal.getters; - const actions = OrgPortal.actions; - const scopeAttr = "data-ui-treasury-card"; - const scopeSelector = `[${scopeAttr}]`; - const [getTreasuryTab, setTreasuryTab] = createSignal("overview"); - const [getTreasuryMenuOpen, setTreasuryMenuOpen] = createSignal(false); - const treasuryCardCss = ` -${scopeSelector} .org-treasury-menu { - position: relative; -} - -${scopeSelector} .org-menu-btn { - width: 2.75rem; - height: 2.75rem; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - border: 1px solid var(--border); - background: #f8fafc; - color: var(--text-muted); -} - -${scopeSelector} .org-menu-btn:hover { - color: var(--primary-hover); - border-color: rgb(148 163 184 / 0.65); -} - -${scopeSelector} .org-menu-btn svg { - width: 1.1rem; - height: 1.1rem; -} - -${scopeSelector} .org-menu-dropdown { - position: absolute; - top: calc(100% + 0.6rem); - right: 0; - min-width: 10.5rem; - padding: 0.45rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #fff; - box-shadow: 0 12px 28px rgb(15 23 42 / 0.12); - display: flex; - flex-direction: column; - gap: 0.35rem; - z-index: 5; -} - -${scopeSelector} .org-menu-option + .org-menu-option { - margin-left: 0; -} - -${scopeSelector} .org-menu-option { - width: 100%; - justify-content: flex-start; - background: transparent; - color: var(--text-main); - border: 1px solid transparent; -} - -${scopeSelector} .org-menu-option:hover { - background: #f8fafc; - border-color: rgb(148 163 184 / 0.35); -} - -${scopeSelector} .org-menu-option.is-active { - background: rgb(226 232 240 / 0.7); - color: var(--primary-hover); - border-color: rgb(148 163 184 / 0.35); -} - -${scopeSelector} .org-finance-meta { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; - margin-bottom: 1.5rem; -} - -${scopeSelector} .org-finance-meta > div { - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; - display: flex; - flex-direction: column; - gap: 0.4rem; -} - -${scopeSelector} .org-meta-label { - font-size: 0.76rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); -} - -${scopeSelector} .org-action-grid { - display: flex; - flex-direction: column; - gap: 0.75rem; - margin-bottom: 1rem; -} - -${scopeSelector} .org-action-grid button + button { - margin-left: 0; -} - -${scopeSelector} .org-action-grid button { - width: 100%; -} - -${scopeSelector} .org-access-note { - margin: 0 0 1rem; - color: var(--text-muted); - font-size: 0.95rem; -} - -${scopeSelector} .org-credit-summary { - display: flex; - flex-direction: column; - gap: 0.25rem; - padding: 0.85rem 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-credit-summary strong { - font-size: 1rem; -} - -${scopeSelector} .org-credit-summary span:last-child { - font-size: 0.92rem; - line-height: 1.45; -} - -${scopeSelector} .org-credit-lines-list { - display: flex; - flex-direction: column; - gap: 0.85rem; -} - -${scopeSelector} .org-treasury-body { - display: flex; - flex: 1; - flex-direction: column; - gap: 1rem; - min-height: 0; - overflow: auto; - padding-right: 0.35rem; - scrollbar-width: thin; - scrollbar-color: #94a3b8 #e2e8f0; -} - -${scopeSelector} .org-credit-line-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-credit-line-row:nth-child(even) { - background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); - border-color: rgb(148 163 184 / 0.45); -} - -${scopeSelector} .org-credit-line-member { - display: flex; - flex-direction: column; - gap: 0.3rem; -} - -${scopeSelector} .org-credit-line-label { - font-size: 0.76rem; - text-transform: uppercase; - letter-spacing: 0.08em; - color: var(--text-muted); -} - -${scopeSelector} .org-credit-line-empty { - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; - color: var(--text-muted); -} - -@media (max-width: 960px) { - ${scopeSelector} .org-finance-meta { - grid-template-columns: 1fr; - } - - ${scopeSelector} .org-credit-line-row { - flex-direction: column; - align-items: flex-start; - } -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.TreasuryCard = function TreasuryCard() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - const creditLines = store.getCreditLines(); - const reputation = store.getReputation(); - const allowTreasuryActions = getters.canManageTreasury(); - const activeTab = getTreasuryTab(); - const isMenuOpen = getTreasuryMenuOpen(); - const activeCreditLabel = - creditLines.length === 1 - ? "1 active credit line" - : `${creditLines.length} active credit lines`; - ensureScopedStyle("portal-treasury-card", treasuryCardCss); - - return PanelCard({ - className: "org-scroll-panel org-span-5", - title: "Treasury", - subtitle: "Organization funds, reputation and payouts.", - headerExtras: h( - "div", - { className: "org-treasury-menu" }, - h( - "button", - { - type: "button", - className: "org-menu-btn", - title: "Treasury views", - "aria-label": "Treasury views", - onClick: () => setTreasuryMenuOpen((open) => !open), - }, - h( - "svg", - { - viewBox: "0 0 24 24", - fill: "none", - stroke: "currentColor", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - "aria-hidden": "true", - }, - h("line", { x1: "4", y1: "7", x2: "20", y2: "7" }), - h("line", { x1: "4", y1: "12", x2: "20", y2: "12" }), - h("line", { x1: "4", y1: "17", x2: "20", y2: "17" }), - ), - ), - isMenuOpen - ? h( - "div", - { className: "org-menu-dropdown" }, - h( - "button", - { - type: "button", - className: - activeTab === "overview" - ? "org-menu-option is-active" - : "org-menu-option", - onClick: () => { - setTreasuryTab("overview"); - setTreasuryMenuOpen(false); - }, - }, - "Overview", - ), - h( - "button", - { - type: "button", - className: - activeTab === "credit" - ? "org-menu-option is-active" - : "org-menu-option", - onClick: () => { - setTreasuryTab("credit"); - setTreasuryMenuOpen(false); - }, - }, - "Credit Lines", - ), - ) - : null, - ), - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - { className: "org-treasury-body" }, - activeTab === "credit" - ? creditLines.length > 0 - ? h( - "div", - { className: "org-credit-lines-list" }, - ...creditLines.map((line) => - h( - "article", - { className: "org-credit-line-row" }, - h( - "div", - { - className: - "org-credit-line-member", - }, - h( - "span", - { - className: - "org-credit-line-label", - }, - "Member", - ), - h("strong", null, line.member), - ), - h( - "div", - { - className: - "org-credit-line-member", - }, - h( - "span", - { - className: - "org-credit-line-label", - }, - "Amount", - ), - h( - "strong", - null, - getters.formatCurrency( - line.amount, - ), - ), - ), - ), - ), - ) - : h( - "div", - { className: "org-credit-line-empty" }, - "No active credit lines.", - ) - : h( - "div", - null, - h( - "div", - { className: "org-finance-meta" }, - h( - "div", - null, - h( - "span", - { className: "org-meta-label" }, - "Funds", - ), - h( - "strong", - null, - getters.formatCurrency(store.getFunds()), - ), - ), - h( - "div", - null, - h( - "span", - { className: "org-meta-label" }, - "Reputation", - ), - h("strong", null, `${reputation}`), - ), - ), - allowTreasuryActions - ? h( - "div", - { className: "org-action-grid" }, - h( - "button", - { - type: "button", - onClick: () => - actions.openModal("payroll"), - }, - "Run Payroll", - ), - h( - "button", - { - type: "button", - className: "org-secondary-btn", - onClick: () => - actions.openModal("transfer"), - }, - "Send Funds", - ), - h( - "button", - { - type: "button", - className: "org-secondary-btn", - onClick: () => - actions.openModal("credit"), - }, - "Credit Line", - ), - ) - : h( - "p", - { className: "org-access-note" }, - "Only the organization leader or CEO can manage treasury actions.", - ), - h( - "div", - { className: "org-credit-summary" }, - h( - "span", - { className: "org-meta-label" }, - "Credit Line Status", - ), - h("strong", null, activeCreditLabel), - h( - "span", - null, - creditLines.length > 0 - ? "Open the Credit Lines tab to review assigned members and amounts." - : "Assign a credit line to create the first approved member limit.", - ), - ), - ), - ), - }); - }; -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const getters = OrgPortal.getters; - const scopeAttr = "data-ui-assets-card"; - const scopeSelector = `[${scopeAttr}]`; - const assetsCardCss = ` -${scopeSelector} .org-simple-list { - display: flex; - flex-direction: column; - flex: 1; - gap: 0.85rem; - min-height: 0; - overflow: auto; - padding-right: 0.35rem; - scrollbar-width: thin; - scrollbar-color: #94a3b8 #e2e8f0; -} - -${scopeSelector} .org-simple-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-simple-row:nth-child(even) { - background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); - border-color: rgb(148 163 184 / 0.45); -} - -${scopeSelector} .org-simple-name { - color: var(--primary-hover); -} - -${scopeSelector} .org-simple-meta { - display: flex; - flex-wrap: wrap; - justify-content: flex-end; - gap: 1rem; -} - -@media (max-width: 960px) { - ${scopeSelector} .org-simple-row { - flex-direction: column; - align-items: flex-start; - } -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.AssetsCard = function AssetsCard() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - const SimpleStat = OrgPortal.componentFns.SimpleStat; - const assets = OrgPortal.store.getAssets(); - ensureScopedStyle("portal-assets-card", assetsCardCss); - - return PanelCard({ - className: "org-scroll-panel org-span-7", - title: "Assets", - subtitle: "Inventory supplies and equipment with quantity totals.", - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - { className: "org-simple-list" }, - ...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", - getters.formatAssetType(asset.type), - ), - SimpleStat("Quantity", asset.quantity), - ), - ), - ), - ), - }); - }; -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const store = OrgPortal.store; - const getters = OrgPortal.getters; - const actions = OrgPortal.actions; - const scopeAttr = "data-ui-members-card"; - const scopeSelector = `[${scopeAttr}]`; - const membersCardCss = ` -${scopeSelector} .org-name-list { - display: flex; - flex-direction: column; - flex: 1; - gap: 0.85rem; - min-height: 0; - overflow: auto; - padding-right: 0.35rem; - scrollbar-width: thin; - scrollbar-color: #94a3b8 #e2e8f0; -} - -${scopeSelector} .org-name-row { - display: flex; - align-items: center; - justify-content: flex-start; - gap: 1rem; - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-name-row:nth-child(even) { - background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); - border-color: rgb(148 163 184 / 0.45); -} - -${scopeSelector} .org-name-row button { - margin-left: auto; -} - -@media (max-width: 960px) { - ${scopeSelector} .org-name-row { - flex-direction: column; - align-items: flex-start; - } - - ${scopeSelector} .org-name-row button { - margin-left: 0; - } -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.MembersCard = function MembersCard() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - const members = store.getMembers(); - const allowMemberManagement = getters.canManageMembers(); - ensureScopedStyle("portal-members-card", membersCardCss); - - return PanelCard({ - className: "org-scroll-panel org-span-5", - title: "Members", - subtitle: - "Current roster listing. The organization owner and your own member entry cannot be removed.", - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - { className: "org-name-list" }, - ...members.map((member) => { - const canRemoveMember = - allowMemberManagement && - !getters.isProtectedMember(member); - - return h( - "article", - { className: "org-name-row" }, - h("strong", null, member.name), - canRemoveMember - ? h( - "button", - { - type: "button", - className: "org-danger-btn org-icon-btn", - title: `Remove ${member.name}`, - "aria-label": `Remove ${member.name}`, - onClick: () => - actions.removeMember(member), - }, - 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, - ); - }), - ), - }); - }; -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const scopeAttr = "data-ui-activity-card"; - const scopeSelector = `[${scopeAttr}]`; - const activityCardCss = ` -${scopeSelector} .org-activity-list { - display: flex; - flex-direction: column; - flex: 1; - gap: 0.85rem; - min-height: 0; - overflow: auto; - padding-right: 0.35rem; - scrollbar-width: thin; - scrollbar-color: #94a3b8 #e2e8f0; -} - -${scopeSelector} .org-activity-row { - padding: 1rem; - border: 1px solid var(--border); - border-left: 3px solid #94a3b8; - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-activity-row:nth-child(even) { - background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); - border-color: rgb(148 163 184 / 0.45); - border-left-color: #64748b; -} - -${scopeSelector} .org-activity-row p { - margin: 0; - color: var(--text-main); -} - -${scopeSelector} .org-activity-time { - display: inline-block; - margin-bottom: 0.35rem; - color: var(--text-muted); - font-size: 0.8rem; - font-weight: 700; - letter-spacing: 0.05em; - text-transform: uppercase; -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.ActivityCard = function ActivityCard() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - const activity = OrgPortal.store.getActivity(); - ensureScopedStyle("portal-activity-card", activityCardCss); - - return PanelCard({ - className: "org-scroll-panel org-span-6", - title: "Command Feed", - subtitle: "Recent organization-level actions and updates.", - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - { className: "org-activity-list" }, - ...activity.map((item) => - h( - "article", - { className: "org-activity-row" }, - h( - "span", - { className: "org-activity-time" }, - item.time, - ), - h("p", null, item.text), - ), - ), - ), - }); - }; -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const scopeAttr = "data-ui-future-card"; - const 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.", - }, - ]; - const scopeSelector = `[${scopeAttr}]`; - const futureCardCss = ` -${scopeSelector} .org-roadmap-grid { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 1rem; - flex: 1; - min-height: 0; - overflow: auto; - padding-right: 0.35rem; - scrollbar-width: thin; - scrollbar-color: #94a3b8 #e2e8f0; -} - -${scopeSelector} .org-roadmap-card { - padding: 1rem; - display: flex; - flex-direction: column; - gap: 0.7rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: #f8fafc; -} - -${scopeSelector} .org-roadmap-card:nth-child(4n + 2), -${scopeSelector} .org-roadmap-card:nth-child(4n + 3) { - background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%); - border-color: rgb(100 116 139 / 0.4); -} - -${scopeSelector} .org-roadmap-card p { - margin: 0; - color: var(--text-main); -} - -${scopeSelector} .org-list-tag { - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0.2rem 0.55rem; - border-radius: 999px; - font-size: 0.72rem; - font-weight: 700; - letter-spacing: 0.06em; - text-transform: uppercase; - background: #e2e8f0; - color: var(--primary-hover); -} - -${scopeSelector} .org-roadmap-card:nth-child(4n + 2) .org-list-tag, -${scopeSelector} .org-roadmap-card:nth-child(4n + 3) .org-list-tag { - background: #cbd5e1; - color: #1e293b; -} - -@media (max-width: 960px) { - ${scopeSelector} .org-roadmap-grid { - grid-template-columns: 1fr; - } - - ${scopeSelector} .org-roadmap-card:nth-child(4n + 3) { - background: #f8fafc; - border-color: var(--border); - } - - ${scopeSelector} .org-roadmap-card:nth-child(4n + 3) .org-list-tag { - background: #e2e8f0; - color: var(--primary-hover); - } -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.FutureCard = function FutureCard() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - ensureScopedStyle("portal-future-card", futureCardCss); - - return PanelCard({ - className: "org-scroll-panel org-span-6", - title: "Expansion Slots", - subtitle: - "Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.", - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - { className: "org-roadmap-grid" }, - ...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), - ), - ), - ), - }); - }; -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const getters = OrgPortal.getters; - const actions = OrgPortal.actions; - const scopeAttr = "data-ui-danger-card"; - const scopeSelector = `[${scopeAttr}]`; - const dangerCardCss = ` -${scopeSelector} { - border-color: #fecaca; - background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%); -} - -${scopeSelector} .org-danger-copy { - margin-bottom: 1rem; -} - -${scopeSelector} .org-danger-copy strong, -${scopeSelector} .org-danger-copy p { - display: block; -} - -${scopeSelector} .org-danger-copy p { - margin: 0.4rem 0 0; - color: var(--text-muted); -} -`; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.DangerCard = function DangerCard() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - ensureScopedStyle("portal-danger-card", dangerCardCss); - - if (!getters.canDisbandOrg()) { - return null; - } - - return PanelCard({ - className: "org-span-12 org-danger-panel", - title: "Organization Controls", - subtitle: - "Leader-only actions for membership and permanent organization removal.", - rootProps: { [scopeAttr]: "" }, - body: h( - "div", - null, - h( - "div", - { className: "org-danger-copy" }, - h("strong", null, "Disband organization"), - h( - "p", - null, - "This removes the organization and revokes access to the portal for all members.", - ), - ), - h( - "button", - { - type: "button", - className: "org-danger-btn", - onClick: () => actions.openModal("disband"), - }, - "Disband Organization", - ), - ), - }); - }; -})(); - -(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 = window.SharedUI.componentFns.Modal; - 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: "app-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: "app-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: "app-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: "app-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: "app-modal-form" }, - h( - "div", - null, - h("label", null, "Member"), - h( - "select", - { id: "treasury-credit-member", ...memberSelectProps }, - ...members.map((member) => - h("option", { value: member.uid }, 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: "app-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: "app-modal-danger" }, - h( - "p", - null, - "This action is permanent. Disband ", - portalData.org.name, - "?", - ), - h( - "div", - { className: "app-modal-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", - ), - ), - ); - } else if (modal.type === "leave") { - title = "Leave Organization"; - body = h( - "div", - { className: "app-modal-danger" }, - h( - "p", - null, - "Leave ", - portalData.org.name, - " and return to the default organization?", - ), - h( - "div", - { className: "app-modal-danger-actions" }, - h( - "button", - { - type: "button", - className: "org-secondary-btn", - onClick: () => actions.closeModal(), - }, - "Cancel", - ), - h( - "button", - { - type: "button", - className: "org-danger-btn", - onClick: () => actions.leaveOrganization(), - }, - "Confirm Leave", - ), - ), - ); - } - - return Modal({ - title, - body, - onClose: () => actions.closeModal(), - }); - }; -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h } = OrgPortal.runtime; - const { portalData } = OrgPortal.data; - const registryStore = window.RegistryApp.store; - - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.DisbandedView = function DisbandedView() { - const PanelCard = window.SharedUI.componentFns.PanelCard; - - return PanelCard({ - className: "org-span-12 org-empty-state", - eyebrow: "Organization Removed", - title: portalData.org.name, - body: h( - "div", - null, - h( - "p", - { className: "org-summary" }, - "This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview.", - ), - h( - "button", - { - type: "button", - className: "org-secondary-btn", - onClick: () => registryStore.setView("home"), - }, - "Return to Registry", - ), - ), - }); - }; -})(); - -(function () { - const OrgPortal = (window.OrgPortal = window.OrgPortal || {}); - const { h, ensureScopedStyle } = OrgPortal.runtime; - const { portalData, session } = OrgPortal.data; - const store = OrgPortal.store; - const portalViewScope = "[data-ui-portal-view]"; - - ensureScopedStyle( - "portal-view", - ` - ${portalViewScope} { - --org-row-card-max-height: 36rem; - } - - ${portalViewScope} .org-toast-stack { - position: fixed; - top: 1.5rem; - right: 2rem; - z-index: 20; - display: flex; - flex-direction: column; - gap: 0.75rem; - pointer-events: none; - } - - ${portalViewScope} .org-toast { - max-width: 24rem; - padding: 0.9rem 1rem; - border-radius: var(--radius); - border: 1px solid var(--border); - background: #fff; - box-shadow: 0 12px 28px rgb(15 23 42 / 0.14); - font-size: 0.92rem; - pointer-events: auto; - } - - ${portalViewScope} .org-toast.is-success { - background: #ecfdf5; - border-color: #bbf7d0; - color: #166534; - } - - ${portalViewScope} .org-toast.is-error { - background: #fef2f2; - border-color: #fecaca; - color: #991b1b; - } - - ${portalViewScope} .org-dashboard-grid { - display: grid; - grid-template-columns: repeat(12, minmax(0, 1fr)); - gap: 1.5rem; - align-items: stretch; - } - - ${portalViewScope} .org-panel { - margin-bottom: 0; - text-align: left; - } - - ${portalViewScope} .org-scroll-panel { - display: flex; - flex-direction: column; - min-height: 0; - max-height: var(--org-row-card-max-height); - overflow: hidden; - } - - ${portalViewScope} .org-island-root { - display: flex; - align-self: stretch; - min-height: 0; - min-width: 0; - } - - ${portalViewScope} .org-island-root > .org-panel { - height: 100%; - width: 100%; - } - - ${portalViewScope} .org-span-12 { - grid-column: span 12; - } - - ${portalViewScope} .org-span-7 { - grid-column: span 7; - } - - ${portalViewScope} .org-span-6 { - grid-column: span 6; - } - - ${portalViewScope} .org-span-5 { - grid-column: span 5; - } - - @media (max-width: 960px) { - ${portalViewScope} .org-toast-stack { - top: 1rem; - right: 1rem; - left: 1rem; - } - - ${portalViewScope} .org-toast { - max-width: none; - } - - ${portalViewScope} .org-span-12, - ${portalViewScope} .org-span-7, - ${portalViewScope} .org-span-6, - ${portalViewScope} .org-span-5 { - grid-column: span 12; - } - - ${portalViewScope} .org-scroll-panel { - max-height: none; - } - - } - `, - ); - - OrgPortal.components = OrgPortal.components || {}; - OrgPortal.componentFns = OrgPortal.componentFns || {}; - - OrgPortal.componentFns.TreasuryNoticeLayer = - function TreasuryNoticeLayer() { - const treasuryNotice = store.getTreasuryNotice(); - if (!treasuryNotice.text) { - return null; - } - - return h( - "div", - { className: "org-toast-stack" }, - h( - "div", - { - className: - treasuryNotice.type === "error" - ? "org-toast is-error" - : "org-toast is-success", - }, - treasuryNotice.text, - ), - ); - }; - - OrgPortal.components.App = function App() { - const Hero = window.SharedUI.componentFns.Hero; - const Footer = window.SharedUI.componentFns.Footer; - const FutureCard = OrgPortal.componentFns.FutureCard; - const DangerCard = OrgPortal.componentFns.DangerCard; - const DisbandedView = OrgPortal.componentFns.DisbandedView; - const footerSections = [ - { - title: "Organization Controls", - items: [ - "Roster Management", - "Fleet Assignment", - "Treasury Permissions", - "Asset Registry", - ], - }, - { - title: "Planned Extensions", - items: [ - "Contracts Board", - "Diplomacy Layer", - "Procurement Queue", - "Reputation History", - ], - }, - ]; - if (store.getOrgDisbanded()) { - return h( - "main", - { "data-ui-portal-view": "" }, - h( - "div", - { className: "container" }, - h( - "div", - { className: "org-dashboard-grid" }, - Hero({ - kicker: portalData.org.tag, - title: portalData.org.name, - subtitle: "Player organization command portal", - meta: `${session.actorName} - ${session.role}`, - }), - DisbandedView(), - ), - ), - h("div", { id: "org-portal-modal-root" }), - Footer({ sections: footerSections }), - ); - } - - return h( - "main", - { "data-ui-portal-view": "" }, - h("div", { id: "org-portal-toast-root" }), - h( - "div", - { className: "container" }, - h( - "div", - { className: "org-dashboard-grid" }, - Hero({ - kicker: portalData.org.tag, - title: portalData.org.name, - subtitle: "Player organization command portal", - meta: `${session.actorName} - ${session.role}`, - }), - h("div", { - className: "org-island-root org-span-12", - id: "org-overview-card-root", - }), - h("div", { - className: "org-island-root org-span-7", - id: "org-fleet-card-root", - }), - h("div", { - className: "org-island-root org-span-5", - id: "org-treasury-card-root", - }), - h("div", { - className: "org-island-root org-span-5", - id: "org-members-card-root", - }), - h("div", { - className: "org-island-root org-span-7", - id: "org-assets-card-root", - }), - h("div", { - className: "org-island-root org-span-6", - id: "org-activity-card-root", - }), - FutureCard(), - DangerCard(), - ), - ), - h("div", { id: "org-portal-modal-root" }), - Footer({ sections: footerSections }), - ); - }; -})(); - -(function () { - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h, ensureScopedStyle } = RegistryApp.runtime; - const store = RegistryApp.store; - const bridge = RegistryApp.bridge; - const scopeAttr = "data-ui-registration-view"; - const scopeSelector = `[${scopeAttr}]`; - const registrationViewCss = ` -${scopeSelector} { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - align-items: center; - width: 100%; -} - -${scopeSelector} .info-panel { - text-align: left; - padding: 1rem; -} - -${scopeSelector} .create-feature-list { - text-align: left; - margin-top: 1.5rem; - list-style-type: none; - padding: 0; -} - -${scopeSelector} .create-feature-item { - margin-bottom: 0.5rem; - display: flex; - align-items: center; - gap: 0.5rem; -} - -${scopeSelector} .create-feature-icon { - width: 1.2rem; - height: 1.2rem; - flex-shrink: 0; -} - -${scopeSelector} .price-tag { - margin-top: 2rem; - padding: 1rem; - background: var(--bg-app); - border-radius: var(--radius); - border: 1px solid var(--border); -} - -${scopeSelector} .price-label { - display: block; - font-size: 0.9rem; - color: var(--text-muted); -} - -${scopeSelector} .price-value { - display: block; - font-size: 2rem; - font-weight: 700; - color: var(--primary); -} - -${scopeSelector} .form-panel { - margin: 0; -} - -${scopeSelector} .app-form { - display: flex; - flex-direction: column; - gap: 1rem; - text-align: left; -} - -${scopeSelector} .app-form label { - display: block; - margin-bottom: 0.5rem; - color: var(--text-muted); - font-weight: 500; - font-size: 0.9rem; -} - -${scopeSelector} .app-form input, -${scopeSelector} .app-form select { - width: 100%; - padding: 0.75rem; - border-radius: var(--radius); - border: 1px solid var(--border); - background: var(--bg-app); - color: var(--text-main); - font-family: inherit; - font-size: 1rem; - box-sizing: border-box; - transition: border-color 0.2s; -} - -${scopeSelector} .app-form input:focus, -${scopeSelector} .app-form select:focus { - outline: none; - border-color: var(--primary); - box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1); -} - -${scopeSelector} .form-actions { - margin-top: 1rem; - display: flex; - flex-direction: column; - gap: 1rem; - align-items: center; -} - -${scopeSelector} .submit-btn { - width: 100%; -} - -${scopeSelector} .cancel-link { - font-size: 0.9rem; - color: var(--text-muted); - cursor: pointer; - text-decoration: underline; -} - -${scopeSelector} .cancel-link:hover { - color: var(--primary); -} - -${scopeSelector} .form-feedback { - padding: 0.85rem 1rem; - border-radius: var(--radius); - font-size: 0.92rem; -} - -${scopeSelector} .form-feedback.is-error { - background: #fef2f2; - border: 1px solid #fecaca; - color: #991b1b; -} - -@media (max-width: 960px) { - ${scopeSelector} { - grid-template-columns: 1fr; - } -} -`; - - RegistryApp.componentFns = RegistryApp.componentFns || {}; - - RegistryApp.componentFns.RegistrationView = function RegistrationView() { - const isCreating = store.getIsCreating(); - const createError = store.getCreateError(); - ensureScopedStyle("main-registration-view", registrationViewCss); - - const handleCreate = () => { - const data = { - orgName: String( - document.getElementById("org-create-name")?.value || "", - ).trim(), - type: String( - document.getElementById("org-create-type")?.value || "", - ), - }; - - if (!bridge || typeof bridge.requestCreateOrg !== "function") { - store.failCreate("Registration bridge is not available."); - return; - } - - bridge.requestCreateOrg(data); - }; - - return h( - "div", - { className: "split-container", [scopeAttr]: "" }, - 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", - { className: "create-feature-list" }, - h( - "li", - { className: "create-feature-item" }, - h( - "svg", - { - viewBox: "0 0 24 24", - fill: "none", - stroke: "#10b981", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - className: "create-feature-icon", - }, - h("path", { d: "M20 6L9 17l-5-5" }), - ), - "Official Organization Designator", - ), - h( - "li", - { className: "create-feature-item" }, - h( - "svg", - { - viewBox: "0 0 24 24", - fill: "none", - stroke: "#10b981", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - className: "create-feature-icon", - }, - h("path", { d: "M20 6L9 17l-5-5" }), - ), - "Secure Comms Channel", - ), - h( - "li", - { className: "create-feature-item" }, - h( - "svg", - { - viewBox: "0 0 24 24", - fill: "none", - stroke: "#10b981", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - className: "create-feature-icon", - }, - h("path", { d: "M20 6L9 17l-5-5" }), - ), - "Deployment Roster Access", - ), - h( - "li", - { className: "create-feature-item" }, - h( - "svg", - { - viewBox: "0 0 24 24", - fill: "none", - stroke: "#10b981", - "stroke-width": "2", - "stroke-linecap": "round", - "stroke-linejoin": "round", - className: "create-feature-icon", - }, - h("path", { d: "M20 6L9 17l-5-5" }), - ), - "After-Action Report Tools", - ), - ), - h( - "div", - { className: "price-tag" }, - h("span", { className: "price-label" }, "Registration Fee"), - h("span", { className: "price-value" }, "$50,000"), - ), - ), - h( - "div", - { className: "form-panel card" }, - 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" }, - createError - ? h( - "div", - { className: "form-feedback is-error" }, - createError, - ) - : null, - h( - "button", - { - type: "button", - className: "submit-btn", - disabled: isCreating, - onClick: handleCreate, - }, - isCreating - ? "Submitting Registration..." - : "Submit Registration", - ), - h( - "span", - { - className: "cancel-link", - onClick: () => store.setView("home"), - }, - "Cancel / Return to Main", - ), - ), - ), - ), - ); - }; -})(); - -(function () { - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h, ensureScopedStyle } = RegistryApp.runtime; - const store = RegistryApp.store; - const bridge = RegistryApp.bridge; - const scopeAttr = "data-ui-home-view"; - const scopeSelector = `[${scopeAttr}]`; - const homeViewCss = ` -${scopeSelector} { - display: grid; - grid-template-columns: 1fr 1fr; - gap: 2rem; - margin-bottom: 2rem; -} - -${scopeSelector} .home-feedback { - padding: 0.85rem 1rem; - border-radius: var(--radius); - font-size: 0.92rem; - background: #fef2f2; - border: 1px solid #fecaca; - color: #991b1b; -} - -@media (max-width: 960px) { - ${scopeSelector} { - grid-template-columns: 1fr; - } -} -`; - - RegistryApp.componentFns = RegistryApp.componentFns || {}; - - RegistryApp.componentFns.HomeView = function HomeView() { - const isAuthenticating = store.getIsAuthenticating(); - const loginError = store.getLoginError(); - ensureScopedStyle("main-home-view", homeViewCss); - - return h( - "div", - { className: "content", [scopeAttr]: "" }, - h( - "div", - { className: "card" }, - h("h2", null, "Create Organization"), - h( - "p", - null, - "Establish your Task Force, PMC, or Milsim unit with the Global Organization Network. Receive your official unit designator and TO&E authorization instantly.", - ), - h( - "button", - { onClick: () => store.setView("create") }, - "Register", - ), - ), - h( - "div", - { className: "card" }, - h("h2", null, "Organization Portal"), - h( - "p", - null, - "Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink.", - ), - loginError - ? h("div", { className: "home-feedback" }, loginError) - : null, - h( - "button", - { - disabled: isAuthenticating, - onClick: () => { - if (!bridge) { - store.failLogin( - "Login bridge is not available.", - ); - return; - } - - bridge.requestLogin({}); - }, - }, - isAuthenticating ? "Opening Portal..." : "Login", - ), - ), - ); - }; -})(); - -(function () { - const RegistryApp = (window.RegistryApp = window.RegistryApp || {}); - const { h } = RegistryApp.runtime; - const store = RegistryApp.store; - - RegistryApp.components = RegistryApp.components || {}; - - RegistryApp.components.App = function App() { - const Navbar = window.SharedUI.componentFns.Navbar; - const Header = window.SharedUI.componentFns.Header; - const Footer = window.SharedUI.componentFns.Footer; - const WindowTitleBar = window.SharedUI.componentFns.WindowTitleBar; - const HomeView = RegistryApp.componentFns.HomeView; - const RegistrationView = RegistryApp.componentFns.RegistrationView; - const PortalApp = - window.OrgPortal && window.OrgPortal.components - ? window.OrgPortal.components.App - : null; - - const view = store.getView(); - const portalGetters = - window.OrgPortal && window.OrgPortal.getters - ? window.OrgPortal.getters - : null; - const portalActions = - window.OrgPortal && window.OrgPortal.actions - ? window.OrgPortal.actions - : null; - const viewLabel = - view === "create" - ? "Organization Registration" - : view === "portal" - ? "Organization Portal" - : "Entry Hub"; - const footerSections = [ - { - title: "Registry Resources", - items: [ - "Registration Guidelines", - "Tax & Fee Schedule", - "Legal Compliance", - "Trademark Database", - ], - }, - { - title: "Bureau Support", - items: [ - "Office: Sector 7 Admin Block", - "Hours: 0800 - 1600 (GST)", - "Helpdesk: 555-01-REGISTRY", - "support@org-bureau.gov", - ], - }, - ]; - - function closeRegistry() { - if ( - RegistryApp.bridge && - typeof RegistryApp.bridge.close === "function" - ) { - RegistryApp.bridge.close({}); - return; - } - - store.setView("home"); - } - - if (view === "portal" && PortalApp) { - const canLeaveOrg = - portalGetters && - typeof portalGetters.canLeaveOrg === "function" && - portalGetters.canLeaveOrg(); - - return h( - "div", - { className: "app-shell" }, - WindowTitleBar({ - kicker: "FORGE ORBIS", - title: "Global Organization Network", - onClose: closeRegistry, - closeLabel: "Close organization interface", - }), - Navbar({ - title: "Global Organization Network", - viewLabel, - actionLabel: canLeaveOrg ? "Leave Organization" : "", - onAction: - canLeaveOrg && - portalActions && - typeof portalActions.openModal === "function" - ? () => portalActions.openModal("leave") - : null, - }), - h("div", { id: "org-portal-frame-root" }), - ); - } - - let mainContent; - if (view === "home") { - mainContent = HomeView(); - } else if (view === "create") { - mainContent = RegistrationView(); - } - - return h( - "div", - { className: "app-shell" }, - WindowTitleBar({ - kicker: "FORGE ORBIS", - title: "Global Organization Network", - onClose: closeRegistry, - closeLabel: "Close organization interface", - }), - h( - "main", - null, - Navbar({ - title: "Global Organization Network", - viewLabel, - }), - h( - "div", - { className: "container" }, - Header({ - title: "Global Organization Network", - onTitleClick: () => store.setView("home"), - }), - mainContent, - ), - Footer({ sections: footerSections }), - ), - ); - }; -})(); - -(function () { - const ForgeWebUI = window.ForgeWebUI; - const RegistryApp = window.RegistryApp; - const OrgPortal = window.OrgPortal; - const islandDefinitions = [ - { - id: "org-portal-frame-root", - preserveScroll: true, - render: () => OrgPortal.components.App(), - }, - { - id: "org-portal-toast-root", - preserveScroll: false, - render: () => OrgPortal.componentFns.TreasuryNoticeLayer(), - }, - { - id: "org-overview-card-root", - preserveScroll: false, - render: () => OrgPortal.componentFns.OverviewCard(), - }, - { - id: "org-fleet-card-root", - preserveScroll: true, - render: () => OrgPortal.componentFns.FleetCard(), - }, - { - id: "org-treasury-card-root", - preserveScroll: false, - render: () => OrgPortal.componentFns.TreasuryCard(), - }, - { - id: "org-members-card-root", - preserveScroll: true, - render: () => OrgPortal.componentFns.MembersCard(), - }, - { - id: "org-assets-card-root", - preserveScroll: true, - render: () => OrgPortal.componentFns.AssetsCard(), - }, - { - id: "org-activity-card-root", - preserveScroll: true, - render: () => OrgPortal.componentFns.ActivityCard(), - }, - { - id: "org-portal-modal-root", - preserveScroll: false, - render: () => OrgPortal.componentFns.ModalLayer(), - }, - ]; - - function createIslandManager() { - const mounts = new Map(); - - function sync() { - islandDefinitions.forEach((definition) => { - const container = document.getElementById(definition.id); - const current = mounts.get(definition.id); - - if (!container) { - if (current) { - current.handle.dispose(); - mounts.delete(definition.id); - } - return; - } - - if (current && current.container === container) { - return; - } - - if (current) { - current.handle.dispose(); - } - - const handle = ForgeWebUI.mount(container, definition.render, { - preserveScroll: definition.preserveScroll, - }); - mounts.set(definition.id, { - container, - handle, - }); - }); - } - - return { - sync, - }; - } - - const app = ForgeWebUI.createApp({ - name: "org", - root: "#app", - setup({ root }) { - const islandManager = createIslandManager(); - - ForgeWebUI.mount(root, () => RegistryApp.components.App(), { - preserveScroll: false, - }); - RegistryApp.bridge.ready({ loaded: true }); - - ForgeWebUI.effect(() => { - RegistryApp.store.getView(); - - requestAnimationFrame(() => { - islandManager.sync(); - }); - }); - }, - }); - - app.start(); -})(); +!function(){const e=window.ForgeWebUI,n=window.RegistryApp=window.RegistryApp||{},r=window.OrgPortal=window.OrgPortal||{};n.runtime=e,r.runtime=e,window.AppRuntime=e}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{createSignal:n}=e.runtime;e.store=new class{constructor(){[this.getView,this.setView]=n("home"),[this.getIsAuthenticating,this.setIsAuthenticating]=n(!1),[this.getLoginError,this.setLoginError]=n(""),[this.getIsCreating,this.setIsCreating]=n(!1),[this.getCreateError,this.setCreateError]=n("")}startLogin(){this.setLoginError(""),this.setIsAuthenticating(!0)}startCreate(){this.setCreateError(""),this.setIsCreating(!0)}failLogin(e){this.setIsAuthenticating(!1),this.setLoginError(e||"Authentication failed.")}failCreate(e){this.setIsCreating(!1),this.setCreateError(e||"Organization registration failed.")}hydratePortal(e){const n=window.OrgPortal&&window.OrgPortal.data?window.OrgPortal.data:null,r=window.OrgPortal&&window.OrgPortal.store?window.OrgPortal.store:null,t=e&&e.portalData?e.portalData:null,a=e&&e.session?e.session:null;return!!(n&&"function"==typeof n.applyLoginPayload&&r&&"function"==typeof r.hydrateFromPayload&&t&&a)&&(n.applyLoginPayload(e),r.hydrateFromPayload(e),!0)}completeLogin(e){this.hydratePortal(e)?(this.setLoginError(""),this.setIsAuthenticating(!1),this.setView("portal")):this.failLogin("Login response was missing portal data.")}completeCreate(e){this.hydratePortal(e)?(this.setCreateError(""),this.setIsCreating(!1),this.setView("portal")):this.failCreate("Organization registration response was missing portal data.")}}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},n=e.store,r=window.ForgeWebUI.createBridge({closeEvent:"org::close",globalName:"ForgeBridge",readyEvent:"org::ready"});function t(e,n){return r.send(e,n)}r.on("org::login::success",e=>{n.completeLogin(e)}),r.on("org::login::failure",e=>{n.failLogin(e.message||"Authentication failed.")}),r.on("org::create::success",e=>{n.completeCreate(e)}),r.on("org::create::failure",e=>{n.failCreate(e.message||"Organization registration failed.")}),r.on("org::sync",e=>{n&&"function"==typeof n.hydratePortal&&n.hydratePortal(e)}),r.on("org::credit::success",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("success",e.message||"Credit line assigned.")}),r.on("org::credit::failure",e=>{const n=window.OrgPortal;n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to assign credit line.")}),r.on("org::member::creditUpdated",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setCreditLines(n=>{const r={amount:e.amount||0,member:e.memberName||"",uid:e.memberUid||""},t=n.findIndex(e=>e.uid===r.uid);return-1===t?[...n,r]:n.map((e,n)=>n===t?r:e)})}),r.on("org::disband::success",()=>{const e=window.OrgPortal;e&&e.store&&(e.store.setModal(null),e.store.setOrgDisbanded(!0))}),r.on("org::disband::failure",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Organization disbanding failed.")}),r.on("org::leave::success",e=>{const r=window.OrgPortal;r&&r.store&&r.store.setModal(null),n.failLogin(e.message||"You have left the organization."),n.setView("home")}),r.on("org::leave::failure",e=>{const n=window.OrgPortal;n&&n.store&&n.store.setModal(null),n&&n.actions&&n.actions.showTreasuryNotice("error",e.message||"Unable to leave the organization.")}),r.on("org::portal::revoked",e=>{const r=window.OrgPortal;r&&r.store&&r.store.setModal(null),n.failLogin(e.message||"Organization access is no longer available."),n.setView("home")}),e.bridge={close:r.close,ready:r.ready,receive:r.receive,requestLogin:function(e){n.startLogin(),t("org::login::request",e)||n.failLogin("Arma login bridge is unavailable.")},requestCreateOrg:function(e){n.startCreate(),t("org::create::request",e)||n.failCreate("Arma registration bridge is unavailable.")},requestDisbandOrg:function(){if(t("org::disband::request",{}))return;const e=window.OrgPortal;e&&e.actions&&e.actions.showTreasuryNotice("error","Arma disband bridge is unavailable.")},requestLeaveOrg:function(){if(t("org::leave::request",{}))return;const e=window.OrgPortal;e&&e.actions&&e.actions.showTreasuryNotice("error","Arma leave bridge is unavailable.")},requestCreditLine:function(e){if(t("org::credit::request",e))return!0;const n=window.OrgPortal;return n&&n.actions&&n.actions.showTreasuryNotice("error","Arma credit line bridge is unavailable."),!1},sendEvent:t}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},n={type:"Organization",status:"Operational",headquarters:"ArmA Verse"};function r(e){return JSON.parse(JSON.stringify(e))}function t(e,n){Object.keys(e).forEach(n=>delete e[n]),Object.assign(e,r(n))}function a(e,n){e.splice(0,e.length,...r(n))}e.data={portalData:{org:Object.assign({name:"",tag:"",owner:"",ownerUid:"",isDefault:!1},n),funds:0,reputation:0,creditLines:[],members:[],fleet:[],assets:[],activity:[],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:"",actorUid:"",role:"",ceo:!1},applyLoginPayload(e){t(this.portalData.org,Object.assign({},e.portalData.org||{},n)),this.portalData.funds=e.portalData.funds||0,this.portalData.reputation=e.portalData.reputation||0,a(this.portalData.creditLines,e.portalData.creditLines||[]),a(this.portalData.members,e.portalData.members||[]),a(this.portalData.fleet,e.portalData.fleet||[]),a(this.portalData.assets,e.portalData.assets||[]),a(this.portalData.activity,e.portalData.activity||[]),a(this.portalData.roadmap,e.portalData.roadmap||[]),t(this.session,e.session||{})}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{createSignal:n}=window.RegistryApp.runtime,{portalData:r}=e.data;e.store=new class{constructor(){[this.getFunds,this.setFunds]=n(r.funds),[this.getReputation,this.setReputation]=n(r.reputation),[this.getMembers,this.setMembers]=n([...r.members]),[this.getCreditLines,this.setCreditLines]=n([...r.creditLines]),[this.getFleet,this.setFleet]=n([...r.fleet]),[this.getAssets,this.setAssets]=n([...r.assets]),[this.getActivity,this.setActivity]=n([...r.activity]),[this.getTreasuryNotice,this.setTreasuryNotice]=n({type:"",text:""}),[this.getModal,this.setModal]=n(null),[this.getOrgDisbanded,this.setOrgDisbanded]=n(!1)}hydrateFromPayload(e){const n=e.portalData||{};this.setFunds(n.funds||0),this.setReputation(n.reputation||0),this.setMembers([...n.members||[]]),this.setCreditLines([...n.creditLines||[]]),this.setFleet([...n.fleet||[]]),this.setAssets([...n.assets||[]]),this.setActivity([...n.activity||[]])}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{portalData:n,session:r}=e.data;e.getters=new class{formatCurrency(e){return"$"+Number(e||0).toLocaleString()}formatVehicleType(e){return e?e.charAt(0).toUpperCase()+e.slice(1):""}formatAssetType(e){return e?e.charAt(0).toUpperCase()+e.slice(1):""}formatDisplayName(e){return e?String(e).trim().split(/\s+/).map(e=>e?e.charAt(0).toUpperCase()+e.slice(1).toLowerCase():"").join(" "):""}getAssetReadiness(){const r=e.store?e.store.getFleet():n.fleet;if(0===r.length)return null;const t=r.reduce((e,n)=>e+(100-parseInt(n.damage,10)),0);return Math.round(t/r.length)}getNormalizedRole(){return String(r.role||"").trim().toUpperCase()}isDefaultOrg(){return!0===n.org.isDefault||"DEFAULT"===String(n.org.tag||"").trim().toUpperCase()}isOrgOwner(){const e=String(n.org.ownerUid||n.org.owner||"").trim().toLowerCase(),t=String(r.actorUid||"").trim().toLowerCase();return e&&t?t===e:String(r.actorName||"").trim().toLowerCase()===String(n.org.owner||"").trim().toLowerCase()}isSessionCeo(){return!0===r.ceo}isOrgLeaderOrCeo(){return this.isOrgOwner()||"LEADER"===this.getNormalizedRole()||this.isDefaultOrg()&&this.isSessionCeo()}canManageMembers(){return this.isOrgLeaderOrCeo()}canManageTreasury(){return this.isOrgLeaderOrCeo()}canDisbandOrg(){return this.isOrgOwner()&&!this.isDefaultOrg()}canLeaveOrg(){return!this.isDefaultOrg()&&!this.isOrgOwner()}getMemberName(e){return String(e&&"object"==typeof e?e.name||"":e||"")}getMemberUid(e){return e&&"object"==typeof e?String(e.uid||""):""}isOwnerMember(e){return this.getMemberName(e).trim().toLowerCase()===String(n.org.owner||"").trim().toLowerCase()}isCurrentMember(e){const n=this.getMemberUid(e).trim().toLowerCase(),t=String(r.actorUid||"").trim().toLowerCase();return n&&t?n===t:this.getMemberName(e).trim().toLowerCase()===String(r.actorName||"").trim().toLowerCase()}isProtectedMember(e){return this.isOwnerMember(e)||this.isCurrentMember(e)}}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{portalData:n}=e.data,r=e.store,t=e.getters,a=window.RegistryApp.store;e.actions=new class{constructor(){this.treasuryNoticeTimer=null}showTreasuryNotice(e,n){r.setTreasuryNotice({type:e,text:n}),this.treasuryNoticeTimer&&clearTimeout(this.treasuryNoticeTimer),this.treasuryNoticeTimer=setTimeout(()=>{r.setTreasuryNotice({type:"",text:""}),this.treasuryNoticeTimer=null},3500)}parseAmount(e){const n=Number(e);return Number.isFinite(n)?Math.round(n):0}getInputValue(e){const n=document.getElementById(e);return n?n.value:""}closePortal(){const e=window.RegistryApp?window.RegistryApp.bridge:null;e&&"function"==typeof e.close?e.close({}):a&&a.setView("home")}openModal(e){"payroll"!==e&&"transfer"!==e&&"credit"!==e||t.canManageTreasury()?("disband"!==e||t.canDisbandOrg())&&("leave"!==e||t.canLeaveOrg())&&r.setModal({type:e}):this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions.")}closeModal(){r.setModal(null)}removeMember(e){if(!t.canManageMembers())return!1;if(t.isProtectedMember(e))return!1;const n=t.getMemberUid(e),a=t.getMemberName(e);return r.setMembers(e=>e.filter(e=>n?e.uid!==n:e.name!==a)),r.setCreditLines(e=>e.filter(e=>n?e.uid!==n:e.member!==a)),!0}disbandOrganization(){if(!t.canDisbandOrg())return!1;const e=window.RegistryApp?window.RegistryApp.bridge:null;return e&&"function"==typeof e.requestDisbandOrg?(this.closeModal(),e.requestDisbandOrg(),!0):(this.showTreasuryNotice("error","Disband bridge is unavailable."),!1)}leaveOrganization(){if(!t.canLeaveOrg())return!1;const e=window.RegistryApp?window.RegistryApp.bridge:null;return e&&"function"==typeof e.requestLeaveOrg?(this.closeModal(),e.requestLeaveOrg(),!0):(this.showTreasuryNotice("error","Leave bridge is unavailable."),!1)}runPayroll(e){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;const n=r.getMembers(),a=r.getFunds();if(0===n.length)return this.showTreasuryNotice("error","No members available for payroll."),!1;if(e<=0)return this.showTreasuryNotice("error","Enter a valid payroll amount."),!1;const o=e*n.length;return o>a?(this.showTreasuryNotice("error","Insufficient org funds for payroll."),!1):(r.setFunds(a-o),this.showTreasuryNotice("success",`Payroll sent to ${n.length} members for ${t.formatCurrency(o)}.`),!0)}sendFundsToMember(e,n){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;const a=r.getFunds();return e?n<=0?(this.showTreasuryNotice("error","Enter a valid transfer amount."),!1):n>a?(this.showTreasuryNotice("error","Insufficient org funds for this transfer."),!1):(r.setFunds(a-n),this.showTreasuryNotice("success",`${t.formatCurrency(n)} sent to ${e}.`),!0):(this.showTreasuryNotice("error","Select a member to receive funds."),!1)}grantCreditLine(e,n){if(!t.canManageTreasury())return this.showTreasuryNotice("error","Only the organization leader or CEO can manage treasury actions."),!1;if(!e)return this.showTreasuryNotice("error","Select a member for the credit line."),!1;if(n<=0)return this.showTreasuryNotice("error","Enter a valid credit line amount."),!1;const a=r.getMembers().find(n=>t.getMemberUid(n)===e),o=a?t.getMemberName(a):"";if(!o)return this.showTreasuryNotice("error","Selected member was not found in the organization roster."),!1;const i=window.RegistryApp?window.RegistryApp.bridge:null;return i&&"function"==typeof i.requestCreditLine?i.requestCreditLine({memberUid:e,memberName:o,amount:n}):(this.showTreasuryNotice("error","Credit line bridge is unavailable."),!1)}}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-navbar",o=`[${a}]`,i=`\n${o} {\n background: var(--bg-surface);\n border-bottom: 1px solid var(--border);\n box-shadow: var(--shadow);\n}\n\n${o} .app-navbar-inner {\n display: flex;\n justify-content: space-between;\n align-items: center;\n max-width: 1200px;\n width: 100%;\n margin: 0 auto;\n padding: 1rem 2rem;\n box-sizing: border-box;\n}\n\n${o} .app-navbar-brand {\n display: flex;\n flex-direction: column;\n gap: 0.125rem;\n}\n\n${o} .app-navbar-kicker {\n font-size: 0.7rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n font-weight: 600;\n}\n\n${o} .app-navbar-title {\n font-size: 1.25rem;\n font-weight: 700;\n color: var(--primary-hover);\n letter-spacing: -0.025em;\n}\n\n${o} .app-navbar-actions {\n display: flex;\n align-items: center;\n gap: 1.5rem;\n}\n\n${o} .app-navbar-view {\n font-size: 0.8rem;\n text-transform: uppercase;\n letter-spacing: 0.05em;\n color: var(--text-muted);\n font-weight: 600;\n}\n\n${o} .app-close-btn {\n background: transparent;\n color: var(--text-muted);\n border: 1px solid var(--border);\n padding: 0.5rem 1rem;\n font-size: 0.85rem;\n}\n\n${o} .app-close-btn:hover {\n background: var(--bg-surface-hover);\n color: var(--primary-hover);\n border-color: var(--primary);\n transform: none;\n box-shadow: none;\n}\n\n@media (max-width: 960px) {\n ${o} .app-navbar-inner {\n flex-direction: column;\n align-items: flex-start;\n padding: 1rem 1.5rem;\n }\n\n ${o} .app-navbar-actions {\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Navbar=function({kicker:e="ORBIS",title:n="",viewLabel:o="",actionLabel:s="",onAction:l=null}){return t("shared-navbar",i),r("nav",{className:"app-navbar",[a]:""},r("div",{className:"app-navbar-inner"},r("div",{className:"app-navbar-brand"},r("span",{className:"app-navbar-kicker"},e),r("span",{className:"app-navbar-title"},n)),r("div",{className:"app-navbar-actions"},r("span",{className:"app-navbar-view"},o),s&&"function"==typeof l?r("button",{type:"button",className:"app-close-btn",onClick:l},s):null)))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Header=function({title:e,subtitle:n="Organization Registration & Management Portal",onTitleClick:t=null}){return r("div",{className:"header"},r("h1",{style:{cursor:t?"pointer":"default"},onClick:t},e),r("p",null,n))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.OrgPortal=window.OrgPortal||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Hero=function({className:e="",kicker:n="",title:t="",subtitle:a="",meta:o=""}){const i=["card org-panel org-span-12 org-page-header",e].filter(Boolean).join(" ");return r("section",{className:i},r("div",{className:"org-page-heading"},r("span",{className:"org-page-kicker"},n),r("h1",{className:"org-page-title"},t),r("p",{className:"org-page-subtitle"},a),r("span",{className:"org-page-meta"},o)))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r}=n.runtime;e.componentFns=e.componentFns||{},e.componentFns.Footer=function({sections:e=[]}){return r("div",{className:"footer"},r("div",{className:"wrapper"},...e.map(e=>r("div",null,r("h3",null,e.title),r("ul",{style:{listStyleType:"none",padding:0}},...(e.items||[]).map(e=>r("li",null,e)))))))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-modal",o=`[${a}]`,i=`\n${o} {\n position: fixed;\n inset: 0;\n background: rgb(15 23 42 / 0.38);\n display: flex;\n align-items: center;\n justify-content: center;\n padding: 1.5rem;\n z-index: 20;\n}\n\n${o} .app-modal-card {\n width: min(100%, 30rem);\n margin-bottom: 0;\n text-align: left;\n}\n\n${o} .app-modal-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1rem;\n}\n\n${o} .app-modal-title {\n margin: 0;\n color: var(--primary-hover);\n font-size: 1.45rem;\n}\n\n${o} .app-modal-close {\n width: 2.25rem;\n height: 2.25rem;\n padding: 0;\n background: var(--bg-surface);\n color: var(--text-main);\n border: 1px solid var(--border);\n box-shadow: none;\n transform: none;\n}\n\n${o} .app-modal-close:hover {\n background: var(--bg-surface-hover);\n color: var(--text-main);\n box-shadow: none;\n transform: none;\n}\n\n${o} .app-modal-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n${o} .app-modal-form label {\n display: block;\n margin-bottom: 0.5rem;\n color: var(--text-muted);\n font-weight: 500;\n font-size: 0.9rem;\n}\n\n${o} .app-modal-form input,\n${o} .app-modal-form select {\n width: 100%;\n padding: 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--bg-app);\n color: var(--text-main);\n font-family: inherit;\n font-size: 1rem;\n box-sizing: border-box;\n transition: border-color 0.2s, box-shadow 0.2s;\n}\n\n${o} .app-modal-form input:focus,\n${o} .app-modal-form select:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px rgb(71 85 105 / 0.12);\n}\n\n${o} .app-modal-form input:disabled,\n${o} .app-modal-form select:disabled {\n background: #f1f5f9;\n color: var(--text-muted);\n cursor: not-allowed;\n}\n\n${o} .app-modal-actions {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 0.75rem;\n margin-top: 0.5rem;\n}\n\n${o} .app-modal-actions button + button,\n${o} .app-modal-danger-actions button + button {\n margin-left: 0;\n}\n\n${o} .app-modal-danger {\n display: flex;\n flex-direction: column;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid #fecaca;\n border-radius: var(--radius);\n background: #fff1f2;\n align-items: flex-start;\n}\n\n${o} .app-modal-danger p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .app-modal-danger-actions {\n display: flex;\n flex-wrap: wrap;\n gap: 0.75rem;\n}\n\n@media (max-width: 960px) {\n ${o} .app-modal-head,\n ${o} .app-modal-danger {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Modal=function({title:e="",body:n=null,onClose:o=null}){return t("shared-modal",i),r("div",{className:"app-modal-backdrop",[a]:"",onClick:e=>{e.target===e.currentTarget&&o&&o()}},r("div",{className:"card app-modal-card"},r("div",{className:"app-modal-head"},r("div",null,r("h2",{className:"app-modal-title"},e)),r("button",{type:"button",className:"app-modal-close",onClick:o,"aria-label":"Close dialog"},"x")),n))}}(),function(){const e=window.SharedUI=window.SharedUI||{},n=window.RegistryApp=window.RegistryApp||{},{h:r,ensureScopedStyle:t}=n.runtime,a="data-ui-panel-card",o=`[${a}]`,i=`\n${o} {\n display: flex;\n flex-direction: column;\n height: 100%;\n min-height: 0;\n}\n\n${o} .org-panel-head {\n display: flex;\n align-items: flex-start;\n justify-content: space-between;\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n\n${o} .org-panel-body {\n display: flex;\n flex: 1 1 auto;\n flex-direction: column;\n min-height: 0;\n}\n\n${o} .org-eyebrow {\n font-size: 0.8rem;\n font-weight: 700;\n letter-spacing: 0.12em;\n text-transform: uppercase;\n color: var(--text-muted);\n margin-bottom: 0.4rem;\n}\n\n${o} .org-panel-title {\n margin: 0;\n color: var(--primary-hover);\n font-size: 1.45rem;\n}\n\n${o} .org-panel-subtitle {\n margin: 0.35rem 0 0;\n color: var(--text-muted);\n font-size: 0.95rem;\n}\n\n@media (max-width: 960px) {\n ${o} .org-panel-head {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.PanelCard=function({className:e="",eyebrow:n="",title:o="",subtitle:s="",headerExtras:l=null,body:d=null,rootProps:c={}}){const m=["card org-panel",e].filter(Boolean).join(" ");return t("shared-panel-card",i),r("section",{className:m,[a]:"",...c},r("div",{className:"org-panel-head"},r("div",null,n?r("div",{className:"org-eyebrow"},n):null,r("h2",{className:"org-panel-title"},o),s?r("p",{className:"org-panel-subtitle"},s):null),l),r("div",{className:"org-panel-body"},d))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-metric-card",a=`[${t}]`,o=`\n${a} {\n display: flex;\n flex-direction: column;\n gap: 0.45rem;\n padding: 1rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);\n}\n\n${a}:nth-child(4n + 2),\n${a}:nth-child(4n + 3) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(226 232 240) 100%);\n border-color: rgb(100 116 139 / 0.35);\n box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.6);\n}\n\n${a} .org-metric-label {\n font-size: 0.76rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n color: var(--text-muted);\n}\n\n${a} .org-metric-value {\n font-size: 1.8rem;\n color: var(--primary-hover);\n line-height: 1.1;\n}\n\n${a}:nth-child(4n + 2) .org-metric-value,\n${a}:nth-child(4n + 3) .org-metric-value {\n color: #334155;\n}\n\n${a} .org-metric-note {\n color: var(--text-muted);\n font-size: 0.9rem;\n}\n\n@media (max-width: 960px) {\n ${a}:nth-child(4n + 3) {\n background: linear-gradient(180deg, #ffffff 0%, #f8fafc 100%);\n border-color: var(--border);\n box-shadow: none;\n }\n\n ${a}:nth-child(4n + 3) .org-metric-value {\n color: var(--primary-hover);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.MetricCard=function(e,a,i){return r("portal-metric-card",o),n("div",{className:"org-metric-card",[t]:""},n("span",{className:"org-metric-label"},e),n("strong",{className:"org-metric-value"},a),n("span",{className:"org-metric-note"},i))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-simple-stat",a=`[${t}]`,o=`\n${a} {\n display: flex;\n flex-direction: column;\n gap: 0.2rem;\n min-width: 90px;\n}\n\n${a} .org-simple-label {\n font-size: 0.72rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${a} .org-simple-value {\n font-size: 0.95rem;\n color: var(--text-main);\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.SimpleStat=function(e,a){return r("portal-simple-stat",o),n("div",{className:"org-simple-stat",[t]:""},n("span",{className:"org-simple-label"},e),n("strong",{className:"org-simple-value"},a))}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.store,o=e.getters,i="data-ui-overview-card",s=`[${i}]`,l=`\n${s} .org-hero-grid {\n display: grid;\n grid-template-columns: 1.3fr 1fr;\n gap: 1.5rem;\n align-items: start;\n}\n\n${s} .org-summary {\n margin: 0;\n font-size: 1.05rem;\n color: var(--text-main);\n}\n\n${s} .org-meta-row {\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 1rem;\n margin-top: 1.5rem;\n}\n\n${s} .org-meta-item {\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${s} .org-meta-item:nth-child(even) {\n background: linear-gradient(180deg, rgb(241 245 249) 0%, rgb(226 232 240) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${s} .org-meta-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${s} .org-meta-value {\n font-size: 1rem;\n font-weight: 600;\n color: var(--primary-hover);\n}\n\n${s} .org-metric-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${s} .org-hero-grid,\n ${s} .org-meta-row,\n ${s} .org-metric-grid {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.OverviewCard=function(){const s=e.componentFns.MetricCard,d=window.SharedUI.componentFns.PanelCard,c=o.getAssetReadiness(),m=t.org.headquarters||"ArmA Verse",g=a.getAssets().length,p=a.getFleet().length,u=a.getFunds(),f=a.getMembers().length,b=a.getReputation();return r("portal-overview-card",l),d({className:"org-span-12",eyebrow:t.org.tag,title:"Organization Overview",rootProps:{[i]:""},body:n("div",{className:"org-hero-grid"},n("div",{className:"org-hero-copy"},n("p",{className:"org-summary"},t.org.type," operating from ",m,". Treasury, fleet status, inventory, and roster management are surfaced here first."),n("div",{className:"org-meta-row"},n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Director"),n("span",{className:"org-meta-value"},o.formatDisplayName(t.org.owner))),n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Active Members"),n("span",{className:"org-meta-value"},`${f} total`)),n("div",{className:"org-meta-item"},n("span",{className:"org-meta-label"},"Fleet Readiness"),n("span",{className:"org-meta-value"},null===c?"N/A":`${c}%`)))),n("div",{className:"org-metric-grid"},s("Org Funds",o.formatCurrency(u),"Organization treasury balance"),s("Reputation",b,"Organization standing"),s("Asset Lines",g,"Tracked supply and equipment entries"),s("Fleet Vehicles",p,"Tracked air, ground, and naval vehicles")))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.getters,o="data-ui-fleet-card",i=`[${o}]`,s=`\n${i} .org-simple-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${i} .org-simple-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${i} .org-simple-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${i} .org-simple-name {\n color: var(--primary-hover);\n}\n\n${i} .org-simple-meta {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${i} .org-simple-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.FleetCard=function(){const t=window.SharedUI.componentFns.PanelCard,i=e.componentFns.SimpleStat,l=e.store.getFleet();return r("portal-fleet-card",s),t({className:"org-scroll-panel org-span-7",title:"Fleet",subtitle:"Individual vehicles with type, status, and overall damage.",rootProps:{[o]:""},body:n("div",{className:"org-simple-list"},...l.map(e=>n("article",{className:"org-simple-row"},n("strong",{className:"org-simple-name"},e.name),n("div",{className:"org-simple-meta"},i("Type",a.formatVehicleType(e.type)),i("Status",e.status),i("Damage",e.damage)))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r,createSignal:t}=e.runtime,{portalData:a}=e.data,o=e.store,i=e.getters,s=e.actions,l="data-ui-treasury-card",d=`[${l}]`,[c,m]=t("overview"),[g,p]=t(!1),u=`\n${d} .org-treasury-menu {\n position: relative;\n}\n\n${d} .org-menu-btn {\n width: 2.75rem;\n height: 2.75rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border: 1px solid var(--border);\n background: #f8fafc;\n color: var(--text-muted);\n}\n\n${d} .org-menu-btn:hover {\n color: var(--primary-hover);\n border-color: rgb(148 163 184 / 0.65);\n}\n\n${d} .org-menu-btn svg {\n width: 1.1rem;\n height: 1.1rem;\n}\n\n${d} .org-menu-dropdown {\n position: absolute;\n top: calc(100% + 0.6rem);\n right: 0;\n min-width: 10.5rem;\n padding: 0.45rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #fff;\n box-shadow: 0 12px 28px rgb(15 23 42 / 0.12);\n display: flex;\n flex-direction: column;\n gap: 0.35rem;\n z-index: 5;\n}\n\n${d} .org-menu-option + .org-menu-option {\n margin-left: 0;\n}\n\n${d} .org-menu-option {\n width: 100%;\n justify-content: flex-start;\n background: transparent;\n color: var(--text-main);\n border: 1px solid transparent;\n}\n\n${d} .org-menu-option:hover {\n background: #f8fafc;\n border-color: rgb(148 163 184 / 0.35);\n}\n\n${d} .org-menu-option.is-active {\n background: rgb(226 232 240 / 0.7);\n color: var(--primary-hover);\n border-color: rgb(148 163 184 / 0.35);\n}\n\n${d} .org-finance-meta {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n margin-bottom: 1.5rem;\n}\n\n${d} .org-finance-meta > div {\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n display: flex;\n flex-direction: column;\n gap: 0.4rem;\n}\n\n${d} .org-meta-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${d} .org-action-grid {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n margin-bottom: 1rem;\n}\n\n${d} .org-action-grid button + button {\n margin-left: 0;\n}\n\n${d} .org-action-grid button {\n width: 100%;\n}\n\n${d} .org-access-note {\n margin: 0 0 1rem;\n color: var(--text-muted);\n font-size: 0.95rem;\n}\n\n${d} .org-credit-summary {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n padding: 0.85rem 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${d} .org-credit-summary strong {\n font-size: 1rem;\n}\n\n${d} .org-credit-summary span:last-child {\n font-size: 0.92rem;\n line-height: 1.45;\n}\n\n${d} .org-credit-lines-list {\n display: flex;\n flex-direction: column;\n gap: 0.85rem;\n}\n\n${d} .org-treasury-body {\n display: flex;\n flex: 1;\n flex-direction: column;\n gap: 1rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${d} .org-credit-line-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${d} .org-credit-line-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${d} .org-credit-line-member {\n display: flex;\n flex-direction: column;\n gap: 0.3rem;\n}\n\n${d} .org-credit-line-label {\n font-size: 0.76rem;\n text-transform: uppercase;\n letter-spacing: 0.08em;\n color: var(--text-muted);\n}\n\n${d} .org-credit-line-empty {\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n color: var(--text-muted);\n}\n\n@media (max-width: 960px) {\n ${d} .org-finance-meta {\n grid-template-columns: 1fr;\n }\n\n ${d} .org-credit-line-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.TreasuryCard=function(){const e=window.SharedUI.componentFns.PanelCard,t=o.getCreditLines(),a=o.getReputation(),d=i.canManageTreasury(),f=c(),b=g(),w=1===t.length?"1 active credit line":`${t.length} active credit lines`;return r("portal-treasury-card",u),e({className:"org-scroll-panel org-span-5",title:"Treasury",subtitle:"Organization funds, reputation and payouts.",headerExtras:n("div",{className:"org-treasury-menu"},n("button",{type:"button",className:"org-menu-btn",title:"Treasury views","aria-label":"Treasury views",onClick:()=>p(e=>!e)},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"currentColor","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round","aria-hidden":"true"},n("line",{x1:"4",y1:"7",x2:"20",y2:"7"}),n("line",{x1:"4",y1:"12",x2:"20",y2:"12"}),n("line",{x1:"4",y1:"17",x2:"20",y2:"17"}))),b?n("div",{className:"org-menu-dropdown"},n("button",{type:"button",className:"overview"===f?"org-menu-option is-active":"org-menu-option",onClick:()=>{m("overview"),p(!1)}},"Overview"),n("button",{type:"button",className:"credit"===f?"org-menu-option is-active":"org-menu-option",onClick:()=>{m("credit"),p(!1)}},"Credit Lines")):null),rootProps:{[l]:""},body:n("div",{className:"org-treasury-body"},"credit"===f?t.length>0?n("div",{className:"org-credit-lines-list"},...t.map(e=>n("article",{className:"org-credit-line-row"},n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Member"),n("strong",null,e.member)),n("div",{className:"org-credit-line-member"},n("span",{className:"org-credit-line-label"},"Amount"),n("strong",null,i.formatCurrency(e.amount)))))):n("div",{className:"org-credit-line-empty"},"No active credit lines."):n("div",null,n("div",{className:"org-finance-meta"},n("div",null,n("span",{className:"org-meta-label"},"Funds"),n("strong",null,i.formatCurrency(o.getFunds()))),n("div",null,n("span",{className:"org-meta-label"},"Reputation"),n("strong",null,`${a}`))),d?n("div",{className:"org-action-grid"},n("button",{type:"button",onClick:()=>s.openModal("payroll")},"Run Payroll"),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>s.openModal("transfer")},"Send Funds"),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>s.openModal("credit")},"Credit Line")):n("p",{className:"org-access-note"},"Only the organization leader or CEO can manage treasury actions."),n("div",{className:"org-credit-summary"},n("span",{className:"org-meta-label"},"Credit Line Status"),n("strong",null,w),n("span",null,t.length>0?"Open the Credit Lines tab to review assigned members and amounts.":"Assign a credit line to create the first approved member limit."))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a=e.getters,o="data-ui-assets-card",i=`[${o}]`,s=`\n${i} .org-simple-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${i} .org-simple-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${i} .org-simple-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${i} .org-simple-name {\n color: var(--primary-hover);\n}\n\n${i} .org-simple-meta {\n display: flex;\n flex-wrap: wrap;\n justify-content: flex-end;\n gap: 1rem;\n}\n\n@media (max-width: 960px) {\n ${i} .org-simple-row {\n flex-direction: column;\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.AssetsCard=function(){const t=window.SharedUI.componentFns.PanelCard,i=e.componentFns.SimpleStat,l=e.store.getAssets();return r("portal-assets-card",s),t({className:"org-scroll-panel org-span-7",title:"Assets",subtitle:"Inventory supplies and equipment with quantity totals.",rootProps:{[o]:""},body:n("div",{className:"org-simple-list"},...l.map(e=>n("article",{className:"org-simple-row"},n("strong",{className:"org-simple-name"},e.name),n("div",{className:"org-simple-meta"},i("Type",a.formatAssetType(e.type)),i("Quantity",e.quantity)))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.getters,o=e.actions,i="data-ui-members-card",s=`[${i}]`,l=`\n${s} .org-name-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${s} .org-name-row {\n display: flex;\n align-items: center;\n justify-content: flex-start;\n gap: 1rem;\n padding: 1rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${s} .org-name-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n}\n\n${s} .org-name-row button {\n margin-left: auto;\n}\n\n@media (max-width: 960px) {\n ${s} .org-name-row {\n flex-direction: column;\n align-items: flex-start;\n }\n\n ${s} .org-name-row button {\n margin-left: 0;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.MembersCard=function(){const e=window.SharedUI.componentFns.PanelCard,s=t.getMembers(),d=a.canManageMembers();return r("portal-members-card",l),e({className:"org-scroll-panel org-span-5",title:"Members",subtitle:"Current roster listing. The organization owner and your own member entry cannot be removed.",rootProps:{[i]:""},body:n("div",{className:"org-name-list"},...s.map(e=>{const r=d&&!a.isProtectedMember(e);return n("article",{className:"org-name-row"},n("strong",null,e.name),r?n("button",{type:"button",className:"org-danger-btn org-icon-btn",title:`Remove ${e.name}`,"aria-label":`Remove ${e.name}`,onClick:()=>o.removeMember(e)},n("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"},n("path",{d:"M9 3h6"}),n("path",{d:"M4 7h16"}),n("path",{d:"M6 7l1 13h10l1-13"}),n("path",{d:"M10 11v6"}),n("path",{d:"M14 11v6"}))):null)}))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t}=e.data,a="data-ui-activity-card",o=`[${a}]`,i=`\n${o} .org-activity-list {\n display: flex;\n flex-direction: column;\n flex: 1;\n gap: 0.85rem;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${o} .org-activity-row {\n padding: 1rem;\n border: 1px solid var(--border);\n border-left: 3px solid #94a3b8;\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${o} .org-activity-row:nth-child(even) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(148 163 184 / 0.45);\n border-left-color: #64748b;\n}\n\n${o} .org-activity-row p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .org-activity-time {\n display: inline-block;\n margin-bottom: 0.35rem;\n color: var(--text-muted);\n font-size: 0.8rem;\n font-weight: 700;\n letter-spacing: 0.05em;\n text-transform: uppercase;\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.ActivityCard=function(){const t=window.SharedUI.componentFns.PanelCard,o=e.store.getActivity();return r("portal-activity-card",i),t({className:"org-scroll-panel org-span-6",title:"Command Feed",subtitle:"Recent organization-level actions and updates.",rootProps:{[a]:""},body:n("div",{className:"org-activity-list"},...o.map(e=>n("article",{className:"org-activity-row"},n("span",{className:"org-activity-time"},e.time),n("p",null,e.text))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t="data-ui-future-card",a=[{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."}],o=`[${t}]`,i=`\n${o} .org-roadmap-grid {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 1rem;\n flex: 1;\n min-height: 0;\n overflow: auto;\n padding-right: 0.35rem;\n scrollbar-width: thin;\n scrollbar-color: #94a3b8 #e2e8f0;\n}\n\n${o} .org-roadmap-card {\n padding: 1rem;\n display: flex;\n flex-direction: column;\n gap: 0.7rem;\n border: 1px solid var(--border);\n border-radius: var(--radius);\n background: #f8fafc;\n}\n\n${o} .org-roadmap-card:nth-child(4n + 2),\n${o} .org-roadmap-card:nth-child(4n + 3) {\n background: linear-gradient(180deg, rgb(248 250 252) 0%, rgb(241 245 249) 100%);\n border-color: rgb(100 116 139 / 0.4);\n}\n\n${o} .org-roadmap-card p {\n margin: 0;\n color: var(--text-main);\n}\n\n${o} .org-list-tag {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0.2rem 0.55rem;\n border-radius: 999px;\n font-size: 0.72rem;\n font-weight: 700;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n background: #e2e8f0;\n color: var(--primary-hover);\n}\n\n${o} .org-roadmap-card:nth-child(4n + 2) .org-list-tag,\n${o} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {\n background: #cbd5e1;\n color: #1e293b;\n}\n\n@media (max-width: 960px) {\n ${o} .org-roadmap-grid {\n grid-template-columns: 1fr;\n }\n\n ${o} .org-roadmap-card:nth-child(4n + 3) {\n background: #f8fafc;\n border-color: var(--border);\n }\n\n ${o} .org-roadmap-card:nth-child(4n + 3) .org-list-tag {\n background: #e2e8f0;\n color: var(--primary-hover);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.FutureCard=function(){const e=window.SharedUI.componentFns.PanelCard;return r("portal-future-card",i),e({className:"org-scroll-panel org-span-6",title:"Expansion Slots",subtitle:"Potential modules are tagged by status such as Planned, In Design, In Review, and Future Review.",rootProps:{[t]:""},body:n("div",{className:"org-roadmap-grid"},...a.map(e=>n("article",{className:"org-roadmap-card"},n("span",{className:"org-list-tag"},e.status),n("strong",null,e.name),n("p",null,e.detail))))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.getters,a=e.actions,o="data-ui-danger-card",i=`[${o}]`,s=`\n${i} {\n border-color: #fecaca;\n background: linear-gradient(180deg, #ffffff 0%, #fff7f7 100%);\n}\n\n${i} .org-danger-copy {\n margin-bottom: 1rem;\n}\n\n${i} .org-danger-copy strong,\n${i} .org-danger-copy p {\n display: block;\n}\n\n${i} .org-danger-copy p {\n margin: 0.4rem 0 0;\n color: var(--text-muted);\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.DangerCard=function(){const e=window.SharedUI.componentFns.PanelCard;return r("portal-danger-card",s),t.canDisbandOrg()?e({className:"org-span-12 org-danger-panel",title:"Organization Controls",subtitle:"Leader-only actions for membership and permanent organization removal.",rootProps:{[o]:""},body:n("div",null,n("div",{className:"org-danger-copy"},n("strong",null,"Disband organization"),n("p",null,"This removes the organization and revokes access to the portal for all members.")),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.openModal("disband")},"Disband Organization"))}):null}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n}=e.runtime,{portalData:r}=e.data,t=e.store,a=e.actions;e.componentFns=e.componentFns||{},e.componentFns.ModalLayer=function(){const e=window.SharedUI.componentFns.Modal,o=t.getModal();if(!o)return null;const i=t.getMembers(),s=0===i.length?{disabled:!0}:{};let l="",d=null;return"payroll"===o.type?(l="Run Payroll",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Amount Per Member"),n("input",{id:"treasury-payroll-amount",type:"number",min:"1",placeholder:"500",autofocus:"true"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",onClick:()=>{a.runPayroll(a.parseAmount(a.getInputValue("treasury-payroll-amount")))&&a.closeModal()}},"Run Payroll")))):"transfer"===o.type?(l="Send Funds",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Member"),n("select",{id:"treasury-transfer-member",...s},...i.map(e=>n("option",{value:e.name},e.name)))),n("div",null,n("label",null,"Amount"),n("input",{id:"treasury-transfer-amount",type:"number",min:"1",placeholder:"1500"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...s,onClick:()=>{a.sendFundsToMember(String(a.getInputValue("treasury-transfer-member")||""),a.parseAmount(a.getInputValue("treasury-transfer-amount")))&&a.closeModal()}},"Send Funds")))):"credit"===o.type?(l="Assign Credit Line",d=n("div",{className:"app-modal-form"},n("div",null,n("label",null,"Member"),n("select",{id:"treasury-credit-member",...s},...i.map(e=>n("option",{value:e.uid},e.name)))),n("div",null,n("label",null,"Credit Amount"),n("input",{id:"treasury-credit-amount",type:"number",min:"1",placeholder:"5000"})),n("div",{className:"app-modal-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",...s,onClick:()=>{a.grantCreditLine(String(a.getInputValue("treasury-credit-member")||""),a.parseAmount(a.getInputValue("treasury-credit-amount")))&&a.closeModal()}},"Assign Credit Line")))):"disband"===o.type?(l="Disband Organization",d=n("div",{className:"app-modal-danger"},n("p",null,"This action is permanent. Disband ",r.org.name,"?"),n("div",{className:"app-modal-danger-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.disbandOrganization()},"Confirm Disband")))):"leave"===o.type&&(l="Leave Organization",d=n("div",{className:"app-modal-danger"},n("p",null,"Leave ",r.org.name," and return to the default organization?"),n("div",{className:"app-modal-danger-actions"},n("button",{type:"button",className:"org-secondary-btn",onClick:()=>a.closeModal()},"Cancel"),n("button",{type:"button",className:"org-danger-btn",onClick:()=>a.leaveOrganization()},"Confirm Leave")))),e({title:l,body:d,onClose:()=>a.closeModal()})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n}=e.runtime,{portalData:r}=e.data,t=window.RegistryApp.store;e.componentFns=e.componentFns||{},e.componentFns.DisbandedView=function(){return(0,window.SharedUI.componentFns.PanelCard)({className:"org-span-12 org-empty-state",eyebrow:"Organization Removed",title:r.org.name,body:n("div",null,n("p",{className:"org-summary"},"This organization has been disbanded. Member access, assets, and fleet management are no longer available from this portal preview."),n("button",{type:"button",className:"org-secondary-btn",onClick:()=>t.setView("home")},"Return to Registry"))})}}(),function(){const e=window.OrgPortal=window.OrgPortal||{},{h:n,ensureScopedStyle:r}=e.runtime,{portalData:t,session:a}=e.data,o=e.store,i="[data-ui-portal-view]";r("portal-view",`\n ${i} {\n --org-row-card-max-height: 36rem;\n }\n\n ${i} .org-toast-stack {\n position: fixed;\n top: 1.5rem;\n right: 2rem;\n z-index: 20;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n pointer-events: none;\n }\n\n ${i} .org-toast {\n max-width: 24rem;\n padding: 0.9rem 1rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: #fff;\n box-shadow: 0 12px 28px rgb(15 23 42 / 0.14);\n font-size: 0.92rem;\n pointer-events: auto;\n }\n\n ${i} .org-toast.is-success {\n background: #ecfdf5;\n border-color: #bbf7d0;\n color: #166534;\n }\n\n ${i} .org-toast.is-error {\n background: #fef2f2;\n border-color: #fecaca;\n color: #991b1b;\n }\n\n ${i} .org-dashboard-grid {\n display: grid;\n grid-template-columns: repeat(12, minmax(0, 1fr));\n gap: 1.5rem;\n align-items: stretch;\n }\n\n ${i} .org-panel {\n margin-bottom: 0;\n text-align: left;\n }\n\n ${i} .org-scroll-panel {\n display: flex;\n flex-direction: column;\n min-height: 0;\n max-height: var(--org-row-card-max-height);\n overflow: hidden;\n }\n\n ${i} .org-island-root {\n display: flex;\n align-self: stretch;\n min-height: 0;\n min-width: 0;\n }\n\n ${i} .org-island-root > .org-panel {\n height: 100%;\n width: 100%;\n }\n\n ${i} .org-span-12 {\n grid-column: span 12;\n }\n\n ${i} .org-span-7 {\n grid-column: span 7;\n }\n\n ${i} .org-span-6 {\n grid-column: span 6;\n }\n\n ${i} .org-span-5 {\n grid-column: span 5;\n }\n\n @media (max-width: 960px) {\n ${i} .org-toast-stack {\n top: 1rem;\n right: 1rem;\n left: 1rem;\n }\n\n ${i} .org-toast {\n max-width: none;\n }\n\n ${i} .org-span-12,\n ${i} .org-span-7,\n ${i} .org-span-6,\n ${i} .org-span-5 {\n grid-column: span 12;\n }\n\n ${i} .org-scroll-panel {\n max-height: none;\n }\n\n }\n `),e.components=e.components||{},e.componentFns=e.componentFns||{},e.componentFns.TreasuryNoticeLayer=function(){const e=o.getTreasuryNotice();return e.text?n("div",{className:"org-toast-stack"},n("div",{className:"error"===e.type?"org-toast is-error":"org-toast is-success"},e.text)):null},e.components.App=function(){const r=window.SharedUI.componentFns.Hero,i=window.SharedUI.componentFns.Footer,s=e.componentFns.FutureCard,l=e.componentFns.DangerCard,d=e.componentFns.DisbandedView,c=[{title:"Organization Controls",items:["Roster Management","Fleet Assignment","Treasury Permissions","Asset Registry"]},{title:"Planned Extensions",items:["Contracts Board","Diplomacy Layer","Procurement Queue","Reputation History"]}];return o.getOrgDisbanded()?n("main",{"data-ui-portal-view":""},n("div",{className:"container"},n("div",{className:"org-dashboard-grid"},r({kicker:t.org.tag,title:t.org.name,subtitle:"Player organization command portal",meta:`${a.actorName} - ${a.role}`}),d())),n("div",{id:"org-portal-modal-root"}),i({sections:c})):n("main",{"data-ui-portal-view":""},n("div",{id:"org-portal-toast-root"}),n("div",{className:"container"},n("div",{className:"org-dashboard-grid"},r({kicker:t.org.tag,title:t.org.name,subtitle:"Player organization command portal",meta:`${a.actorName} - ${a.role}`}),n("div",{className:"org-island-root org-span-12",id:"org-overview-card-root"}),n("div",{className:"org-island-root org-span-7",id:"org-fleet-card-root"}),n("div",{className:"org-island-root org-span-5",id:"org-treasury-card-root"}),n("div",{className:"org-island-root org-span-5",id:"org-members-card-root"}),n("div",{className:"org-island-root org-span-7",id:"org-assets-card-root"}),n("div",{className:"org-island-root org-span-6",id:"org-activity-card-root"}),s(),l())),n("div",{id:"org-portal-modal-root"}),i({sections:c}))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.bridge,o="data-ui-registration-view",i=`[${o}]`,s=`\n${i} {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2rem;\n align-items: center;\n width: 100%;\n}\n\n${i} .info-panel {\n text-align: left;\n padding: 1rem;\n}\n\n${i} .create-feature-list {\n text-align: left;\n margin-top: 1.5rem;\n list-style-type: none;\n padding: 0;\n}\n\n${i} .create-feature-item {\n margin-bottom: 0.5rem;\n display: flex;\n align-items: center;\n gap: 0.5rem;\n}\n\n${i} .create-feature-icon {\n width: 1.2rem;\n height: 1.2rem;\n flex-shrink: 0;\n}\n\n${i} .price-tag {\n margin-top: 2rem;\n padding: 1rem;\n background: var(--bg-app);\n border-radius: var(--radius);\n border: 1px solid var(--border);\n}\n\n${i} .price-label {\n display: block;\n font-size: 0.9rem;\n color: var(--text-muted);\n}\n\n${i} .price-value {\n display: block;\n font-size: 2rem;\n font-weight: 700;\n color: var(--primary);\n}\n\n${i} .form-panel {\n margin: 0;\n}\n\n${i} .app-form {\n display: flex;\n flex-direction: column;\n gap: 1rem;\n text-align: left;\n}\n\n${i} .app-form label {\n display: block;\n margin-bottom: 0.5rem;\n color: var(--text-muted);\n font-weight: 500;\n font-size: 0.9rem;\n}\n\n${i} .app-form input,\n${i} .app-form select {\n width: 100%;\n padding: 0.75rem;\n border-radius: var(--radius);\n border: 1px solid var(--border);\n background: var(--bg-app);\n color: var(--text-main);\n font-family: inherit;\n font-size: 1rem;\n box-sizing: border-box;\n transition: border-color 0.2s;\n}\n\n${i} .app-form input:focus,\n${i} .app-form select:focus {\n outline: none;\n border-color: var(--primary);\n box-shadow: 0 0 0 2px rgb(59 130 246 / 0.1);\n}\n\n${i} .form-actions {\n margin-top: 1rem;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n align-items: center;\n}\n\n${i} .submit-btn {\n width: 100%;\n}\n\n${i} .cancel-link {\n font-size: 0.9rem;\n color: var(--text-muted);\n cursor: pointer;\n text-decoration: underline;\n}\n\n${i} .cancel-link:hover {\n color: var(--primary);\n}\n\n${i} .form-feedback {\n padding: 0.85rem 1rem;\n border-radius: var(--radius);\n font-size: 0.92rem;\n}\n\n${i} .form-feedback.is-error {\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 960px) {\n ${i} {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.RegistrationView=function(){const e=t.getIsCreating(),i=t.getCreateError();r("main-registration-view",s);return n("div",{className:"split-container",[o]:""},n("div",{className:"info-panel"},n("h2",null,"Registration Details"),n("p",null,"Complete the form to add your organization to the Global Organization Registry."),n("ul",{className:"create-feature-list"},n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Official Organization Designator"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Secure Comms Channel"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"Deployment Roster Access"),n("li",{className:"create-feature-item"},n("svg",{viewBox:"0 0 24 24",fill:"none",stroke:"#10b981","stroke-width":"2","stroke-linecap":"round","stroke-linejoin":"round",className:"create-feature-icon"},n("path",{d:"M20 6L9 17l-5-5"})),"After-Action Report Tools")),n("div",{className:"price-tag"},n("span",{className:"price-label"},"Registration Fee"),n("span",{className:"price-value"},"$50,000"))),n("div",{className:"form-panel card"},n("h2",null,"Organization Registration"),n("div",{className:"app-form"},n("div",null,n("label",null,"Organization Name"),n("input",{id:"org-create-name",type:"text",placeholder:"e.g. Task Force 141"})),n("div",null,n("label",null,"Organization Type"),n("select",{id:"org-create-type"},n("option",{value:"infantry"},"Infantry / Milsim"),n("option",{value:"aviation"},"Aviation Wing"),n("option",{value:"pmc"},"Private Military Company"),n("option",{value:"support"},"Logistics & Support"))),n("div",{className:"form-actions"},i?n("div",{className:"form-feedback is-error"},i):null,n("button",{type:"button",className:"submit-btn",disabled:e,onClick:()=>{const e={orgName:String(document.getElementById("org-create-name")?.value||"").trim(),type:String(document.getElementById("org-create-type")?.value||"")};a&&"function"==typeof a.requestCreateOrg?a.requestCreateOrg(e):t.failCreate("Registration bridge is not available.")}},e?"Submitting Registration...":"Submit Registration"),n("span",{className:"cancel-link",onClick:()=>t.setView("home")},"Cancel / Return to Main")))))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n,ensureScopedStyle:r}=e.runtime,t=e.store,a=e.bridge,o="data-ui-home-view",i=`[${o}]`,s=`\n${i} {\n display: grid;\n grid-template-columns: 1fr 1fr;\n gap: 2rem;\n margin-bottom: 2rem;\n}\n\n${i} .home-feedback {\n padding: 0.85rem 1rem;\n border-radius: var(--radius);\n font-size: 0.92rem;\n background: #fef2f2;\n border: 1px solid #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 960px) {\n ${i} {\n grid-template-columns: 1fr;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.HomeView=function(){const e=t.getIsAuthenticating(),i=t.getLoginError();return r("main-home-view",s),n("div",{className:"content",[o]:""},n("div",{className:"card"},n("h2",null,"Create Organization"),n("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."),n("button",{onClick:()=>t.setView("create")},"Register")),n("div",{className:"card"},n("h2",null,"Organization Portal"),n("p",null,"Access your unit dashboard to modify rosters, adjust active deployments, and submit after-action reports through the secure field uplink."),i?n("div",{className:"home-feedback"},i):null,n("button",{disabled:e,onClick:()=>{a?a.requestLogin({}):t.failLogin("Login bridge is not available.")}},e?"Opening Portal...":"Login")))}}(),function(){const e=window.RegistryApp=window.RegistryApp||{},{h:n}=e.runtime,r=e.store;e.components=e.components||{},e.components.App=function(){const t=window.SharedUI.componentFns.Navbar,a=window.SharedUI.componentFns.Header,o=window.SharedUI.componentFns.Footer,i=window.SharedUI.componentFns.WindowTitleBar,s=e.componentFns.HomeView,l=e.componentFns.RegistrationView,d=window.OrgPortal&&window.OrgPortal.components?window.OrgPortal.components.App:null,c=r.getView(),m=window.OrgPortal&&window.OrgPortal.getters?window.OrgPortal.getters:null,g=window.OrgPortal&&window.OrgPortal.actions?window.OrgPortal.actions:null,p="create"===c?"Organization Registration":"portal"===c?"Organization Portal":"Entry Hub";function u(){e.bridge&&"function"==typeof e.bridge.close?e.bridge.close({}):r.setView("home")}if("portal"===c&&d){const e=m&&"function"==typeof m.canLeaveOrg&&m.canLeaveOrg();return n("div",{className:"app-shell"},i({kicker:"FORGE ORBIS",title:"Global Organization Network",onClose:u,closeLabel:"Close organization interface"}),t({title:"Global Organization Network",viewLabel:p,actionLabel:e?"Leave Organization":"",onAction:e&&g&&"function"==typeof g.openModal?()=>g.openModal("leave"):null}),n("div",{id:"org-portal-frame-root"}))}let f;return"home"===c?f=s():"create"===c&&(f=l()),n("div",{className:"app-shell"},i({kicker:"FORGE ORBIS",title:"Global Organization Network",onClose:u,closeLabel:"Close organization interface"}),n("main",null,t({title:"Global Organization Network",viewLabel:p}),n("div",{className:"container"},a({title:"Global Organization Network",onTitleClick:()=>r.setView("home")}),f),o({sections:[{title:"Registry Resources",items:["Registration Guidelines","Tax & Fee Schedule","Legal Compliance","Trademark Database"]},{title:"Bureau Support",items:["Office: Sector 7 Admin Block","Hours: 0800 - 1600 (GST)","Helpdesk: 555-01-REGISTRY","support@org-bureau.gov"]}]})))}}(),function(){const e=window.ForgeWebUI,n=window.RegistryApp,r=window.OrgPortal,t=[{id:"org-portal-frame-root",preserveScroll:!0,render:()=>r.components.App()},{id:"org-portal-toast-root",preserveScroll:!1,render:()=>r.componentFns.TreasuryNoticeLayer()},{id:"org-overview-card-root",preserveScroll:!1,render:()=>r.componentFns.OverviewCard()},{id:"org-fleet-card-root",preserveScroll:!0,render:()=>r.componentFns.FleetCard()},{id:"org-treasury-card-root",preserveScroll:!1,render:()=>r.componentFns.TreasuryCard()},{id:"org-members-card-root",preserveScroll:!0,render:()=>r.componentFns.MembersCard()},{id:"org-assets-card-root",preserveScroll:!0,render:()=>r.componentFns.AssetsCard()},{id:"org-activity-card-root",preserveScroll:!0,render:()=>r.componentFns.ActivityCard()},{id:"org-portal-modal-root",preserveScroll:!1,render:()=>r.componentFns.ModalLayer()}];e.createApp({name:"org",root:"#app",setup({root:r}){const a=function(){const n=new Map;return{sync:function(){t.forEach(r=>{const t=document.getElementById(r.id),a=n.get(r.id);if(!t)return void(a&&(a.handle.dispose(),n.delete(r.id)));if(a&&a.container===t)return;a&&a.handle.dispose();const o=e.mount(t,r.render,{preserveScroll:r.preserveScroll});n.set(r.id,{container:t,handle:o})})}}}();e.mount(r,()=>n.components.App(),{preserveScroll:!1}),n.bridge.ready({loaded:!0}),e.effect(()=>{n.store.getView(),requestAnimationFrame(()=>{a.sync()})})}}).start()}(); \ No newline at end of file diff --git a/arma/client/addons/store/XEH_PREP.hpp b/arma/client/addons/store/XEH_PREP.hpp index 780173e..868066a 100644 --- a/arma/client/addons/store/XEH_PREP.hpp +++ b/arma/client/addons/store/XEH_PREP.hpp @@ -1,6 +1,5 @@ PREP(buildUIPayload); PREP(handleUIEvents); -PREP(initCatalogService); PREP(initClass); PREP(initUIBridge); PREP(openUI); diff --git a/arma/client/addons/store/XEH_postInitClient.sqf b/arma/client/addons/store/XEH_postInitClient.sqf index bcf83fa..ac1eb81 100644 --- a/arma/client/addons/store/XEH_postInitClient.sqf +++ b/arma/client/addons/store/XEH_postInitClient.sqf @@ -1,9 +1,14 @@ #include "script_component.hpp" -if (isNil QGVAR(StoreCatalogService)) then { call FUNC(initCatalogService); }; if (isNil QGVAR(StoreClass)) then { call FUNC(initClass); }; if (isNil QGVAR(StoreUIBridge)) then { call FUNC(initUIBridge); }; +[QGVAR(responseCategory), { + params [["_payload", createHashMap, [createHashMap]]]; + + GVAR(StoreUIBridge) call ["handleCategoryResponse", [_payload]]; +}] call CFUNC(addEventHandler); + [QGVAR(responseCheckout), { params [["_payload", createHashMap, [createHashMap]]]; diff --git a/arma/client/addons/store/functions/fnc_initCatalogService.sqf b/arma/client/addons/store/functions/fnc_initCatalogService.sqf deleted file mode 100644 index 6f63302..0000000 --- a/arma/client/addons/store/functions/fnc_initCatalogService.sqf +++ /dev/null @@ -1,294 +0,0 @@ -#include "..\script_component.hpp" - -/* - * File: fnc_initCatalogService.sqf - * Author: IDSolutions - * Date: 2026-03-13 - * Public: No - * - * Description: - * Initializes the store catalog service for category discovery, pricing, and payload shaping. - */ - -#pragma hemtt ignore_variables ["_self"] -GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ - ["#type", "StoreCatalogServiceBaseClass"], - ["#create", compileFinal { - _self set ["catalogCache", createHashMap]; - }], - ["isVisibleConfig", compileFinal { - params [["_cfg", configNull, [configNull]]]; - - isClass _cfg - && { getNumber (_cfg >> "scope") >= 2 } - && { (getText (_cfg >> "displayName")) isNotEqualTo "" } - }], - ["buildDescription", compileFinal { - params [["_cfg", configNull, [configNull]], ["_fallback", "", [""]]]; - - private _description = getText (_cfg >> "descriptionShort"); - if (_description isEqualTo "") then { _description = _fallback; }; - - _description - }], - ["formatPriceValue", compileFinal { - params [["_priceValue", 0, [0]]]; - - format ["$%1", [_priceValue max 0] call BIS_fnc_numberText] - }], - ["calculateItemPrice", compileFinal { - params [ - ["_cfg", configNull, [configNull]], - ["_isVehicle", false, [false]] - ]; - - if (isNull _cfg) exitWith { "$50" }; - - private _mass = 0; - private _priceValue = 0; - - if (_isVehicle) then { - _priceValue = getNumber (_cfg >> "cost"); - } else { - _mass = getNumber (_cfg >> "ItemInfo" >> "mass"); - if (_mass <= 0) then { - _mass = getNumber (_cfg >> "mass"); - }; - - _priceValue = ceil ((_mass max 0) * 0.1); - }; - - _priceValue = _priceValue max 50; - _self call ["formatPriceValue", [_priceValue]] - }], - ["buildItem", compileFinal { - params [["_cfg", configNull, [configNull]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]], ["_imageField", "picture", [""]], ["_isVehicle", false, [false]]]; - - if (isNull _cfg) exitWith { createHashMap }; - - private _className = configName _cfg; - private _displayName = getText (_cfg >> "displayName"); - private _picture = getText (_cfg >> _imageField); - if (_picture isEqualTo "" && { _imageField isNotEqualTo "picture" }) then { _picture = getText (_cfg >> "picture"); }; - - createHashMapFromArray [ - ["className", _className], - ["code", _className], - ["name", _displayName], - ["description", _self call ["buildDescription", [_cfg, _fallbackDescription]]], - ["price", _self call ["calculateItemPrice", [_cfg, _isVehicle]]], - ["image", _picture], - ["type", _typeLabel] - ] - }], - ["appendCfgWeaponsByItemInfoType", compileFinal { - params [["_items", [], [[]]], ["_itemInfoType", -1, [0]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]]; - - { - private _cfg = _x; - if ( - _self call ["isVisibleConfig", [_cfg]] - && { getNumber (_cfg >> "ItemInfo" >> "type") isEqualTo _itemInfoType } - ) then { - _items pushBack (_self call ["buildItem", [_cfg, _typeLabel, _fallbackDescription]]); - }; - } forEach ("true" configClasses (configFile >> "CfgWeapons")); - - _items - }], - ["appendCfgWeaponsByType", compileFinal { - params [["_items", [], [[]]], ["_weaponType", -1, [0]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]]; - - { - private _cfg = _x; - if ( - _self call ["isVisibleConfig", [_cfg]] - && { getNumber (_cfg >> "type") isEqualTo _weaponType } - ) then { - _items pushBack (_self call ["buildItem", [_cfg, _typeLabel, _fallbackDescription]]); - }; - } forEach ("true" configClasses (configFile >> "CfgWeapons")); - - _items - }], - ["appendCfgVehiclesByKind", compileFinal { - params [["_items", [], [[]]], ["_baseClass", "", [""]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]]; - - { - private _cfg = _x; - private _className = configName _cfg; - if ( - _self call ["isVisibleConfig", [_cfg]] - && { getNumber (_cfg >> "isBackpack") isEqualTo 0 } - && { !(_className isKindOf ["CAManBase", configFile >> "CfgVehicles"]) } - && { !(_className isKindOf ["StaticWeapon", configFile >> "CfgVehicles"]) } - && { _className isKindOf [_baseClass, configFile >> "CfgVehicles"] } - ) then { - _items pushBack (_self call ["buildItem", [_cfg, _typeLabel, _fallbackDescription, "editorPreview", true]]); - }; - } forEach ("true" configClasses (configFile >> "CfgVehicles")); - - _items - }], - ["scanCategoryItems", compileFinal { - params [["_category", "", [""]]]; - - private _categoryKey = toLowerANSI _category; - if (_categoryKey isEqualTo "") exitWith { [] }; - - private _items = []; - - switch (_categoryKey) do { - case "uniforms": { - _items = _self call ["appendCfgWeaponsByItemInfoType", [_items, 801, "Uniform", "Live uniform entry generated from the game inventory."]]; - }; - case "headgear": { - _items = _self call ["appendCfgWeaponsByItemInfoType", [_items, 605, "Headgear", "Live headgear entry generated from the game inventory."]]; - }; - case "vests": { - _items = _self call ["appendCfgWeaponsByItemInfoType", [_items, 701, "Vest", "Live vest entry generated from the game inventory."]]; - }; - case "facewear": { - { - private _cfg = _x; - if (_self call ["isVisibleConfig", [_cfg]]) then { - _items pushBack (_self call ["buildItem", [_cfg, "Facewear", "Live facewear entry generated from the game inventory."]]); - }; - } forEach ("true" configClasses (configFile >> "CfgGlasses")); - }; - case "ammo": { - { - private _cfg = _x; - if (_self call ["isVisibleConfig", [_cfg]]) then { - _items pushBack (_self call ["buildItem", [_cfg, "Magazine", "Live ammunition entry generated from the game inventory."]]); - }; - } forEach ("true" configClasses (configFile >> "CfgMagazines")); - }; - case "items": { - { - private _cfg = _x; - private _className = configName _cfg; - private _itemType = [_className] call BIS_fnc_itemType; - private _group = _itemType param [0, ""]; - private _kind = _itemType param [1, ""]; - - if ( - _self call ["isVisibleConfig", [_cfg]] - && { _group in ["Item", "Equipment"] } - && { !(_kind in ["Uniform", "Vest", "Headgear"]) } - ) then { - private _typeLabel = [_kind, "Item"] select (_kind isEqualTo ""); - _items pushBack (_self call ["buildItem", [_cfg, _typeLabel, "Live utility entry generated from the game inventory."]]); - }; - } forEach ("true" configClasses (configFile >> "CfgWeapons")); - }; - case "primary": { - _items = _self call ["appendCfgWeaponsByType", [_items, 1, "Primary Weapon", "Live primary weapon entry generated from the game inventory."]]; - }; - case "handgun": { - _items = _self call ["appendCfgWeaponsByType", [_items, 2, "Handgun", "Live sidearm entry generated from the game inventory."]]; - }; - case "secondary": { - _items = _self call ["appendCfgWeaponsByType", [_items, 4, "Launcher", "Live launcher entry generated from the game inventory."]]; - }; - case "cars": { - _items = _self call ["appendCfgVehiclesByKind", [_items, "Car", "Vehicle", "Live wheeled vehicle entry generated from the game inventory."]]; - }; - case "armor": { - _items = _self call ["appendCfgVehiclesByKind", [_items, "Tank", "Vehicle", "Live armored vehicle entry generated from the game inventory."]]; - }; - case "helis": { - _items = _self call ["appendCfgVehiclesByKind", [_items, "Helicopter", "Aircraft", "Live helicopter entry generated from the game inventory."]]; - }; - case "planes": { - _items = _self call ["appendCfgVehiclesByKind", [_items, "Plane", "Aircraft", "Live fixed-wing entry generated from the game inventory."]]; - }; - case "naval": { - _items = _self call ["appendCfgVehiclesByKind", [_items, "Ship", "Naval", "Live naval vehicle entry generated from the game inventory."]]; - }; - case "other": { - { - private _cfg = _x; - private _className = configName _cfg; - private _isSupportedVehicle = _className isKindOf ["AllVehicles", configFile >> "CfgVehicles"]; - private _isKnownCategory = - _className isKindOf ["Car", configFile >> "CfgVehicles"] - || { _className isKindOf ["Tank", configFile >> "CfgVehicles"] } - || { _className isKindOf ["Helicopter", configFile >> "CfgVehicles"] } - || { _className isKindOf ["Plane", configFile >> "CfgVehicles"] } - || { _className isKindOf ["Ship", configFile >> "CfgVehicles"] }; - - if ( - _self call ["isVisibleConfig", [_cfg]] - && { _isSupportedVehicle } - && { !_isKnownCategory } - && { getNumber (_cfg >> "isBackpack") isEqualTo 0 } - && { !(_className isKindOf ["CAManBase", configFile >> "CfgVehicles"]) } - && { !(_className isKindOf ["StaticWeapon", configFile >> "CfgVehicles"]) } - ) then { - _items pushBack (_self call ["buildItem", [ - _cfg, - "Special Vehicle", - "Live specialty vehicle entry generated from the game inventory.", - "editorPreview", - true - ]]); - }; - } forEach ("true" configClasses (configFile >> "CfgVehicles")); - }; - }; - - private _sortedItems = _items apply { [toLowerANSI (_x getOrDefault ["name", ""]), _x] }; - - _sortedItems sort true; - _sortedItems apply { _x select 1 } - }], - ["isVehicleCategory", compileFinal { - params [["_category", "", [""]]]; - - (toLowerANSI _category) in ["cars", "armor", "helis", "planes", "naval", "other"] - }], - ["buildPayloadCategory", compileFinal { - params [["_category", "", [""]]]; - - switch (toLowerANSI _category) do { - case "ammo": { "magazine" }; - case "primary"; - case "secondary"; - case "handgun": { "weapon" }; - case "cars"; - case "armor"; - case "helis"; - case "planes"; - case "naval"; - case "other": { toLowerANSI _category }; - default { "item" }; - } - }], - ["buildCategoryItems", compileFinal { - params [["_category", "", [""]]]; - - private _categoryKey = toLowerANSI _category; - if (_categoryKey isEqualTo "") exitWith { [] }; - - private _catalogCache = _self getOrDefault ["catalogCache", createHashMap]; - if (_categoryKey in (keys _catalogCache)) exitWith { _catalogCache get _categoryKey }; - - private _items = _self call ["scanCategoryItems", [_categoryKey]]; - private _payloadCategory = _self call ["buildPayloadCategory", [_categoryKey]]; - private _entryKind = ["item", "vehicle"] select (_self call ["isVehicleCategory", [_categoryKey]]); - - { - _x set ["category", _payloadCategory]; - _x set ["entryKind", _entryKind]; - } forEach _items; - - _catalogCache set [_categoryKey, _items]; - _self set ["catalogCache", _catalogCache]; - - _items - }] -]; - -GVAR(StoreCatalogService) = createHashMapObject [GVAR(StoreCatalogServiceBaseClass)]; -GVAR(StoreCatalogService) diff --git a/arma/client/addons/store/functions/fnc_initUIBridge.sqf b/arma/client/addons/store/functions/fnc_initUIBridge.sqf index f6cba7e..2e707d0 100644 --- a/arma/client/addons/store/functions/fnc_initUIBridge.sqf +++ b/arma/client/addons/store/functions/fnc_initUIBridge.sqf @@ -37,7 +37,6 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [ private _targetControl = _control; if (isNull _targetControl) then { _targetControl = _self call ["getActiveBrowserControl", []]; }; - if (isNull _targetControl) exitWith { false }; _self call ["execBridge", [_targetControl, "receive", createHashMapFromArray [ @@ -55,27 +54,29 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [ params [["_data", createHashMap, [createHashMap]]]; private _category = toLowerANSI (_data getOrDefault ["category", ""]); + private _uid = getPlayerUID player; if (_category isEqualTo "") exitWith { _self call ["sendBridgeEvent", ["store::category::failure", createHashMapFromArray [ ["message", "No store category was provided."] ]]]; }; - if (isNil QGVAR(StoreCatalogService)) exitWith { + if (_uid isEqualTo "") exitWith { _self call ["sendBridgeEvent", ["store::category::failure", createHashMapFromArray [ ["category", _category], - ["message", "Store catalog is unavailable."] + ["message", "Store catalog request is unavailable."] ]]]; }; - private _items = GVAR(StoreCatalogService) call ["buildCategoryItems", [_category]]; + diag_log format ["[FORGE:Client:Store] Category request forwarded to server: %1", _category]; + [SRPC(store,requestCategory), [_uid, _category]] call CFUNC(serverEvent); + }], + ["handleCategoryResponse", compileFinal { + params [["_payload", createHashMap, [createHashMap]]]; - diag_log format ["[FORGE:Client:Store] Category request handled for %1 with %2 item(s).", _category, count _items]; - - _self call ["sendBridgeEvent", ["store::category::hydrate", createHashMapFromArray [ - ["category", _category], - ["items", _items] - ]]]; + private _success = _payload getOrDefault ["success", false]; + private _bridgeEvent = ["store::category::failure", "store::category::hydrate"] select _success; + _self call ["sendBridgeEvent", [_bridgeEvent, _payload]]; }], ["refreshStoreConfig", compileFinal { private _payload = call FUNC(buildUIPayload); diff --git a/arma/client/addons/store/ui/_site/index.html b/arma/client/addons/store/ui/_site/index.html index 019f432..87ac510 100644 --- a/arma/client/addons/store/ui/_site/index.html +++ b/arma/client/addons/store/ui/_site/index.html @@ -1,64 +1 @@ - - - - - - - FORGE Supply Exchange - - - - -
- - +FORGE Supply Exchange
\ No newline at end of file diff --git a/arma/client/addons/store/ui/_site/store-ui.css b/arma/client/addons/store/ui/_site/store-ui.css index 10869de..92a725c 100644 --- a/arma/client/addons/store/ui/_site/store-ui.css +++ b/arma/client/addons/store/ui/_site/store-ui.css @@ -1,90 +1 @@ -/* Generated by tools/build-webui.mjs for Store UI styles. Do not edit directly. */ -:root { - --store-shell-bg: #e4e3df; - --store-surface: #f5f3ef; - --store-surface-alt: #ece8e2; - --store-surface-strong: #ffffff; - --store-border: rgba(74, 91, 110, 0.2); - --store-border-strong: rgba(20, 46, 79, 0.2); - --store-text-main: #1f2d3d; - --store-text-muted: #6a7787; - --store-text-subtle: #8792a0; - --store-accent: #12365d; - --store-accent-soft: #dbe7f3; - --store-accent-line: rgba(18, 54, 93, 0.12); - --store-success: #2f7d5b; - --store-danger: #8a3d3d; -} - -* { - box-sizing: border-box; -} - -html, -body { - width: 100%; - height: 100%; - margin: 0; - overflow: hidden; -} - -body { - font-family: "Segoe UI", "Trebuchet MS", sans-serif; - color: var(--store-text-main); - background: var(--store-shell-bg); -} - -button, -input, -select { - font: inherit; -} - -button { - cursor: pointer; -} - -button:disabled { - cursor: not-allowed; - opacity: 0.7; -} - -:focus-visible { - outline: 2px solid rgb(18 54 93 / 0.35); - outline-offset: 2px; -} - -#app { - width: 100%; - height: 100%; -} - -.store-btn { - min-height: 2.75rem; - padding: 0.72rem 1rem; - border-radius: 0.8rem; - border: 1px solid var(--store-border-strong); - font-size: 0.82rem; - font-weight: 700; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -.store-btn.store-btn-primary { - background: rgb(255 255 255 / 0.68); - color: var(--store-accent); -} - -.store-btn.store-btn-primary:hover { - background: rgb(219 231 243 / 0.88); -} - -.store-btn.store-btn-secondary { - background: rgb(255 255 255 / 0.42); - color: var(--store-text-muted); -} - -.store-btn.store-btn-secondary:hover { - background: rgb(255 255 255 / 0.6); - color: var(--store-text-main); -} +:root{--store-shell-bg:#e4e3df;--store-surface:#f5f3ef;--store-surface-alt:#ece8e2;--store-surface-strong:#fff;--store-border:#4a5b6e33;--store-border-strong:#142e4f33;--store-text-main:#1f2d3d;--store-text-muted:#6a7787;--store-text-subtle:#8792a0;--store-accent:#12365d;--store-accent-soft:#dbe7f3;--store-accent-line:#12365d1f;--store-success:#2f7d5b;--store-danger:#8a3d3d}*{box-sizing:border-box}html,body{width:100%;height:100%;margin:0;overflow:hidden}body{color:var(--store-text-main);background:var(--store-shell-bg);font-family:Segoe UI,Trebuchet MS,sans-serif}button,input,select{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.7}:focus-visible{outline-offset:2px;outline:2px solid #12365d59}#app{width:100%;height:100%}.store-btn{border:1px solid var(--store-border-strong);letter-spacing:.08em;text-transform:uppercase;border-radius:.8rem;min-height:2.75rem;padding:.72rem 1rem;font-size:.82rem;font-weight:700}.store-btn.store-btn-primary{color:var(--store-accent);background:#ffffffad}.store-btn.store-btn-primary:hover{background:#dbe7f3e0}.store-btn.store-btn-secondary{color:var(--store-text-muted);background:#ffffff6b}.store-btn.store-btn-secondary:hover{color:var(--store-text-main);background:#fff9} \ No newline at end of file diff --git a/arma/client/addons/store/ui/_site/store-ui.js b/arma/client/addons/store/ui/_site/store-ui.js index 920b347..ffa5a91 100644 --- a/arma/client/addons/store/ui/_site/store-ui.js +++ b/arma/client/addons/store/ui/_site/store-ui.js @@ -1,3501 +1 @@ -/* Generated by tools/build-webui.mjs for Store UI app. Do not edit directly. */ -(function () { - const runtime = window.ForgeWebUI; - const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {}); - StorefrontApp.runtime = runtime; - window.AppRuntime = runtime; -})(); - -(function () { - const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {}); - const runtime = StorefrontApp.runtime; - const [getTextureVersion, setTextureVersion] = runtime.createSignal(0); - const MAX_CONCURRENT_TEXTURES = 6; - const RERENDER_DELAY_MS = 48; - const textureCache = Object.create(null); - const textureRequests = Object.create(null); - const queuedTexturePaths = []; - const queuedTextureLookup = Object.create(null); - const visibleTexturePaths = Object.create(null); - const observedTextureNodes = new WeakSet(); - let activeTextureRequests = 0; - let observer = null; - let observerRoot = null; - let rerenderTimer = 0; - - function normalizeTexturePath(path) { - let normalizedPath = String(path || "").trim(); - if (!normalizedPath) { - return ""; - } - - while ( - normalizedPath.startsWith("\\") || - normalizedPath.startsWith("/") - ) { - normalizedPath = normalizedPath.slice(1); - } - - if (!/\.[A-Za-z0-9]+$/.test(normalizedPath)) { - normalizedPath += ".paa"; - } - - return normalizedPath; - } - - function isBrowserTextureSource(path) { - const value = String(path || "") - .trim() - .toLowerCase(); - return ( - value.startsWith("data:image/") || - value.startsWith("blob:") || - value.startsWith("http://") || - value.startsWith("https://") - ); - } - - function finalizeTextureSource(path, source) { - textureCache[path] = source; - - scheduleRerender(); - } - - function scheduleRerender() { - if (rerenderTimer) { - return; - } - - rerenderTimer = window.setTimeout(() => { - rerenderTimer = 0; - setTextureVersion((currentVersion) => currentVersion + 1); - }, RERENDER_DELAY_MS); - } - - function pumpTextureQueue() { - if ( - typeof A3API === "undefined" || - typeof A3API.RequestTexture !== "function" - ) { - return; - } - - while ( - activeTextureRequests < MAX_CONCURRENT_TEXTURES && - queuedTexturePaths.length > 0 - ) { - const normalizedPath = queuedTexturePaths.shift(); - delete queuedTextureLookup[normalizedPath]; - - if ( - !normalizedPath || - textureCache[normalizedPath] !== undefined || - textureRequests[normalizedPath] - ) { - continue; - } - - activeTextureRequests += 1; - textureRequests[normalizedPath] = Promise.resolve( - A3API.RequestTexture(normalizedPath, 512), - ) - .then((resolvedPath) => { - const textureSource = String(resolvedPath || "").trim(); - - if (isBrowserTextureSource(textureSource)) { - finalizeTextureSource(normalizedPath, textureSource); - return; - } - - console.warn( - "[Store UI] Ignoring unsupported texture response.", - normalizedPath, - textureSource, - ); - finalizeTextureSource(normalizedPath, ""); - }) - .catch((error) => { - console.warn( - "[Store UI] Failed to resolve texture.", - normalizedPath, - error, - ); - finalizeTextureSource(normalizedPath, ""); - }) - .finally(() => { - activeTextureRequests = Math.max( - 0, - activeTextureRequests - 1, - ); - delete textureRequests[normalizedPath]; - pumpTextureQueue(); - }); - } - } - - function queueTextureRequest(path) { - if (!path || queuedTextureLookup[path] || textureRequests[path]) { - return; - } - - queuedTextureLookup[path] = true; - queuedTexturePaths.push(path); - pumpTextureQueue(); - } - - function markTextureVisible(path) { - const normalizedPath = normalizeTexturePath(path); - if (!normalizedPath || visibleTexturePaths[normalizedPath]) { - return; - } - - visibleTexturePaths[normalizedPath] = true; - if ( - !isBrowserTextureSource(textureCache[normalizedPath]) && - !textureRequests[normalizedPath] - ) { - queueTextureRequest(normalizedPath); - } - } - - function ensureObserver() { - const currentRoot = document.querySelector(".catalog-grid"); - if (typeof IntersectionObserver !== "function") { - return null; - } - - if (observer && observerRoot === currentRoot) { - return observer; - } - - if (observer) { - observer.disconnect(); - } - - observerRoot = currentRoot; - observer = new IntersectionObserver( - (entries) => { - entries.forEach((entry) => { - if (!entry.isIntersecting) { - return; - } - - const rawPath = entry.target.getAttribute( - "data-store-texture-path", - ); - markTextureVisible(rawPath); - observer.unobserve(entry.target); - }); - }, - { - root: currentRoot, - rootMargin: "240px 0px", - threshold: 0.01, - }, - ); - - return observer; - } - - function observeTextureTargets() { - const targets = document.querySelectorAll("[data-store-texture-path]"); - if (targets.length === 0) { - return; - } - - const activeObserver = ensureObserver(); - targets.forEach((target) => { - if (observedTextureNodes.has(target)) { - return; - } - - observedTextureNodes.add(target); - - const rawPath = target.getAttribute("data-store-texture-path"); - if (!activeObserver) { - markTextureVisible(rawPath); - return; - } - - activeObserver.observe(target); - }); - } - - function scheduleTextureObservation() { - window.requestAnimationFrame(() => { - observeTextureTargets(); - }); - } - - function getTextureState(path) { - getTextureVersion(); - const normalizedPath = normalizeTexturePath(path); - return { - path: normalizedPath, - isVisible: Boolean( - normalizedPath && visibleTexturePaths[normalizedPath], - ), - isLoaded: Boolean( - normalizedPath && - textureCache[normalizedPath] && - isBrowserTextureSource(textureCache[normalizedPath]), - ), - }; - } - - function getTextureSource(path) { - getTextureVersion(); - const normalizedPath = normalizeTexturePath(path); - if (!normalizedPath) { - return ""; - } - - if (isBrowserTextureSource(path)) { - textureCache[normalizedPath] = String(path).trim(); - return textureCache[normalizedPath]; - } - - if (textureCache[normalizedPath] !== undefined) { - return textureCache[normalizedPath]; - } - - if (visibleTexturePaths[normalizedPath]) { - queueTextureRequest(normalizedPath); - return ""; - } - - return ""; - } - - StorefrontApp.media = { - getTextureState, - getTextureSource, - scheduleTextureObservation, - }; -})(); - -(function () { - const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {}); - - const defaultSession = { - actorName: "", - actorUid: "", - approval: "Field Access", - orgId: "", - orgName: "", - orgLeader: false, - defaultOrgCeo: false, - canUseOrgFunds: false, - }; - - const defaultStoreConfig = { - budget: 50000, - creditLine: 0, - availability: "In-Stock", - moduleState: "Preview", - searchTags: [ - "Attachment", - "Grenade", - "Medical", - "Consumable", - "Static", - "Scope", - "Item", - "Misc", - ], - paymentSources: [ - { - id: "cash", - label: "Cash", - balance: 0, - enabled: false, - detail: "Use on-hand cash carried by the player.", - }, - { - id: "bank", - label: "Bank", - balance: 0, - enabled: false, - detail: "Charge the player bank account.", - }, - { - id: "org_funds", - label: "Org Funds", - balance: 0, - enabled: false, - detail: "Only organization leaders or the default-org CEO can use treasury funds.", - }, - { - id: "credit_line", - label: "Credit Line", - balance: 0, - enabled: false, - detail: "No approved credit line is assigned to this member.", - }, - ], - defaultPaymentSource: "cash", - }; - - function cloneValue(value) { - return JSON.parse(JSON.stringify(value)); - } - - function replaceObject(target, source) { - Object.keys(target).forEach((key) => delete target[key]); - Object.assign(target, cloneValue(source)); - } - - const catalog = { - categoryCards: [ - { id: "uniforms", label: "Uniforms" }, - { id: "headgear", label: "Headgear" }, - { id: "facewear", label: "Facewear" }, - { id: "vests", label: "Vests" }, - { id: "weapons", label: "Weapons" }, - { id: "ammo", label: "Ammo" }, - { id: "items", label: "Items" }, - { id: "vehicles", label: "Vehicles" }, - ], - vehicleCards: [ - { id: "cars", label: "Cars" }, - { id: "armor", label: "Armor" }, - { id: "helis", label: "Helicopters" }, - { id: "planes", label: "Planes" }, - { id: "naval", label: "Naval" }, - { id: "other", label: "Other" }, - ], - weaponCards: [ - { id: "primary", label: "Primary" }, - { id: "secondary", label: "Secondary" }, - { id: "handgun", label: "Handgun" }, - ], - previewItems: { - uniforms: [], - headgear: [], - facewear: [], - vests: [], - ammo: [], - items: [], - primary: [], - secondary: [], - handgun: [], - cars: [], - armor: [], - helis: [], - planes: [], - naval: [], - other: [], - }, - }; - - StorefrontApp.data = { - catalog, - session: Object.assign({}, defaultSession), - storeConfig: Object.assign({}, defaultStoreConfig), - applyHydratePayload(payload) { - replaceObject( - this.session, - Object.assign({}, defaultSession, payload?.session || {}), - ); - replaceObject( - this.storeConfig, - Object.assign( - {}, - defaultStoreConfig, - payload?.storeConfig || {}, - ), - ); - }, - }; -})(); - -(function () { - const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {}); - const { createSignal } = StorefrontApp.runtime; - const SharedLogic = (window.SharedLogic = window.SharedLogic || {}); - - SharedLogic.createStorefrontStore = function createStorefrontStore({ - createSignal, - }) { - function normalizeCatalogItem(item) { - return { - className: String(item?.className || item?.code || ""), - code: String(item?.code || item?.className || ""), - name: String(item?.name || item?.displayName || ""), - description: String(item?.description || ""), - price: String(item?.price || ""), - image: String(item?.image || ""), - type: String(item?.type || ""), - category: String(item?.category || ""), - entryKind: String(item?.entryKind || "item"), - quantity: Math.max(0, Number(item?.quantity || 0)), - }; - } - - function normalizeCartItem(item) { - return { - code: String(item?.code || ""), - name: String(item?.name || ""), - price: String(item?.price || "$0"), - category: String(item?.category || ""), - entryKind: String(item?.entryKind || "item"), - quantity: Math.max(1, Number(item?.quantity || 1)), - }; - } - - class StorefrontStore { - constructor() { - [this.getView, this.setView] = createSignal("categories"); - [this.getSelectedCategory, this.setSelectedCategory] = - createSignal(""); - [this.getSelectedWeaponSlot, this.setSelectedWeaponSlot] = - createSignal(""); - [this.getSelectedVehicleSlot, this.setSelectedVehicleSlot] = - createSignal(""); - [this.getCartOpen, this.setCartOpen] = createSignal(false); - [this.getSearchQuery, this.setSearchQuery] = createSignal(""); - [this.getCartItems, this.setCartItems] = createSignal([]); - [this.getCatalogItemsByKey, this.setCatalogItemsByKey] = - createSignal({}); - [this.getIsCatalogLoading, this.setIsCatalogLoading] = - createSignal(false); - [this.getCatalogRequestKey, this.setCatalogRequestKey] = - createSignal(""); - [this.getCatalogPage, this.setCatalogPage] = createSignal(1); - [this.getNotice, this.setNotice] = createSignal({ - type: "", - text: "", - }); - [this.getIsCheckingOut, this.setIsCheckingOut] = - createSignal(false); - [this.getSelectedPaymentSource, this.setSelectedPaymentSource] = - createSignal("cash"); - } - - resetToCategories() { - this.setView("categories"); - this.setSelectedCategory(""); - this.setSelectedWeaponSlot(""); - this.setSelectedVehicleSlot(""); - this.setIsCatalogLoading(false); - this.setCatalogRequestKey(""); - this.setCatalogPage(1); - } - - openWeaponsRoot() { - this.setView("weapons"); - this.setSelectedCategory("weapons"); - this.setSelectedWeaponSlot(""); - this.setSelectedVehicleSlot(""); - this.setIsCatalogLoading(false); - this.setCatalogRequestKey(""); - this.setCatalogPage(1); - } - - openVehiclesRoot() { - this.setView("vehicles"); - this.setSelectedCategory("vehicles"); - this.setSelectedVehicleSlot(""); - this.setSelectedWeaponSlot(""); - this.setIsCatalogLoading(false); - this.setCatalogRequestKey(""); - this.setCatalogPage(1); - } - - resetCatalogPage() { - this.setCatalogPage(1); - } - - setCatalogPageNumber(page) { - const nextPage = Math.max(1, Number(page || 1)); - this.setCatalogPage(nextPage); - } - - selectCategory(category) { - this.setSelectedCategory(category); - this.setSelectedWeaponSlot(""); - this.setSelectedVehicleSlot(""); - this.setCatalogPage(1); - - if (category === "weapons") { - this.openWeaponsRoot(); - return; - } - - if (category === "vehicles") { - this.openVehiclesRoot(); - return; - } - - this.setView("items"); - } - - selectSubcategory(subcategory, slotType) { - if (slotType === "vehicle") { - this.setSelectedVehicleSlot(subcategory); - this.setSelectedWeaponSlot(""); - } else { - this.setSelectedWeaponSlot(subcategory); - this.setSelectedVehicleSlot(""); - } - - this.setCatalogPage(1); - this.setView("items"); - } - - startCategoryRequest(category) { - const categoryKey = String(category || "") - .trim() - .toLowerCase(); - if (!categoryKey) { - return false; - } - - this.setCatalogRequestKey(categoryKey); - this.setIsCatalogLoading(true); - return true; - } - - finishCategoryRequest(category) { - const categoryKey = String(category || "") - .trim() - .toLowerCase(); - const activeKey = String(this.getCatalogRequestKey() || "") - .trim() - .toLowerCase(); - - if (!categoryKey || !activeKey || activeKey === categoryKey) { - this.setCatalogRequestKey(""); - this.setIsCatalogLoading(false); - } - } - - hydrateCategoryItems(payload) { - const categoryKey = String(payload?.category || "") - .trim() - .toLowerCase(); - const items = Array.isArray(payload?.items) - ? payload.items - : []; - - if (!categoryKey) { - this.setCatalogRequestKey(""); - this.setIsCatalogLoading(false); - return; - } - - this.setCatalogItemsByKey((currentItemsByKey) => - Object.assign({}, currentItemsByKey, { - [categoryKey]: items.map(normalizeCatalogItem), - }), - ); - - this.finishCategoryRequest(categoryKey); - } - - ensureSelectedPaymentSource(storeConfig) { - const paymentSources = Array.isArray( - storeConfig?.paymentSources, - ) - ? storeConfig.paymentSources - : []; - const currentSource = String( - this.getSelectedPaymentSource() || "", - ).trim(); - const defaultSource = String( - storeConfig?.defaultPaymentSource || "", - ).trim(); - const sourceIds = paymentSources.map((source) => - String(source?.id || "").trim(), - ); - const enabledSource = paymentSources.find( - (source) => source && source.enabled !== false, - ); - const defaultAvailable = - defaultSource && sourceIds.includes(defaultSource) - ? paymentSources.find( - (source) => - String(source?.id || "").trim() === - defaultSource, - ) - : null; - - if ( - currentSource && - sourceIds.includes(currentSource) && - paymentSources.some( - (source) => - String(source?.id || "").trim() === currentSource && - source?.enabled !== false, - ) - ) { - return; - } - - if (defaultAvailable && defaultAvailable.enabled !== false) { - this.setSelectedPaymentSource(defaultSource); - return; - } - - if (enabledSource) { - this.setSelectedPaymentSource( - String(enabledSource.id || "cash"), - ); - return; - } - - this.setSelectedPaymentSource(defaultSource || "cash"); - } - - navigateToBreadcrumb(target) { - switch (target) { - case "categories": - this.resetToCategories(); - return true; - case "weapons": - this.openWeaponsRoot(); - return true; - case "vehicles": - this.openVehiclesRoot(); - return true; - default: - return false; - } - } - - hydrateFromPayload(payload) { - const cartItems = Array.isArray(payload?.cartItems) - ? payload.cartItems - : []; - - this.setCartItems(cartItems.map(normalizeCartItem)); - this.setCartOpen(false); - this.setIsCheckingOut(false); - this.setCatalogItemsByKey({}); - this.setCatalogRequestKey(""); - this.setIsCatalogLoading(false); - this.setCatalogPage(1); - this.ensureSelectedPaymentSource(payload?.storeConfig || {}); - } - - hydrateStoreConfig(payload) { - const cartItems = Array.isArray(payload?.cartItems) - ? payload.cartItems - : []; - - this.setCartItems(cartItems.map(normalizeCartItem)); - this.setCartOpen(false); - this.setIsCheckingOut(false); - this.ensureSelectedPaymentSource(payload?.storeConfig || {}); - } - } - - return new StorefrontStore(); - }; - - StorefrontApp.store = SharedLogic.createStorefrontStore({ - createSignal, - }); -})(); - -(function () { - const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {}); - const CATALOG_PAGE_SIZE = 6; - - function getSelectionKey(state) { - return ( - state.selectedWeaponSlot || - state.selectedVehicleSlot || - state.selectedCategory - ); - } - - function matchesQuery(query, values) { - if (!query) { - return true; - } - - const normalizedQuery = String(query).trim().toLowerCase(); - if (!normalizedQuery) { - return true; - } - - return values.some((value) => - String(value || "") - .toLowerCase() - .includes(normalizedQuery), - ); - } - - function parsePrice(value) { - const parsed = Number(String(value || "0").replace(/[^0-9.-]+/g, "")); - return Number.isFinite(parsed) ? parsed : 0; - } - - function formatCurrency(value) { - return `$${Number(value || 0).toLocaleString()}`; - } - - function formatTitle(value) { - return String(value || "") - .replace(/[-_]+/g, " ") - .split(/\s+/) - .filter(Boolean) - .map( - (part) => - part.charAt(0).toUpperCase() + part.slice(1).toLowerCase(), - ) - .join(" "); - } - - function getStoreState(store) { - return { - view: store.getView(), - selectedCategory: store.getSelectedCategory(), - selectedWeaponSlot: store.getSelectedWeaponSlot(), - selectedVehicleSlot: store.getSelectedVehicleSlot(), - selectedPaymentSource: store.getSelectedPaymentSource(), - cartOpen: store.getCartOpen(), - searchQuery: store.getSearchQuery(), - cartItems: store.getCartItems(), - catalogItemsByKey: store.getCatalogItemsByKey(), - isCatalogLoading: store.getIsCatalogLoading(), - catalogRequestKey: store.getCatalogRequestKey(), - catalogPage: store.getCatalogPage(), - isCheckingOut: store.getIsCheckingOut(), - }; - } - - function getStoreHeader(state) { - if (state.view === "weapons") { - return { - eyebrow: "Weapons Division", - title: "Weapon Categories", - copy: "Select a weapon slot to open the next supply tier. Primary, secondary, and handgun are staged with the same state and bridge flow as the org portal.", - badge: "3 Slots", - }; - } - - if (state.view === "vehicles") { - return { - eyebrow: "Vehicle Motorpool", - title: "Vehicle Categories", - copy: "Select a vehicle class to open the next supply tier. Cars, armor, airframes, and naval options stay inside the same local store and bridge lifecycle.", - badge: "6 Classes", - }; - } - - if (state.view === "items") { - const label = getSelectionKey(state) || "catalog"; - const queryLabel = state.searchQuery - ? ` Filtered by "${state.searchQuery}".` - : ""; - const loadingLabel = state.isCatalogLoading - ? " Pulling live inventory from the game engine." - : ""; - - return { - eyebrow: "Catalog Preview", - title: formatTitle(label), - copy: `Live category inventory generated from the game engine for the selected department.${queryLabel}${loadingLabel}`, - badge: "Preview Items", - }; - } - - return { - eyebrow: "Supply Categories", - title: "Procurement Dashboard", - copy: "Choose a category to enter the exchange. Weapons and vehicles open a second tier, while the other departments display placeholder product inventory inside the new runtime/store architecture.", - badge: "8 Categories", - }; - } - - function getStoreBreadcrumbs(state) { - const items = [{ id: "categories", label: "Supply Exchange" }]; - - if (state.view === "weapons") { - items.push({ id: "weapons", label: "Weapons" }); - return items; - } - - if (state.view === "vehicles") { - items.push({ id: "vehicles", label: "Vehicles" }); - return items; - } - - if (state.view === "items") { - if (state.selectedWeaponSlot) { - items.push({ id: "weapons", label: "Weapons" }); - items.push({ - id: "weapon-slot", - label: formatTitle(state.selectedWeaponSlot), - }); - return items; - } - - if (state.selectedVehicleSlot) { - items.push({ id: "vehicles", label: "Vehicles" }); - items.push({ - id: "vehicle-slot", - label: formatTitle(state.selectedVehicleSlot), - }); - return items; - } - - if (state.selectedCategory) { - items.push({ - id: "category", - label: formatTitle(state.selectedCategory), - }); - } - } - - return items; - } - - function getVisibleCategoryCards(state, catalog) { - return catalog.categoryCards.filter((category) => - matchesQuery(state.searchQuery, [category.id, category.label]), - ); - } - - function getVisibleSubcategoryCards(state, catalog) { - const source = - state.view === "vehicles" - ? catalog.vehicleCards - : catalog.weaponCards; - - return source.filter((category) => - matchesQuery(state.searchQuery, [category.id, category.label]), - ); - } - - function getVisibleItems(state, catalog) { - const key = getSelectionKey(state); - const categoryKey = String(key || "") - .trim() - .toLowerCase(); - const itemsByKey = state.catalogItemsByKey || {}; - const items = Array.isArray(itemsByKey[categoryKey]) - ? itemsByKey[categoryKey] - : []; - - return items.filter((item) => - matchesQuery(state.searchQuery, [ - item.className, - item.code, - item.name, - item.description, - item.price, - item.type, - ]), - ); - } - - function getCatalogPagination(state, catalog) { - const totalItems = getVisibleItems(state, catalog).length; - const totalPages = Math.max( - 1, - Math.ceil(totalItems / CATALOG_PAGE_SIZE), - ); - const currentPage = Math.min( - totalPages, - Math.max(1, Number(state.catalogPage || 1)), - ); - - return { - pageSize: CATALOG_PAGE_SIZE, - totalItems, - totalPages, - currentPage, - startIndex: - totalItems === 0 - ? 0 - : (currentPage - 1) * CATALOG_PAGE_SIZE + 1, - endIndex: Math.min(currentPage * CATALOG_PAGE_SIZE, totalItems), - }; - } - - function getVisibleItemsPage(state, catalog) { - const items = getVisibleItems(state, catalog); - const pagination = getCatalogPagination(state, catalog); - const startOffset = (pagination.currentPage - 1) * pagination.pageSize; - return items.slice(startOffset, startOffset + pagination.pageSize); - } - - function summarizeCart(cartItems) { - const itemCount = cartItems.reduce( - (sum, item) => sum + Number(item.quantity || 0), - 0, - ); - const subtotal = cartItems.reduce( - (sum, item) => - sum + parsePrice(item.price) * Number(item.quantity || 0), - 0, - ); - - return { - lineCount: cartItems.length, - itemCount, - subtotal, - total: subtotal, - }; - } - - function getPaymentSources(storeConfig) { - const paymentSources = Array.isArray(storeConfig?.paymentSources) - ? storeConfig.paymentSources - : []; - - return paymentSources.map((source) => ({ - id: String(source?.id || "").trim(), - label: String(source?.label || source?.id || "").trim(), - balance: Number(source?.balance || 0), - enabled: source?.enabled !== false, - detail: String(source?.detail || "").trim(), - })); - } - - function getPaymentSourceById(storeConfig, paymentSourceId) { - const sourceId = String(paymentSourceId || "").trim(); - return getPaymentSources(storeConfig).find( - (source) => source.id === sourceId, - ); - } - - StorefrontApp.getters = { - formatTitle, - formatCurrency, - parsePrice, - getSelectionKey, - getStoreState, - getStoreHeader, - getStoreBreadcrumbs, - getVisibleCategoryCards, - getVisibleSubcategoryCards, - getVisibleItems, - getVisibleItemsPage, - getCatalogPagination, - summarizeCart, - getPaymentSources, - getPaymentSourceById, - }; -})(); - -(function () { - const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {}); - const store = StorefrontApp.store; - const bridge = window.ForgeWebUI.createBridge({ - closeEvent: "store::close", - globalName: "StoreUIBridge", - readyEvent: "store::ready", - }); - - function requestClose() { - return bridge.close({}); - } - - function requestCheckout(payload) { - return bridge.send("store::checkout::request", payload); - } - - function requestCategory(payload) { - return bridge.send("store::category::request", payload); - } - - function notifyReady() { - return bridge.ready({ loaded: true }); - } - - bridge.on("store::hydrate", (payloadData) => { - StorefrontApp.data.applyHydratePayload(payloadData); - store.hydrateFromPayload(payloadData); - }); - - bridge.on("store::config::hydrate", (payloadData) => { - StorefrontApp.data.applyHydratePayload(payloadData); - store.hydrateStoreConfig(payloadData); - }); - - bridge.on("store::checkout::success", (payloadData) => { - store.setIsCheckingOut(false); - store.setCartItems([]); - store.setCartOpen(false); - if (StorefrontApp.actions) { - StorefrontApp.actions.showNotice( - "success", - payloadData.message || "Checkout completed.", - ); - } - }); - - bridge.on("store::category::hydrate", (payloadData) => { - store.hydrateCategoryItems(payloadData); - }); - - bridge.on("store::category::failure", (payloadData) => { - store.finishCategoryRequest(payloadData.category || ""); - if (StorefrontApp.actions) { - StorefrontApp.actions.showNotice( - "error", - payloadData.message || "Category request failed.", - ); - } - }); - - bridge.on("store::checkout::failure", (payloadData) => { - store.setIsCheckingOut(false); - if (StorefrontApp.actions) { - StorefrontApp.actions.showNotice( - "error", - payloadData.message || "Checkout failed.", - ); - } - }); - - StorefrontApp.bridge = { - close: bridge.close, - requestClose, - requestCheckout, - requestCategory, - notifyReady, - receive: bridge.receive, - sendEvent: bridge.send, - }; -})(); - -(function () { - const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {}); - const store = StorefrontApp.store; - const getters = StorefrontApp.getters; - const { storeConfig, session } = StorefrontApp.data; - - let noticeTimer = null; - - function showNotice(type, text) { - store.setNotice({ type, text }); - - if (noticeTimer) { - clearTimeout(noticeTimer); - } - - noticeTimer = setTimeout(() => { - store.setNotice({ type: "", text: "" }); - noticeTimer = null; - }, 3200); - } - - function normalizeCheckoutItem(item) { - return { - classname: String(item?.code || "").trim(), - category: String(item?.category || "") - .trim() - .toLowerCase(), - entryKind: String(item?.entryKind || "item") - .trim() - .toLowerCase(), - quantity: Math.max(1, Number(item?.quantity || 1)), - }; - } - - function buildCheckoutPayload(cartItems, paymentMethod, totalPrice) { - const payload = { - items: [], - vehicles: [], - totalPrice, - paymentMethod, - }; - - cartItems.forEach((item) => { - const normalizedItem = normalizeCheckoutItem(item); - - if (normalizedItem.entryKind === "vehicle") { - for ( - let index = 0; - index < normalizedItem.quantity; - index += 1 - ) { - payload.vehicles.push({ - classname: normalizedItem.classname, - category: normalizedItem.category, - }); - } - return; - } - - payload.items.push({ - classname: normalizedItem.classname, - category: normalizedItem.category, - quantity: normalizedItem.quantity, - }); - }); - - return payload; - } - - function applySearchQuery(value) { - store.setSearchQuery(String(value || "").trim()); - store.resetCatalogPage(); - } - - function clearSearch() { - store.setSearchQuery(""); - store.resetCatalogPage(); - } - - function toggleCart() { - store.setCartOpen((open) => !open); - } - - function closeCart() { - store.setCartOpen(false); - } - - function closeStore() { - const bridge = StorefrontApp.bridge; - if (bridge && typeof bridge.requestClose === "function") { - const sent = bridge.requestClose(); - if (sent) { - return true; - } - } - - showNotice("error", "Store bridge is unavailable."); - return false; - } - - function navigateToBreadcrumb(target) { - return store.navigateToBreadcrumb(target); - } - - function scrollCatalogToTop() { - const catalogGrid = document.querySelector( - '[data-preserve-scroll-id="catalog-grid"]', - ); - if (catalogGrid) { - catalogGrid.scrollTop = 0; - } - } - - function selectCategory(category) { - store.selectCategory(category); - scrollCatalogToTop(); - - if (!["weapons", "vehicles"].includes(String(category || ""))) { - requestCategoryItems(category); - } - } - - function selectSubcategory(subcategory, slotType) { - store.selectSubcategory(subcategory, slotType); - scrollCatalogToTop(); - requestCategoryItems(subcategory); - } - - function goToCatalogPage(page) { - store.setCatalogPageNumber(page); - scrollCatalogToTop(); - } - - function goToNextCatalogPage(totalPages) { - const currentPage = Number(store.getCatalogPage() || 1); - const lastPage = Math.max(1, Number(totalPages || 1)); - if (currentPage >= lastPage) { - return false; - } - - goToCatalogPage(currentPage + 1); - return true; - } - - function goToPreviousCatalogPage() { - const currentPage = Number(store.getCatalogPage() || 1); - if (currentPage <= 1) { - return false; - } - - goToCatalogPage(currentPage - 1); - return true; - } - - function requestCategoryItems(category) { - const categoryKey = String(category || "") - .trim() - .toLowerCase(); - if (!categoryKey) { - return false; - } - - const cachedItems = store.getCatalogItemsByKey(); - if (Array.isArray(cachedItems[categoryKey])) { - store.finishCategoryRequest(""); - return true; - } - - store.startCategoryRequest(categoryKey); - - const bridge = StorefrontApp.bridge; - if (!bridge || typeof bridge.requestCategory !== "function") { - store.finishCategoryRequest(categoryKey); - showNotice("error", "Store bridge is unavailable."); - return false; - } - - const sent = bridge.requestCategory({ category: categoryKey }); - if (!sent) { - store.finishCategoryRequest(categoryKey); - showNotice("error", "Category request bridge is unavailable."); - return false; - } - - return true; - } - - function addToCart(item) { - store.setCartItems((currentItems) => { - const existingIndex = currentItems.findIndex( - (entry) => entry.code === item.code, - ); - if (existingIndex === -1) { - return [ - ...currentItems, - { - code: item.code, - name: item.name, - price: item.price, - category: item.category, - entryKind: item.entryKind, - quantity: 1, - }, - ]; - } - - const nextItems = [...currentItems]; - nextItems[existingIndex] = Object.assign( - {}, - nextItems[existingIndex], - { - category: item.category, - entryKind: item.entryKind, - quantity: nextItems[existingIndex].quantity + 1, - }, - ); - return nextItems; - }); - - showNotice("success", `${item.name} added to the acquisition queue.`); - } - - function incrementCartItem(code) { - store.setCartItems((currentItems) => - currentItems.map((item) => - item.code === code - ? Object.assign({}, item, { quantity: item.quantity + 1 }) - : item, - ), - ); - } - - function decrementCartItem(code) { - store.setCartItems((currentItems) => - currentItems - .map((item) => - item.code === code - ? Object.assign({}, item, { - quantity: Math.max(0, item.quantity - 1), - }) - : item, - ) - .filter((item) => item.quantity > 0), - ); - } - - function removeCartItem(code) { - store.setCartItems((currentItems) => - currentItems.filter((item) => item.code !== code), - ); - } - - function selectPaymentSource(paymentSourceId) { - const sourceId = String(paymentSourceId || "").trim(); - const paymentSources = getters.getPaymentSources(storeConfig); - const selectedSource = paymentSources.find( - (source) => source.id === sourceId, - ); - - if (!selectedSource) { - showNotice("error", "Selected payment source is unavailable."); - return false; - } - - if (selectedSource.enabled === false) { - showNotice( - "error", - selectedSource.detail || - "Selected payment source is not available.", - ); - return false; - } - - store.setSelectedPaymentSource(sourceId); - return true; - } - - function requestCheckout() { - const cartItems = store.getCartItems(); - if (cartItems.length === 0) { - showNotice("error", "Add at least one item before checkout."); - return false; - } - - const summary = getters.summarizeCart(cartItems); - const selectedPaymentSource = getters.getPaymentSourceById( - storeConfig, - store.getSelectedPaymentSource(), - ); - - if (!selectedPaymentSource) { - showNotice("error", "Select a payment source before checkout."); - return false; - } - - if (selectedPaymentSource.enabled === false) { - showNotice( - "error", - selectedPaymentSource.detail || - "Selected payment source is unavailable.", - ); - return false; - } - - if (summary.total > Number(selectedPaymentSource.balance || 0)) { - showNotice( - "error", - `${selectedPaymentSource.label} cannot cover this checkout total.`, - ); - return false; - } - - const bridge = StorefrontApp.bridge; - if (!bridge || typeof bridge.requestCheckout !== "function") { - showNotice("error", "Checkout bridge is unavailable."); - return false; - } - - store.setIsCheckingOut(true); - - const checkoutPayload = buildCheckoutPayload( - cartItems, - selectedPaymentSource.id, - summary.total, - ); - - const sent = bridge.requestCheckout({ - checkoutJson: JSON.stringify(checkoutPayload), - }); - - if (!sent) { - store.setIsCheckingOut(false); - showNotice("error", "Checkout bridge is unavailable."); - return false; - } - - return true; - } - - StorefrontApp.actions = { - showNotice, - applySearchQuery, - clearSearch, - toggleCart, - closeCart, - closeStore, - navigateToBreadcrumb, - selectCategory, - selectSubcategory, - goToCatalogPage, - goToNextCatalogPage, - goToPreviousCatalogPage, - addToCart, - incrementCartItem, - decrementCartItem, - removeCartItem, - selectPaymentSource, - requestCheckout, - formatTitle: getters.formatTitle, - formatCurrency: getters.formatCurrency, - }; -})(); - -(function () { - const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {}); - const { h, ensureScopedStyle } = StorefrontApp.runtime; - const WindowTitleBar = window.SharedUI.componentFns.WindowTitleBar; - const store = StorefrontApp.store; - const getters = StorefrontApp.getters; - const actions = StorefrontApp.actions; - const { catalog, session, storeConfig } = StorefrontApp.data; - const scopeAttr = "data-ui-store-app-shell"; - const scopeSelector = `[${scopeAttr}]`; - const appShellCss = ` -${scopeSelector} { - display: flex; - flex-direction: column; - width: 100%; - height: 100%; - overflow: hidden; - background: var(--store-shell-bg); -} - -${scopeSelector} .footer-title, -${scopeSelector} .eyebrow { - font-size: 0.68rem; - letter-spacing: 0.18em; - text-transform: uppercase; - color: var(--store-text-subtle); - font-weight: 700; -} - -${scopeSelector} .module-header, -${scopeSelector} .store-panel-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; -} - -${scopeSelector} .store-app { - flex: 1; - min-height: 0; - width: min(100%, 1613px); - margin: 0 auto; - display: grid; - grid-template-columns: 308px minmax(0, 1fr); - gap: 1.25rem; - padding: 1.25rem; -} - -${scopeSelector} .store-sidebar, -${scopeSelector} .store-main { - min-height: 0; - display: flex; - flex-direction: column; - gap: 1rem; -} - -${scopeSelector} .store-main { - position: relative; - overflow: hidden; -} - -${scopeSelector} .module-card, -${scopeSelector} .store-panel { - background: linear-gradient(180deg, var(--store-surface) 0%, var(--store-surface-alt) 100%); - border: 1px solid var(--store-border); - border-radius: 1.35rem; -} - -${scopeSelector} .module-card { - padding: 1rem; -} - -${scopeSelector} .store-panel { - min-height: 0; - flex: 1 1 auto; - display: flex; - flex-direction: column; - width: min(100%, 1280px); - overflow: hidden; -} - -${scopeSelector} .module-header { - margin-bottom: 0.85rem; -} - -${scopeSelector} .store-panel-header { - padding: 1rem 1rem 0; -} - -${scopeSelector} .section-title { - margin: 0; - font-size: 1.1rem; - font-weight: 700; - letter-spacing: -0.02em; - color: var(--store-text-main); -} - -${scopeSelector} .section-copy, -${scopeSelector} .footer-copy { - margin: 0.2rem 0 0; - font-size: 0.9rem; - line-height: 1.45; - color: var(--store-text-muted); -} - -${scopeSelector} .pill { - padding: 0.48rem 0.8rem; - border-radius: 999px; - background: var(--store-accent-soft); - color: var(--store-accent); - font-size: 0.74rem; - font-weight: 700; - letter-spacing: 0.1em; - text-transform: uppercase; -} - -${scopeSelector} .search-module { - display: flex; - flex-direction: column; - gap: 0.8rem; -} - -${scopeSelector} .search-form { - display: grid; - gap: 0.7rem; -} - -${scopeSelector} .search-input { - width: 100%; - height: 2.9rem; - padding: 0 0.95rem; - border-radius: 0.8rem; - border: 1px solid var(--store-border); - background: rgb(255 255 255 / 0.75); - color: var(--store-text-main); -} - -${scopeSelector} .quick-tags { - display: flex; - flex-wrap: wrap; - gap: 0.5rem; -} - -${scopeSelector} .quick-tag { - padding: 0.55rem 0.72rem; - border-radius: 999px; - border: 1px solid var(--store-border); - background: rgb(255 255 255 / 0.52); - color: var(--store-text-muted); - font-size: 0.75rem; - letter-spacing: 0.08em; - text-transform: uppercase; -} - -${scopeSelector} .filter-stack { - display: grid; - gap: 0.85rem; -} - -${scopeSelector} .filter-group { - padding: 0.95rem; - border-radius: 0.8rem; - background: rgb(255 255 255 / 0.48); - border: 1px solid var(--store-border); -} - -${scopeSelector} .filter-label { - display: block; - margin-bottom: 0.55rem; - font-size: 0.72rem; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--store-text-subtle); - font-weight: 700; -} - -${scopeSelector} .filter-value { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - color: var(--store-text-main); - font-size: 0.92rem; - font-weight: 600; -} - -${scopeSelector} .filter-placeholder { - color: var(--store-text-muted); - font-weight: 500; -} - -${scopeSelector} .store-panel-intro { - padding: 0 1rem 1rem; - border-bottom: 1px solid var(--store-accent-line); -} - -${scopeSelector} .store-footer-bar { - width: 100%; - border-top: 1px solid rgb(18 54 93 / 0.1); - background: transparent; -} - -${scopeSelector} .store-footer { - width: min(100%, 1613px); - margin: 0 auto; - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 1rem; - padding: 0.95rem 1.25rem 1.15rem; -} - -${scopeSelector} .footer-block { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -${scopeSelector} .store-toast-stack { - position: fixed; - top: 1.2rem; - right: 1.5rem; - z-index: 10; - display: flex; - flex-direction: column; - gap: 0.65rem; -} - -${scopeSelector} .store-toast { - max-width: 24rem; - padding: 0.85rem 1rem; - border-radius: 0.9rem; - border: 1px solid var(--store-border); - background: #fff; - box-shadow: 0 14px 28px rgb(16 34 56 / 0.14); - font-size: 0.92rem; -} - -${scopeSelector} .store-toast.is-success { - background: #ecfdf5; - border-color: #bbf7d0; - color: #166534; -} - -${scopeSelector} .store-toast.is-error { - background: #fef2f2; - border-color: #fecaca; - color: #991b1b; -} - -@media (max-width: 1440px) { - ${scopeSelector} .store-app { - grid-template-columns: 284px minmax(0, 1fr); - } -} - -@media (max-width: 1120px) { - ${scopeSelector} .store-app { - grid-template-columns: 1fr; - overflow: auto; - } - - ${scopeSelector} .store-sidebar, - ${scopeSelector} .store-main { - min-height: auto; - } - - ${scopeSelector} .store-main { - overflow: visible; - } - - ${scopeSelector} .store-footer { - grid-template-columns: 1fr; - } - - ${scopeSelector} .store-toast-stack { - right: 1rem; - left: 1rem; - } -} -`; - - StorefrontApp.components = StorefrontApp.components || {}; - StorefrontApp.componentFns = StorefrontApp.componentFns || {}; - - function renderStoreBody(state) { - const { - CategoryCard, - SubcategoryCard, - ProductCard, - EmptyStateCard, - CategoryGrid, - SubcategoryGrid, - ProductGrid, - CatalogPager, - } = StorefrontApp.componentFns; - - if (state.view === "weapons" || state.view === "vehicles") { - const slotType = state.view === "vehicles" ? "vehicle" : "weapon"; - const items = getters.getVisibleSubcategoryCards(state, catalog); - - return SubcategoryGrid( - items.length > 0 - ? items.map((category) => - SubcategoryCard(category, slotType), - ) - : EmptyStateCard({ - title: "No matching slots", - copy: "Try a different search query or clear the current filter.", - actionLabel: "Clear Search", - onAction: () => actions.clearSearch(), - }), - ); - } - - if (state.view === "items") { - const items = getters.getVisibleItems(state, catalog); - const pagedItems = getters.getVisibleItemsPage(state, catalog); - const pagination = getters.getCatalogPagination(state, catalog); - const quantityByCode = state.cartItems.reduce((acc, item) => { - acc[item.code] = item.quantity; - return acc; - }, {}); - const selectionKey = String( - getters.getSelectionKey(state) || "", - ).toLowerCase(); - - return [ - ProductGrid( - state.isCatalogLoading && - state.catalogRequestKey === selectionKey && - items.length === 0 - ? EmptyStateCard({ - title: "Loading inventory", - copy: "Pulling live category items from the game engine.", - }) - : items.length > 0 - ? pagedItems.map((item) => - ProductCard( - item, - quantityByCode[item.code] || 0, - ), - ) - : EmptyStateCard({ - title: "No category items", - copy: state.searchQuery - ? "Your search filter excluded the live inventory returned for this category." - : "The game engine did not return any items for this category yet.", - actionLabel: "Clear Search", - onAction: () => actions.clearSearch(), - }), - ), - items.length > 0 ? CatalogPager(pagination) : null, - ]; - } - - const items = getters.getVisibleCategoryCards(state, catalog); - return CategoryGrid( - items.length > 0 - ? items.map((category) => CategoryCard(category)) - : EmptyStateCard({ - title: "No matching departments", - copy: "Your search filter excluded every top-level department.", - actionLabel: "Clear Search", - onAction: () => actions.clearSearch(), - }), - ); - } - - StorefrontApp.components.App = function App() { - const Navbar = StorefrontApp.componentFns.Navbar; - const Cart = StorefrontApp.componentFns.Cart; - const state = getters.getStoreState(store); - const header = getters.getStoreHeader(state); - const notice = store.getNotice(); - const activeQuery = state.searchQuery; - const paymentSources = getters.getPaymentSources(storeConfig); - const availablePaymentSourceCount = paymentSources.filter( - (source) => source.enabled !== false, - ).length; - const filterDepartment = - state.view === "items" - ? actions.formatTitle( - getters.getSelectionKey(state) || "Catalog", - ) - : actions.formatTitle(state.view); - const selectedPaymentSource = - getters.getPaymentSourceById( - storeConfig, - state.selectedPaymentSource, - ) || null; - - ensureScopedStyle("storefront-app-shell", appShellCss); - - return h( - "div", - { [scopeAttr]: "" }, - WindowTitleBar({ - kicker: "FORGE Logistics", - title: "Supply Exchange", - onClose: () => actions.closeStore(), - closeLabel: "Close store interface", - }), - notice.text - ? h( - "div", - { className: "store-toast-stack" }, - h( - "div", - { - className: - notice.type === "error" - ? "store-toast is-error" - : "store-toast is-success", - }, - notice.text, - ), - ) - : null, - h( - "div", - { className: "store-app" }, - h( - "aside", - { className: "store-sidebar" }, - h( - "section", - { className: "module-card search-module" }, - h( - "div", - { className: "module-header" }, - h( - "div", - null, - h("span", { className: "eyebrow" }, "Search"), - h( - "h2", - { className: "section-title" }, - "Inventory Search", - ), - ), - h("span", { className: "pill" }, "Live"), - ), - h( - "div", - { className: "search-form" }, - h("input", { - id: "store-search-input", - type: "text", - className: "search-input", - placeholder: - "Search inventory, classes, or suppliers", - value: activeQuery, - }), - h( - "div", - { - style: { - display: "flex", - gap: "0.65rem", - }, - }, - h( - "button", - { - type: "button", - className: - "store-btn store-btn-primary", - onClick: () => - actions.applySearchQuery( - document.getElementById( - "store-search-input", - )?.value || "", - ), - }, - "Apply Search", - ), - h( - "button", - { - type: "button", - className: - "store-btn store-btn-secondary", - onClick: () => actions.clearSearch(), - }, - "Clear", - ), - ), - ), - h( - "div", - { className: "quick-tags" }, - (storeConfig.searchTags || []).map((tag) => - h("span", { className: "quick-tag" }, tag), - ), - ), - ), - h( - "section", - { className: "module-card" }, - h( - "div", - { className: "module-header" }, - h( - "div", - null, - h("span", { className: "eyebrow" }, "Filter"), - h( - "h2", - { className: "section-title" }, - "Procurement Filters", - ), - ), - h( - "span", - { className: "pill" }, - storeConfig.moduleState, - ), - ), - h( - "div", - { className: "filter-stack" }, - h( - "div", - { className: "filter-group" }, - h( - "span", - { className: "filter-label" }, - "Department", - ), - h( - "div", - { className: "filter-value" }, - h( - "span", - { className: "filter-placeholder" }, - filterDepartment, - ), - ), - ), - h( - "div", - { className: "filter-group" }, - h( - "span", - { className: "filter-label" }, - "Availability", - ), - h( - "div", - { className: "filter-value" }, - h( - "span", - { className: "filter-placeholder" }, - storeConfig.availability, - ), - ), - ), - h( - "div", - { className: "filter-group" }, - h( - "span", - { className: "filter-label" }, - "Payment", - ), - h( - "div", - { className: "filter-value" }, - h( - "span", - { className: "filter-placeholder" }, - selectedPaymentSource - ? selectedPaymentSource.label - : "Cash", - ), - ), - ), - ), - ), - ), - h( - "main", - { className: "store-main" }, - h( - "section", - { className: "store-panel" }, - Navbar(), - h( - "div", - { className: "store-panel-header" }, - h( - "div", - null, - h( - "span", - { className: "eyebrow" }, - header.eyebrow, - ), - h( - "h1", - { className: "section-title" }, - header.title, - ), - ), - h("span", { className: "pill" }, header.badge), - ), - h( - "div", - { className: "store-panel-intro" }, - h("p", { className: "section-copy" }, header.copy), - ), - renderStoreBody(state), - ), - Cart(), - ), - ), - h( - "footer", - { className: "store-footer-bar" }, - h( - "div", - { className: "store-footer" }, - h( - "div", - { className: "footer-block" }, - h( - "span", - { className: "footer-title" }, - "Procurement Desk", - ), - h( - "span", - { className: "footer-copy" }, - "Authorized supply browsing for personnel loadout preparation and mission staging.", - ), - ), - h( - "div", - { className: "footer-block" }, - h( - "span", - { className: "footer-title" }, - "Catalog Scope", - ), - h( - "span", - { className: "footer-copy" }, - "Uniforms, protective gear, weapon slots, vehicles, ammunition groups, and general support inventory.", - ), - ), - h( - "div", - { className: "footer-block" }, - h( - "span", - { className: "footer-title" }, - "Purchase Access", - ), - h( - "span", - { className: "footer-copy" }, - `${session.approval} approval. ${availablePaymentSourceCount} payment source(s) currently available${session.orgName ? ` for ${session.orgName}.` : "."}`, - ), - ), - ), - ), - ); - }; -})(); - -(function () { - const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {}); - const { h, ensureScopedStyle } = StorefrontApp.runtime; - const actions = StorefrontApp.actions; - const media = StorefrontApp.media; - const scopeAttr = "data-ui-store-cards"; - const scopeSelector = `[${scopeAttr}]`; - const cardsCss = ` -${scopeSelector}.catalog-grid-shell { - flex: 1; - min-height: 0; - display: flex; -} - -${scopeSelector}.catalog-pager-shell { - display: block; -} - -${scopeSelector} .catalog-grid { - flex: 1; - min-height: 0; - width: 100%; - padding: 1rem; - display: grid; - gap: 1rem; - align-content: start; - overflow-y: auto; - overflow-x: hidden; - scrollbar-gutter: stable; - scrollbar-width: auto; - scrollbar-color: rgb(120 136 155 / 0.9) rgb(255 255 255 / 0.45); -} - -${scopeSelector} .catalog-grid::-webkit-scrollbar { - width: 12px; -} - -${scopeSelector} .catalog-grid::-webkit-scrollbar-track { - background: rgb(255 255 255 / 0.45); - border-radius: 999px; -} - -${scopeSelector} .catalog-grid::-webkit-scrollbar-thumb { - background: rgb(120 136 155 / 0.9); - border-radius: 999px; - border: 2px solid rgb(255 255 255 / 0.45); -} - -${scopeSelector} .catalog-grid.is-categories, -${scopeSelector} .catalog-grid.is-products { - grid-template-columns: repeat(3, minmax(0, 1fr)); -} - -${scopeSelector} .catalog-grid.is-subcategories { - grid-template-columns: repeat(2, minmax(0, 1fr)); -} - -${scopeSelector} .card-button, -${scopeSelector} .product-card, -${scopeSelector} .empty-state { - border: 1px solid var(--store-border); - border-radius: 1.15rem; - background: - linear-gradient(180deg, rgb(255 255 255 / 0.72) 0%, rgb(226 233 239 / 0.9) 100%), - var(--store-surface-strong); - color: var(--store-accent); - box-shadow: - inset 0 1px 0 rgb(255 255 255 / 0.8), - 0 10px 24px rgb(16 34 56 / 0.06); -} - -${scopeSelector} .card-button { - min-height: 12.5rem; - display: flex; - flex-direction: column; - justify-content: center; - gap: 0.75rem; - padding: 1.35rem; - text-align: left; - transition: - transform 120ms ease, - box-shadow 120ms ease, - border-color 120ms ease; -} - -${scopeSelector} .card-button:hover, -${scopeSelector} .product-card:hover { - transform: translateY(-2px); - border-color: rgb(18 54 93 / 0.32); - box-shadow: - 0 16px 28px rgb(16 34 56 / 0.11), - inset 0 1px 0 rgb(255 255 255 / 0.88); -} - -${scopeSelector} .card-kicker, -${scopeSelector} .product-code, -${scopeSelector} .empty-state-kicker { - font-size: 0.72rem; - letter-spacing: 0.14em; - text-transform: uppercase; - font-weight: 700; - color: var(--store-text-subtle); -} - -${scopeSelector} .card-label { - font-size: 1.08rem; - font-weight: 700; - letter-spacing: 0.06em; - text-transform: uppercase; -} - -${scopeSelector} .card-copy, -${scopeSelector} .product-copy, -${scopeSelector} .empty-state-copy { - margin: 0; - color: var(--store-text-muted); - line-height: 1.45; -} - -${scopeSelector} .product-copy { - white-space: pre-line; -} - -${scopeSelector} .product-card { - min-height: 15.5rem; - padding: 0.8rem; - display: flex; - flex-direction: column; - gap: 0.65rem; -} - -${scopeSelector} .product-image { - height: 5.9rem; - border-radius: 0.95rem; - border: 1px dashed rgb(18 54 93 / 0.24); - background: linear-gradient(135deg, rgb(235 240 245) 0%, rgb(221 228 235) 100%); - display: flex; - align-items: center; - justify-content: center; - color: var(--store-text-subtle); - font-size: 0.78rem; - letter-spacing: 0.16em; - text-transform: uppercase; - overflow: hidden; -} - -${scopeSelector} .product-image-asset { - width: 100%; - height: 100%; - object-fit: contain; -} - -${scopeSelector} .product-meta { - display: flex; - flex-direction: column; - gap: 0.2rem; -} - -${scopeSelector} .product-name { - font-size: 0.96rem; - font-weight: 700; - color: var(--store-text-main); - line-height: 1.3; -} - -${scopeSelector} .product-footer { - margin-top: auto; - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; -} - -${scopeSelector} .product-price { - font-size: 0.96rem; - font-weight: 700; - color: var(--store-success); -} - -${scopeSelector} .product-qty { - display: inline-flex; - align-items: center; - justify-content: center; - min-width: 1.85rem; - height: 1.85rem; - border-radius: 999px; - background: var(--store-accent-soft); - color: var(--store-accent); - font-size: 0.76rem; - font-weight: 700; -} - -${scopeSelector} .empty-state { - padding: 1.35rem; - display: flex; - flex-direction: column; - gap: 0.65rem; -} - -${scopeSelector} .catalog-pager { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.9rem; - padding: 0.55rem 0.9rem 0.75rem; - border-top: 1px solid var(--store-accent-line); -} - -${scopeSelector} .catalog-pager-meta { - display: flex; - flex-direction: column; - gap: 0.15rem; -} - -${scopeSelector} .catalog-pager-summary { - font-size: 0.86rem; - color: var(--store-text-muted); -} - -${scopeSelector} .catalog-pager-actions { - display: inline-flex; - align-items: center; - gap: 0.6rem; -} - -${scopeSelector} .catalog-pager-page { - min-width: 5.75rem; - text-align: center; - font-size: 0.82rem; - font-weight: 700; - color: var(--store-accent); - letter-spacing: 0.08em; - text-transform: uppercase; -} - -${scopeSelector} .product-copy { - display: -webkit-box; - overflow: hidden; - -webkit-box-orient: vertical; - -webkit-line-clamp: 2; -} - -@media (max-width: 1440px) { - ${scopeSelector} .catalog-grid.is-categories, - ${scopeSelector} .catalog-grid.is-products { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } -} - -@media (max-width: 1120px) { - ${scopeSelector} .catalog-grid.is-categories, - ${scopeSelector} .catalog-grid.is-subcategories, - ${scopeSelector} .catalog-grid.is-products { - grid-template-columns: 1fr; - } -} -`; - - StorefrontApp.componentFns = StorefrontApp.componentFns || {}; - - function createGrid(className, children) { - ensureScopedStyle("storefront-cards", cardsCss); - - if ( - className === "is-products" && - media && - typeof media.scheduleTextureObservation === "function" - ) { - media.scheduleTextureObservation(); - } - - return h( - "div", - { - [scopeAttr]: "", - className: "catalog-grid-shell", - }, - h( - "div", - { - className: `catalog-grid ${className}`, - "data-preserve-scroll-id": "catalog-grid", - }, - children, - ), - ); - } - - function formatDescription(description, fallbackValue) { - const rawDescription = String(description || "").trim(); - if (!rawDescription) { - return fallbackValue; - } - - const htmlDescription = rawDescription - .replace(/<\s*br\s*\/?\s*>/gi, "\n") - .replace(/<\/\s*p\s*>/gi, "\n") - .replace(/<\s*li\s*>/gi, "- ") - .replace(/<\/\s*li\s*>/gi, "\n"); - const scratch = document.createElement("div"); - scratch.innerHTML = htmlDescription; - - const textDescription = String( - scratch.textContent || scratch.innerText || "", - ) - .replace(/\u00a0/g, " ") - .replace(/[ \t]+\n/g, "\n") - .replace(/\n{3,}/g, "\n\n") - .trim(); - - return textDescription || fallbackValue; - } - - StorefrontApp.componentFns.CategoryCard = function CategoryCard(category) { - return h( - "button", - { - type: "button", - className: "card-button", - onClick: () => actions.selectCategory(category.id), - }, - h("span", { className: "card-kicker" }, "Department"), - h("strong", { className: "card-label" }, category.label), - h( - "p", - { className: "card-copy" }, - "Open this department and move into staged inventory browsing.", - ), - ); - }; - - StorefrontApp.componentFns.SubcategoryCard = function SubcategoryCard( - category, - slotType, - ) { - return h( - "button", - { - type: "button", - className: "card-button", - onClick: () => actions.selectSubcategory(category.id, slotType), - }, - h( - "span", - { className: "card-kicker" }, - slotType === "vehicle" ? "Vehicle Class" : "Weapon Slot", - ), - h("strong", { className: "card-label" }, category.label), - h( - "p", - { className: "card-copy" }, - "Open the next tier and review product previews for this selection.", - ), - ); - }; - - StorefrontApp.componentFns.ProductCard = function ProductCard( - item, - quantityInCart, - ) { - const textureState = - media && typeof media.getTextureState === "function" - ? media.getTextureState(item.image) - : { isVisible: true }; - const textureSource = - media && typeof media.getTextureSource === "function" - ? media.getTextureSource(item.image) - : ""; - const description = formatDescription( - item.description, - item.className || item.code, - ); - - return h( - "article", - { className: "product-card" }, - h( - "div", - { - className: "product-image", - "data-store-texture-path": item.image || "", - }, - textureSource - ? h("img", { - className: "product-image-asset", - src: textureSource, - alt: item.name, - loading: "lazy", - }) - : textureState.isVisible - ? "Loading Image" - : "Image Placeholder", - ), - h( - "div", - { className: "product-meta" }, - h( - "span", - { className: "product-code" }, - item.type || item.code || item.className, - ), - h("strong", { className: "product-name" }, item.name), - ), - h("p", { className: "product-copy" }, description), - h( - "div", - { className: "product-footer" }, - h( - "span", - { className: "product-price" }, - item.price || "Pending", - ), - h( - "div", - { - style: { - display: "flex", - alignItems: "center", - gap: "0.55rem", - }, - }, - quantityInCart > 0 - ? h( - "span", - { className: "product-qty" }, - quantityInCart, - ) - : null, - h( - "button", - { - type: "button", - className: "store-btn store-btn-primary", - onClick: () => actions.addToCart(item), - }, - "Add to Cart", - ), - ), - ), - ); - }; - - StorefrontApp.componentFns.EmptyStateCard = function EmptyStateCard({ - title, - copy, - actionLabel, - onAction, - }) { - return h( - "article", - { className: "empty-state" }, - h("span", { className: "empty-state-kicker" }, "No Results"), - h("strong", { className: "card-label" }, title), - h("p", { className: "empty-state-copy" }, copy), - actionLabel && typeof onAction === "function" - ? h( - "button", - { - type: "button", - className: "store-btn store-btn-secondary", - onClick: onAction, - }, - actionLabel, - ) - : null, - ); - }; - - StorefrontApp.componentFns.CategoryGrid = function CategoryGrid(children) { - return createGrid("is-categories", children); - }; - - StorefrontApp.componentFns.SubcategoryGrid = function SubcategoryGrid( - children, - ) { - return createGrid("is-subcategories", children); - }; - - StorefrontApp.componentFns.ProductGrid = function ProductGrid(children) { - return createGrid("is-products", children); - }; - - StorefrontApp.componentFns.CatalogPager = function CatalogPager({ - currentPage, - totalPages, - startIndex, - endIndex, - totalItems, - }) { - ensureScopedStyle("storefront-cards", cardsCss); - - return h( - "div", - { - [scopeAttr]: "", - className: "catalog-pager-shell", - }, - h( - "div", - { className: "catalog-pager" }, - h( - "div", - { className: "catalog-pager-meta" }, - h("span", { className: "card-kicker" }, "Catalog Page"), - h( - "span", - { className: "catalog-pager-summary" }, - totalItems > 0 - ? `Showing ${startIndex}-${endIndex} of ${totalItems} items` - : "No items available", - ), - ), - h( - "div", - { className: "catalog-pager-actions" }, - h( - "button", - { - type: "button", - className: "store-btn store-btn-secondary", - disabled: currentPage <= 1, - onClick: () => actions.goToPreviousCatalogPage(), - }, - "Previous", - ), - h( - "span", - { className: "catalog-pager-page" }, - `Page ${currentPage} / ${totalPages}`, - ), - h( - "button", - { - type: "button", - className: "store-btn store-btn-secondary", - disabled: currentPage >= totalPages, - onClick: () => - actions.goToNextCatalogPage(totalPages), - }, - "Next", - ), - ), - ), - ); - }; -})(); - -(function () { - const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {}); - const { h, ensureScopedStyle } = StorefrontApp.runtime; - const store = StorefrontApp.store; - const getters = StorefrontApp.getters; - const actions = StorefrontApp.actions; - const { storeConfig } = StorefrontApp.data; - const scopeAttr = "data-ui-store-cart"; - const scopeSelector = `[${scopeAttr}]`; - const cartCss = ` -${scopeSelector} { - position: absolute; - inset: 0; - z-index: 4; - pointer-events: none; -} - -${scopeSelector}.is-open { - pointer-events: auto; -} - -${scopeSelector} .store-cart { - position: absolute; - top: 0.5rem; - right: 0.5rem; - bottom: 0.5rem; - width: min(24rem, calc(100% - 1rem)); - transform: translateX(calc(100% + 1rem)); - transition: transform 180ms ease; -} - -${scopeSelector}.is-open .store-cart { - transform: translateX(0); -} - -${scopeSelector} .cart-card { - height: 100%; - display: flex; - flex-direction: column; - gap: 1rem; - padding: 1rem; - border-radius: 1.5rem; - border: 1px solid var(--store-border); - background: linear-gradient(180deg, var(--store-surface) 0%, var(--store-surface-alt) 100%); - box-shadow: - 0 18px 40px rgb(11 27 46 / 0.16), - 0 4px 12px rgb(11 27 46 / 0.08); -} - -${scopeSelector} .cart-header { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; -} - -${scopeSelector} .cart-close { - min-width: 2.1rem; - height: 2.1rem; - display: inline-flex; - align-items: center; - justify-content: center; - padding: 0; - border-radius: 0.6rem; - border: 1px solid var(--store-border-strong); - background: rgb(255 255 255 / 0.78); - color: var(--store-accent); - font-size: 0.92rem; - font-weight: 800; - line-height: 1; - box-shadow: 0 6px 16px rgb(18 54 93 / 0.08); -} - -${scopeSelector} .cart-close:hover { - background: var(--store-accent-soft); - border-color: rgb(18 54 93 / 0.24); - color: var(--store-accent); -} - -${scopeSelector} .cart-close:focus-visible { - outline: 2px solid rgb(18 54 93 / 0.25); -} - -${scopeSelector} .cart-status, -${scopeSelector} .cart-kpi-card, -${scopeSelector} .cart-line { - border-radius: 0.95rem; - background: rgb(255 255 255 / 0.58); - border: 1px solid var(--store-border); -} - -${scopeSelector} .cart-status, -${scopeSelector} .cart-kpi-card, -${scopeSelector} .cart-line { - padding: 0.95rem; -} - -${scopeSelector} .cart-kpi { - display: grid; - grid-template-columns: repeat(2, minmax(0, 1fr)); - gap: 0.75rem; -} - -${scopeSelector} .kpi-label { - display: block; - margin-bottom: 0.3rem; - font-size: 0.68rem; - letter-spacing: 0.14em; - text-transform: uppercase; - font-weight: 700; - color: var(--store-text-subtle); -} - -${scopeSelector} .kpi-value { - font-size: 1rem; - font-weight: 700; -} - -${scopeSelector} .cart-lines { - flex: 1; - min-height: 0; - display: flex; - flex-direction: column; - gap: 0.75rem; - overflow-y: auto; - overflow-x: hidden; - scrollbar-gutter: stable; - scrollbar-width: auto; - scrollbar-color: rgb(120 136 155 / 0.9) rgb(255 255 255 / 0.55); -} - -${scopeSelector} .cart-lines::-webkit-scrollbar { - width: 12px; -} - -${scopeSelector} .cart-lines::-webkit-scrollbar-track { - background: rgb(255 255 255 / 0.55); - border-radius: 999px; -} - -${scopeSelector} .cart-lines::-webkit-scrollbar-thumb { - background: rgb(120 136 155 / 0.9); - border-radius: 999px; - border: 2px solid rgb(255 255 255 / 0.55); -} - -${scopeSelector} .cart-line { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -${scopeSelector} .cart-line-copy { - min-width: 0; - display: grid; - gap: 0.18rem; -} - -${scopeSelector} .cart-line-top, -${scopeSelector} .cart-line-controls, -${scopeSelector} .summary-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; -} - -${scopeSelector} .cart-line-title { - font-size: 0.92rem; - font-weight: 700; - line-height: 1.32; - overflow-wrap: anywhere; - word-break: break-word; -} - -${scopeSelector} .qty-controls { - display: inline-flex; - align-items: center; - gap: 0.45rem; -} - -${scopeSelector} .qty-badge { - min-width: 1.9rem; - text-align: center; - font-weight: 700; -} - -${scopeSelector} .qty-btn, -${scopeSelector} .remove-btn { - min-width: 2rem; - height: 2rem; - padding: 0 0.65rem; -} - -${scopeSelector} .cart-summary { - padding-top: 0.25rem; - border-top: 1px solid var(--store-accent-line); - display: grid; - gap: 0.7rem; -} - -${scopeSelector} .payment-source-field { - display: grid; - gap: 0.65rem; -} - -${scopeSelector} .payment-source-select { - width: 100%; - min-height: 2.9rem; - padding: 0 0.95rem; - border-radius: 0.8rem; - border: 1px solid var(--store-border); - background: rgb(255 255 255 / 0.78); - color: var(--store-text-main); -} - -${scopeSelector} .payment-source-meta, -${scopeSelector} .payment-source-row { - display: flex; - align-items: center; - justify-content: space-between; - gap: 0.75rem; -} - -${scopeSelector} .payment-source-meta { - padding: 0.85rem 0.9rem; - border-radius: 0.95rem; - border: 1px solid var(--store-border); - background: rgb(255 255 255 / 0.44); -} - -${scopeSelector} .payment-source-detail { - margin: 0.2rem 0 0; - font-size: 0.82rem; - line-height: 1.4; - color: var(--store-text-muted); -} - -${scopeSelector} .payment-source-label { - font-weight: 700; - color: var(--store-text-main); -} - -${scopeSelector} .payment-source-balance { - font-weight: 700; - color: var(--store-success); -} - -${scopeSelector} .payment-source-state { - font-size: 0.7rem; - letter-spacing: 0.14em; - text-transform: uppercase; - color: var(--store-text-subtle); -} - -${scopeSelector} .summary-row.total { - font-size: 1rem; - font-weight: 700; -} - -${scopeSelector} .summary-label, -${scopeSelector} .cart-line-meta { - color: var(--store-text-muted); -} - -${scopeSelector} .summary-value { - font-weight: 700; -} - -${scopeSelector} .summary-actions { - display: grid; - gap: 0.65rem; -} - -${scopeSelector} .cart-empty { - padding: 1rem; - border-radius: 0.95rem; - border: 1px dashed var(--store-border); - color: var(--store-text-muted); - background: rgb(255 255 255 / 0.38); -} - -@media (max-width: 1120px) { - ${scopeSelector} .store-cart { - top: 0; - right: 0; - bottom: 0; - width: min(24rem, 100%); - } -} -`; - - StorefrontApp.componentFns = StorefrontApp.componentFns || {}; - - StorefrontApp.componentFns.Cart = function Cart() { - const state = getters.getStoreState(store); - const summary = getters.summarizeCart(state.cartItems); - const paymentSources = getters.getPaymentSources(storeConfig); - const selectedPaymentSource = - getters.getPaymentSourceById( - storeConfig, - state.selectedPaymentSource, - ) || - paymentSources[0] || - null; - const availablePaymentSourceCount = paymentSources.filter( - (source) => source.enabled !== false, - ).length; - const selectedPaymentLabel = selectedPaymentSource - ? selectedPaymentSource.label - : "Unavailable"; - const selectedPaymentBalance = selectedPaymentSource - ? Number(selectedPaymentSource.balance || 0) - : 0; - const remainingSourceBalance = Math.max( - 0, - selectedPaymentBalance - summary.total, - ); - - ensureScopedStyle("storefront-cart", cartCss); - - return h( - "div", - { - className: state.cartOpen ? "is-open" : "", - [scopeAttr]: "", - "aria-hidden": state.cartOpen ? "false" : "true", - }, - h( - "aside", - { className: "store-cart" }, - h( - "section", - { className: "cart-card" }, - h( - "div", - { className: "cart-header" }, - h( - "div", - null, - h("span", { className: "eyebrow" }, "Cart"), - h( - "h2", - { className: "section-title" }, - "Acquisition Queue", - ), - ), - h( - "button", - { - type: "button", - className: "cart-close", - "aria-label": "Close cart", - title: "Close cart", - onClick: () => actions.closeCart(), - }, - "X", - ), - ), - h( - "div", - { className: "cart-kpi" }, - h( - "div", - { className: "cart-kpi-card" }, - h("span", { className: "kpi-label" }, "Items"), - h( - "span", - { className: "kpi-value" }, - summary.lineCount, - ), - ), - h( - "div", - { className: "cart-kpi-card" }, - h("span", { className: "kpi-label" }, "Payment"), - h( - "span", - { className: "kpi-value" }, - selectedPaymentLabel, - ), - ), - ), - h( - "div", - { className: "cart-status" }, - h("span", { className: "eyebrow" }, "Payment Source"), - h( - "div", - { className: "payment-source-field" }, - h( - "select", - { - className: "payment-source-select", - value: state.selectedPaymentSource, - onChange: (event) => - actions.selectPaymentSource( - event.target.value, - ), - }, - paymentSources.map((source) => - h( - "option", - { - value: source.id, - disabled: source.enabled === false, - }, - source.enabled === false - ? `${source.label} (Locked)` - : source.label, - ), - ), - ), - selectedPaymentSource - ? h( - "div", - { - className: "payment-source-meta", - }, - h( - "div", - null, - h( - "div", - { - className: - "payment-source-row", - }, - h( - "span", - { - className: - "payment-source-label", - }, - selectedPaymentSource.label, - ), - h( - "span", - { - className: - "payment-source-balance", - }, - getters.formatCurrency( - selectedPaymentSource.balance, - ), - ), - ), - h( - "p", - { - className: - "payment-source-detail", - }, - selectedPaymentSource.detail, - ), - ), - h( - "span", - { - className: "payment-source-state", - }, - availablePaymentSourceCount > 0 - ? selectedPaymentSource.enabled === - false - ? "Locked" - : "Available" - : "Unavailable", - ), - ) - : null, - ), - ), - h( - "div", - { - className: "cart-lines", - "data-preserve-scroll-id": "cart-lines", - }, - summary.lineCount > 0 - ? state.cartItems.map((item) => - h( - "div", - { className: "cart-line" }, - h( - "div", - { className: "cart-line-top" }, - h( - "div", - { - className: "cart-line-copy", - }, - h( - "div", - { - className: - "cart-line-title", - }, - item.name, - ), - ), - h( - "strong", - null, - getters.formatCurrency( - getters.parsePrice( - item.price, - ) * item.quantity, - ), - ), - ), - h( - "div", - { className: "cart-line-controls" }, - h( - "div", - { className: "qty-controls" }, - h( - "button", - { - type: "button", - className: - "store-btn store-btn-secondary qty-btn", - onClick: () => - actions.decrementCartItem( - item.code, - ), - }, - "-", - ), - h( - "span", - { className: "qty-badge" }, - item.quantity, - ), - h( - "button", - { - type: "button", - className: - "store-btn store-btn-secondary qty-btn", - onClick: () => - actions.incrementCartItem( - item.code, - ), - }, - "+", - ), - ), - h( - "button", - { - type: "button", - className: - "store-btn store-btn-secondary remove-btn", - onClick: () => - actions.removeCartItem( - item.code, - ), - }, - "Remove", - ), - ), - ), - ) - : h( - "div", - { className: "cart-empty" }, - "No items are queued yet. Add products from the catalog to build a checkout payload.", - ), - ), - h( - "div", - { className: "cart-summary" }, - h( - "div", - { className: "summary-row" }, - h("span", { className: "summary-label" }, "Items"), - h( - "span", - { className: "summary-value" }, - summary.itemCount, - ), - ), - h( - "div", - { className: "summary-row" }, - h( - "span", - { className: "summary-label" }, - "Subtotal", - ), - h( - "span", - { className: "summary-value" }, - getters.formatCurrency(summary.subtotal), - ), - ), - h( - "div", - { className: "summary-row" }, - h( - "span", - { className: "summary-label" }, - "Remaining Source", - ), - h( - "span", - { className: "summary-value" }, - getters.formatCurrency(remainingSourceBalance), - ), - ), - h( - "div", - { className: "summary-row total" }, - h("span", { className: "summary-label" }, "Total"), - h( - "span", - { className: "summary-value" }, - getters.formatCurrency(summary.total), - ), - ), - ), - h( - "div", - { className: "summary-actions" }, - h( - "button", - { - type: "button", - className: "store-btn store-btn-primary", - disabled: - summary.lineCount === 0 || - state.isCheckingOut, - onClick: () => actions.requestCheckout(), - }, - state.isCheckingOut - ? "Submitting Request..." - : "Submit Checkout", - ), - ), - ), - ), - ); - }; -})(); - -(function () { - const StorefrontApp = (window.StorefrontApp = window.StorefrontApp || {}); - const { h, ensureScopedStyle } = StorefrontApp.runtime; - const getters = StorefrontApp.getters; - const store = StorefrontApp.store; - const actions = StorefrontApp.actions; - const scopeAttr = "data-ui-store-navbar"; - const scopeSelector = `[${scopeAttr}]`; - const navbarCss = ` -${scopeSelector} { - display: flex; - align-items: center; - justify-content: space-between; - gap: 1rem; - padding: 0.9rem 1rem; - margin-bottom: 0.95rem; - border-bottom: 1px solid var(--store-accent-line); - background: - linear-gradient(180deg, rgb(255 255 255 / 0.52) 0%, transparent 100%), - linear-gradient(180deg, rgb(236 241 246 / 0.52) 0%, rgb(245 243 239 / 0.2) 100%); -} - -${scopeSelector} .store-breadcrumbs { - display: flex; - align-items: center; - gap: 0.55rem; - min-width: 0; - flex-wrap: wrap; -} - -${scopeSelector} .breadcrumb-link, -${scopeSelector} .breadcrumb-current, -${scopeSelector} .breadcrumb-separator { - font-size: 0.78rem; - letter-spacing: 0.1em; - text-transform: uppercase; - font-weight: 700; -} - -${scopeSelector} .breadcrumb-link { - padding: 0; - border: 0; - background: transparent; - color: var(--store-text-subtle); -} - -${scopeSelector} .breadcrumb-link:hover { - color: var(--store-accent); -} - -${scopeSelector} .breadcrumb-current { - color: var(--store-accent); -} - -${scopeSelector} .breadcrumb-separator { - color: rgb(124 138 155 / 0.72); -} - -${scopeSelector} .store-cart-btn { - position: relative; - width: 2.6rem; - height: 2.6rem; - display: inline-flex; - align-items: center; - justify-content: center; - flex: 0 0 auto; - border-radius: 0.7rem; - border: 1px solid var(--store-border-strong); - background: rgb(255 255 255 / 0.68); - color: var(--store-accent); - box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.75); -} - -${scopeSelector} .store-cart-btn:hover { - background: rgb(219 231 243 / 0.88); -} - -${scopeSelector} .cart-toggle-icon { - position: relative; - width: 0.95rem; - height: 0.8rem; - border: 1.5px solid currentColor; - border-radius: 0.16rem 0.16rem 0.24rem 0.24rem; -} - -${scopeSelector} .cart-toggle-icon::before { - content: ""; - position: absolute; - top: -0.34rem; - left: 0.2rem; - width: 0.5rem; - height: 0.3rem; - border: 1.5px solid currentColor; - border-bottom: 0; - border-radius: 0.35rem 0.35rem 0 0; -} - -${scopeSelector} .cart-count { - position: absolute; - top: -0.35rem; - right: -0.35rem; - min-width: 1.25rem; - height: 1.25rem; - padding: 0 0.3rem; - display: inline-flex; - align-items: center; - justify-content: center; - border-radius: 999px; - background: var(--store-accent); - color: #fff; - font-size: 0.68rem; - font-weight: 700; -} - -@media (max-width: 1120px) { - ${scopeSelector} { - align-items: flex-start; - } -} -`; - - StorefrontApp.componentFns = StorefrontApp.componentFns || {}; - - StorefrontApp.componentFns.Navbar = function Navbar() { - const state = getters.getStoreState(store); - const items = getters.getStoreBreadcrumbs(state); - const cartSummary = getters.summarizeCart(state.cartItems); - - ensureScopedStyle("storefront-navbar", navbarCss); - - return h( - "nav", - { [scopeAttr]: "" }, - h( - "div", - { - className: "store-breadcrumbs", - "aria-label": "Store navigation", - }, - items.map((item, index) => { - const isCurrent = index === items.length - 1; - - if (isCurrent) { - return h( - "span", - { className: "breadcrumb-current" }, - item.label, - ); - } - - return [ - h( - "button", - { - type: "button", - className: "breadcrumb-link", - onClick: () => - actions.navigateToBreadcrumb(item.id), - }, - item.label, - ), - h("span", { className: "breadcrumb-separator" }, "/"), - ]; - }), - ), - h( - "button", - { - type: "button", - className: "store-cart-btn", - onClick: () => actions.toggleCart(), - title: state.cartOpen ? "Close cart" : "Open cart", - "aria-label": state.cartOpen ? "Close cart" : "Open cart", - }, - h("span", { - className: "cart-toggle-icon", - "aria-hidden": "true", - }), - cartSummary.itemCount > 0 - ? h( - "span", - { className: "cart-count" }, - cartSummary.itemCount, - ) - : null, - ), - ); - }; -})(); - -(function () { - const ForgeWebUI = window.ForgeWebUI; - const StorefrontApp = window.StorefrontApp; - const app = ForgeWebUI.createApp({ - name: "store", - root: "#app", - setup({ root }) { - ForgeWebUI.mount(root, () => StorefrontApp.components.App(), { - preserveScroll: false, - }); - - if (StorefrontApp.bridge) { - StorefrontApp.bridge.notifyReady(); - } - }, - }); - - app.start(); -})(); +!function(){const e=window.ForgeWebUI;(window.StorefrontApp=window.StorefrontApp||{}).runtime=e,window.AppRuntime=e}(),function(){const e=window.StorefrontApp=window.StorefrontApp||{},t=e.runtime,[n,r]=t.createSignal(0),a=Object.create(null),o=Object.create(null),s=[],i=Object.create(null),c=Object.create(null),l=new WeakSet;let d=0,m=null,u=null,g=0;function p(e){let t=String(e||"").trim();if(!t)return"";for(;t.startsWith("\\")||t.startsWith("/");)t=t.slice(1);return/\.[A-Za-z0-9]+$/.test(t)||(t+=".paa"),t}function b(e){const t=String(e||"").trim().toLowerCase();return t.startsWith("data:image/")||t.startsWith("blob:")||t.startsWith("http://")||t.startsWith("https://")}function h(e,t){a[e]=t,function(){if(g)return;g=window.setTimeout(()=>{g=0,r(e=>e+1)},48)}()}function y(){if("undefined"!=typeof A3API&&"function"==typeof A3API.RequestTexture)for(;d<6&&s.length>0;){const e=s.shift();delete i[e],e&&void 0===a[e]&&!o[e]&&(d+=1,o[e]=Promise.resolve(A3API.RequestTexture(e,512)).then(t=>{const n=String(t||"").trim();b(n)?h(e,n):(console.warn("[Store UI] Ignoring unsupported texture response.",e,n),h(e,""))}).catch(t=>{console.warn("[Store UI] Failed to resolve texture.",e,t),h(e,"")}).finally(()=>{d=Math.max(0,d-1),delete o[e],y()}))}}function f(e){!e||i[e]||o[e]||(i[e]=!0,s.push(e),y())}function v(e){const t=p(e);t&&!c[t]&&(c[t]=!0,b(a[t])||o[t]||f(t))}function S(){const e=document.querySelectorAll("[data-store-texture-path]");if(0===e.length)return;const t=function(){const e=document.querySelector(".catalog-grid");return"function"!=typeof IntersectionObserver?null:(m&&u===e||(m&&m.disconnect(),u=e,m=new IntersectionObserver(e=>{e.forEach(e=>{e.isIntersecting&&(v(e.target.getAttribute("data-store-texture-path")),m.unobserve(e.target))})},{root:e,rootMargin:"240px 0px",threshold:.01})),m)}();e.forEach(e=>{if(l.has(e))return;l.add(e);const n=e.getAttribute("data-store-texture-path");t?t.observe(e):v(n)})}e.media={getTextureState:function(e){n();const t=p(e);return{path:t,isVisible:Boolean(t&&c[t]),isLoaded:Boolean(t&&a[t]&&b(a[t]))}},getTextureSource:function(e){n();const t=p(e);return t?b(e)?(a[t]=String(e).trim(),a[t]):void 0!==a[t]?a[t]:c[t]?(f(t),""):"":""},scheduleTextureObservation:function(){window.requestAnimationFrame(()=>{S()})}}}(),function(){const e=window.StorefrontApp=window.StorefrontApp||{},t={actorName:"",actorUid:"",approval:"Field Access",orgId:"",orgName:"",orgLeader:!1,defaultOrgCeo:!1,canUseOrgFunds:!1},n={budget:5e4,creditLine:0,availability:"In-Stock",moduleState:"Preview",searchTags:["Attachment","Grenade","Medical","Consumable","Static","Scope","Item","Misc"],paymentSources:[{id:"cash",label:"Cash",balance:0,enabled:!1,detail:"Use on-hand cash carried by the player."},{id:"bank",label:"Bank",balance:0,enabled:!1,detail:"Charge the player bank account."},{id:"org_funds",label:"Org Funds",balance:0,enabled:!1,detail:"Only organization leaders or the default-org CEO can use treasury funds."},{id:"credit_line",label:"Credit Line",balance:0,enabled:!1,detail:"No approved credit line is assigned to this member."}],defaultPaymentSource:"cash"};function r(e,t){var n;Object.keys(e).forEach(t=>delete e[t]),Object.assign(e,(n=t,JSON.parse(JSON.stringify(n))))}e.data={catalog:{categoryCards:[{id:"uniforms",label:"Uniforms"},{id:"headgear",label:"Headgear"},{id:"facewear",label:"Facewear"},{id:"vests",label:"Vests"},{id:"backpacks",label:"Backpacks"},{id:"attachments",label:"Attachments"},{id:"weapons",label:"Weapons"},{id:"ammo",label:"Ammo"},{id:"misc",label:"Misc"},{id:"vehicles",label:"Vehicles"}],vehicleCards:[{id:"cars",label:"Cars"},{id:"armor",label:"Armor"},{id:"helis",label:"Helicopters"},{id:"planes",label:"Planes"},{id:"naval",label:"Naval"},{id:"other",label:"Other"}],weaponCards:[{id:"primary",label:"Primary"},{id:"secondary",label:"Secondary"},{id:"handgun",label:"Handgun"}],previewItems:{uniforms:[],headgear:[],facewear:[],vests:[],backpacks:[],attachments:[],ammo:[],misc:[],primary:[],secondary:[],handgun:[],cars:[],armor:[],helis:[],planes:[],naval:[],other:[]}},session:Object.assign({},t),storeConfig:Object.assign({},n),applyHydratePayload(e){r(this.session,Object.assign({},t,e?.session||{})),r(this.storeConfig,Object.assign({},n,e?.storeConfig||{}))}}}(),function(){const e=window.StorefrontApp=window.StorefrontApp||{},{createSignal:t}=e.runtime,n=window.SharedLogic=window.SharedLogic||{};n.createStorefrontStore=function({createSignal:e}){function t(e){return{className:String(e?.className||e?.code||""),code:String(e?.code||e?.className||""),name:String(e?.name||e?.displayName||""),description:String(e?.description||""),price:String(e?.price||""),image:String(e?.image||""),type:String(e?.type||""),category:String(e?.category||""),entryKind:String(e?.entryKind||"item"),quantity:Math.max(0,Number(e?.quantity||0))}}function n(e){return{code:String(e?.code||""),name:String(e?.name||""),price:String(e?.price||"$0"),category:String(e?.category||""),entryKind:String(e?.entryKind||"item"),quantity:Math.max(1,Number(e?.quantity||1))}}return new class{constructor(){[this.getView,this.setView]=e("categories"),[this.getSelectedCategory,this.setSelectedCategory]=e(""),[this.getSelectedWeaponSlot,this.setSelectedWeaponSlot]=e(""),[this.getSelectedVehicleSlot,this.setSelectedVehicleSlot]=e(""),[this.getCartOpen,this.setCartOpen]=e(!1),[this.getSearchQuery,this.setSearchQuery]=e(""),[this.getCartItems,this.setCartItems]=e([]),[this.getCatalogItemsByKey,this.setCatalogItemsByKey]=e({}),[this.getIsCatalogLoading,this.setIsCatalogLoading]=e(!1),[this.getCatalogRequestKey,this.setCatalogRequestKey]=e(""),[this.getCatalogPage,this.setCatalogPage]=e(1),[this.getNotice,this.setNotice]=e({type:"",text:""}),[this.getIsCheckingOut,this.setIsCheckingOut]=e(!1),[this.getSelectedPaymentSource,this.setSelectedPaymentSource]=e("cash")}resetToCategories(){this.setView("categories"),this.setSelectedCategory(""),this.setSelectedWeaponSlot(""),this.setSelectedVehicleSlot(""),this.setIsCatalogLoading(!1),this.setCatalogRequestKey(""),this.setCatalogPage(1)}openWeaponsRoot(){this.setView("weapons"),this.setSelectedCategory("weapons"),this.setSelectedWeaponSlot(""),this.setSelectedVehicleSlot(""),this.setIsCatalogLoading(!1),this.setCatalogRequestKey(""),this.setCatalogPage(1)}openVehiclesRoot(){this.setView("vehicles"),this.setSelectedCategory("vehicles"),this.setSelectedVehicleSlot(""),this.setSelectedWeaponSlot(""),this.setIsCatalogLoading(!1),this.setCatalogRequestKey(""),this.setCatalogPage(1)}resetCatalogPage(){this.setCatalogPage(1)}setCatalogPageNumber(e){const t=Math.max(1,Number(e||1));this.setCatalogPage(t)}selectCategory(e){this.setSelectedCategory(e),this.setSelectedWeaponSlot(""),this.setSelectedVehicleSlot(""),this.setCatalogPage(1),"weapons"!==e?"vehicles"!==e?this.setView("items"):this.openVehiclesRoot():this.openWeaponsRoot()}selectSubcategory(e,t){"vehicle"===t?(this.setSelectedVehicleSlot(e),this.setSelectedWeaponSlot("")):(this.setSelectedWeaponSlot(e),this.setSelectedVehicleSlot("")),this.setCatalogPage(1),this.setView("items")}startCategoryRequest(e){const t=String(e||"").trim().toLowerCase();return!!t&&(this.setCatalogRequestKey(t),this.setIsCatalogLoading(!0),!0)}finishCategoryRequest(e){const t=String(e||"").trim().toLowerCase(),n=String(this.getCatalogRequestKey()||"").trim().toLowerCase();t&&n&&n!==t||(this.setCatalogRequestKey(""),this.setIsCatalogLoading(!1))}hydrateCategoryItems(e){const n=String(e?.category||"").trim().toLowerCase(),r=Array.isArray(e?.items)?e.items:[];if(!n)return this.setCatalogRequestKey(""),void this.setIsCatalogLoading(!1);this.setCatalogItemsByKey(e=>Object.assign({},e,{[n]:r.map(t)})),this.finishCategoryRequest(n)}ensureSelectedPaymentSource(e){const t=Array.isArray(e?.paymentSources)?e.paymentSources:[],n=String(this.getSelectedPaymentSource()||"").trim(),r=String(e?.defaultPaymentSource||"").trim(),a=t.map(e=>String(e?.id||"").trim()),o=t.find(e=>e&&!1!==e.enabled),s=r&&a.includes(r)?t.find(e=>String(e?.id||"").trim()===r):null;n&&a.includes(n)&&t.some(e=>String(e?.id||"").trim()===n&&!1!==e?.enabled)||(s&&!1!==s.enabled?this.setSelectedPaymentSource(r):o?this.setSelectedPaymentSource(String(o.id||"cash")):this.setSelectedPaymentSource(r||"cash"))}navigateToBreadcrumb(e){switch(e){case"categories":return this.resetToCategories(),!0;case"weapons":return this.openWeaponsRoot(),!0;case"vehicles":return this.openVehiclesRoot(),!0;default:return!1}}hydrateFromPayload(e){const t=Array.isArray(e?.cartItems)?e.cartItems:[];this.setCartItems(t.map(n)),this.setCartOpen(!1),this.setIsCheckingOut(!1),this.setCatalogItemsByKey({}),this.setCatalogRequestKey(""),this.setIsCatalogLoading(!1),this.setCatalogPage(1),this.ensureSelectedPaymentSource(e?.storeConfig||{})}hydrateStoreConfig(e){const t=Array.isArray(e?.cartItems)?e.cartItems:[];this.setCartItems(t.map(n)),this.setCartOpen(!1),this.setIsCheckingOut(!1),this.ensureSelectedPaymentSource(e?.storeConfig||{})}}},e.store=n.createStorefrontStore({createSignal:t})}(),function(){const e=window.StorefrontApp=window.StorefrontApp||{};function t(e){return e.selectedWeaponSlot||e.selectedVehicleSlot||e.selectedCategory}function n(e,t){if(!e)return!0;const n=String(e).trim().toLowerCase();return!n||t.some(e=>String(e||"").toLowerCase().includes(n))}function r(e){const t=Number(String(e||"0").replace(/[^0-9.-]+/g,""));return Number.isFinite(t)?t:0}function a(e){const t=String(e||"").trim().toLowerCase();return["items","misc"].includes(t)?"Misc":String(e||"").replace(/[-_]+/g," ").split(/\s+/).filter(Boolean).map(e=>e.charAt(0).toUpperCase()+e.slice(1).toLowerCase()).join(" ")}function o(e,r){const a=t(e),o=String(a||"").trim().toLowerCase(),s=e.catalogItemsByKey||{};return(Array.isArray(s[o])?s[o]:[]).filter(t=>n(e.searchQuery,[t.className,t.code,t.name,t.description,t.price,t.type]))}function s(e,t){const n=o(e).length,r=Math.max(1,Math.ceil(n/6)),a=Math.min(r,Math.max(1,Number(e.catalogPage||1)));return{pageSize:6,totalItems:n,totalPages:r,currentPage:a,startIndex:0===n?0:6*(a-1)+1,endIndex:Math.min(6*a,n)}}function i(e){return(Array.isArray(e?.paymentSources)?e.paymentSources:[]).map(e=>({id:String(e?.id||"").trim(),label:String(e?.label||e?.id||"").trim(),balance:Number(e?.balance||0),enabled:!1!==e?.enabled,detail:String(e?.detail||"").trim()}))}e.getters={formatTitle:a,formatCurrency:function(e){return`$${Number(e||0).toLocaleString()}`},parsePrice:r,getSelectionKey:t,getStoreState:function(e){return{view:e.getView(),selectedCategory:e.getSelectedCategory(),selectedWeaponSlot:e.getSelectedWeaponSlot(),selectedVehicleSlot:e.getSelectedVehicleSlot(),selectedPaymentSource:e.getSelectedPaymentSource(),cartOpen:e.getCartOpen(),searchQuery:e.getSearchQuery(),cartItems:e.getCartItems(),catalogItemsByKey:e.getCatalogItemsByKey(),isCatalogLoading:e.getIsCatalogLoading(),catalogRequestKey:e.getCatalogRequestKey(),catalogPage:e.getCatalogPage(),isCheckingOut:e.getIsCheckingOut()}},getStoreHeader:function(e){if("weapons"===e.view)return{eyebrow:"Weapons Division",title:"Weapon Categories",copy:"Select a weapon slot to open the next supply tier. Primary, secondary, and handgun are staged with the same state and bridge flow as the org portal.",badge:"3 Slots"};if("vehicles"===e.view)return{eyebrow:"Vehicle Motorpool",title:"Vehicle Categories",copy:"Select a vehicle class to open the next supply tier. Cars, armor, airframes, and naval options stay inside the same local store and bridge lifecycle.",badge:"6 Classes"};if("items"===e.view){const n=t(e)||"catalog",r=e.searchQuery?` Filtered by "${e.searchQuery}".`:"",o=e.isCatalogLoading?" Pulling live inventory from the game engine.":"";return{eyebrow:"Catalog Preview",title:a(n),copy:`Live category inventory generated from the game engine for the selected department.${r}${o}`,badge:"Preview Items"}}return{eyebrow:"Supply Categories",title:"Procurement Dashboard",copy:"Choose a category to enter the exchange. Weapons and vehicles open a second tier, while the other departments display placeholder product inventory inside the new runtime/store architecture.",badge:"8 Categories"}},getStoreBreadcrumbs:function(e){const t=[{id:"categories",label:"Supply Exchange"}];if("weapons"===e.view)return t.push({id:"weapons",label:"Weapons"}),t;if("vehicles"===e.view)return t.push({id:"vehicles",label:"Vehicles"}),t;if("items"===e.view){if(e.selectedWeaponSlot)return t.push({id:"weapons",label:"Weapons"}),t.push({id:"weapon-slot",label:a(e.selectedWeaponSlot)}),t;if(e.selectedVehicleSlot)return t.push({id:"vehicles",label:"Vehicles"}),t.push({id:"vehicle-slot",label:a(e.selectedVehicleSlot)}),t;e.selectedCategory&&t.push({id:"category",label:a(e.selectedCategory)})}return t},getVisibleCategoryCards:function(e,t){return t.categoryCards.filter(t=>n(e.searchQuery,[t.id,t.label]))},getVisibleSubcategoryCards:function(e,t){return("vehicles"===e.view?t.vehicleCards:t.weaponCards).filter(t=>n(e.searchQuery,[t.id,t.label]))},getVisibleItems:o,getVisibleItemsPage:function(e,t){const n=o(e),r=s(e),a=(r.currentPage-1)*r.pageSize;return n.slice(a,a+r.pageSize)},getCatalogPagination:s,summarizeCart:function(e){const t=e.reduce((e,t)=>e+Number(t.quantity||0),0),n=e.reduce((e,t)=>e+r(t.price)*Number(t.quantity||0),0);return{lineCount:e.length,itemCount:t,subtotal:n,total:n}},getPaymentSources:i,getPaymentSourceById:function(e,t){const n=String(t||"").trim();return i(e).find(e=>e.id===n)}}}(),function(){const e=window.StorefrontApp=window.StorefrontApp||{},t=e.store,n=window.ForgeWebUI.createBridge({closeEvent:"store::close",globalName:"StoreUIBridge",readyEvent:"store::ready"});n.on("store::hydrate",n=>{e.data.applyHydratePayload(n),t.hydrateFromPayload(n)}),n.on("store::config::hydrate",n=>{e.data.applyHydratePayload(n),t.hydrateStoreConfig(n)}),n.on("store::checkout::success",n=>{t.setIsCheckingOut(!1),t.setCartItems([]),t.setCartOpen(!1),e.actions&&e.actions.showNotice("success",n.message||"Checkout completed.")}),n.on("store::category::hydrate",e=>{t.hydrateCategoryItems(e)}),n.on("store::category::failure",n=>{t.finishCategoryRequest(n.category||""),e.actions&&e.actions.showNotice("error",n.message||"Category request failed.")}),n.on("store::checkout::failure",n=>{t.setIsCheckingOut(!1),e.actions&&e.actions.showNotice("error",n.message||"Checkout failed.")}),e.bridge={close:n.close,requestClose:function(){return n.close({})},requestCheckout:function(e){return n.send("store::checkout::request",e)},requestCategory:function(e){return n.send("store::category::request",e)},notifyReady:function(){return n.ready({loaded:!0})},receive:n.receive,sendEvent:n.send}}(),function(){const e=window.StorefrontApp=window.StorefrontApp||{},t=e.store,n=e.getters,{storeConfig:r,session:a}=e.data;let o=null;function s(e,n){t.setNotice({type:e,text:n}),o&&clearTimeout(o),o=setTimeout(()=>{t.setNotice({type:"",text:""}),o=null},3200)}function i(e,t,n){const r={items:[],vehicles:[],totalPrice:n,paymentMethod:t};return e.forEach(e=>{const t=function(e){return{classname:String(e?.code||"").trim(),category:String(e?.category||"").trim().toLowerCase(),entryKind:String(e?.entryKind||"item").trim().toLowerCase(),quantity:Math.max(1,Number(e?.quantity||1))}}(e);if("vehicle"!==t.entryKind)r.items.push({classname:t.classname,category:t.category,quantity:t.quantity});else for(let e=0;e!e)},closeCart:function(){t.setCartOpen(!1)},closeStore:function(){const t=e.bridge;if(t&&"function"==typeof t.requestClose){if(t.requestClose())return!0}return s("error","Store bridge is unavailable."),!1},navigateToBreadcrumb:function(e){return t.navigateToBreadcrumb(e)},selectCategory:function(e){t.selectCategory(e),c(),["weapons","vehicles"].includes(String(e||""))||d(e)},selectSubcategory:function(e,n){t.selectSubcategory(e,n),c(),d(e)},goToCatalogPage:l,goToNextCatalogPage:function(e){const n=Number(t.getCatalogPage()||1);return!(n>=Math.max(1,Number(e||1)))&&(l(n+1),!0)},goToPreviousCatalogPage:function(){const e=Number(t.getCatalogPage()||1);return!(e<=1)&&(l(e-1),!0)},addToCart:function(e){t.setCartItems(t=>{const n=t.findIndex(t=>t.code===e.code);if(-1===n)return[...t,{code:e.code,name:e.name,price:e.price,category:e.category,entryKind:e.entryKind,quantity:1}];const r=[...t];return r[n]=Object.assign({},r[n],{category:e.category,entryKind:e.entryKind,quantity:r[n].quantity+1}),r}),s("success",`${e.name} added to the acquisition queue.`)},incrementCartItem:function(e){t.setCartItems(t=>t.map(t=>t.code===e?Object.assign({},t,{quantity:t.quantity+1}):t))},decrementCartItem:function(e){t.setCartItems(t=>t.map(t=>t.code===e?Object.assign({},t,{quantity:Math.max(0,t.quantity-1)}):t).filter(e=>e.quantity>0))},removeCartItem:function(e){t.setCartItems(t=>t.filter(t=>t.code!==e))},selectPaymentSource:function(e){const a=String(e||"").trim(),o=n.getPaymentSources(r).find(e=>e.id===a);return o?!1===o.enabled?(s("error",o.detail||"Selected payment source is not available."),!1):(t.setSelectedPaymentSource(a),!0):(s("error","Selected payment source is unavailable."),!1)},requestCheckout:function(){const a=t.getCartItems();if(0===a.length)return s("error","Add at least one item before checkout."),!1;const o=n.summarizeCart(a),c=n.getPaymentSourceById(r,t.getSelectedPaymentSource());if(!c)return s("error","Select a payment source before checkout."),!1;if(!1===c.enabled)return s("error",c.detail||"Selected payment source is unavailable."),!1;if(o.total>Number(c.balance||0))return s("error",`${c.label} cannot cover this checkout total.`),!1;const l=e.bridge;if(!l||"function"!=typeof l.requestCheckout)return s("error","Checkout bridge is unavailable."),!1;t.setIsCheckingOut(!0);const d=i(a,c.id,o.total);return!!l.requestCheckout({checkoutJson:JSON.stringify(d)})||(t.setIsCheckingOut(!1),s("error","Checkout bridge is unavailable."),!1)},formatTitle:n.formatTitle,formatCurrency:n.formatCurrency}}(),function(){const e=window.StorefrontApp=window.StorefrontApp||{},{h:t,ensureScopedStyle:n}=e.runtime,r=window.SharedUI.componentFns.WindowTitleBar,a=e.store,o=e.getters,s=e.actions,{catalog:i,session:c,storeConfig:l}=e.data,d="data-ui-store-app-shell",m=`[${d}]`,u=`\n${m} {\n display: flex;\n flex-direction: column;\n width: 100%;\n height: 100%;\n overflow: hidden;\n background: var(--store-shell-bg);\n}\n\n${m} .footer-title,\n${m} .eyebrow {\n font-size: 0.68rem;\n letter-spacing: 0.18em;\n text-transform: uppercase;\n color: var(--store-text-subtle);\n font-weight: 700;\n}\n\n${m} .module-header,\n${m} .store-panel-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n}\n\n${m} .store-app {\n flex: 1;\n min-height: 0;\n width: min(100%, 1613px);\n margin: 0 auto;\n display: grid;\n grid-template-columns: 308px minmax(0, 1fr);\n gap: 1.25rem;\n padding: 1.25rem;\n}\n\n${m} .store-sidebar,\n${m} .store-main {\n min-height: 0;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n}\n\n${m} .store-main {\n position: relative;\n overflow: hidden;\n}\n\n${m} .module-card,\n${m} .store-panel {\n background: linear-gradient(180deg, var(--store-surface) 0%, var(--store-surface-alt) 100%);\n border: 1px solid var(--store-border);\n border-radius: 1.35rem;\n}\n\n${m} .module-card {\n padding: 1rem;\n}\n\n${m} .store-panel {\n min-height: 0;\n flex: 1 1 auto;\n display: flex;\n flex-direction: column;\n width: min(100%, 1280px);\n overflow: hidden;\n}\n\n${m} .module-header {\n margin-bottom: 0.85rem;\n}\n\n${m} .store-panel-header {\n padding: 1rem 1rem 0;\n}\n\n${m} .section-title {\n margin: 0;\n font-size: 1.1rem;\n font-weight: 700;\n letter-spacing: -0.02em;\n color: var(--store-text-main);\n}\n\n${m} .section-copy,\n${m} .footer-copy {\n margin: 0.2rem 0 0;\n font-size: 0.9rem;\n line-height: 1.45;\n color: var(--store-text-muted);\n}\n\n${m} .pill {\n padding: 0.48rem 0.8rem;\n border-radius: 999px;\n background: var(--store-accent-soft);\n color: var(--store-accent);\n font-size: 0.74rem;\n font-weight: 700;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n}\n\n${m} .search-module {\n display: flex;\n flex-direction: column;\n gap: 0.8rem;\n}\n\n${m} .search-form {\n display: grid;\n gap: 0.7rem;\n}\n\n${m} .search-input {\n width: 100%;\n height: 2.9rem;\n padding: 0 0.95rem;\n border-radius: 0.8rem;\n border: 1px solid var(--store-border);\n background: rgb(255 255 255 / 0.75);\n color: var(--store-text-main);\n}\n\n${m} .quick-tags {\n display: flex;\n flex-wrap: wrap;\n gap: 0.5rem;\n}\n\n${m} .quick-tag {\n padding: 0.55rem 0.72rem;\n border-radius: 999px;\n border: 1px solid var(--store-border);\n background: rgb(255 255 255 / 0.52);\n color: var(--store-text-muted);\n font-size: 0.75rem;\n letter-spacing: 0.08em;\n text-transform: uppercase;\n}\n\n${m} .filter-stack {\n display: grid;\n gap: 0.85rem;\n}\n\n${m} .filter-group {\n padding: 0.95rem;\n border-radius: 0.8rem;\n background: rgb(255 255 255 / 0.48);\n border: 1px solid var(--store-border);\n}\n\n${m} .filter-label {\n display: block;\n margin-bottom: 0.55rem;\n font-size: 0.72rem;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--store-text-subtle);\n font-weight: 700;\n}\n\n${m} .filter-value {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n color: var(--store-text-main);\n font-size: 0.92rem;\n font-weight: 600;\n}\n\n${m} .filter-placeholder {\n color: var(--store-text-muted);\n font-weight: 500;\n}\n\n${m} .store-panel-intro {\n padding: 0 1rem 1rem;\n border-bottom: 1px solid var(--store-accent-line);\n}\n\n${m} .store-footer-bar {\n width: 100%;\n border-top: 1px solid rgb(18 54 93 / 0.1);\n background: transparent;\n}\n\n${m} .store-footer {\n width: min(100%, 1613px);\n margin: 0 auto;\n display: grid;\n grid-template-columns: repeat(3, minmax(0, 1fr));\n gap: 1rem;\n padding: 0.95rem 1.25rem 1.15rem;\n}\n\n${m} .footer-block {\n display: flex;\n flex-direction: column;\n gap: 0.25rem;\n}\n\n${m} .store-toast-stack {\n position: fixed;\n top: 1.2rem;\n right: 1.5rem;\n z-index: 10;\n display: flex;\n flex-direction: column;\n gap: 0.65rem;\n}\n\n${m} .store-toast {\n max-width: 24rem;\n padding: 0.85rem 1rem;\n border-radius: 0.9rem;\n border: 1px solid var(--store-border);\n background: #fff;\n box-shadow: 0 14px 28px rgb(16 34 56 / 0.14);\n font-size: 0.92rem;\n}\n\n${m} .store-toast.is-success {\n background: #ecfdf5;\n border-color: #bbf7d0;\n color: #166534;\n}\n\n${m} .store-toast.is-error {\n background: #fef2f2;\n border-color: #fecaca;\n color: #991b1b;\n}\n\n@media (max-width: 1440px) {\n ${m} .store-app {\n grid-template-columns: 284px minmax(0, 1fr);\n }\n}\n\n@media (max-width: 1120px) {\n ${m} .store-app {\n grid-template-columns: 1fr;\n overflow: auto;\n }\n\n ${m} .store-sidebar,\n ${m} .store-main {\n min-height: auto;\n }\n\n ${m} .store-main {\n overflow: visible;\n }\n\n ${m} .store-footer {\n grid-template-columns: 1fr;\n }\n\n ${m} .store-toast-stack {\n right: 1rem;\n left: 1rem;\n }\n}\n`;e.components=e.components||{},e.componentFns=e.componentFns||{},e.components.App=function(){const m=e.componentFns.Navbar,g=e.componentFns.Cart,p=o.getStoreState(a),b=o.getStoreHeader(p),h=a.getNotice(),y=p.searchQuery,f=o.getPaymentSources(l).filter(e=>!1!==e.enabled).length,v="items"===p.view?s.formatTitle(o.getSelectionKey(p)||"Catalog"):s.formatTitle(p.view),S=o.getPaymentSourceById(l,p.selectedPaymentSource)||null;return n("storefront-app-shell",u),t("div",{[d]:""},r({kicker:"FORGE Logistics",title:"Supply Exchange",onClose:()=>s.closeStore(),closeLabel:"Close store interface"}),h.text?t("div",{className:"store-toast-stack"},t("div",{className:"error"===h.type?"store-toast is-error":"store-toast is-success"},h.text)):null,t("div",{className:"store-app"},t("aside",{className:"store-sidebar"},t("section",{className:"module-card search-module"},t("div",{className:"module-header"},t("div",null,t("span",{className:"eyebrow"},"Search"),t("h2",{className:"section-title"},"Inventory Search")),t("span",{className:"pill"},"Live")),t("div",{className:"search-form"},t("input",{id:"store-search-input",type:"text",className:"search-input",placeholder:"Search inventory, classes, or suppliers",value:y}),t("div",{style:{display:"flex",gap:"0.65rem"}},t("button",{type:"button",className:"store-btn store-btn-primary",onClick:()=>s.applySearchQuery(document.getElementById("store-search-input")?.value||"")},"Apply Search"),t("button",{type:"button",className:"store-btn store-btn-secondary",onClick:()=>s.clearSearch()},"Clear"))),t("div",{className:"quick-tags"},(l.searchTags||[]).map(e=>t("span",{className:"quick-tag"},e)))),t("section",{className:"module-card"},t("div",{className:"module-header"},t("div",null,t("span",{className:"eyebrow"},"Filter"),t("h2",{className:"section-title"},"Procurement Filters")),t("span",{className:"pill"},l.moduleState)),t("div",{className:"filter-stack"},t("div",{className:"filter-group"},t("span",{className:"filter-label"},"Department"),t("div",{className:"filter-value"},t("span",{className:"filter-placeholder"},v))),t("div",{className:"filter-group"},t("span",{className:"filter-label"},"Availability"),t("div",{className:"filter-value"},t("span",{className:"filter-placeholder"},l.availability))),t("div",{className:"filter-group"},t("span",{className:"filter-label"},"Payment"),t("div",{className:"filter-value"},t("span",{className:"filter-placeholder"},S?S.label:"Cash")))))),t("main",{className:"store-main"},t("section",{className:"store-panel"},m(),t("div",{className:"store-panel-header"},t("div",null,t("span",{className:"eyebrow"},b.eyebrow),t("h1",{className:"section-title"},b.title)),t("span",{className:"pill"},b.badge)),t("div",{className:"store-panel-intro"},t("p",{className:"section-copy"},b.copy)),function(t){const{CategoryCard:n,SubcategoryCard:r,ProductCard:a,EmptyStateCard:c,CategoryGrid:l,SubcategoryGrid:d,ProductGrid:m,CatalogPager:u}=e.componentFns;if("weapons"===t.view||"vehicles"===t.view){const e="vehicles"===t.view?"vehicle":"weapon",n=o.getVisibleSubcategoryCards(t,i);return d(n.length>0?n.map(t=>r(t,e)):c({title:"No matching slots",copy:"Try a different search query or clear the current filter.",actionLabel:"Clear Search",onAction:()=>s.clearSearch()}))}if("items"===t.view){const e=o.getVisibleItems(t,i),n=o.getVisibleItemsPage(t,i),r=o.getCatalogPagination(t,i),l=t.cartItems.reduce((e,t)=>(e[t.code]=t.quantity,e),{}),d=String(o.getSelectionKey(t)||"").toLowerCase();return[m(t.isCatalogLoading&&t.catalogRequestKey===d&&0===e.length?c({title:"Loading inventory",copy:"Pulling live category items from the game engine."}):e.length>0?n.map(e=>a(e,l[e.code]||0)):c({title:"No category items",copy:t.searchQuery?"Your search filter excluded the live inventory returned for this category.":"The game engine did not return any items for this category yet.",actionLabel:"Clear Search",onAction:()=>s.clearSearch()})),e.length>0?u(r):null]}const g=o.getVisibleCategoryCards(t,i);return l(g.length>0?g.map(e=>n(e)):c({title:"No matching departments",copy:"Your search filter excluded every top-level department.",actionLabel:"Clear Search",onAction:()=>s.clearSearch()}))}(p)),g())),t("footer",{className:"store-footer-bar"},t("div",{className:"store-footer"},t("div",{className:"footer-block"},t("span",{className:"footer-title"},"Procurement Desk"),t("span",{className:"footer-copy"},"Authorized supply browsing for personnel loadout preparation and mission staging.")),t("div",{className:"footer-block"},t("span",{className:"footer-title"},"Catalog Scope"),t("span",{className:"footer-copy"},"Uniforms, protective gear, weapon slots, vehicles, ammunition groups, and general support inventory.")),t("div",{className:"footer-block"},t("span",{className:"footer-title"},"Purchase Access"),t("span",{className:"footer-copy"},`${c.approval} approval. ${f} payment source(s) currently available${c.orgName?` for ${c.orgName}.`:"."}`)))))}}(),function(){const e=window.StorefrontApp=window.StorefrontApp||{},{h:t,ensureScopedStyle:n}=e.runtime,r=e.actions,a=e.media,o="data-ui-store-cards",s=`[${o}]`,i=`\n${s}.catalog-grid-shell {\n flex: 1;\n min-height: 0;\n display: flex;\n}\n\n${s}.catalog-pager-shell {\n display: block;\n}\n\n${s} .catalog-grid {\n flex: 1;\n min-height: 0;\n width: 100%;\n padding: 1rem;\n display: grid;\n gap: 1rem;\n align-content: start;\n overflow-y: auto;\n overflow-x: hidden;\n scrollbar-gutter: stable;\n scrollbar-width: auto;\n scrollbar-color: rgb(120 136 155 / 0.9) rgb(255 255 255 / 0.45);\n}\n\n${s} .catalog-grid::-webkit-scrollbar {\n width: 12px;\n}\n\n${s} .catalog-grid::-webkit-scrollbar-track {\n background: rgb(255 255 255 / 0.45);\n border-radius: 999px;\n}\n\n${s} .catalog-grid::-webkit-scrollbar-thumb {\n background: rgb(120 136 155 / 0.9);\n border-radius: 999px;\n border: 2px solid rgb(255 255 255 / 0.45);\n}\n\n${s} .catalog-grid.is-categories,\n${s} .catalog-grid.is-products {\n grid-template-columns: repeat(3, minmax(0, 1fr));\n}\n\n${s} .catalog-grid.is-subcategories {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n}\n\n${s} .card-button,\n${s} .product-card,\n${s} .empty-state {\n border: 1px solid var(--store-border);\n border-radius: 1.15rem;\n background:\n linear-gradient(180deg, rgb(255 255 255 / 0.72) 0%, rgb(226 233 239 / 0.9) 100%),\n var(--store-surface-strong);\n color: var(--store-accent);\n box-shadow:\n inset 0 1px 0 rgb(255 255 255 / 0.8),\n 0 10px 24px rgb(16 34 56 / 0.06);\n}\n\n${s} .card-button {\n min-height: 12.5rem;\n display: flex;\n flex-direction: column;\n justify-content: center;\n gap: 0.75rem;\n padding: 1.35rem;\n text-align: left;\n transition:\n transform 120ms ease,\n box-shadow 120ms ease,\n border-color 120ms ease;\n}\n\n${s} .card-button:hover,\n${s} .product-card:hover {\n transform: translateY(-2px);\n border-color: rgb(18 54 93 / 0.32);\n box-shadow:\n 0 16px 28px rgb(16 34 56 / 0.11),\n inset 0 1px 0 rgb(255 255 255 / 0.88);\n}\n\n${s} .card-kicker,\n${s} .product-code,\n${s} .empty-state-kicker {\n font-size: 0.72rem;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n font-weight: 700;\n color: var(--store-text-subtle);\n}\n\n${s} .card-label {\n font-size: 1.08rem;\n font-weight: 700;\n letter-spacing: 0.06em;\n text-transform: uppercase;\n}\n\n${s} .card-copy,\n${s} .product-copy,\n${s} .empty-state-copy {\n margin: 0;\n color: var(--store-text-muted);\n line-height: 1.45;\n}\n\n${s} .product-copy {\n white-space: pre-line;\n}\n\n${s} .product-card {\n min-height: 15.5rem;\n padding: 0.8rem;\n display: flex;\n flex-direction: column;\n gap: 0.65rem;\n}\n\n${s} .product-image {\n height: 5.9rem;\n border-radius: 0.95rem;\n border: 1px dashed rgb(18 54 93 / 0.24);\n background: linear-gradient(135deg, rgb(235 240 245) 0%, rgb(221 228 235) 100%);\n display: flex;\n align-items: center;\n justify-content: center;\n color: var(--store-text-subtle);\n font-size: 0.78rem;\n letter-spacing: 0.16em;\n text-transform: uppercase;\n overflow: hidden;\n}\n\n${s} .product-image-asset {\n width: 100%;\n height: 100%;\n object-fit: contain;\n}\n\n${s} .product-meta {\n display: flex;\n flex-direction: column;\n gap: 0.2rem;\n}\n\n${s} .product-name {\n font-size: 0.96rem;\n font-weight: 700;\n color: var(--store-text-main);\n line-height: 1.3;\n}\n\n${s} .product-footer {\n margin-top: auto;\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n}\n\n${s} .product-price {\n font-size: 0.96rem;\n font-weight: 700;\n color: var(--store-success);\n}\n\n${s} .product-qty {\n display: inline-flex;\n align-items: center;\n justify-content: center;\n min-width: 1.85rem;\n height: 1.85rem;\n border-radius: 999px;\n background: var(--store-accent-soft);\n color: var(--store-accent);\n font-size: 0.76rem;\n font-weight: 700;\n}\n\n${s} .empty-state {\n padding: 1.35rem;\n display: flex;\n flex-direction: column;\n gap: 0.65rem;\n}\n\n${s} .catalog-pager {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.9rem;\n padding: 0.55rem 0.9rem 0.75rem;\n border-top: 1px solid var(--store-accent-line);\n}\n\n${s} .catalog-pager-meta {\n display: flex;\n flex-direction: column;\n gap: 0.15rem;\n}\n\n${s} .catalog-pager-summary {\n font-size: 0.86rem;\n color: var(--store-text-muted);\n}\n\n${s} .catalog-pager-actions {\n display: inline-flex;\n align-items: center;\n gap: 0.6rem;\n}\n\n${s} .catalog-pager-page {\n min-width: 5.75rem;\n text-align: center;\n font-size: 0.82rem;\n font-weight: 700;\n color: var(--store-accent);\n letter-spacing: 0.08em;\n text-transform: uppercase;\n}\n\n${s} .product-copy {\n display: -webkit-box;\n overflow: hidden;\n -webkit-box-orient: vertical;\n -webkit-line-clamp: 2;\n}\n\n@media (max-width: 1440px) {\n ${s} .catalog-grid.is-categories,\n ${s} .catalog-grid.is-products {\n grid-template-columns: repeat(2, minmax(0, 1fr));\n }\n}\n\n@media (max-width: 1120px) {\n ${s} .catalog-grid.is-categories,\n ${s} .catalog-grid.is-subcategories,\n ${s} .catalog-grid.is-products {\n grid-template-columns: 1fr;\n }\n}\n`;function c(e,r){return n("storefront-cards",i),"is-products"===e&&a&&"function"==typeof a.scheduleTextureObservation&&a.scheduleTextureObservation(),t("div",{[o]:"",className:"catalog-grid-shell"},t("div",{className:`catalog-grid ${e}`,"data-preserve-scroll-id":"catalog-grid"},r))}e.componentFns=e.componentFns||{},e.componentFns.CategoryCard=function(e){return t("button",{type:"button",className:"card-button",onClick:()=>r.selectCategory(e.id)},t("span",{className:"card-kicker"},"Department"),t("strong",{className:"card-label"},e.label),t("p",{className:"card-copy"},"Open this department and move into staged inventory browsing."))},e.componentFns.SubcategoryCard=function(e,n){return t("button",{type:"button",className:"card-button",onClick:()=>r.selectSubcategory(e.id,n)},t("span",{className:"card-kicker"},"vehicle"===n?"Vehicle Class":"Weapon Slot"),t("strong",{className:"card-label"},e.label),t("p",{className:"card-copy"},"Open the next tier and review product previews for this selection."))},e.componentFns.ProductCard=function(e,n){const o=a&&"function"==typeof a.getTextureState?a.getTextureState(e.image):{isVisible:!0},s=a&&"function"==typeof a.getTextureSource?a.getTextureSource(e.image):"",i=function(e,t){const n=String(e||"").trim();if(!n)return t;const r=n.replace(/<\s*br\s*\/?\s*>/gi,"\n").replace(/<\/\s*p\s*>/gi,"\n").replace(/<\s*li\s*>/gi,"- ").replace(/<\/\s*li\s*>/gi,"\n"),a=document.createElement("div");return a.innerHTML=r,String(a.textContent||a.innerText||"").replace(/\u00a0/g," ").replace(/[ \t]+\n/g,"\n").replace(/\n{3,}/g,"\n\n").trim()||t}(e.description,e.className||e.code);return t("article",{className:"product-card"},t("div",{className:"product-image","data-store-texture-path":e.image||""},s?t("img",{className:"product-image-asset",src:s,alt:e.name,loading:"lazy"}):o.isVisible?"Loading Image":"Image Placeholder"),t("div",{className:"product-meta"},t("span",{className:"product-code"},e.type||e.code||e.className),t("strong",{className:"product-name"},e.name)),t("p",{className:"product-copy"},i),t("div",{className:"product-footer"},t("span",{className:"product-price"},e.price||"Pending"),t("div",{style:{display:"flex",alignItems:"center",gap:"0.55rem"}},n>0?t("span",{className:"product-qty"},n):null,t("button",{type:"button",className:"store-btn store-btn-primary",onClick:()=>r.addToCart(e)},"Add to Cart"))))},e.componentFns.EmptyStateCard=function({title:e,copy:n,actionLabel:r,onAction:a}){return t("article",{className:"empty-state"},t("span",{className:"empty-state-kicker"},"No Results"),t("strong",{className:"card-label"},e),t("p",{className:"empty-state-copy"},n),r&&"function"==typeof a?t("button",{type:"button",className:"store-btn store-btn-secondary",onClick:a},r):null)},e.componentFns.CategoryGrid=function(e){return c("is-categories",e)},e.componentFns.SubcategoryGrid=function(e){return c("is-subcategories",e)},e.componentFns.ProductGrid=function(e){return c("is-products",e)},e.componentFns.CatalogPager=function({currentPage:e,totalPages:a,startIndex:s,endIndex:c,totalItems:l}){return n("storefront-cards",i),t("div",{[o]:"",className:"catalog-pager-shell"},t("div",{className:"catalog-pager"},t("div",{className:"catalog-pager-meta"},t("span",{className:"card-kicker"},"Catalog Page"),t("span",{className:"catalog-pager-summary"},l>0?`Showing ${s}-${c} of ${l} items`:"No items available")),t("div",{className:"catalog-pager-actions"},t("button",{type:"button",className:"store-btn store-btn-secondary",disabled:e<=1,onClick:()=>r.goToPreviousCatalogPage()},"Previous"),t("span",{className:"catalog-pager-page"},`Page ${e} / ${a}`),t("button",{type:"button",className:"store-btn store-btn-secondary",disabled:e>=a,onClick:()=>r.goToNextCatalogPage(a)},"Next"))))}}(),function(){const e=window.StorefrontApp=window.StorefrontApp||{},{h:t,ensureScopedStyle:n}=e.runtime,r=e.store,a=e.getters,o=e.actions,{storeConfig:s}=e.data,i="data-ui-store-cart",c=`[${i}]`,l=`\n${c} {\n position: absolute;\n inset: 0;\n z-index: 4;\n pointer-events: none;\n}\n\n${c}.is-open {\n pointer-events: auto;\n}\n\n${c} .store-cart {\n position: absolute;\n top: 0.5rem;\n right: 0.5rem;\n bottom: 0.5rem;\n width: min(24rem, calc(100% - 1rem));\n transform: translateX(calc(100% + 1rem));\n transition: transform 180ms ease;\n}\n\n${c}.is-open .store-cart {\n transform: translateX(0);\n}\n\n${c} .cart-card {\n height: 100%;\n display: flex;\n flex-direction: column;\n gap: 1rem;\n padding: 1rem;\n border-radius: 1.5rem;\n border: 1px solid var(--store-border);\n background: linear-gradient(180deg, var(--store-surface) 0%, var(--store-surface-alt) 100%);\n box-shadow:\n 0 18px 40px rgb(11 27 46 / 0.16),\n 0 4px 12px rgb(11 27 46 / 0.08);\n}\n\n${c} .cart-header {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n}\n\n${c} .cart-close {\n min-width: 2.1rem;\n height: 2.1rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n padding: 0;\n border-radius: 0.6rem;\n border: 1px solid var(--store-border-strong);\n background: rgb(255 255 255 / 0.78);\n color: var(--store-accent);\n font-size: 0.92rem;\n font-weight: 800;\n line-height: 1;\n box-shadow: 0 6px 16px rgb(18 54 93 / 0.08);\n}\n\n${c} .cart-close:hover {\n background: var(--store-accent-soft);\n border-color: rgb(18 54 93 / 0.24);\n color: var(--store-accent);\n}\n\n${c} .cart-close:focus-visible {\n outline: 2px solid rgb(18 54 93 / 0.25);\n}\n\n${c} .cart-status,\n${c} .cart-kpi-card,\n${c} .cart-line {\n border-radius: 0.95rem;\n background: rgb(255 255 255 / 0.58);\n border: 1px solid var(--store-border);\n}\n\n${c} .cart-status,\n${c} .cart-kpi-card,\n${c} .cart-line {\n padding: 0.95rem;\n}\n\n${c} .cart-kpi {\n display: grid;\n grid-template-columns: repeat(2, minmax(0, 1fr));\n gap: 0.75rem;\n}\n\n${c} .kpi-label {\n display: block;\n margin-bottom: 0.3rem;\n font-size: 0.68rem;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n font-weight: 700;\n color: var(--store-text-subtle);\n}\n\n${c} .kpi-value {\n font-size: 1rem;\n font-weight: 700;\n}\n\n${c} .cart-lines {\n flex: 1;\n min-height: 0;\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n overflow-y: auto;\n overflow-x: hidden;\n scrollbar-gutter: stable;\n scrollbar-width: auto;\n scrollbar-color: rgb(120 136 155 / 0.9) rgb(255 255 255 / 0.55);\n}\n\n${c} .cart-lines::-webkit-scrollbar {\n width: 12px;\n}\n\n${c} .cart-lines::-webkit-scrollbar-track {\n background: rgb(255 255 255 / 0.55);\n border-radius: 999px;\n}\n\n${c} .cart-lines::-webkit-scrollbar-thumb {\n background: rgb(120 136 155 / 0.9);\n border-radius: 999px;\n border: 2px solid rgb(255 255 255 / 0.55);\n}\n\n${c} .cart-line {\n display: flex;\n flex-direction: column;\n gap: 0.75rem;\n}\n\n${c} .cart-line-copy {\n min-width: 0;\n display: grid;\n gap: 0.18rem;\n}\n\n${c} .cart-line-top,\n${c} .cart-line-controls,\n${c} .summary-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n}\n\n${c} .cart-line-title {\n font-size: 0.92rem;\n font-weight: 700;\n line-height: 1.32;\n overflow-wrap: anywhere;\n word-break: break-word;\n}\n\n${c} .qty-controls {\n display: inline-flex;\n align-items: center;\n gap: 0.45rem;\n}\n\n${c} .qty-badge {\n min-width: 1.9rem;\n text-align: center;\n font-weight: 700;\n}\n\n${c} .qty-btn,\n${c} .remove-btn {\n min-width: 2rem;\n height: 2rem;\n padding: 0 0.65rem;\n}\n\n${c} .cart-summary {\n padding-top: 0.25rem;\n border-top: 1px solid var(--store-accent-line);\n display: grid;\n gap: 0.7rem;\n}\n\n${c} .payment-source-field {\n display: grid;\n gap: 0.65rem;\n}\n\n${c} .payment-source-select {\n width: 100%;\n min-height: 2.9rem;\n padding: 0 0.95rem;\n border-radius: 0.8rem;\n border: 1px solid var(--store-border);\n background: rgb(255 255 255 / 0.78);\n color: var(--store-text-main);\n}\n\n${c} .payment-source-meta,\n${c} .payment-source-row {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 0.75rem;\n}\n\n${c} .payment-source-meta {\n padding: 0.85rem 0.9rem;\n border-radius: 0.95rem;\n border: 1px solid var(--store-border);\n background: rgb(255 255 255 / 0.44);\n}\n\n${c} .payment-source-detail {\n margin: 0.2rem 0 0;\n font-size: 0.82rem;\n line-height: 1.4;\n color: var(--store-text-muted);\n}\n\n${c} .payment-source-label {\n font-weight: 700;\n color: var(--store-text-main);\n}\n\n${c} .payment-source-balance {\n font-weight: 700;\n color: var(--store-success);\n}\n\n${c} .payment-source-state {\n font-size: 0.7rem;\n letter-spacing: 0.14em;\n text-transform: uppercase;\n color: var(--store-text-subtle);\n}\n\n${c} .summary-row.total {\n font-size: 1rem;\n font-weight: 700;\n}\n\n${c} .summary-label,\n${c} .cart-line-meta {\n color: var(--store-text-muted);\n}\n\n${c} .summary-value {\n font-weight: 700;\n}\n\n${c} .summary-actions {\n display: grid;\n gap: 0.65rem;\n}\n\n${c} .cart-empty {\n padding: 1rem;\n border-radius: 0.95rem;\n border: 1px dashed var(--store-border);\n color: var(--store-text-muted);\n background: rgb(255 255 255 / 0.38);\n}\n\n@media (max-width: 1120px) {\n ${c} .store-cart {\n top: 0;\n right: 0;\n bottom: 0;\n width: min(24rem, 100%);\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Cart=function(){const e=a.getStoreState(r),c=a.summarizeCart(e.cartItems),d=a.getPaymentSources(s),m=a.getPaymentSourceById(s,e.selectedPaymentSource)||d[0]||null,u=d.filter(e=>!1!==e.enabled).length,g=m?m.label:"Unavailable",p=m?Number(m.balance||0):0,b=Math.max(0,p-c.total);return n("storefront-cart",l),t("div",{className:e.cartOpen?"is-open":"",[i]:"","aria-hidden":e.cartOpen?"false":"true"},t("aside",{className:"store-cart"},t("section",{className:"cart-card"},t("div",{className:"cart-header"},t("div",null,t("span",{className:"eyebrow"},"Cart"),t("h2",{className:"section-title"},"Acquisition Queue")),t("button",{type:"button",className:"cart-close","aria-label":"Close cart",title:"Close cart",onClick:()=>o.closeCart()},"X")),t("div",{className:"cart-kpi"},t("div",{className:"cart-kpi-card"},t("span",{className:"kpi-label"},"Items"),t("span",{className:"kpi-value"},c.lineCount)),t("div",{className:"cart-kpi-card"},t("span",{className:"kpi-label"},"Payment"),t("span",{className:"kpi-value"},g))),t("div",{className:"cart-status"},t("span",{className:"eyebrow"},"Payment Source"),t("div",{className:"payment-source-field"},t("select",{className:"payment-source-select",value:e.selectedPaymentSource,onChange:e=>o.selectPaymentSource(e.target.value)},d.map(e=>t("option",{value:e.id,disabled:!1===e.enabled},!1===e.enabled?`${e.label} (Locked)`:e.label))),m?t("div",{className:"payment-source-meta"},t("div",null,t("div",{className:"payment-source-row"},t("span",{className:"payment-source-label"},m.label),t("span",{className:"payment-source-balance"},a.formatCurrency(m.balance))),t("p",{className:"payment-source-detail"},m.detail)),t("span",{className:"payment-source-state"},u>0?!1===m.enabled?"Locked":"Available":"Unavailable")):null)),t("div",{className:"cart-lines","data-preserve-scroll-id":"cart-lines"},c.lineCount>0?e.cartItems.map(e=>t("div",{className:"cart-line"},t("div",{className:"cart-line-top"},t("div",{className:"cart-line-copy"},t("div",{className:"cart-line-title"},e.name)),t("strong",null,a.formatCurrency(a.parsePrice(e.price)*e.quantity))),t("div",{className:"cart-line-controls"},t("div",{className:"qty-controls"},t("button",{type:"button",className:"store-btn store-btn-secondary qty-btn",onClick:()=>o.decrementCartItem(e.code)},"-"),t("span",{className:"qty-badge"},e.quantity),t("button",{type:"button",className:"store-btn store-btn-secondary qty-btn",onClick:()=>o.incrementCartItem(e.code)},"+")),t("button",{type:"button",className:"store-btn store-btn-secondary remove-btn",onClick:()=>o.removeCartItem(e.code)},"Remove")))):t("div",{className:"cart-empty"},"No items are queued yet. Add products from the catalog to build a checkout payload.")),t("div",{className:"cart-summary"},t("div",{className:"summary-row"},t("span",{className:"summary-label"},"Items"),t("span",{className:"summary-value"},c.itemCount)),t("div",{className:"summary-row"},t("span",{className:"summary-label"},"Subtotal"),t("span",{className:"summary-value"},a.formatCurrency(c.subtotal))),t("div",{className:"summary-row"},t("span",{className:"summary-label"},"Remaining Source"),t("span",{className:"summary-value"},a.formatCurrency(b))),t("div",{className:"summary-row total"},t("span",{className:"summary-label"},"Total"),t("span",{className:"summary-value"},a.formatCurrency(c.total)))),t("div",{className:"summary-actions"},t("button",{type:"button",className:"store-btn store-btn-primary",disabled:0===c.lineCount||e.isCheckingOut,onClick:()=>o.requestCheckout()},e.isCheckingOut?"Submitting Request...":"Submit Checkout")))))}}(),function(){const e=window.StorefrontApp=window.StorefrontApp||{},{h:t,ensureScopedStyle:n}=e.runtime,r=e.getters,a=e.store,o=e.actions,s="data-ui-store-navbar",i=`[${s}]`,c=`\n${i} {\n display: flex;\n align-items: center;\n justify-content: space-between;\n gap: 1rem;\n padding: 0.9rem 1rem;\n margin-bottom: 0.95rem;\n border-bottom: 1px solid var(--store-accent-line);\n background:\n linear-gradient(180deg, rgb(255 255 255 / 0.52) 0%, transparent 100%),\n linear-gradient(180deg, rgb(236 241 246 / 0.52) 0%, rgb(245 243 239 / 0.2) 100%);\n}\n\n${i} .store-breadcrumbs {\n display: flex;\n align-items: center;\n gap: 0.55rem;\n min-width: 0;\n flex-wrap: wrap;\n}\n\n${i} .breadcrumb-link,\n${i} .breadcrumb-current,\n${i} .breadcrumb-separator {\n font-size: 0.78rem;\n letter-spacing: 0.1em;\n text-transform: uppercase;\n font-weight: 700;\n}\n\n${i} .breadcrumb-link {\n padding: 0;\n border: 0;\n background: transparent;\n color: var(--store-text-subtle);\n}\n\n${i} .breadcrumb-link:hover {\n color: var(--store-accent);\n}\n\n${i} .breadcrumb-current {\n color: var(--store-accent);\n}\n\n${i} .breadcrumb-separator {\n color: rgb(124 138 155 / 0.72);\n}\n\n${i} .store-cart-btn {\n position: relative;\n width: 2.6rem;\n height: 2.6rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n flex: 0 0 auto;\n border-radius: 0.7rem;\n border: 1px solid var(--store-border-strong);\n background: rgb(255 255 255 / 0.68);\n color: var(--store-accent);\n box-shadow: inset 0 1px 0 rgb(255 255 255 / 0.75);\n}\n\n${i} .store-cart-btn:hover {\n background: rgb(219 231 243 / 0.88);\n}\n\n${i} .cart-toggle-icon {\n position: relative;\n width: 0.95rem;\n height: 0.8rem;\n border: 1.5px solid currentColor;\n border-radius: 0.16rem 0.16rem 0.24rem 0.24rem;\n}\n\n${i} .cart-toggle-icon::before {\n content: "";\n position: absolute;\n top: -0.34rem;\n left: 0.2rem;\n width: 0.5rem;\n height: 0.3rem;\n border: 1.5px solid currentColor;\n border-bottom: 0;\n border-radius: 0.35rem 0.35rem 0 0;\n}\n\n${i} .cart-count {\n position: absolute;\n top: -0.35rem;\n right: -0.35rem;\n min-width: 1.25rem;\n height: 1.25rem;\n padding: 0 0.3rem;\n display: inline-flex;\n align-items: center;\n justify-content: center;\n border-radius: 999px;\n background: var(--store-accent);\n color: #fff;\n font-size: 0.68rem;\n font-weight: 700;\n}\n\n@media (max-width: 1120px) {\n ${i} {\n align-items: flex-start;\n }\n}\n`;e.componentFns=e.componentFns||{},e.componentFns.Navbar=function(){const e=r.getStoreState(a),i=r.getStoreBreadcrumbs(e),l=r.summarizeCart(e.cartItems);return n("storefront-navbar",c),t("nav",{[s]:""},t("div",{className:"store-breadcrumbs","aria-label":"Store navigation"},i.map((e,n)=>n===i.length-1?t("span",{className:"breadcrumb-current"},e.label):[t("button",{type:"button",className:"breadcrumb-link",onClick:()=>o.navigateToBreadcrumb(e.id)},e.label),t("span",{className:"breadcrumb-separator"},"/")])),t("button",{type:"button",className:"store-cart-btn",onClick:()=>o.toggleCart(),title:e.cartOpen?"Close cart":"Open cart","aria-label":e.cartOpen?"Close cart":"Open cart"},t("span",{className:"cart-toggle-icon","aria-hidden":"true"}),l.itemCount>0?t("span",{className:"cart-count"},l.itemCount):null))}}(),function(){const e=window.ForgeWebUI,t=window.StorefrontApp;e.createApp({name:"store",root:"#app",setup({root:n}){e.mount(n,()=>t.components.App(),{preserveScroll:!1}),t.bridge&&t.bridge.notifyReady()}}).start()}(); \ No newline at end of file diff --git a/arma/client/addons/store/ui/src/data.js b/arma/client/addons/store/ui/src/data.js index eced997..5132da7 100644 --- a/arma/client/addons/store/ui/src/data.js +++ b/arma/client/addons/store/ui/src/data.js @@ -75,9 +75,11 @@ { id: "headgear", label: "Headgear" }, { id: "facewear", label: "Facewear" }, { id: "vests", label: "Vests" }, + { id: "backpacks", label: "Backpacks" }, + { id: "attachments", label: "Attachments" }, { id: "weapons", label: "Weapons" }, { id: "ammo", label: "Ammo" }, - { id: "items", label: "Items" }, + { id: "misc", label: "Misc" }, { id: "vehicles", label: "Vehicles" }, ], vehicleCards: [ @@ -98,8 +100,10 @@ headgear: [], facewear: [], vests: [], + backpacks: [], + attachments: [], ammo: [], - items: [], + misc: [], primary: [], secondary: [], handgun: [], diff --git a/arma/client/addons/store/ui/src/pages/StoreView.js b/arma/client/addons/store/ui/src/pages/StoreView.js index 93f6144..c1ec182 100644 --- a/arma/client/addons/store/ui/src/pages/StoreView.js +++ b/arma/client/addons/store/ui/src/pages/StoreView.js @@ -37,6 +37,13 @@ } function formatTitle(value) { + const normalizedValue = String(value || "") + .trim() + .toLowerCase(); + if (["items", "misc"].includes(normalizedValue)) { + return "Misc"; + } + return String(value || "") .replace(/[-_]+/g, " ") .split(/\s+/) diff --git a/arma/server/addons/store/XEH_PREP.hpp b/arma/server/addons/store/XEH_PREP.hpp index 7c098e7..cf3f040 100644 --- a/arma/server/addons/store/XEH_PREP.hpp +++ b/arma/server/addons/store/XEH_PREP.hpp @@ -1 +1,2 @@ +PREP(initCatalogService); PREP(initStoreStore); diff --git a/arma/server/addons/store/XEH_preInit.sqf b/arma/server/addons/store/XEH_preInit.sqf index dc3d5a0..54bfe32 100644 --- a/arma/server/addons/store/XEH_preInit.sqf +++ b/arma/server/addons/store/XEH_preInit.sqf @@ -6,6 +6,24 @@ PREP_RECOMPILE_END; // private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)]; +[QGVAR(requestCategory), { + params [["_uid", "", [""]], ["_category", "", [""]]]; + + if (_uid isEqualTo "" || { _category isEqualTo "" }) exitWith { + diag_log "[FORGE:Server:Store] Invalid category request payload." + }; + + private _player = [_uid] call EFUNC(common,getPlayer); + if (_player isEqualTo objNull) exitWith {}; + + if (isNil QGVAR(StoreCatalogService)) exitWith { + diag_log "[FORGE:Server:Store] Store catalog service is unavailable." + }; + + private _result = GVAR(StoreCatalogService) call ["buildCategoryResponse", [_category]]; + [CRPC(store,responseCategory), [_result], _player] call CFUNC(targetEvent); +}] call CFUNC(addEventHandler); + [QGVAR(requestCheckout), { params [["_uid", "", [""]], ["_payloadJson", "", [""]]]; diff --git a/arma/server/addons/store/functions/fnc_initCatalogService.sqf b/arma/server/addons/store/functions/fnc_initCatalogService.sqf new file mode 100644 index 0000000..2e81ba4 --- /dev/null +++ b/arma/server/addons/store/functions/fnc_initCatalogService.sqf @@ -0,0 +1,460 @@ +#include "..\script_component.hpp" + +/* + * File: fnc_initCatalogService.sqf + * Author: IDSolutions + * Date: 2026-03-14 + * Public: No + * + * Description: + * Initializes the server-side store catalog service for authoritative category hydration and pricing. + */ + +#pragma hemtt ignore_variables ["_self"] +GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ + ["#type", "StoreCatalogServiceBaseClass"], + ["#create", compileFinal { + _self set ["catalogCache", createHashMap]; + ["INFO", "Store catalog service initialized!"] call EFUNC(common,log); + }], + ["formatCurrency", compileFinal { + params [["_amount", 0, [0]]]; + + format ["$%1", [_amount max 0] call BIS_fnc_numberText] + }], + ["isVisibleConfig", compileFinal { + params [["_cfg", configNull, [configNull]]]; + + isClass _cfg + && { getNumber (_cfg >> "scope") >= 2 } + && { (getText (_cfg >> "displayName")) isNotEqualTo "" } + }], + ["buildDescription", compileFinal { + params [["_cfg", configNull, [configNull]], ["_fallback", "", [""]]]; + + private _description = getText (_cfg >> "descriptionShort"); + if (_description isEqualTo "") then { _description = _fallback; }; + + _description + }], + ["normalizeCategoryKey", compileFinal { + params [["_category", "", [""]]]; + + private _categoryKey = toLowerANSI _category; + if (_categoryKey isEqualTo "items") exitWith { "misc" }; + + _categoryKey + }], + ["calculateCatalogPriceValue", compileFinal { + params [ + ["_cfg", configNull, [configNull]], + ["_isVehicle", false, [false]] + ]; + + if (isNull _cfg) exitWith { 50 }; + + private _mass = 0; + private _priceValue = 0; + + if (_isVehicle) then { + _priceValue = getNumber (_cfg >> "cost"); + } else { + private _weaponType = getNumber (_cfg >> "type"); + if (_weaponType in [1, 2, 4]) then { _mass = getNumber (_cfg >> "WeaponSlotsInfo" >> "mass"); }; + if (_mass <= 0) then { _mass = getNumber (_cfg >> "ItemInfo" >> "mass"); }; + if (_mass <= 0) then { _mass = getNumber (_cfg >> "mass"); }; + + _priceValue = ceil ((_mass max 0) * 7.5); + }; + + _priceValue max 50 + }], + ["buildCatalogItem", compileFinal { + params [ + ["_cfg", configNull, [configNull]], + ["_typeLabel", "", [""]], + ["_fallbackDescription", "", [""]], + ["_imageField", "picture", [""]], + ["_isVehicle", false, [false]] + ]; + + if (isNull _cfg) exitWith { createHashMap }; + + private _className = configName _cfg; + private _displayName = getText (_cfg >> "displayName"); + private _picture = getText (_cfg >> _imageField); + if (_picture isEqualTo "" && { _imageField isNotEqualTo "picture" }) then { + _picture = getText (_cfg >> "picture"); + }; + + private _priceValue = _self call ["calculateCatalogPriceValue", [_cfg, _isVehicle]]; + + createHashMapFromArray [ + ["className", _className], + ["code", _className], + ["name", _displayName], + ["description", _self call ["buildDescription", [_cfg, _fallbackDescription]]], + ["price", _self call ["formatCurrency", [_priceValue]]], + ["priceValue", _priceValue], + ["image", _picture], + ["type", _typeLabel] + ] + }], + ["appendCfgWeaponsByItemInfoType", compileFinal { + params [["_items", [], [[]]], ["_itemInfoType", -1, [0]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]]; + + { + private _cfg = _x; + if ( + _self call ["isVisibleConfig", [_cfg]] + && { getNumber (_cfg >> "ItemInfo" >> "type") isEqualTo _itemInfoType } + ) then { + _items pushBack (_self call ["buildCatalogItem", [_cfg, _typeLabel, _fallbackDescription]]); + }; + } forEach ("true" configClasses (configFile >> "CfgWeapons")); + + _items + }], + ["appendCfgWeaponsByType", compileFinal { + params [["_items", [], [[]]], ["_weaponType", -1, [0]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]]; + + { + private _cfg = _x; + if ( + _self call ["isVisibleConfig", [_cfg]] + && { getNumber (_cfg >> "type") isEqualTo _weaponType } + ) then { + _items pushBack (_self call ["buildCatalogItem", [_cfg, _typeLabel, _fallbackDescription]]); + }; + } forEach ("true" configClasses (configFile >> "CfgWeapons")); + + _items + }], + ["isAceClassName", compileFinal { + params [["_cfg", configNull, [configNull]]]; + + ((toLowerANSI (configName _cfg)) select [0, 4]) isEqualTo "ace_" + }], + ["isAttachmentConfig", compileFinal { + params [["_cfg", configNull, [configNull]]]; + + if !(_self call ["isVisibleConfig", [_cfg]]) exitWith { false }; + if (_self call ["isAceClassName", [_cfg]]) exitWith { false }; + + private _className = configName _cfg; + private _itemType = [_className] call BIS_fnc_itemType; + private _group = toLowerANSI (_itemType param [0, ""]); + private _kind = toLowerANSI (_itemType param [1, ""]); + + (_group find "accessory") >= 0 + || { (_kind find "accessory") >= 0 } + || { _kind in ["accessorymuzzle", "accessorypointer", "accessorysights", "accessorybipod"] } + }], + ["resolveAttachmentTypeLabel", compileFinal { + params [["_cfg", configNull, [configNull]]]; + + private _className = configName _cfg; + private _itemType = [_className] call BIS_fnc_itemType; + private _kind = toLowerANSI (_itemType param [1, ""]); + + if ((_kind find "muzzle") >= 0) exitWith { "Muzzle Attachment" }; + if ((_kind find "optic") >= 0 || { (_kind find "sight") >= 0 }) exitWith { "Optic Attachment" }; + if ((_kind find "pointer") >= 0 || { (_kind find "flash") >= 0 } || { (_kind find "light") >= 0 }) exitWith { "Light Attachment" }; + if ((_kind find "bipod") >= 0) exitWith { "Bipod Attachment" }; + + "Attachment" + }], + ["appendCfgAttachments", compileFinal { + params [["_items", [], [[]]], ["_fallbackDescription", "", [""]]]; + + { + private _cfg = _x; + if (_self call ["isAttachmentConfig", [_cfg]]) then { + private _typeLabel = _self call ["resolveAttachmentTypeLabel", [_cfg]]; + _items pushBack (_self call ["buildCatalogItem", [_cfg, _typeLabel, _fallbackDescription]]); + }; + } forEach ("true" configClasses (configFile >> "CfgWeapons")); + + _items + }], + ["appendCfgVehiclesByKind", compileFinal { + params [["_items", [], [[]]], ["_baseClass", "", [""]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]]; + + { + private _cfg = _x; + private _className = configName _cfg; + if ( + _self call ["isVisibleConfig", [_cfg]] + && { getNumber (_cfg >> "isBackpack") isEqualTo 0 } + && { !(_className isKindOf ["CAManBase", configFile >> "CfgVehicles"]) } + && { !(_className isKindOf ["StaticWeapon", configFile >> "CfgVehicles"]) } + && { _className isKindOf [_baseClass, configFile >> "CfgVehicles"] } + ) then { + _items pushBack (_self call ["buildCatalogItem", [_cfg, _typeLabel, _fallbackDescription, "editorPreview", true]]); + }; + } forEach ("true" configClasses (configFile >> "CfgVehicles")); + + _items + }], + ["isBackpackConfig", compileFinal { + params [["_cfg", configNull, [configNull]]]; + + getNumber (_cfg >> "isBackpack") isEqualTo 1 + || { getNumber (_cfg >> "ItemInfo" >> "type") isEqualTo TYPE_BACKPACK } + }], + ["appendCfgBackpacks", compileFinal { + params [["_items", [], [[]]], ["_typeLabel", "Backpack", [""]], ["_fallbackDescription", "", [""]]]; + + { + private _cfg = _x; + if ( + _self call ["isVisibleConfig", [_cfg]] + && { _self call ["isBackpackConfig", [_cfg]] } + ) then { + _items pushBack (_self call ["buildCatalogItem", [_cfg, _typeLabel, _fallbackDescription]]); + }; + } forEach ("true" configClasses (configFile >> "CfgVehicles")); + + _items + }], + ["scanCategoryItems", compileFinal { + params [["_category", "", [""]]]; + + private _categoryKey = _self call ["normalizeCategoryKey", [_category]]; + if (_categoryKey isEqualTo "") exitWith { [] }; + + private _items = []; + + switch (_categoryKey) do { + case "uniforms": { _items = _self call ["appendCfgWeaponsByItemInfoType", [_items, TYPE_UNIFORM, "Uniform", "Live uniform entry generated from the game inventory."]]; }; + case "headgear": { _items = _self call ["appendCfgWeaponsByItemInfoType", [_items, TYPE_HEADGEAR, "Headgear", "Live headgear entry generated from the game inventory."]]; }; + case "vests": { _items = _self call ["appendCfgWeaponsByItemInfoType", [_items, TYPE_VEST, "Vest", "Live vest entry generated from the game inventory."]]; }; + case "backpacks": { _items = _self call ["appendCfgBackpacks", [_items, "Backpack", "Live backpack entry generated from the game inventory."]]; }; + case "attachments": { + _items = _self call ["appendCfgAttachments", [_items, "Live attachment entry generated from the game inventory."]]; + }; + case "facewear": { + { if (_self call ["isVisibleConfig", [_x]]) then { _items pushBack (_self call ["buildCatalogItem", [_x, "Facewear", "Live facewear entry generated from the game inventory."]]); }; } forEach ("true" configClasses (configFile >> "CfgGlasses")); + }; + case "ammo": { + { if (_self call ["isVisibleConfig", [_x]]) then { _items pushBack (_self call ["buildCatalogItem", [_x, "Magazine", "Live ammunition entry generated from the game inventory."]]); }; } forEach ("true" configClasses (configFile >> "CfgMagazines")); + }; + case "misc": { + { + private _cfg = _x; + private _className = configName _cfg; + private _itemType = [_className] call BIS_fnc_itemType; + private _group = _itemType param [0, ""]; + private _kind = _itemType param [1, ""]; + private _weaponType = getNumber (_cfg >> "type"); + private _isAceClass = _self call ["isAceClassName", [_cfg]]; + + if ( + _self call ["isVisibleConfig", [_cfg]] + && { !(_weaponType in [1, 2, 4]) } + && { (_group in ["Item", "Equipment"]) || { _isAceClass } } + && { !(_kind in ["Uniform", "Vest", "Headgear"]) } + && { !(_self call ["isAttachmentConfig", [_cfg]]) } + && { (getNumber (_cfg >> "ItemInfo" >> "type") isNotEqualTo TYPE_BACKPACK) } + ) then { + private _typeLabel = [_kind, "Item"] select (_kind isEqualTo ""); + _items pushBack (_self call ["buildCatalogItem", [_cfg, _typeLabel, "Live utility entry generated from the game inventory."]]); + }; + } forEach ("true" configClasses (configFile >> "CfgWeapons")); + }; + case "primary": { _items = _self call ["appendCfgWeaponsByType", [_items, 1, "Primary Weapon", "Live primary weapon entry generated from the game inventory."]]; }; + case "handgun": { _items = _self call ["appendCfgWeaponsByType", [_items, 2, "Handgun", "Live sidearm entry generated from the game inventory."]]; }; + case "secondary": { _items = _self call ["appendCfgWeaponsByType", [_items, 4, "Launcher", "Live launcher entry generated from the game inventory."]]; }; + case "cars": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Car", "Vehicle", "Live wheeled vehicle entry generated from the game inventory."]]; }; + case "armor": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Tank", "Vehicle", "Live armored vehicle entry generated from the game inventory."]]; }; + case "helis": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Helicopter", "Aircraft", "Live helicopter entry generated from the game inventory."]]; }; + case "planes": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Plane", "Aircraft", "Live fixed-wing entry generated from the game inventory."]]; }; + case "naval": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Ship", "Naval", "Live naval vehicle entry generated from the game inventory."]]; }; + case "other": { + { + private _cfg = _x; + private _className = configName _cfg; + private _isSupportedVehicle = _className isKindOf ["AllVehicles", configFile >> "CfgVehicles"]; + private _isKnownCategory = + _className isKindOf ["Car", configFile >> "CfgVehicles"] + || { _className isKindOf ["Tank", configFile >> "CfgVehicles"] } + || { _className isKindOf ["Helicopter", configFile >> "CfgVehicles"] } + || { _className isKindOf ["Plane", configFile >> "CfgVehicles"] } + || { _className isKindOf ["Ship", configFile >> "CfgVehicles"] }; + + if ( + _self call ["isVisibleConfig", [_cfg]] + && { _isSupportedVehicle } + && { !_isKnownCategory } + && { getNumber (_cfg >> "isBackpack") isEqualTo 0 } + && { !(_className isKindOf ["CAManBase", configFile >> "CfgVehicles"]) } + && { !(_className isKindOf ["StaticWeapon", configFile >> "CfgVehicles"]) } + ) then { + _items pushBack (_self call ["buildCatalogItem", [_cfg, "Special Vehicle", "Live specialty vehicle entry generated from the game inventory.", "editorPreview", true]]); + }; + } forEach ("true" configClasses (configFile >> "CfgVehicles")); + }; + }; + + private _sortedItems = _items apply { [toLowerANSI (_x getOrDefault ["name", ""]), _x] }; + _sortedItems sort true; + _sortedItems apply { _x select 1 } + }], + ["isVehicleCategory", compileFinal { + params [["_category", "", [""]]]; + + (toLowerANSI _category) in ["cars", "armor", "helis", "planes", "naval", "other"] + }], + ["buildPayloadCategory", compileFinal { + params [["_category", "", [""]]]; + + switch (toLowerANSI _category) do { + case "backpacks": { "backpack" }; + case "attachments": { "attachment" }; + case "ammo": { "magazine" }; + case "primary"; + case "secondary"; + case "handgun": { "weapon" }; + case "cars"; + case "armor"; + case "helis"; + case "planes"; + case "naval"; + case "other": { toLowerANSI _category }; + default { "item" }; + } + }], + ["isSupportedCategory", compileFinal { + params [["_category", "", [""]]]; + + (_self call ["normalizeCategoryKey", [_category]]) in ["uniforms", "headgear", "vests", "backpacks", "attachments", "facewear", "ammo", "misc", "primary", "handgun", "secondary", "cars", "armor", "helis", "planes", "naval", "other"] + }], + ["buildCategoryItems", compileFinal { + params [["_category", "", [""]]]; + + private _categoryKey = _self call ["normalizeCategoryKey", [_category]]; + if (_categoryKey isEqualTo "") exitWith { [] }; + + private _catalogCache = _self getOrDefault ["catalogCache", createHashMap]; + if (_categoryKey in (keys _catalogCache)) exitWith { _catalogCache get _categoryKey }; + + private _items = _self call ["scanCategoryItems", [_categoryKey]]; + private _payloadCategory = _self call ["buildPayloadCategory", [_categoryKey]]; + private _entryKind = ["item", "vehicle"] select (_self call ["isVehicleCategory", [_categoryKey]]); + + { + _x set ["category", _payloadCategory]; + _x set ["entryKind", _entryKind]; + } forEach _items; + + _catalogCache set [_categoryKey, _items]; + _self set ["catalogCache", _catalogCache]; + + _items + }], + ["buildCategoryResponse", compileFinal { + params [["_category", "", [""]]]; + + private _categoryKey = _self call ["normalizeCategoryKey", [_category]]; + private _response = createHashMapFromArray [["success", false], ["category", _categoryKey], ["items", []], ["message", "No store category was provided."]]; + + if (_categoryKey isEqualTo "") exitWith { _response }; + if !(_self call ["isSupportedCategory", [_categoryKey]]) exitWith { + _response set ["message", format ["Unsupported store category: %1", _categoryKey]]; + _response + }; + + _response set ["success", true]; + _response set ["message", ""]; + _response set ["items", _self call ["buildCategoryItems", [_categoryKey]]]; + _response + }], + ["resolveCheckoutCategories", compileFinal { + params [["_entry", createHashMap, [createHashMap]]]; + + private _entryKind = toLowerANSI (_entry getOrDefault ["entryKind", "item"]); + private _category = toLowerANSI (_entry getOrDefault ["category", ""]); + + if (_entryKind isEqualTo "vehicle") exitWith { ["cars", "armor", "helis", "planes", "naval", "other"] }; + if (_category isEqualTo "weapon") exitWith { ["primary", "handgun", "secondary"] }; + if (_category isEqualTo "backpack") exitWith { ["backpacks"] }; + if (_category isEqualTo "attachment") exitWith { ["attachments"] }; + if (_category isEqualTo "magazine") exitWith { ["ammo"] }; + + ["uniforms", "headgear", "vests", "facewear", "misc", "attachments", "backpacks"] + }], + ["resolveCheckoutCatalogEntry", compileFinal { + params [["_entry", createHashMap, [createHashMap]]]; + + private _className = toLowerANSI (_entry getOrDefault ["classname", ""]); + if (_className isEqualTo "") exitWith { createHashMap }; + + private _resolved = createHashMap; + { + private _catalogEntries = _self call ["buildCategoryItems", [_x]]; + private _match = _catalogEntries select { (toLowerANSI (_x getOrDefault ["className", ""])) isEqualTo _className }; + + if (_match isNotEqualTo []) exitWith { _resolved = _match select 0; }; + } forEach (_self call ["resolveCheckoutCategories", [_entry]]); + + _resolved + }], + ["calculateCheckoutTotal", compileFinal { + params [["_items", [], [[]]], ["_vehicles", [], [[]]]]; + + private _result = createHashMapFromArray [["success", false], ["total", 0], ["message", "Checkout total must be greater than zero."]]; + private _total = 0; + private _message = ""; + + { + if (_message isEqualTo "") then { + private _className = _x getOrDefault ["classname", ""]; + private _quantity = floor ((_x getOrDefault ["quantity", 1]) max 0); + + if (_className isEqualTo "" || { _quantity <= 0 }) then { + _message = "Checkout contains an invalid item entry."; + } else { + private _catalogEntry = _self call ["resolveCheckoutCatalogEntry", [createHashMapFromArray [["classname", _className], ["category", _x getOrDefault ["category", "item"]], ["entryKind", "item"]]]]; + + if (_catalogEntry isEqualTo createHashMap) then { + _message = format ["Unsupported store item: %1", _className]; + } else { + _total = _total + ((_catalogEntry getOrDefault ["priceValue", 0]) * _quantity); + }; + }; + }; + } forEach _items; + + { + if (_message isEqualTo "") then { + private _className = _x getOrDefault ["classname", ""]; + if (_className isEqualTo "") then { + _message = "Checkout contains an invalid vehicle entry."; + } else { + private _catalogEntry = _self call ["resolveCheckoutCatalogEntry", [createHashMapFromArray [["classname", _className], ["category", _x getOrDefault ["category", ""]], ["entryKind", "vehicle"]]]]; + + if (_catalogEntry isEqualTo createHashMap) then { + _message = format ["Unsupported store vehicle: %1", _className]; + } else { + _total = _total + (_catalogEntry getOrDefault ["priceValue", 0]); + }; + }; + }; + } forEach _vehicles; + + if (_message isNotEqualTo "") exitWith { + _result set ["message", _message]; + _result + }; + + if (_total <= 0) exitWith { _result }; + + _result set ["success", true]; + _result set ["total", floor _total]; + _result set ["message", ""]; + _result + }] +]; + +GVAR(StoreCatalogService) = createHashMapObject [GVAR(StoreCatalogServiceBaseClass)]; +GVAR(StoreCatalogService) diff --git a/arma/server/addons/store/functions/fnc_initStoreStore.sqf b/arma/server/addons/store/functions/fnc_initStoreStore.sqf index bba6bbb..6e99752 100644 --- a/arma/server/addons/store/functions/fnc_initStoreStore.sqf +++ b/arma/server/addons/store/functions/fnc_initStoreStore.sqf @@ -4,13 +4,15 @@ * File: fnc_initStoreStore.sqf * Author: IDSolutions * Date: 2026-03-12 - * Last Update: 2026-03-12 + * Last Update: 2026-03-14 * Public: No * * Description: * Initializes the server-side store checkout flow. */ +if (isNil QGVAR(StoreCatalogService)) then { call FUNC(initCatalogService); }; + #pragma hemtt ignore_variables ["_self"] GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ ["#type", "StoreBaseStore"], @@ -82,11 +84,23 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ private _result = _self call ["buildResult", ["Checkout failed.", "cash"]]; private _payload = fromJSON _payloadJson; + if !(_payload isEqualType createHashMap) exitWith { + _result set ["message", "Checkout request payload is invalid."]; + _result + }; + private _paymentMethod = toLowerANSI (_payload getOrDefault ["paymentMethod", "cash"]); - private _totalPrice = floor ((_payload getOrDefault ["totalPrice", 0]) max 0); private _items = _payload getOrDefault ["items", []]; private _vehicles = _payload getOrDefault ["vehicles", []]; + if (isNil QGVAR(StoreCatalogService)) exitWith { + _result set ["message", "Store catalog service is unavailable."]; + _result + }; + + private _priceResult = GVAR(StoreCatalogService) call ["calculateCheckoutTotal", [_items, _vehicles]]; + private _totalPrice = _priceResult getOrDefault ["total", 0]; + _result set ["paymentMethod", _paymentMethod]; _result set ["chargedTotal", _totalPrice]; @@ -95,8 +109,8 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ _result }; - if (_totalPrice <= 0) exitWith { - _result set ["message", "Checkout total must be greater than zero."]; + if !(_priceResult getOrDefault ["success", false]) exitWith { + _result set ["message", _priceResult getOrDefault ["message", "Checkout total must be greater than zero."]]; _result }; @@ -144,10 +158,10 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ if (_paymentMethod isEqualTo "org_funds" && { _vehicles isNotEqualTo [] }) then { _orgFleetResult = EGVAR(org,OrgStore) call ["addFleetVehicles", [_uid, _vehicles, true]]; }; + private _lockerPatch = _lockerResult getOrDefault ["patch", createHashMap]; private _vaPatch = _vaResult getOrDefault ["patch", createHashMap]; private _vgPatch = _vgResult getOrDefault ["patch", createHashMap]; - if (keys _lockerPatch isNotEqualTo []) then { [CRPC(locker,responseSyncLocker), [_lockerPatch], _player] call CFUNC(targetEvent); }; if (keys _vaPatch isNotEqualTo []) then { [CRPC(locker,responseSyncVA), [_vaPatch], _player] call CFUNC(targetEvent); }; if (keys _vgPatch isNotEqualTo []) then { [CRPC(garage,responseSyncVG), [_vgPatch], _player] call CFUNC(targetEvent); }; @@ -157,9 +171,7 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ private _orgPatch = _payment getOrDefault ["orgPatch", createHashMap]; private _orgFleetPatch = _orgFleetResult getOrDefault ["patch", createHashMap]; - if (keys _orgFleetPatch isNotEqualTo []) then { - { _orgPatch set [_x, _y]; } forEach _orgFleetPatch; - }; + if (keys _orgFleetPatch isNotEqualTo []) then { { _orgPatch set [_x, _y]; } forEach _orgFleetPatch; }; if (keys _orgPatch isNotEqualTo []) then { private _orgTargetUids = _payment getOrDefault ["orgTargetUids", []]; { @@ -168,9 +180,7 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [ { private _memberPlayer = [_x] call EFUNC(common,getPlayer); - if (_memberPlayer isNotEqualTo objNull) then { - [CRPC(org,responseSyncOrg), [_orgPatch], _memberPlayer] call CFUNC(targetEvent); - }; + if (_memberPlayer isNotEqualTo objNull) then { [CRPC(org,responseSyncOrg), [_orgPatch], _memberPlayer] call CFUNC(targetEvent); }; } forEach _orgTargetUids; }; diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..d65ee4d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,699 @@ +{ + "name": "forge-webui", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "forge-webui", + "devDependencies": { + "html-minifier-terser": "^7.2.0", + "lightningcss": "^1.29.3", + "postcss": "^8.5.6", + "postcss-nested": "^7.0.2", + "prettier": "^3.6.2", + "terser": "^5.44.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } + }, + "node_modules/clean-css": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", + "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" + } + }, + "node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/html-minifier-terser": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", + "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "camel-case": "^4.1.2", + "clean-css": "~5.3.2", + "commander": "^10.0.0", + "entities": "^4.4.0", + "param-case": "^3.0.4", + "relateurl": "^0.2.7", + "terser": "^5.15.1" + }, + "bin": { + "html-minifier-terser": "cli.js" + }, + "engines": { + "node": "^14.13.1 || >=16.0.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-nested": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-7.0.2.tgz", + "integrity": "sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.14" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/prettier": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz", + "integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/relateurl": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", + "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + } + } +} diff --git a/package.json b/package.json index bc0565f..637fc5d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,14 @@ { "name": "forge-webui", "private": true, + "devDependencies": { + "html-minifier-terser": "^7.2.0", + "lightningcss": "^1.29.3", + "postcss": "^8.5.6", + "postcss-nested": "^7.0.2", + "prettier": "^3.6.2", + "terser": "^5.44.0" + }, "scripts": { "build:webui": "node tools/build-webui.mjs" } diff --git a/tools/build-webui.mjs b/tools/build-webui.mjs index fd43b08..900d143 100644 --- a/tools/build-webui.mjs +++ b/tools/build-webui.mjs @@ -2,6 +2,11 @@ import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises" import { spawn } from "node:child_process"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; +import { minify as minifyHtml } from "html-minifier-terser"; +import { transform as transformCss } from "lightningcss"; +import postcss from "postcss"; +import postcssNested from "postcss-nested"; +import { minify as minifyJs } from "terser"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const rootDir = path.resolve(__dirname, ".."); @@ -70,16 +75,39 @@ async function cleanOutputDirs(outputDirs) { } async function buildJsBundle({ name, output, sources }) { - const banner = `/* Generated by tools/build-webui.mjs for ${name}. Do not edit directly. */\n`; const chunks = await Promise.all(sources.map(readSource)); - await writeBundle(output, banner + chunks.join("\n\n")); + const bundleSource = chunks.join("\n\n"); + const result = await minifyJs(bundleSource, { + compress: true, + mangle: true, + format: { + comments: false, + }, + }); + + if (!result?.code) { + throw new Error(`Failed to minify JavaScript bundle for ${name}.`); + } + + await writeBundle(output, result.code); console.log(`Built ${output}`); } async function buildCssBundle({ name, output, sources }) { - const banner = `/* Generated by tools/build-webui.mjs for ${name}. Do not edit directly. */\n`; const chunks = await Promise.all(sources.map(readSource)); - await writeBundle(output, banner + chunks.join("\n\n")); + const nestedResult = await postcss([postcssNested]).process( + chunks.join("\n\n"), + { + from: undefined, + }, + ); + const result = transformCss({ + filename: output, + code: Buffer.from(nestedResult.css), + minify: true, + }); + + await writeBundle(output, result.code.toString("utf8")); console.log(`Built ${output}`); } @@ -149,8 +177,17 @@ function renderSiteIndex({ title, siteConfig }) { } async function buildHtmlPage({ name, output, title, siteConfig }) { - const banner = `\n`; - await writeBundle(output, banner + renderSiteIndex({ title, siteConfig })); + const html = renderSiteIndex({ title, siteConfig }); + const minifiedHtml = await minifyHtml(html, { + collapseBooleanAttributes: true, + collapseWhitespace: true, + minifyCSS: true, + minifyJS: true, + removeComments: true, + removeRedundantAttributes: true, + }); + + await writeBundle(output, minifiedHtml); console.log(`Built ${output}`); } @@ -277,11 +314,6 @@ async function loadUiConfig(absoluteConfigPath) { cssBundles, htmlPage, formatSourceTargets, - formatGeneratedTargets: [ - ...jsBundles.map((bundle) => bundle.output), - ...cssBundles.map((bundle) => bundle.output), - htmlPage.output, - ], }; } @@ -297,15 +329,11 @@ async function collectUiBuildArtifacts() { formatSourceTargets: uiConfigs.flatMap( (config) => config.formatSourceTargets, ), - formatGeneratedTargets: uiConfigs.flatMap( - (config) => config.formatGeneratedTargets, - ), }; } async function build() { const uiArtifacts = await collectUiBuildArtifacts(); - const commonGeneratedTargets = commonJsBundles.map((bundle) => bundle.output); const commonOutputDirs = [resolveFromRoot(commonUiSiteDir)]; await runPrettier([ @@ -321,11 +349,6 @@ async function build() { ]); await Promise.all(uiArtifacts.cssBundles.map(buildCssBundle)); await Promise.all(uiArtifacts.htmlPages.map(buildHtmlPage)); - - await runPrettier([ - ...commonGeneratedTargets, - ...uiArtifacts.formatGeneratedTargets, - ]); } build().catch((error) => {