From 19eae5acfa376598188f6c5fe611aaa2e40b88ca Mon Sep 17 00:00:00 2001 From: Jacob Schmidt Date: Tue, 9 Jun 2026 17:25:28 -0500 Subject: [PATCH] feat(store): enhance product card with tags and metadata - Added support for displaying product tags in the UI, including side and faction labels. - Updated CSS to style product tags for better visibility. - Modified StoreView and store registry to include side and faction information for items. - Enhanced server-side catalog service to filter items based on the player's side. - Updated documentation to reflect changes in store behavior and metadata handling. --- .gitignore | 1 + arma/client/addons/store/ui/_site/store-ui.js | 2 +- .../addons/store/ui/src/components/cards.js | 29 +++++ .../addons/store/ui/src/pages/StoreView.js | 2 + .../addons/store/ui/src/registry/store.js | 4 + arma/server/addons/store/README.md | 17 +-- arma/server/addons/store/XEH_preInit.sqf | 2 +- .../functions/fnc_initCatalogService.sqf | 108 +++++++++++++++--- .../functions/fnc_initStorefrontStore.sqf | 2 +- docs/STORE_USAGE_GUIDE.md | 21 ++-- 10 files changed, 157 insertions(+), 31 deletions(-) diff --git a/.gitignore b/.gitignore index 9c88b99..fa2dbca 100644 --- a/.gitignore +++ b/.gitignore @@ -37,4 +37,5 @@ Thumbs.db arma/ui/map-viewer/ arma/mod/.hemttout/ arma/server/surrealdb/forge.db/ +arma/temp/ promo/ diff --git a/arma/client/addons/store/ui/_site/store-ui.js b/arma/client/addons/store/ui/_site/store-ui.js index a3c974a..988ea42 100644 --- a/arma/client/addons/store/ui/_site/store-ui.js +++ b/arma/client/addons/store/ui/_site/store-ui.js @@ -1 +1 @@ -!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"},{id:"units",label:"Units"}],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:[],units:[]}},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("")}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=t.map(e=>String(e?.id||"").trim());n&&r.includes(n)&&t.some(e=>String(e?.id||"").trim()===n&&!1!==e?.enabled)||this.setSelectedPaymentSource("")}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 live product inventory inside the runtime store architecture.",badge:"11 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:[],units:[],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)if("unit"!==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:"Select Payment")))))),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, units, 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)||null,u=d.filter(e=>!1!==e.enabled).length,g=m?m.label:"Select Payment",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)},t("option",{value:"",disabled:!0},"Select Payment"),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")):t("div",{className:"payment-source-meta"},t("span",{className:"payment-source-label"},"Select Payment"),t("span",{className:"payment-source-state"},u>0?"Required":"Unavailable")))),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||!m||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 +!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"},{id:"units",label:"Units"}],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:[],units:[]}},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"),side:String(e?.side||""),sideLabel:String(e?.sideLabel||""),faction:String(e?.faction||""),factionName:String(e?.factionName||""),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("")}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=t.map(e=>String(e?.id||"").trim());n&&r.includes(n)&&t.some(e=>String(e?.id||"").trim()===n&&!1!==e?.enabled)||this.setSelectedPaymentSource("")}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,t.sideLabel,t.factionName]))}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 live product inventory inside the runtime store architecture.",badge:"11 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:[],units:[],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)if("unit"!==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:"Select Payment")))))),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, units, 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} .product-tag,\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-tags {\n display: flex;\n flex-wrap: wrap;\n gap: 0.35rem;\n}\n\n${s} .product-tag {\n width: fit-content;\n max-width: 100%;\n padding: 0.18rem 0.42rem;\n border-radius: 0.5rem;\n border: 1px solid var(--store-accent-line);\n background: rgb(255 255 255 / 0.42);\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),c=[e.sideLabel?e.sideLabel:"",e.factionName?e.factionName:""].filter(Boolean);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),c.length>0?t("div",{className:"product-tags"},c.map(e=>t("span",{className:"product-tag"},e))):null,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)||null,u=d.filter(e=>!1!==e.enabled).length,g=m?m.label:"Select Payment",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)},t("option",{value:"",disabled:!0},"Select Payment"),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")):t("div",{className:"payment-source-meta"},t("span",{className:"payment-source-label"},"Select Payment"),t("span",{className:"payment-source-state"},u>0?"Required":"Unavailable")))),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||!m||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/components/cards.js b/arma/client/addons/store/ui/src/components/cards.js index fecf0cd..7646ddd 100644 --- a/arma/client/addons/store/ui/src/components/cards.js +++ b/arma/client/addons/store/ui/src/components/cards.js @@ -94,6 +94,7 @@ ${scopeSelector} .product-card:hover { ${scopeSelector} .card-kicker, ${scopeSelector} .product-code, +${scopeSelector} .product-tag, ${scopeSelector} .empty-state-kicker { font-size: 0.72rem; letter-spacing: 0.14em; @@ -156,6 +157,21 @@ ${scopeSelector} .product-meta { gap: 0.2rem; } +${scopeSelector} .product-tags { + display: flex; + flex-wrap: wrap; + gap: 0.35rem; +} + +${scopeSelector} .product-tag { + width: fit-content; + max-width: 100%; + padding: 0.18rem 0.42rem; + border-radius: 0.5rem; + border: 1px solid var(--store-accent-line); + background: rgb(255 255 255 / 0.42); +} + ${scopeSelector} .product-name { font-size: 0.96rem; font-weight: 700; @@ -370,6 +386,10 @@ ${scopeSelector} .product-copy { item.description, item.className || item.code, ); + const tags = [ + item.sideLabel ? item.sideLabel : "", + item.factionName ? item.factionName : "", + ].filter(Boolean); return h( "article", @@ -399,6 +419,15 @@ ${scopeSelector} .product-copy { { className: "product-code" }, item.type || item.code || item.className, ), + tags.length > 0 + ? h( + "div", + { className: "product-tags" }, + tags.map((tag) => + h("span", { className: "product-tag" }, tag), + ), + ) + : null, h("strong", { className: "product-name" }, item.name), ), h("p", { className: "product-copy" }, description), diff --git a/arma/client/addons/store/ui/src/pages/StoreView.js b/arma/client/addons/store/ui/src/pages/StoreView.js index 81c20c8..1ea3e19 100644 --- a/arma/client/addons/store/ui/src/pages/StoreView.js +++ b/arma/client/addons/store/ui/src/pages/StoreView.js @@ -195,6 +195,8 @@ item.description, item.price, item.type, + item.sideLabel, + item.factionName, ]), ); } diff --git a/arma/client/addons/store/ui/src/registry/store.js b/arma/client/addons/store/ui/src/registry/store.js index ed19794..0c9adb0 100644 --- a/arma/client/addons/store/ui/src/registry/store.js +++ b/arma/client/addons/store/ui/src/registry/store.js @@ -17,6 +17,10 @@ type: String(item?.type || ""), category: String(item?.category || ""), entryKind: String(item?.entryKind || "item"), + side: String(item?.side || ""), + sideLabel: String(item?.sideLabel || ""), + faction: String(item?.faction || ""), + factionName: String(item?.factionName || ""), quantity: Math.max(0, Number(item?.quantity || 0)), }; } diff --git a/arma/server/addons/store/README.md b/arma/server/addons/store/README.md index 03fe25f..ab89e12 100644 --- a/arma/server/addons/store/README.md +++ b/arma/server/addons/store/README.md @@ -71,10 +71,12 @@ class CfgStore { }; ``` -`dynamic` keeps the full generated catalog. `allowlist` only shows classnames -listed for the requested category. `denylist` removes listed classnames from the -generated category. Overrides are applied server-side, so checkout validation -uses the same prices and descriptions the UI displays. +`modMode` is applied first. After that, any non-empty +`Categories.[]` array acts as an explicit allowlist for only that +category, regardless of `mode`. Categories with empty arrays follow `mode`: +`dynamic` keeps generated entries and `allowlist` hides the category. Overrides +are applied server-side, so checkout validation uses the same prices and +descriptions the UI displays. `modMode` applies before category filtering. `dynamic` means no mod-source filtering. `allowlist` only keeps generated entries that match one of the @@ -86,9 +88,10 @@ metadata tokens that can appear anywhere, and `dlcs[]` for DLC/source/author labels used by Creator DLC content. If a mod source defines no patches, it is treated as available and only the source/prefix/contains/DLC checks are used. -`units[]` follows the same `dynamic`, `allowlist`, and `denylist` behavior as -item and vehicle categories. Unit purchases are immediate spawn grants, not -durable virtual garage unlocks. +Unit catalog responses are additionally filtered to the requesting player's +side. Unit entries include side and faction metadata for UI display, and +checkout validation rejects unit classnames from another side. Unit purchases +are immediate spawn grants, not durable virtual garage unlocks. The filter is currently global for the mission. Revisit per-store profile support if individual vendors need different inventories. diff --git a/arma/server/addons/store/XEH_preInit.sqf b/arma/server/addons/store/XEH_preInit.sqf index 21a2764..7f3d5fc 100644 --- a/arma/server/addons/store/XEH_preInit.sqf +++ b/arma/server/addons/store/XEH_preInit.sqf @@ -20,7 +20,7 @@ PREP_RECOMPILE_END; diag_log "[FORGE:Server:Store] Store catalog service is unavailable." }; - private _result = GVAR(StoreCatalogService) call ["buildCategoryResponse", [_category]]; + private _result = GVAR(StoreCatalogService) call ["buildCategoryResponse", [_category, _player]]; [CRPC(store,responseCategory), [_result], _player] call CFUNC(targetEvent); }] call CFUNC(addEventHandler); diff --git a/arma/server/addons/store/functions/fnc_initCatalogService.sqf b/arma/server/addons/store/functions/fnc_initCatalogService.sqf index 2ae7085..14392d2 100644 --- a/arma/server/addons/store/functions/fnc_initCatalogService.sqf +++ b/arma/server/addons/store/functions/fnc_initCatalogService.sqf @@ -294,21 +294,74 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ private _classNames = _self call ["getMissionStoreCategoryList", [_category]]; private _filteredItems = _self call ["applyMissionStoreModFilter", [_items]]; - switch (_mode) do { - case "allowlist": { - _filteredItems = _filteredItems select { - (toLowerANSI (_x getOrDefault ["className", ""])) in _classNames - }; + if (_classNames isNotEqualTo []) then { + _filteredItems = _filteredItems select { + (toLowerANSI (_x getOrDefault ["className", ""])) in _classNames }; - case "denylist": { - _filteredItems = _filteredItems select { - !((toLowerANSI (_x getOrDefault ["className", ""])) in _classNames) + } else { + switch (_mode) do { + case "allowlist": { _filteredItems = []; }; + case "denylist": { + _filteredItems = _filteredItems select { + !((toLowerANSI (_x getOrDefault ["className", ""])) in _classNames) + }; }; }; }; _filteredItems apply { _self call ["applyMissionStoreOverrides", [_x]] } }], + ["resolveSideLabel", compileFinal { + params [["_sideValue", -1, [0]]]; + + switch _sideValue do { + case 0: { "OPFOR" }; + case 1: { "BLUFOR" }; + case 2: { "Independent" }; + case 3: { "Civilian" }; + default { "Unknown" }; + } + }], + ["resolveSideKey", compileFinal { + params [["_sideValue", -1, [0]]]; + + switch _sideValue do { + case 0: { "east" }; + case 1: { "west" }; + case 2: { "resistance" }; + case 3: { "civilian" }; + default { "" }; + } + }], + ["resolvePlayerSideKey", compileFinal { + params [["_player", objNull, [objNull]]]; + + if (isNull _player) exitWith { "" }; + switch (side group _player) do { + case west: { "west" }; + case east: { "east" }; + case independent: { "resistance" }; + case civilian: { "civilian" }; + default { "" }; + } + }], + ["doesItemMatchPlayerSide", compileFinal { + params [["_item", createHashMap, [createHashMap]], ["_player", objNull, [objNull]]]; + + if (_item isEqualTo createHashMap) exitWith { false }; + + private _playerSideKey = _self call ["resolvePlayerSideKey", [_player]]; + if (_playerSideKey isEqualTo "") exitWith { true }; + + private _itemSideKey = _item getOrDefault ["side", ""]; + _itemSideKey isEqualTo "" || { _itemSideKey isEqualTo _playerSideKey } + }], + ["applyPlayerSideFilter", compileFinal { + params [["_category", "", [""]], ["_items", [], [[]]], ["_player", objNull, [objNull]]]; + + if !(_self call ["isUnitCategory", [_category]]) exitWith { +_items }; + _items select { _self call ["doesItemMatchPlayerSide", [_x, _player]] } + }], ["formatCurrency", compileFinal { params [["_amount", 0, [0]]]; @@ -384,8 +437,7 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ }; private _priceValue = _self call ["calculateCatalogPriceValue", [_cfg, _isVehicle]]; - - createHashMapFromArray [ + private _item = createHashMapFromArray [ ["className", _className], ["code", _className], ["name", _displayName], @@ -398,7 +450,26 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ ["sourceMod", _sourceMod], ["sourceDLC", _sourceDLC], ["sourceAuthor", _sourceAuthor] - ] + ]; + + if (isNumber (_cfg >> "side")) then { + private _sideValue = getNumber (_cfg >> "side"); + _item set ["sideValue", _sideValue]; + _item set ["side", _self call ["resolveSideKey", [_sideValue]]]; + _item set ["sideLabel", _self call ["resolveSideLabel", [_sideValue]]]; + }; + + if (isText (_cfg >> "faction")) then { + private _faction = getText (_cfg >> "faction"); + if (_faction isNotEqualTo "") then { + private _factionName = getText (configFile >> "CfgFactionClasses" >> _faction >> "displayName"); + if (_factionName isEqualTo "") then { _factionName = _faction; }; + _item set ["faction", _faction]; + _item set ["factionName", _factionName]; + }; + }; + + _item }], ["appendCfgWeaponsByItemInfoType", compileFinal { params [["_items", [], [[]]], ["_itemInfoType", -1, [0]], ["_itemKind", "", [""]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]]; @@ -686,7 +757,7 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ _items }], ["buildCategoryResponse", compileFinal { - params [["_category", "", [""]]]; + params [["_category", "", [""]], ["_player", objNull, [objNull]]]; private _categoryKey = _self call ["normalizeCategoryKey", [_category]]; private _response = createHashMapFromArray [["success", false], ["category", _categoryKey], ["items", []], ["message", "No store category was provided."]]; @@ -699,7 +770,10 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ _response set ["success", true]; _response set ["message", ""]; - _response set ["items", _self call ["buildCategoryItems", [_categoryKey]]]; + + private _items = _self call ["buildCategoryItems", [_categoryKey]]; + _items = _self call ["applyPlayerSideFilter", [_categoryKey, _items, _player]]; + _response set ["items", _items]; _response }], ["resolveCheckoutCategories", compileFinal { @@ -734,7 +808,7 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ _resolved }], ["buildCheckoutRequest", compileFinal { - params [["_items", [], [[]]], ["_vehicles", [], [[]]], ["_units", [], [[]]]]; + params [["_items", [], [[]]], ["_vehicles", [], [[]]], ["_units", [], [[]]], ["_player", objNull, [objNull]]]; private _result = createHashMapFromArray [ ["success", false], @@ -810,6 +884,12 @@ GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [ if (_catalogEntry isEqualTo createHashMap) then { _message = format ["Unsupported store unit: %1", _className]; } else { + if !(_self call ["doesItemMatchPlayerSide", [_catalogEntry, _player]]) then { + _message = format ["Store unit is not available for your side: %1", _className]; + }; + }; + + if (_message isEqualTo "") then { private _priceValue = _catalogEntry getOrDefault ["priceValue", 0]; _total = _total + _priceValue; _resolvedUnits pushBack (createHashMapFromArray [ diff --git a/arma/server/addons/store/functions/fnc_initStorefrontStore.sqf b/arma/server/addons/store/functions/fnc_initStorefrontStore.sqf index 8aa46bb..0d9c1b3 100644 --- a/arma/server/addons/store/functions/fnc_initStorefrontStore.sqf +++ b/arma/server/addons/store/functions/fnc_initStorefrontStore.sqf @@ -530,7 +530,7 @@ GVAR(StorefrontBaseStore) = compileFinal createHashMapFromArray [ _result }; - private _checkoutRequest = GVAR(StoreCatalogService) call ["buildCheckoutRequest", [_items, _vehicles, _units]]; + private _checkoutRequest = GVAR(StoreCatalogService) call ["buildCheckoutRequest", [_items, _vehicles, _units, _player]]; private _totalPrice = _checkoutRequest getOrDefault ["total", 0]; _result set ["paymentMethod", _paymentMethod]; diff --git a/docs/STORE_USAGE_GUIDE.md b/docs/STORE_USAGE_GUIDE.md index a309a2c..23b4358 100644 --- a/docs/STORE_USAGE_GUIDE.md +++ b/docs/STORE_USAGE_GUIDE.md @@ -71,10 +71,17 @@ class CfgStore { }; ``` -`dynamic` keeps the full generated catalog. `allowlist` only shows classnames -listed for each category. `denylist` removes listed classnames. Overrides are -server-side and are used by both the UI payload and checkout validation. -`units[]` uses the same filter behavior as every other category. +`modMode` is applied first. After that, any non-empty +`Categories.[]` array acts as an explicit allowlist for only that +category, regardless of `mode`. Categories with empty arrays follow `mode`: +`dynamic` keeps the generated entries and `allowlist` hides the category. +Overrides are server-side and are used by both the UI payload and checkout +validation. + +Unit catalog responses are additionally filtered to the requesting player's +side. Unit entries include side and faction metadata so the UI can show the +available faction before purchase, and checkout validation rejects unit +classnames from another side. `modMode` applies before category filtering. `dynamic` means no mod-source filtering. `allowlist` only keeps generated entries that match one of the @@ -93,9 +100,9 @@ modMode = "allowlist"; mods[] = {"rhs"}; ``` -The matching `class rhs` must exist under `ModSources`. Category `mode` is still -applied afterward, so leave `mode = "dynamic"` if the mod filter should be the -only inventory filter. +The matching `class rhs` must exist under `ModSources`. Category arrays are +still applied afterward, so leave arrays empty and `mode = "dynamic"` if the mod +filter should be the only inventory filter. For Creator DLCs such as RF or WS, prefer both prefixes and DLC labels: