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

Merged
J.Schmidt92 merged 37 commits from development into master 2026-03-14 20:12:08 -05:00
10 changed files with 1105 additions and 717 deletions
Showing only changes of commit 06f1bad878 - Show all commits

View File

@ -0,0 +1,140 @@
(function () {
const components = (window.StoreComponents = window.StoreComponents || {});
components.renderAppShell = function renderAppShell(options) {
const { header, workspaceNavbar, workspaceBody, cartPanel } = options;
return `
<div class="store-shell">
<div class="window-titlebar">
<div class="window-titlebar-brand">
<span class="window-titlebar-kicker">FORGE Logistics</span>
<span class="window-titlebar-title">Supply Exchange</span>
</div>
<div class="window-titlebar-controls">
<button
type="button"
class="window-control-btn"
disabled
title="Minimize unavailable"
aria-label="Minimize unavailable"
>
-
</button>
<button
type="button"
class="window-control-btn"
disabled
title="Maximize unavailable"
aria-label="Maximize unavailable"
>
[ ]
</button>
<button
type="button"
class="window-control-btn is-close"
id="store-close-btn"
title="Close"
aria-label="Close store interface"
>
X
</button>
</div>
</div>
<div class="store-app">
<aside class="store-sidebar">
<section class="module-card search-module">
<div class="module-header">
<div>
<span class="eyebrow">Search</span>
<h2 class="section-title">Inventory Search</h2>
</div>
<span class="pill">Module</span>
</div>
<input
type="text"
class="search-input"
placeholder="Search inventory, classes, or suppliers"
/>
<div class="quick-tags">
<span class="quick-tag">Field</span>
<span class="quick-tag">Logistics</span>
<span class="quick-tag">Issued</span>
<span class="quick-tag">Restricted</span>
</div>
</section>
<section class="module-card">
<div class="module-header">
<div>
<span class="eyebrow">Filter</span>
<h2 class="section-title">Procurement Filters</h2>
</div>
<span class="pill">Pending</span>
</div>
<div class="filter-stack">
<div class="filter-group">
<span class="filter-label">Department</span>
<div class="filter-value">
<span>Operational Tier</span>
<span class="filter-placeholder">Any</span>
</div>
</div>
<div class="filter-group">
<span class="filter-label">Availability</span>
<div class="filter-value">
<span>Stock Window</span>
<span class="filter-placeholder">Open</span>
</div>
</div>
<div class="filter-group">
<span class="filter-label">Approval</span>
<div class="filter-value">
<span>Purchase Level</span>
<span class="filter-placeholder">All</span>
</div>
</div>
</div>
</section>
</aside>
<main class="store-main">
<section class="workspace-card">
${workspaceNavbar}
<div class="workspace-header">
<div>
<span class="eyebrow">${header.eyebrow}</span>
<h1 class="section-title">${header.title}</h1>
</div>
<span class="pill">${header.badge}</span>
</div>
<div class="workspace-intro">
<p class="section-copy">${header.copy}</p>
<span class="inventory-ribbon">${header.ribbon}</span>
</div>
${workspaceBody}
</section>
${cartPanel}
</main>
</div>
<footer class="store-footer">
<div class="footer-block">
<span class="footer-title">Procurement Desk</span>
<span class="footer-copy">Authorized supply browsing for personnel loadout preparation and mission staging.</span>
</div>
<div class="footer-block">
<span class="footer-title">Catalog Scope</span>
<span class="footer-copy">Uniforms, protective gear, weapon slots, vehicles, ammunition groups, and general support inventory.</span>
</div>
<div class="footer-block">
<span class="footer-title">Module State</span>
<span class="footer-copy">Search, filters, and cart presentation are staged now. Purchase logic and item wiring will follow.</span>
</div>
</footer>
</div>
`;
};
})();

View File

@ -0,0 +1,39 @@
(function () {
const components = (window.StoreComponents = window.StoreComponents || {});
components.createCategoryCard = function createCategoryCard(category) {
return `
<button class="card-button category-card" type="button" data-category="${category.id}">
<span class="card-label">${category.label}</span>
</button>
`;
};
components.createSubCategoryCard = function createSubCategoryCard(
category,
slotType,
) {
return `
<button class="card-button subcategory-card" type="button" data-subcategory="${category.id}" data-subcategory-type="${slotType}">
<span class="card-label">${category.label}</span>
</button>
`;
};
components.createProductCard = function createProductCard(item) {
return `
<article class="card-button product-card">
<div class="product-image">Image Placeholder</div>
<div class="product-meta">
<span class="product-code">${item.code}</span>
<strong class="product-name">${item.name}</strong>
</div>
<p class="product-copy">${item.description}</p>
<div class="product-footer">
<span class="product-price">${item.price}</span>
<button class="action-btn" type="button">Add to Cart</button>
</div>
</article>
`;
};
})();

View File

@ -0,0 +1,89 @@
(function () {
const components = (window.StoreComponents = window.StoreComponents || {});
components.renderCartPanel = function renderCartPanel(state) {
return `
<div class="cart-overlay ${state.cartOpen ? "is-open" : ""}" aria-hidden="${state.cartOpen ? "false" : "true"}">
<button
type="button"
class="cart-overlay-backdrop"
id="store-cart-backdrop"
aria-label="Close cart"
></button>
<aside class="store-cart-panel">
<section class="cart-card">
<div class="cart-header">
<div>
<span class="eyebrow">Cart</span>
<h2 class="section-title">Acquisition Queue</h2>
</div>
<button
type="button"
class="window-control-btn cart-panel-close"
id="store-cart-close-btn"
aria-label="Close cart"
title="Close cart"
>
X
</button>
</div>
<div class="cart-status">
<span class="eyebrow">Status</span>
<p class="section-copy">
Cart wiring is intentionally deferred. This panel is laid out for order lines, approval totals, and checkout actions.
</p>
</div>
<div class="cart-kpi">
<div class="cart-kpi-card">
<span class="kpi-label">Lines</span>
<span class="kpi-value">0</span>
</div>
<div class="cart-kpi-card">
<span class="kpi-label">Budget</span>
<span class="kpi-value">$48,000</span>
</div>
</div>
<div class="cart-lines">
<div class="cart-line">
<div class="cart-line-title">Cart Placeholder A</div>
<div class="cart-line-meta">Awaiting selection and quantity logic</div>
</div>
<div class="cart-line">
<div class="cart-line-title">Cart Placeholder B</div>
<div class="cart-line-meta">Reserved for grouped order summaries</div>
</div>
<div class="cart-line">
<div class="cart-line-title">Cart Placeholder C</div>
<div class="cart-line-meta">Reserved for checkout validation status</div>
</div>
</div>
<div class="cart-summary">
<div class="summary-row">
<span class="summary-label">Subtotal</span>
<span class="summary-value">$0</span>
</div>
<div class="summary-row">
<span class="summary-label">Fees</span>
<span class="summary-value">$0</span>
</div>
<div class="summary-row total">
<span class="summary-label">Total</span>
<span class="summary-value">$0</span>
</div>
</div>
<div class="summary-actions">
<button type="button" class="action-btn">Review Cart</button>
<button type="button" class="action-btn muted-btn">Checkout Locked</button>
</div>
</section>
</aside>
</div>
`;
};
})();

View File

@ -0,0 +1,110 @@
(function () {
const components = (window.StoreComponents = window.StoreComponents || {});
components.getBreadcrumbItems = function getBreadcrumbItems(
state,
formatTitle,
) {
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;
};
components.renderWorkspaceBreadcrumbs = function renderWorkspaceBreadcrumbs(
state,
formatTitle,
) {
const items = components.getBreadcrumbItems(state, formatTitle);
return `
<div class="workspace-breadcrumbs" aria-label="Breadcrumb">
${items
.map((item, index) => {
const isCurrent = index === items.length - 1;
if (isCurrent) {
return `
<span class="breadcrumb-current">${item.label}</span>
`;
}
return `
<button
type="button"
class="breadcrumb-link"
data-breadcrumb-target="${item.id}"
>
${item.label}
</button>
`;
})
.join('<span class="breadcrumb-separator">/</span>')}
</div>
`;
};
components.renderWorkspaceCartToggle = function renderWorkspaceCartToggle(
state,
) {
return `
<button
type="button"
class="workspace-cart-btn"
id="store-cart-toggle-btn"
aria-label="${state.cartOpen ? "Close cart" : "Open cart"}"
title="${state.cartOpen ? "Close cart" : "Open cart"}"
>
<span class="cart-toggle-icon" aria-hidden="true"></span>
</button>
`;
};
components.renderWorkspaceNavbar = function renderWorkspaceNavbar(
state,
formatTitle,
) {
return `
<nav class="workspace-navbar" aria-label="Store navigation">
${components.renderWorkspaceBreadcrumbs(state, formatTitle)}
${components.renderWorkspaceCartToggle(state)}
</nav>
`;
};
})();

View File

@ -0,0 +1,374 @@
(function () {
window.StoreData = {
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: [
{
code: "UNF-102",
name: "Field Uniform",
description:
"Standard issue apparel block reserved for mission-ready clothing sets.",
price: "$1,250",
},
{
code: "UNF-214",
name: "Combat Uniform",
description:
"Hardened kit placeholder for armored and specialized duty loadouts.",
price: "$1,980",
},
{
code: "UNF-330",
name: "Duty Uniform",
description:
"Administrative and garrison wear preview for storefront layout validation.",
price: "$890",
},
],
headgear: [
{
code: "HDG-044",
name: "Patrol Helmet",
description:
"Protective headgear module with placeholder image frame and pricing slot.",
price: "$640",
},
{
code: "HDG-107",
name: "Operator Cap",
description:
"Soft headwear entry for non-armored and low-profile equipment sets.",
price: "$120",
},
{
code: "HDG-221",
name: "Boonie Hat",
description:
"Terrain-adapted headwear card for storefront presentation.",
price: "$95",
},
],
facewear: [
{
code: "FAC-015",
name: "Protective Goggles",
description:
"Facewear module placeholder aligned to the shared supply exchange layout.",
price: "$220",
},
{
code: "FAC-028",
name: "Balaclava",
description:
"Low-profile face covering preview card for catalog expansion.",
price: "$74",
},
{
code: "FAC-091",
name: "Respirator Mask",
description:
"Filtered facewear placeholder for hazard and industrial kit sets.",
price: "$410",
},
],
vests: [
{
code: "VST-311",
name: "Carrier Rig",
description:
"Plate carrier preview item with a reserved image zone and pricing footer.",
price: "$2,430",
},
{
code: "VST-414",
name: "Patrol Vest",
description:
"Mid-weight vest card intended for security and checkpoint loadouts.",
price: "$1,320",
},
{
code: "VST-558",
name: "Utility Harness",
description:
"Storage-focused chest rig placeholder for non-ballistic kit sets.",
price: "$760",
},
],
ammo: [
{
code: "AMM-556",
name: "5.56 Cartridge Pack",
description:
"Grouped ammunition supply card with placeholder product art.",
price: "$180",
},
{
code: "AMM-762",
name: "7.62 Cartridge Pack",
description:
"Extended-caliber ammunition block for rifle and marksman loadouts.",
price: "$220",
},
{
code: "AMM-9MM",
name: "9mm Cartridge Pack",
description:
"Compact sidearm ammunition placeholder entry for layout review.",
price: "$70",
},
],
items: [
{
code: "ITM-004",
name: "First Aid Kit",
description:
"Support item placeholder designed to preview general utility inventory cards.",
price: "$65",
},
{
code: "ITM-089",
name: "Radio Module",
description:
"Communications item block with the same product card treatment as all categories.",
price: "$330",
},
{
code: "ITM-217",
name: "Tool Kit",
description:
"Repair and engineering support module placeholder for store browsing.",
price: "$145",
},
],
primary: [
{
code: "WPN-PRI-01",
name: "Primary Platform A",
description:
"Primary weapon slot placeholder card for mock store review.",
price: "$3,250",
},
{
code: "WPN-PRI-02",
name: "Primary Platform B",
description:
"Alternate long-arm placeholder with image frame and metadata treatment.",
price: "$3,980",
},
{
code: "WPN-PRI-03",
name: "Primary Platform C",
description:
"General-purpose primary slot preview for future catalog wiring.",
price: "$2,890",
},
],
secondary: [
{
code: "WPN-SEC-01",
name: "Secondary Launcher A",
description:
"Secondary slot placeholder card for support and utility weapon systems.",
price: "$5,600",
},
{
code: "WPN-SEC-02",
name: "Secondary Launcher B",
description:
"Compact shoulder-fired placeholder entry in the shared product style.",
price: "$4,950",
},
{
code: "WPN-SEC-03",
name: "Secondary Launcher C",
description:
"Reserved card for extended secondary inventory logic tomorrow.",
price: "$6,120",
},
],
handgun: [
{
code: "WPN-HND-01",
name: "Sidearm A",
description:
"Handgun slot placeholder card with shared visual language.",
price: "$780",
},
{
code: "WPN-HND-02",
name: "Sidearm B",
description:
"Secondary sidearm preview block for storefront evaluation.",
price: "$920",
},
{
code: "WPN-HND-03",
name: "Sidearm C",
description:
"Compact sidearm placeholder with the same product framing.",
price: "$860",
},
],
cars: [
{
code: "VEH-CAR-01",
name: "Patrol Utility Car",
description:
"Light wheeled vehicle placeholder for quick response and urban transport.",
price: "$12,500",
},
{
code: "VEH-CAR-02",
name: "Transport Van",
description:
"Personnel transport preview card using the shared product layout.",
price: "$18,200",
},
{
code: "VEH-CAR-03",
name: "Recon SUV",
description:
"Recon-focused platform placeholder with pricing and metadata treatment.",
price: "$21,900",
},
],
armor: [
{
code: "VEH-ARM-01",
name: "APC Variant A",
description:
"Armored personnel carrier placeholder reserved for heavy vehicle inventory.",
price: "$145,000",
},
{
code: "VEH-ARM-02",
name: "IFV Variant B",
description:
"Tracked armored platform preview aligned to category card behavior.",
price: "$228,000",
},
{
code: "VEH-ARM-03",
name: "Support Armor C",
description:
"Heavy support vehicle placeholder for future role-based filtering.",
price: "$174,500",
},
],
helis: [
{
code: "VEH-HEL-01",
name: "Light Heli A",
description:
"Rotorcraft placeholder for scouting and rapid insertion use cases.",
price: "$325,000",
},
{
code: "VEH-HEL-02",
name: "Transport Heli B",
description:
"Medium-lift helicopter preview item with staged catalog metadata.",
price: "$482,000",
},
{
code: "VEH-HEL-03",
name: "Attack Heli C",
description:
"Combat helicopter placeholder for future weapon package wiring.",
price: "$690,000",
},
],
planes: [
{
code: "VEH-PLN-01",
name: "Fixed-Wing Trainer",
description:
"Basic aircraft placeholder for pilot training and logistics transfer.",
price: "$760,000",
},
{
code: "VEH-PLN-02",
name: "Utility Plane",
description:
"General-purpose plane preview card in the shared storefront style.",
price: "$1,120,000",
},
{
code: "VEH-PLN-03",
name: "Strike Plane",
description:
"Fixed-wing strike platform placeholder for high-tier procurement.",
price: "$1,860,000",
},
],
naval: [
{
code: "VEH-NAV-01",
name: "Patrol Boat",
description:
"Shallow-water patrol vessel placeholder for littoral operations.",
price: "$92,000",
},
{
code: "VEH-NAV-02",
name: "Assault Boat",
description:
"Assault transport craft preview entry with staged catalog attributes.",
price: "$128,000",
},
{
code: "VEH-NAV-03",
name: "Support Craft",
description:
"Utility naval craft placeholder for resupply and extraction workflows.",
price: "$104,000",
},
],
other: [
{
code: "VEH-OTH-01",
name: "UAV Support Unit",
description:
"Unmanned vehicle placeholder grouped under miscellaneous platforms.",
price: "$48,000",
},
{
code: "VEH-OTH-02",
name: "Static Transport Module",
description:
"Special transport asset preview for non-standard deployment types.",
price: "$67,000",
},
{
code: "VEH-OTH-03",
name: "Service Platform",
description:
"General support platform placeholder for future specialty categories.",
price: "$58,500",
},
],
},
};
})();

View File

@ -7,14 +7,26 @@
<script> <script>
const addonRoot = "forge\\forge_client\\addons\\store\\ui\\_site\\"; const addonRoot = "forge\\forge_client\\addons\\store\\ui\\_site\\";
const styleFiles = ["style.css"]; const styleFiles = ["style.css"];
const scriptFiles = ["script.js"]; const scriptFiles = [
"components/AppShell.js",
"components/cards.js",
"components/cart-panel.js",
"components/workspace-navbar.js",
"data.js",
"logic/state-transitions.js",
"logic/workspace.js",
"logic/events.js",
"script.js",
];
function requestText(path) { function requestText(path) {
if ( if (
typeof A3API !== "undefined" && typeof A3API !== "undefined" &&
typeof A3API.RequestFile === "function" typeof A3API.RequestFile === "function"
) { ) {
return A3API.RequestFile(addonRoot + path); return A3API.RequestFile(
addonRoot + path.replace(/\//g, "\\"),
);
} }
return fetch(path).then((response) => { return fetch(path).then((response) => {

View File

@ -0,0 +1,77 @@
(function () {
const logic = (window.StoreLogic = window.StoreLogic || {});
logic.bindEvents = function bindEvents(options) {
const {
state,
closeStore,
renderApp,
toggleCart,
closeCart,
navigateToBreadcrumb,
selectCategory,
selectSubcategory,
} = options;
const closeBtn = document.getElementById("store-close-btn");
if (closeBtn) {
closeBtn.addEventListener("click", closeStore);
}
const cartToggleBtn = document.getElementById("store-cart-toggle-btn");
if (cartToggleBtn) {
cartToggleBtn.addEventListener("click", () => {
toggleCart(state);
renderApp();
});
}
const cartCloseBtn = document.getElementById("store-cart-close-btn");
if (cartCloseBtn) {
cartCloseBtn.addEventListener("click", () => {
if (closeCart(state)) {
renderApp();
}
});
}
const cartBackdrop = document.getElementById("store-cart-backdrop");
if (cartBackdrop) {
cartBackdrop.addEventListener("click", () => {
if (closeCart(state)) {
renderApp();
}
});
}
document
.querySelectorAll("[data-breadcrumb-target]")
.forEach((button) => {
button.addEventListener("click", () => {
const target = button.getAttribute(
"data-breadcrumb-target",
);
if (navigateToBreadcrumb(state, target)) {
renderApp();
}
});
});
document.querySelectorAll("[data-category]").forEach((button) => {
button.addEventListener("click", () => {
const category = button.getAttribute("data-category");
selectCategory(state, category);
renderApp();
});
});
document.querySelectorAll("[data-subcategory]").forEach((button) => {
button.addEventListener("click", () => {
const subcategory = button.getAttribute("data-subcategory");
const slotType = button.getAttribute("data-subcategory-type");
selectSubcategory(state, subcategory, slotType);
renderApp();
});
});
};
})();

View File

@ -0,0 +1,78 @@
(function () {
const logic = (window.StoreLogic = window.StoreLogic || {});
logic.toggleCart = function toggleCart(state) {
state.cartOpen = !state.cartOpen;
return true;
};
logic.closeCart = function closeCart(state) {
if (!state.cartOpen) {
return false;
}
state.cartOpen = false;
return true;
};
logic.navigateToBreadcrumb = function navigateToBreadcrumb(state, target) {
switch (target) {
case "categories":
state.view = "categories";
state.selectedCategory = "";
state.selectedWeaponSlot = "";
state.selectedVehicleSlot = "";
return true;
case "weapons":
state.view = "weapons";
state.selectedCategory = "weapons";
state.selectedWeaponSlot = "";
state.selectedVehicleSlot = "";
return true;
case "vehicles":
state.view = "vehicles";
state.selectedCategory = "vehicles";
state.selectedVehicleSlot = "";
state.selectedWeaponSlot = "";
return true;
default:
return false;
}
};
logic.selectCategory = function selectCategory(state, category) {
state.selectedCategory = category;
state.selectedWeaponSlot = "";
state.selectedVehicleSlot = "";
if (category === "weapons") {
state.view = "weapons";
return true;
}
if (category === "vehicles") {
state.view = "vehicles";
return true;
}
state.view = "items";
return true;
};
logic.selectSubcategory = function selectSubcategory(
state,
subcategory,
slotType,
) {
if (slotType === "vehicle") {
state.selectedVehicleSlot = subcategory;
state.selectedWeaponSlot = "";
} else {
state.selectedWeaponSlot = subcategory;
state.selectedVehicleSlot = "";
}
state.view = "items";
return true;
};
})();

View File

@ -0,0 +1,113 @@
(function () {
const logic = (window.StoreLogic = window.StoreLogic || {});
logic.formatTitle = function formatTitle(value) {
return String(value || "")
.split(/\s+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
};
logic.getWorkspaceHeader = function getWorkspaceHeader(state, formatTitle) {
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 scaffolded for tomorrow's item wiring.",
badge: "3 Slots",
ribbon: "Category Routing Active",
};
}
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 are scaffolded for item wiring.",
badge: "6 Classes",
ribbon: "Category Routing Active",
};
}
if (state.view === "items") {
const label =
state.selectedWeaponSlot ||
state.selectedVehicleSlot ||
state.selectedCategory ||
"catalog";
return {
eyebrow: "Catalog Preview",
title: formatTitle(label),
copy: "Mock product cards with placeholder imagery sized for future filtering, search, and cart logic.",
badge: "Preview Items",
ribbon: "Selection Locked",
};
}
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.",
badge: "8 Categories",
ribbon: "Mock Layout",
};
};
logic.renderWorkspaceBody = function renderWorkspaceBody(
state,
data,
components,
) {
if (state.view === "weapons") {
return `
<div class="workspace-grid is-wide">
${data.weaponCards
.map((category) =>
components.createSubCategoryCard(
category,
"weapon",
),
)
.join("")}
</div>
`;
}
if (state.view === "vehicles") {
return `
<div class="workspace-grid is-wide">
${data.vehicleCards
.map((category) =>
components.createSubCategoryCard(
category,
"vehicle",
),
)
.join("")}
</div>
`;
}
if (state.view === "items") {
const key =
state.selectedWeaponSlot ||
state.selectedVehicleSlot ||
state.selectedCategory;
const items = data.previewItems[key] || [];
return `
<div class="workspace-grid is-products">
${items.map(components.createProductCard).join("")}
</div>
`;
}
return `
<div class="workspace-grid">
${data.categoryCards.map(components.createCategoryCard).join("")}
</div>
`;
};
})();

View File

@ -3,236 +3,61 @@
view: "categories", view: "categories",
selectedCategory: "", selectedCategory: "",
selectedWeaponSlot: "", selectedWeaponSlot: "",
selectedVehicleSlot: "",
cartOpen: false, cartOpen: false,
}; };
const categoryCards = [ function getStoreData(name) {
{ id: "uniforms", label: "Uniforms" }, const data = window.StoreData && window.StoreData[name];
{ id: "headgear", label: "Headgear" }, if (typeof data === "undefined") {
{ id: "facewear", label: "Facewear" }, throw new Error(`[Store UI] Missing store data: ${name}`);
{ id: "vests", label: "Vests" }, }
{ id: "weapons", label: "Weapons" }, return data;
{ id: "ammo", label: "Ammo" }, }
{ id: "items", label: "Items" },
];
const weaponCards = [ function getComponentFn(name) {
{ id: "primary", label: "Primary" }, const fn = window.StoreComponents && window.StoreComponents[name];
{ id: "secondary", label: "Secondary" }, if (typeof fn !== "function") {
{ id: "handgun", label: "Handgun" }, throw new Error(`[Store UI] Missing component function: ${name}`);
]; }
return fn;
}
const previewItems = { function getLogicFn(name) {
uniforms: [ const fn = window.StoreLogic && window.StoreLogic[name];
{ if (typeof fn !== "function") {
code: "UNF-102", throw new Error(`[Store UI] Missing logic function: ${name}`);
name: "Field Uniform", }
description: return fn;
"Standard issue apparel block reserved for mission-ready clothing sets.", }
price: "$1,250",
}, const data = {
{ categoryCards: getStoreData("categoryCards"),
code: "UNF-214", vehicleCards: getStoreData("vehicleCards"),
name: "Combat Uniform", weaponCards: getStoreData("weaponCards"),
description: previewItems: getStoreData("previewItems"),
"Hardened kit placeholder for armored and specialized duty loadouts.",
price: "$1,980",
},
{
code: "UNF-330",
name: "Duty Uniform",
description:
"Administrative and garrison wear preview for storefront layout validation.",
price: "$890",
},
],
headgear: [
{
code: "HDG-044",
name: "Patrol Helmet",
description:
"Protective headgear module with placeholder image frame and pricing slot.",
price: "$640",
},
{
code: "HDG-107",
name: "Operator Cap",
description:
"Soft headwear entry for non-armored and low-profile equipment sets.",
price: "$120",
},
{
code: "HDG-221",
name: "Boonie Hat",
description:
"Terrain-adapted headwear card for storefront presentation.",
price: "$95",
},
],
facewear: [
{
code: "FAC-015",
name: "Protective Goggles",
description:
"Facewear module placeholder aligned to the shared supply exchange layout.",
price: "$220",
},
{
code: "FAC-028",
name: "Balaclava",
description:
"Low-profile face covering preview card for catalog expansion.",
price: "$74",
},
{
code: "FAC-091",
name: "Respirator Mask",
description:
"Filtered facewear placeholder for hazard and industrial kit sets.",
price: "$410",
},
],
vests: [
{
code: "VST-311",
name: "Carrier Rig",
description:
"Plate carrier preview item with a reserved image zone and pricing footer.",
price: "$2,430",
},
{
code: "VST-414",
name: "Patrol Vest",
description:
"Mid-weight vest card intended for security and checkpoint loadouts.",
price: "$1,320",
},
{
code: "VST-558",
name: "Utility Harness",
description:
"Storage-focused chest rig placeholder for non-ballistic kit sets.",
price: "$760",
},
],
ammo: [
{
code: "AMM-556",
name: "5.56 Cartridge Pack",
description:
"Grouped ammunition supply card with placeholder product art.",
price: "$180",
},
{
code: "AMM-762",
name: "7.62 Cartridge Pack",
description:
"Extended-caliber ammunition block for rifle and marksman loadouts.",
price: "$220",
},
{
code: "AMM-9MM",
name: "9mm Cartridge Pack",
description:
"Compact sidearm ammunition placeholder entry for layout review.",
price: "$70",
},
],
items: [
{
code: "ITM-004",
name: "First Aid Kit",
description:
"Support item placeholder designed to preview general utility inventory cards.",
price: "$65",
},
{
code: "ITM-089",
name: "Radio Module",
description:
"Communications item block with the same product card treatment as all categories.",
price: "$330",
},
{
code: "ITM-217",
name: "Tool Kit",
description:
"Repair and engineering support module placeholder for store browsing.",
price: "$145",
},
],
primary: [
{
code: "WPN-PRI-01",
name: "Primary Platform A",
description:
"Primary weapon slot placeholder card for mock store review.",
price: "$3,250",
},
{
code: "WPN-PRI-02",
name: "Primary Platform B",
description:
"Alternate long-arm placeholder with image frame and metadata treatment.",
price: "$3,980",
},
{
code: "WPN-PRI-03",
name: "Primary Platform C",
description:
"General-purpose primary slot preview for future catalog wiring.",
price: "$2,890",
},
],
secondary: [
{
code: "WPN-SEC-01",
name: "Secondary Launcher A",
description:
"Secondary slot placeholder card for support and utility weapon systems.",
price: "$5,600",
},
{
code: "WPN-SEC-02",
name: "Secondary Launcher B",
description:
"Compact shoulder-fired placeholder entry in the shared product style.",
price: "$4,950",
},
{
code: "WPN-SEC-03",
name: "Secondary Launcher C",
description:
"Reserved card for extended secondary inventory logic tomorrow.",
price: "$6,120",
},
],
handgun: [
{
code: "WPN-HND-01",
name: "Sidearm A",
description:
"Handgun slot placeholder card with shared visual language.",
price: "$780",
},
{
code: "WPN-HND-02",
name: "Sidearm B",
description:
"Secondary sidearm preview block for storefront evaluation.",
price: "$920",
},
{
code: "WPN-HND-03",
name: "Sidearm C",
description:
"Compact sidearm placeholder with the same product framing.",
price: "$860",
},
],
}; };
function sendEvent(event, data) { const components = {
renderAppShell: getComponentFn("renderAppShell"),
createCategoryCard: getComponentFn("createCategoryCard"),
createSubCategoryCard: getComponentFn("createSubCategoryCard"),
createProductCard: getComponentFn("createProductCard"),
renderCartPanel: getComponentFn("renderCartPanel"),
renderWorkspaceNavbar: getComponentFn("renderWorkspaceNavbar"),
};
const formatTitle = getLogicFn("formatTitle");
const getWorkspaceHeader = getLogicFn("getWorkspaceHeader");
const renderWorkspaceBody = getLogicFn("renderWorkspaceBody");
const bindEvents = getLogicFn("bindEvents");
const toggleCart = getLogicFn("toggleCart");
const closeCart = getLogicFn("closeCart");
const navigateToBreadcrumb = getLogicFn("navigateToBreadcrumb");
const selectCategory = getLogicFn("selectCategory");
const selectSubcategory = getLogicFn("selectSubcategory");
function sendEvent(event, payload) {
if ( if (
typeof A3API !== "undefined" && typeof A3API !== "undefined" &&
typeof A3API.SendAlert === "function" typeof A3API.SendAlert === "function"
@ -240,513 +65,44 @@
A3API.SendAlert( A3API.SendAlert(
JSON.stringify({ JSON.stringify({
event, event,
data, data: payload,
}), }),
); );
return; return;
} }
console.log("[Store UI]", event, data); console.log("[Store UI]", event, payload);
} }
function closeStore() { function closeStore() {
sendEvent("store::close", {}); sendEvent("store::close", {});
} }
function toggleCart() {
state.cartOpen = !state.cartOpen;
renderApp();
}
function closeCart() {
if (!state.cartOpen) {
return;
}
state.cartOpen = false;
renderApp();
}
function createCategoryCard(category) {
return `
<button class="card-button category-card" type="button" data-category="${category.id}">
<span class="card-label">${category.label}</span>
</button>
`;
}
function createWeaponCard(category) {
return `
<button class="card-button subcategory-card" type="button" data-weapon-slot="${category.id}">
<span class="card-label">${category.label}</span>
</button>
`;
}
function createProductCard(item) {
return `
<article class="card-button product-card">
<div class="product-image">Image Placeholder</div>
<div class="product-meta">
<span class="product-code">${item.code}</span>
<strong class="product-name">${item.name}</strong>
</div>
<p class="product-copy">${item.description}</p>
<div class="product-footer">
<span class="product-price">${item.price}</span>
<button class="action-btn" type="button">Add to Cart</button>
</div>
</article>
`;
}
function getWorkspaceHeader() {
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 scaffolded for tomorrow's item wiring.",
badge: "3 Slots",
ribbon: "Category Routing Active",
};
}
if (state.view === "items") {
const label =
state.selectedWeaponSlot || state.selectedCategory || "catalog";
return {
eyebrow: "Catalog Preview",
title: formatTitle(label),
copy: "Mock product cards with placeholder imagery sized for future filtering, search, and cart logic.",
badge: "Preview Items",
ribbon: "Selection Locked",
};
}
return {
eyebrow: "Supply Categories",
title: "Procurement Dashboard",
copy: "Choose a category to enter the exchange. Weapons opens a second tier, while the other departments display placeholder product inventory.",
badge: "7 Categories",
ribbon: "Mock Layout",
};
}
function formatTitle(value) {
return String(value || "")
.split(/\s+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join(" ");
}
function renderWorkspaceBody() {
if (state.view === "weapons") {
return `
<div class="workspace-grid is-wide">
${weaponCards.map(createWeaponCard).join("")}
</div>
`;
}
if (state.view === "items") {
const key = state.selectedWeaponSlot || state.selectedCategory;
const items = previewItems[key] || [];
return `
<div class="workspace-grid is-products">
${items.map(createProductCard).join("")}
</div>
`;
}
return `
<div class="workspace-grid">
${categoryCards.map(createCategoryCard).join("")}
</div>
`;
}
function renderCartPanel() {
return `
<div class="cart-overlay ${state.cartOpen ? "is-open" : ""}" aria-hidden="${state.cartOpen ? "false" : "true"}">
<button
type="button"
class="cart-overlay-backdrop"
id="store-cart-backdrop"
aria-label="Close cart"
></button>
<aside class="store-cart-panel">
<section class="cart-card">
<div class="cart-header">
<div>
<span class="eyebrow">Cart</span>
<h2 class="section-title">Acquisition Queue</h2>
</div>
<button
type="button"
class="window-control-btn cart-panel-close"
id="store-cart-close-btn"
aria-label="Close cart"
title="Close cart"
>
X
</button>
</div>
<div class="cart-status">
<span class="eyebrow">Status</span>
<p class="section-copy">
Cart wiring is intentionally deferred. This panel is laid out for order lines, approval totals, and checkout actions.
</p>
</div>
<div class="cart-kpi">
<div class="cart-kpi-card">
<span class="kpi-label">Lines</span>
<span class="kpi-value">0</span>
</div>
<div class="cart-kpi-card">
<span class="kpi-label">Budget</span>
<span class="kpi-value">$48,000</span>
</div>
</div>
<div class="cart-lines">
<div class="cart-line">
<div class="cart-line-title">Cart Placeholder A</div>
<div class="cart-line-meta">Awaiting selection and quantity logic</div>
</div>
<div class="cart-line">
<div class="cart-line-title">Cart Placeholder B</div>
<div class="cart-line-meta">Reserved for grouped order summaries</div>
</div>
<div class="cart-line">
<div class="cart-line-title">Cart Placeholder C</div>
<div class="cart-line-meta">Reserved for checkout validation status</div>
</div>
</div>
<div class="cart-summary">
<div class="summary-row">
<span class="summary-label">Subtotal</span>
<span class="summary-value">$0</span>
</div>
<div class="summary-row">
<span class="summary-label">Fees</span>
<span class="summary-value">$0</span>
</div>
<div class="summary-row total">
<span class="summary-label">Total</span>
<span class="summary-value">$0</span>
</div>
</div>
<div class="summary-actions">
<button type="button" class="action-btn">Review Cart</button>
<button type="button" class="action-btn muted-btn">Checkout Locked</button>
</div>
</section>
</aside>
</div>
`;
}
function getBreadcrumbItems() {
const items = [{ id: "categories", label: "Supply Exchange" }];
if (state.view === "weapons") {
items.push({ id: "weapons", label: "Weapons" });
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.selectedCategory) {
items.push({
id: "category",
label: formatTitle(state.selectedCategory),
});
}
}
return items;
}
function renderWorkspaceBreadcrumbs() {
const items = getBreadcrumbItems();
return `
<div class="workspace-breadcrumbs" aria-label="Breadcrumb">
${items
.map((item, index) => {
const isCurrent = index === items.length - 1;
if (isCurrent) {
return `
<span class="breadcrumb-current">${item.label}</span>
`;
}
return `
<button
type="button"
class="breadcrumb-link"
data-breadcrumb-target="${item.id}"
>
${item.label}
</button>
`;
})
.join('<span class="breadcrumb-separator">/</span>')}
</div>
`;
}
function renderWorkspaceCartToggle() {
return `
<button
type="button"
class="workspace-cart-btn"
id="store-cart-toggle-btn"
aria-label="${state.cartOpen ? "Close cart" : "Open cart"}"
title="${state.cartOpen ? "Close cart" : "Open cart"}"
>
<span class="cart-toggle-icon" aria-hidden="true"></span>
</button>
`;
}
function renderWorkspaceNavbar() {
return `
<nav class="workspace-navbar" aria-label="Store navigation">
${renderWorkspaceBreadcrumbs()}
${renderWorkspaceCartToggle()}
</nav>
`;
}
function navigateToBreadcrumb(target) {
switch (target) {
case "categories":
state.view = "categories";
state.selectedCategory = "";
state.selectedWeaponSlot = "";
break;
case "weapons":
state.view = "weapons";
state.selectedCategory = "weapons";
state.selectedWeaponSlot = "";
break;
default:
return;
}
renderApp();
}
function renderApp() { function renderApp() {
const header = getWorkspaceHeader(); const header = getWorkspaceHeader(state, formatTitle);
const workspaceNavbar = components.renderWorkspaceNavbar(
document.getElementById("app").innerHTML = ` state,
<div class="store-shell"> formatTitle,
<div class="window-titlebar">
<div class="window-titlebar-brand">
<span class="window-titlebar-kicker">FORGE Logistics</span>
<span class="window-titlebar-title">Supply Exchange</span>
</div>
<div class="window-titlebar-controls">
<button
type="button"
class="window-control-btn"
disabled
title="Minimize unavailable"
aria-label="Minimize unavailable"
>
-
</button>
<button
type="button"
class="window-control-btn"
disabled
title="Maximize unavailable"
aria-label="Maximize unavailable"
>
[ ]
</button>
<button
type="button"
class="window-control-btn is-close"
id="store-close-btn"
title="Close"
aria-label="Close store interface"
>
X
</button>
</div>
</div>
<div class="store-app">
<aside class="store-sidebar">
<section class="module-card search-module">
<div class="module-header">
<div>
<span class="eyebrow">Search</span>
<h2 class="section-title">Inventory Search</h2>
</div>
<span class="pill">Module</span>
</div>
<input
type="text"
class="search-input"
placeholder="Search inventory, classes, or suppliers"
/>
<div class="quick-tags">
<span class="quick-tag">Field</span>
<span class="quick-tag">Logistics</span>
<span class="quick-tag">Issued</span>
<span class="quick-tag">Restricted</span>
</div>
</section>
<section class="module-card">
<div class="module-header">
<div>
<span class="eyebrow">Filter</span>
<h2 class="section-title">Procurement Filters</h2>
</div>
<span class="pill">Pending</span>
</div>
<div class="filter-stack">
<div class="filter-group">
<span class="filter-label">Department</span>
<div class="filter-value">
<span>Operational Tier</span>
<span class="filter-placeholder">Any</span>
</div>
</div>
<div class="filter-group">
<span class="filter-label">Availability</span>
<div class="filter-value">
<span>Stock Window</span>
<span class="filter-placeholder">Open</span>
</div>
</div>
<div class="filter-group">
<span class="filter-label">Approval</span>
<div class="filter-value">
<span>Purchase Level</span>
<span class="filter-placeholder">All</span>
</div>
</div>
</div>
</section>
</aside>
<main class="store-main">
<section class="workspace-card">
${renderWorkspaceNavbar()}
<div class="workspace-header">
<div>
<span class="eyebrow">${header.eyebrow}</span>
<h1 class="section-title">${header.title}</h1>
</div>
<span class="pill">${header.badge}</span>
</div>
<div class="workspace-intro">
<p class="section-copy">${header.copy}</p>
<span class="inventory-ribbon">${header.ribbon}</span>
</div>
${renderWorkspaceBody()}
</section>
${renderCartPanel()}
</main>
</div>
<footer class="store-footer">
<div class="footer-block">
<span class="footer-title">Procurement Desk</span>
<span class="footer-copy">Authorized supply browsing for personnel loadout preparation and mission staging.</span>
</div>
<div class="footer-block">
<span class="footer-title">Catalog Scope</span>
<span class="footer-copy">Uniforms, protective gear, weapon slots, ammunition groups, and general support inventory.</span>
</div>
<div class="footer-block">
<span class="footer-title">Module State</span>
<span class="footer-copy">Search, filters, and cart presentation are staged now. Purchase logic and item wiring will follow.</span>
</div>
</footer>
</div>
`;
bindEvents();
}
function bindEvents() {
const closeBtn = document.getElementById("store-close-btn");
if (closeBtn) {
closeBtn.addEventListener("click", closeStore);
}
const cartToggleBtn = document.getElementById("store-cart-toggle-btn");
if (cartToggleBtn) {
cartToggleBtn.addEventListener("click", toggleCart);
}
const cartCloseBtn = document.getElementById("store-cart-close-btn");
if (cartCloseBtn) {
cartCloseBtn.addEventListener("click", closeCart);
}
const cartBackdrop = document.getElementById("store-cart-backdrop");
if (cartBackdrop) {
cartBackdrop.addEventListener("click", closeCart);
}
document
.querySelectorAll("[data-breadcrumb-target]")
.forEach((button) => {
button.addEventListener("click", () => {
navigateToBreadcrumb(
button.getAttribute("data-breadcrumb-target"),
); );
}); const workspaceBody = renderWorkspaceBody(state, data, components);
const cartPanel = components.renderCartPanel(state);
document.getElementById("app").innerHTML = components.renderAppShell({
header,
workspaceNavbar,
workspaceBody,
cartPanel,
}); });
document.querySelectorAll("[data-category]").forEach((button) => { bindEvents({
button.addEventListener("click", () => { state,
const category = button.getAttribute("data-category"); closeStore,
state.selectedCategory = category; renderApp,
state.selectedWeaponSlot = ""; toggleCart,
closeCart,
if (category === "weapons") { navigateToBreadcrumb,
state.view = "weapons"; selectCategory,
} else { selectSubcategory,
state.view = "items";
}
renderApp();
});
});
document.querySelectorAll("[data-weapon-slot]").forEach((button) => {
button.addEventListener("click", () => {
state.selectedWeaponSlot =
button.getAttribute("data-weapon-slot");
state.view = "items";
renderApp();
});
}); });
} }