Move store catalog service server-side and rebuild WebUI bundles
- Shift store catalog initialization from client addon to server addon - Rebuild/minify bank, garage, org, and store WebUI site assets - Update webui build tooling and npm metadata; ignore node_modules
This commit is contained in:
parent
603963c935
commit
68218304ab
3
.gitignore
vendored
3
.gitignore
vendored
@ -21,6 +21,9 @@ target/
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Misc
|
||||
node_modules/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,64 +1 @@
|
||||
<!-- Generated by tools/build-webui.mjs for bank UI index. Do not edit directly. -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>FORGE Banking Console</title>
|
||||
<script>
|
||||
window.ForgeSiteConfig = {
|
||||
addonName: "bank",
|
||||
logLabel: "Bank UI",
|
||||
styles: ["bank-ui.css"],
|
||||
commonScripts: ["forge-webui.js"],
|
||||
scripts: ["bank-ui.js"],
|
||||
};
|
||||
|
||||
(function loadForgeSiteLoader() {
|
||||
const armaLoaderPath =
|
||||
"forge\\forge_client\\addons\\common\\ui\\_site\\forge-site-loader.js";
|
||||
const browserLoaderPath =
|
||||
"../../../common/ui/_site/forge-site-loader.js";
|
||||
|
||||
function appendScript(js) {
|
||||
const script = document.createElement("script");
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function requestLoader() {
|
||||
if (
|
||||
typeof A3API !== "undefined" &&
|
||||
A3API &&
|
||||
typeof A3API.RequestFile === "function"
|
||||
) {
|
||||
return A3API.RequestFile(armaLoaderPath);
|
||||
}
|
||||
|
||||
return fetch(browserLoaderPath).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
"Failed to load " + browserLoaderPath,
|
||||
);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
});
|
||||
}
|
||||
|
||||
requestLoader()
|
||||
.then(appendScript)
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"[Bank UI] Failed to load Forge site loader.",
|
||||
error,
|
||||
);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>FORGE Banking Console</title><script>window.ForgeSiteConfig={addonName:"bank",logLabel:"Bank UI",styles:["bank-ui.css"],commonScripts:["forge-webui.js"],scripts:["bank-ui.js"]},function(){const e="../../../common/ui/_site/forge-site-loader.js";("undefined"!=typeof A3API&&A3API&&"function"==typeof A3API.RequestFile?A3API.RequestFile("forge\\forge_client\\addons\\common\\ui\\_site\\forge-site-loader.js"):fetch(e).then(o=>{if(!o.ok)throw new Error("Failed to load "+e);return o.text()})).then(function(e){const o=document.createElement("script");o.text=e,document.head.appendChild(o)}).catch(e=>{console.error("[Bank UI] Failed to load Forge site loader.",e)})}()</script></head><body><div id="app"></div></body></html>
|
||||
@ -1,127 +1 @@
|
||||
/* Generated by tools/build-webui.mjs for Forge Web UI site loader. Do not edit directly. */
|
||||
(function (global) {
|
||||
const ForgeSiteLoader = (global.ForgeSiteLoader =
|
||||
global.ForgeSiteLoader || {});
|
||||
const commonAddonRoot = "forge\\forge_client\\addons\\common\\ui\\_site\\";
|
||||
const defaultBrowserCommonBase = "../../../common/ui/_site/";
|
||||
|
||||
function isArmaAvailable() {
|
||||
return (
|
||||
typeof A3API !== "undefined" &&
|
||||
A3API &&
|
||||
typeof A3API.RequestFile === "function"
|
||||
);
|
||||
}
|
||||
|
||||
function isAbsoluteAddonPath(path) {
|
||||
return typeof path === "string" && path.startsWith("forge\\");
|
||||
}
|
||||
|
||||
function normalizeAddonRoot(addonName) {
|
||||
return `forge\\forge_client\\addons\\${addonName}\\ui\\_site\\`;
|
||||
}
|
||||
|
||||
function normalizeBrowserPath(basePath, assetPath) {
|
||||
const normalizedBase = String(basePath || "./").replace(/\\/g, "/");
|
||||
const normalizedAssetPath = String(assetPath || "").replace(/\\/g, "/");
|
||||
return `${normalizedBase}${normalizedAssetPath}`;
|
||||
}
|
||||
|
||||
function requestText({ addonRoot, browserBase, assetPath }) {
|
||||
if (isArmaAvailable()) {
|
||||
const resolvedPath = isAbsoluteAddonPath(assetPath)
|
||||
? assetPath
|
||||
: addonRoot + String(assetPath || "").replace(/\//g, "\\");
|
||||
return A3API.RequestFile(resolvedPath);
|
||||
}
|
||||
|
||||
const browserPath = isAbsoluteAddonPath(assetPath)
|
||||
? assetPath
|
||||
: normalizeBrowserPath(browserBase, assetPath);
|
||||
|
||||
return fetch(browserPath).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to load ${browserPath}`);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
});
|
||||
}
|
||||
|
||||
function appendStyle(cssText) {
|
||||
const style = document.createElement("style");
|
||||
style.textContent = cssText;
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
function appendScript(jsText) {
|
||||
const script = document.createElement("script");
|
||||
script.text = jsText;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
async function boot(config) {
|
||||
const addonName = config && config.addonName ? config.addonName : "";
|
||||
|
||||
if (!addonName) {
|
||||
throw new Error(
|
||||
"ForgeSiteLoader requires a config.addonName value.",
|
||||
);
|
||||
}
|
||||
|
||||
const addonRoot = normalizeAddonRoot(addonName);
|
||||
const browserAddonBase = config.browserAddonBase || "./";
|
||||
const browserCommonBase =
|
||||
config.browserCommonBase || defaultBrowserCommonBase;
|
||||
const styles = Array.isArray(config.styles) ? config.styles : [];
|
||||
const commonScripts = Array.isArray(config.commonScripts)
|
||||
? config.commonScripts
|
||||
: [];
|
||||
const scripts = Array.isArray(config.scripts) ? config.scripts : [];
|
||||
|
||||
const styleChunks = await Promise.all(
|
||||
styles.map((assetPath) =>
|
||||
requestText({
|
||||
addonRoot,
|
||||
browserBase: browserAddonBase,
|
||||
assetPath,
|
||||
}),
|
||||
),
|
||||
);
|
||||
styleChunks.forEach(appendStyle);
|
||||
|
||||
const commonScriptChunks = await Promise.all(
|
||||
commonScripts.map((assetPath) =>
|
||||
requestText({
|
||||
addonRoot: commonAddonRoot,
|
||||
browserBase: browserCommonBase,
|
||||
assetPath,
|
||||
}),
|
||||
),
|
||||
);
|
||||
commonScriptChunks.forEach(appendScript);
|
||||
|
||||
const scriptChunks = await Promise.all(
|
||||
scripts.map((assetPath) =>
|
||||
requestText({
|
||||
addonRoot,
|
||||
browserBase: browserAddonBase,
|
||||
assetPath,
|
||||
}),
|
||||
),
|
||||
);
|
||||
scriptChunks.forEach(appendScript);
|
||||
}
|
||||
|
||||
ForgeSiteLoader.boot = boot;
|
||||
|
||||
if (global.ForgeSiteConfig && global.ForgeSiteConfig.autoBoot !== false) {
|
||||
boot(global.ForgeSiteConfig).catch((error) => {
|
||||
const logLabel =
|
||||
global.ForgeSiteConfig.logLabel ||
|
||||
global.ForgeSiteConfig.addonName ||
|
||||
"Forge UI";
|
||||
console.error(`[${logLabel}] Failed to load site assets.`, error);
|
||||
});
|
||||
}
|
||||
})(window);
|
||||
!function(e){const o=e.ForgeSiteLoader=e.ForgeSiteLoader||{};function t(e){return"string"==typeof e&&e.startsWith("forge\\")}function r({addonRoot:e,browserBase:o,assetPath:r}){if("undefined"!=typeof A3API&&A3API&&"function"==typeof A3API.RequestFile){const o=t(r)?r:e+String(r||"").replace(/\//g,"\\");return A3API.RequestFile(o)}const n=t(r)?r:function(e,o){return`${String(e||"./").replace(/\\/g,"/")}${String(o||"").replace(/\\/g,"/")}`}(o,r);return fetch(n).then(e=>{if(!e.ok)throw new Error(`Failed to load ${n}`);return e.text()})}function n(e){const o=document.createElement("style");o.textContent=e,document.head.appendChild(o)}function a(e){const o=document.createElement("script");o.text=e,document.head.appendChild(o)}async function i(e){const o=e&&e.addonName?e.addonName:"";if(!o)throw new Error("ForgeSiteLoader requires a config.addonName value.");const t=function(e){return`forge\\forge_client\\addons\\${e}\\ui\\_site\\`}(o),i=e.browserAddonBase||"./",s=e.browserCommonBase||"../../../common/ui/_site/",c=Array.isArray(e.styles)?e.styles:[],d=Array.isArray(e.commonScripts)?e.commonScripts:[],f=Array.isArray(e.scripts)?e.scripts:[];(await Promise.all(c.map(e=>r({addonRoot:t,browserBase:i,assetPath:e})))).forEach(n);(await Promise.all(d.map(e=>r({addonRoot:"forge\\forge_client\\addons\\common\\ui\\_site\\",browserBase:s,assetPath:e})))).forEach(a);(await Promise.all(f.map(e=>r({addonRoot:t,browserBase:i,assetPath:e})))).forEach(a)}o.boot=i,e.ForgeSiteConfig&&!1!==e.ForgeSiteConfig.autoBoot&&i(e.ForgeSiteConfig).catch(o=>{const t=e.ForgeSiteConfig.logLabel||e.ForgeSiteConfig.addonName||"Forge UI";console.error(`[${t}] Failed to load site assets.`,o)})}(window);
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,64 +1 @@
|
||||
<!-- Generated by tools/build-webui.mjs for garage UI index. Do not edit directly. -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>FORGE Vehicle Garage</title>
|
||||
<script>
|
||||
window.ForgeSiteConfig = {
|
||||
addonName: "garage",
|
||||
logLabel: "Garage UI",
|
||||
styles: ["garage-ui.css"],
|
||||
commonScripts: ["forge-webui.js"],
|
||||
scripts: ["garage-ui.js"],
|
||||
};
|
||||
|
||||
(function loadForgeSiteLoader() {
|
||||
const armaLoaderPath =
|
||||
"forge\\forge_client\\addons\\common\\ui\\_site\\forge-site-loader.js";
|
||||
const browserLoaderPath =
|
||||
"../../../common/ui/_site/forge-site-loader.js";
|
||||
|
||||
function appendScript(js) {
|
||||
const script = document.createElement("script");
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function requestLoader() {
|
||||
if (
|
||||
typeof A3API !== "undefined" &&
|
||||
A3API &&
|
||||
typeof A3API.RequestFile === "function"
|
||||
) {
|
||||
return A3API.RequestFile(armaLoaderPath);
|
||||
}
|
||||
|
||||
return fetch(browserLoaderPath).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
"Failed to load " + browserLoaderPath,
|
||||
);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
});
|
||||
}
|
||||
|
||||
requestLoader()
|
||||
.then(appendScript)
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"[Garage UI] Failed to load Forge site loader.",
|
||||
error,
|
||||
);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>FORGE Vehicle Garage</title><script>window.ForgeSiteConfig={addonName:"garage",logLabel:"Garage UI",styles:["garage-ui.css"],commonScripts:["forge-webui.js"],scripts:["garage-ui.js"]},function(){const e="../../../common/ui/_site/forge-site-loader.js";("undefined"!=typeof A3API&&A3API&&"function"==typeof A3API.RequestFile?A3API.RequestFile("forge\\forge_client\\addons\\common\\ui\\_site\\forge-site-loader.js"):fetch(e).then(o=>{if(!o.ok)throw new Error("Failed to load "+e);return o.text()})).then(function(e){const o=document.createElement("script");o.text=e,document.head.appendChild(o)}).catch(e=>{console.error("[Garage UI] Failed to load Forge site loader.",e)})}()</script></head><body><div id="app"></div></body></html>
|
||||
@ -1,64 +1 @@
|
||||
<!-- Generated by tools/build-webui.mjs for org UI index. Do not edit directly. -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>ORBIS - Global Organization Network</title>
|
||||
<script>
|
||||
window.ForgeSiteConfig = {
|
||||
addonName: "org",
|
||||
logLabel: "Org UI",
|
||||
styles: ["org-ui.css"],
|
||||
commonScripts: ["forge-webui.js"],
|
||||
scripts: ["org-ui.js"],
|
||||
};
|
||||
|
||||
(function loadForgeSiteLoader() {
|
||||
const armaLoaderPath =
|
||||
"forge\\forge_client\\addons\\common\\ui\\_site\\forge-site-loader.js";
|
||||
const browserLoaderPath =
|
||||
"../../../common/ui/_site/forge-site-loader.js";
|
||||
|
||||
function appendScript(js) {
|
||||
const script = document.createElement("script");
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function requestLoader() {
|
||||
if (
|
||||
typeof A3API !== "undefined" &&
|
||||
A3API &&
|
||||
typeof A3API.RequestFile === "function"
|
||||
) {
|
||||
return A3API.RequestFile(armaLoaderPath);
|
||||
}
|
||||
|
||||
return fetch(browserLoaderPath).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
"Failed to load " + browserLoaderPath,
|
||||
);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
});
|
||||
}
|
||||
|
||||
requestLoader()
|
||||
.then(appendScript)
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"[Org UI] Failed to load Forge site loader.",
|
||||
error,
|
||||
);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>ORBIS - Global Organization Network</title><script>window.ForgeSiteConfig={addonName:"org",logLabel:"Org UI",styles:["org-ui.css"],commonScripts:["forge-webui.js"],scripts:["org-ui.js"]},function(){const e="../../../common/ui/_site/forge-site-loader.js";("undefined"!=typeof A3API&&A3API&&"function"==typeof A3API.RequestFile?A3API.RequestFile("forge\\forge_client\\addons\\common\\ui\\_site\\forge-site-loader.js"):fetch(e).then(o=>{if(!o.ok)throw new Error("Failed to load "+e);return o.text()})).then(function(e){const o=document.createElement("script");o.text=e,document.head.appendChild(o)}).catch(e=>{console.error("[Org UI] Failed to load Forge site loader.",e)})}()</script></head><body><div id="app"></div></body></html>
|
||||
@ -1,281 +1 @@
|
||||
/* Generated by tools/build-webui.mjs for Org UI styles. Do not edit directly. */
|
||||
:root {
|
||||
--bg-app: #fdfcf8;
|
||||
--bg-surface: #ffffff;
|
||||
--bg-surface-hover: #f1f5f9;
|
||||
--primary: #475569;
|
||||
--primary-hover: #1e293b;
|
||||
--text-main: #1f2937;
|
||||
--text-muted: #64748b;
|
||||
--text-inverse: #f8fafc;
|
||||
--border: #e2e8f0;
|
||||
--radius: 8px;
|
||||
--shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1);
|
||||
--footer-bg: #1e293b;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
*,
|
||||
*::before,
|
||||
*::after {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family:
|
||||
"Inter",
|
||||
system-ui,
|
||||
-apple-system,
|
||||
sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg-app);
|
||||
color: var(--text-main);
|
||||
line-height: 1.6;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#org-portal-frame-root {
|
||||
display: flex;
|
||||
flex: 1 1 auto;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
main {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
overscroll-behavior: contain;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.header {
|
||||
text-align: center;
|
||||
margin-bottom: 3rem;
|
||||
padding-bottom: 2rem;
|
||||
border-bottom: 1px solid var(--border);
|
||||
|
||||
h1 {
|
||||
font-size: 2.5rem;
|
||||
font-weight: 700;
|
||||
margin-bottom: 0.5rem;
|
||||
letter-spacing: -0.025em;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
|
||||
p {
|
||||
color: var(--text-muted);
|
||||
font-size: 1.1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--bg-surface);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
padding: 2rem;
|
||||
box-shadow: var(--shadow);
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
margin-top: 0;
|
||||
font-size: 1.8rem;
|
||||
color: var(--primary-hover);
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
background: var(--primary);
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 0.75rem 1.5rem;
|
||||
border-radius: var(--radius);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
|
||||
&:hover {
|
||||
background: var(--primary-hover);
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1);
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.65;
|
||||
transform: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
& + & {
|
||||
margin-left: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
margin-top: auto;
|
||||
background: var(--footer-bg);
|
||||
color: var(--text-inverse);
|
||||
display: block;
|
||||
|
||||
.wrapper {
|
||||
max-width: 1200px;
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 3rem 2rem;
|
||||
box-sizing: border-box;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 4rem;
|
||||
}
|
||||
|
||||
h3 {
|
||||
color: var(--text-inverse);
|
||||
font-size: 0.85rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.1em;
|
||||
font-weight: 700;
|
||||
margin-bottom: 1.5rem;
|
||||
border-bottom: 1px solid #475569;
|
||||
padding-bottom: 0.5rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
ul {
|
||||
li {
|
||||
color: #cbd5e1;
|
||||
font-size: 0.95rem;
|
||||
margin-bottom: 0.75rem;
|
||||
cursor: pointer;
|
||||
transition: color 0.2s;
|
||||
|
||||
&:hover {
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.org-secondary-btn {
|
||||
background: var(--bg-surface);
|
||||
color: var(--text-main);
|
||||
border: 1px solid var(--border);
|
||||
|
||||
&:hover {
|
||||
background: var(--bg-surface-hover);
|
||||
color: var(--text-main);
|
||||
}
|
||||
}
|
||||
|
||||
.org-danger-btn {
|
||||
background: #7f1d1d;
|
||||
color: #fef2f2;
|
||||
|
||||
&:hover {
|
||||
background: #991b1b;
|
||||
}
|
||||
}
|
||||
|
||||
.org-icon-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 2.5rem;
|
||||
height: 2.5rem;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.org-icon {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
}
|
||||
|
||||
.org-page-header {
|
||||
text-align: left;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.org-page-heading {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
|
||||
.org-page-kicker {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.org-page-title {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.org-page-subtitle {
|
||||
font-size: 0.9rem;
|
||||
color: var(--text-muted);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.org-page-meta {
|
||||
font-size: 0.75rem;
|
||||
color: var(--text-muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.05em;
|
||||
}
|
||||
|
||||
@media (max-width: 960px) {
|
||||
.container {
|
||||
padding: 1.5rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
margin-bottom: 2rem;
|
||||
padding-bottom: 1.5rem;
|
||||
|
||||
h1 {
|
||||
font-size: 2rem;
|
||||
}
|
||||
}
|
||||
|
||||
.footer .wrapper {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.org-page-heading {
|
||||
gap: 0.3rem;
|
||||
}
|
||||
}
|
||||
:root{--bg-app:#fdfcf8;--bg-surface:#fff;--bg-surface-hover:#f1f5f9;--primary:#475569;--primary-hover:#1e293b;--text-main:#1f2937;--text-muted:#64748b;--text-inverse:#f8fafc;--border:#e2e8f0;--radius:8px;--shadow:0 1px 3px 0 #0000001a, 0 1px 2px -1px #0000001a;--footer-bg:#1e293b}html,body{height:100%}*,:before,:after{box-sizing:border-box}body{background:var(--bg-app);color:var(--text-main);margin:0;padding:0;font-family:Inter,system-ui,-apple-system,sans-serif;line-height:1.6;overflow:hidden}#app{height:100vh;overflow:hidden}.app-shell{flex-direction:column;height:100vh;display:flex;overflow:hidden}#org-portal-frame-root{flex-direction:column;flex:auto;min-height:0;display:flex;overflow:hidden}main{overscroll-behavior:contain;flex-direction:column;flex:auto;min-height:0;display:flex;overflow:auto}.container{box-sizing:border-box;flex-direction:column;flex:1;width:100%;max-width:1200px;margin:0 auto;padding:2rem;display:flex}.header{text-align:center;border-bottom:1px solid var(--border);margin-bottom:3rem;padding-bottom:2rem}.header h1{letter-spacing:-.025em;color:var(--primary-hover);margin-bottom:.5rem;font-size:2.5rem;font-weight:700}.header p{color:var(--text-muted);font-size:1.1rem}.card{background:var(--bg-surface);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);text-align:center;padding:2rem}.card h2{color:var(--primary-hover);margin-top:0;font-size:1.8rem}button{background:var(--primary);color:#fff;border-radius:var(--radius);cursor:pointer;border:none;padding:.75rem 1.5rem;font-size:1rem;font-weight:500;transition:all .2s}button:hover{background:var(--primary-hover);transform:translateY(-1px);box-shadow:0 4px 6px -1px #0000001a}button:disabled{cursor:not-allowed;opacity:.65;box-shadow:none;transform:none}button+button{margin-left:1rem}.footer{background:var(--footer-bg);color:var(--text-inverse);margin-top:auto;display:block}.footer .wrapper{box-sizing:border-box;grid-template-columns:1fr 1fr;gap:4rem;width:100%;max-width:1200px;margin:0 auto;padding:3rem 2rem;display:grid}.footer h3{color:var(--text-inverse);text-transform:uppercase;letter-spacing:.1em;border-bottom:1px solid #475569;margin-bottom:1.5rem;margin-right:1rem;padding-bottom:.5rem;font-size:.85rem;font-weight:700}.footer ul li{color:#cbd5e1;cursor:pointer;margin-bottom:.75rem;font-size:.95rem;transition:color .2s}.footer ul li:hover{color:#fff}.org-secondary-btn{background:var(--bg-surface);color:var(--text-main);border:1px solid var(--border)}.org-secondary-btn:hover{background:var(--bg-surface-hover);color:var(--text-main)}.org-danger-btn{color:#fef2f2;background:#7f1d1d}.org-danger-btn:hover{background:#991b1b}.org-icon-btn{justify-content:center;align-items:center;width:2.5rem;height:2.5rem;padding:0;display:inline-flex}.org-icon{width:1rem;height:1rem}.org-page-header{text-align:left;margin-bottom:0}.org-page-heading{flex-direction:column;gap:.35rem;display:flex}.org-page-kicker{text-transform:uppercase;letter-spacing:.08em;color:var(--text-muted);font-size:.7rem;font-weight:600}.org-page-title{margin:0}.org-page-subtitle{color:var(--text-muted);margin:0;font-size:.9rem}.org-page-meta{color:var(--text-muted);text-transform:uppercase;letter-spacing:.05em;font-size:.75rem}@media (width<=960px){.container{padding:1.5rem}.header{margin-bottom:2rem;padding-bottom:1.5rem}.header h1{font-size:2rem}.footer .wrapper{grid-template-columns:1fr}.org-page-heading{gap:.3rem}}
|
||||
File diff suppressed because one or more lines are too long
@ -1,6 +1,5 @@
|
||||
PREP(buildUIPayload);
|
||||
PREP(handleUIEvents);
|
||||
PREP(initCatalogService);
|
||||
PREP(initClass);
|
||||
PREP(initUIBridge);
|
||||
PREP(openUI);
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
#include "script_component.hpp"
|
||||
|
||||
if (isNil QGVAR(StoreCatalogService)) then { call FUNC(initCatalogService); };
|
||||
if (isNil QGVAR(StoreClass)) then { call FUNC(initClass); };
|
||||
if (isNil QGVAR(StoreUIBridge)) then { call FUNC(initUIBridge); };
|
||||
|
||||
[QGVAR(responseCategory), {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
GVAR(StoreUIBridge) call ["handleCategoryResponse", [_payload]];
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(responseCheckout), {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
|
||||
@ -1,294 +0,0 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initCatalogService.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-13
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the store catalog service for category discovery, pricing, and payload shaping.
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "StoreCatalogServiceBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["catalogCache", createHashMap];
|
||||
}],
|
||||
["isVisibleConfig", compileFinal {
|
||||
params [["_cfg", configNull, [configNull]]];
|
||||
|
||||
isClass _cfg
|
||||
&& { getNumber (_cfg >> "scope") >= 2 }
|
||||
&& { (getText (_cfg >> "displayName")) isNotEqualTo "" }
|
||||
}],
|
||||
["buildDescription", compileFinal {
|
||||
params [["_cfg", configNull, [configNull]], ["_fallback", "", [""]]];
|
||||
|
||||
private _description = getText (_cfg >> "descriptionShort");
|
||||
if (_description isEqualTo "") then { _description = _fallback; };
|
||||
|
||||
_description
|
||||
}],
|
||||
["formatPriceValue", compileFinal {
|
||||
params [["_priceValue", 0, [0]]];
|
||||
|
||||
format ["$%1", [_priceValue max 0] call BIS_fnc_numberText]
|
||||
}],
|
||||
["calculateItemPrice", compileFinal {
|
||||
params [
|
||||
["_cfg", configNull, [configNull]],
|
||||
["_isVehicle", false, [false]]
|
||||
];
|
||||
|
||||
if (isNull _cfg) exitWith { "$50" };
|
||||
|
||||
private _mass = 0;
|
||||
private _priceValue = 0;
|
||||
|
||||
if (_isVehicle) then {
|
||||
_priceValue = getNumber (_cfg >> "cost");
|
||||
} else {
|
||||
_mass = getNumber (_cfg >> "ItemInfo" >> "mass");
|
||||
if (_mass <= 0) then {
|
||||
_mass = getNumber (_cfg >> "mass");
|
||||
};
|
||||
|
||||
_priceValue = ceil ((_mass max 0) * 0.1);
|
||||
};
|
||||
|
||||
_priceValue = _priceValue max 50;
|
||||
_self call ["formatPriceValue", [_priceValue]]
|
||||
}],
|
||||
["buildItem", compileFinal {
|
||||
params [["_cfg", configNull, [configNull]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]], ["_imageField", "picture", [""]], ["_isVehicle", false, [false]]];
|
||||
|
||||
if (isNull _cfg) exitWith { createHashMap };
|
||||
|
||||
private _className = configName _cfg;
|
||||
private _displayName = getText (_cfg >> "displayName");
|
||||
private _picture = getText (_cfg >> _imageField);
|
||||
if (_picture isEqualTo "" && { _imageField isNotEqualTo "picture" }) then { _picture = getText (_cfg >> "picture"); };
|
||||
|
||||
createHashMapFromArray [
|
||||
["className", _className],
|
||||
["code", _className],
|
||||
["name", _displayName],
|
||||
["description", _self call ["buildDescription", [_cfg, _fallbackDescription]]],
|
||||
["price", _self call ["calculateItemPrice", [_cfg, _isVehicle]]],
|
||||
["image", _picture],
|
||||
["type", _typeLabel]
|
||||
]
|
||||
}],
|
||||
["appendCfgWeaponsByItemInfoType", compileFinal {
|
||||
params [["_items", [], [[]]], ["_itemInfoType", -1, [0]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]];
|
||||
|
||||
{
|
||||
private _cfg = _x;
|
||||
if (
|
||||
_self call ["isVisibleConfig", [_cfg]]
|
||||
&& { getNumber (_cfg >> "ItemInfo" >> "type") isEqualTo _itemInfoType }
|
||||
) then {
|
||||
_items pushBack (_self call ["buildItem", [_cfg, _typeLabel, _fallbackDescription]]);
|
||||
};
|
||||
} forEach ("true" configClasses (configFile >> "CfgWeapons"));
|
||||
|
||||
_items
|
||||
}],
|
||||
["appendCfgWeaponsByType", compileFinal {
|
||||
params [["_items", [], [[]]], ["_weaponType", -1, [0]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]];
|
||||
|
||||
{
|
||||
private _cfg = _x;
|
||||
if (
|
||||
_self call ["isVisibleConfig", [_cfg]]
|
||||
&& { getNumber (_cfg >> "type") isEqualTo _weaponType }
|
||||
) then {
|
||||
_items pushBack (_self call ["buildItem", [_cfg, _typeLabel, _fallbackDescription]]);
|
||||
};
|
||||
} forEach ("true" configClasses (configFile >> "CfgWeapons"));
|
||||
|
||||
_items
|
||||
}],
|
||||
["appendCfgVehiclesByKind", compileFinal {
|
||||
params [["_items", [], [[]]], ["_baseClass", "", [""]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]];
|
||||
|
||||
{
|
||||
private _cfg = _x;
|
||||
private _className = configName _cfg;
|
||||
if (
|
||||
_self call ["isVisibleConfig", [_cfg]]
|
||||
&& { getNumber (_cfg >> "isBackpack") isEqualTo 0 }
|
||||
&& { !(_className isKindOf ["CAManBase", configFile >> "CfgVehicles"]) }
|
||||
&& { !(_className isKindOf ["StaticWeapon", configFile >> "CfgVehicles"]) }
|
||||
&& { _className isKindOf [_baseClass, configFile >> "CfgVehicles"] }
|
||||
) then {
|
||||
_items pushBack (_self call ["buildItem", [_cfg, _typeLabel, _fallbackDescription, "editorPreview", true]]);
|
||||
};
|
||||
} forEach ("true" configClasses (configFile >> "CfgVehicles"));
|
||||
|
||||
_items
|
||||
}],
|
||||
["scanCategoryItems", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
|
||||
private _categoryKey = toLowerANSI _category;
|
||||
if (_categoryKey isEqualTo "") exitWith { [] };
|
||||
|
||||
private _items = [];
|
||||
|
||||
switch (_categoryKey) do {
|
||||
case "uniforms": {
|
||||
_items = _self call ["appendCfgWeaponsByItemInfoType", [_items, 801, "Uniform", "Live uniform entry generated from the game inventory."]];
|
||||
};
|
||||
case "headgear": {
|
||||
_items = _self call ["appendCfgWeaponsByItemInfoType", [_items, 605, "Headgear", "Live headgear entry generated from the game inventory."]];
|
||||
};
|
||||
case "vests": {
|
||||
_items = _self call ["appendCfgWeaponsByItemInfoType", [_items, 701, "Vest", "Live vest entry generated from the game inventory."]];
|
||||
};
|
||||
case "facewear": {
|
||||
{
|
||||
private _cfg = _x;
|
||||
if (_self call ["isVisibleConfig", [_cfg]]) then {
|
||||
_items pushBack (_self call ["buildItem", [_cfg, "Facewear", "Live facewear entry generated from the game inventory."]]);
|
||||
};
|
||||
} forEach ("true" configClasses (configFile >> "CfgGlasses"));
|
||||
};
|
||||
case "ammo": {
|
||||
{
|
||||
private _cfg = _x;
|
||||
if (_self call ["isVisibleConfig", [_cfg]]) then {
|
||||
_items pushBack (_self call ["buildItem", [_cfg, "Magazine", "Live ammunition entry generated from the game inventory."]]);
|
||||
};
|
||||
} forEach ("true" configClasses (configFile >> "CfgMagazines"));
|
||||
};
|
||||
case "items": {
|
||||
{
|
||||
private _cfg = _x;
|
||||
private _className = configName _cfg;
|
||||
private _itemType = [_className] call BIS_fnc_itemType;
|
||||
private _group = _itemType param [0, ""];
|
||||
private _kind = _itemType param [1, ""];
|
||||
|
||||
if (
|
||||
_self call ["isVisibleConfig", [_cfg]]
|
||||
&& { _group in ["Item", "Equipment"] }
|
||||
&& { !(_kind in ["Uniform", "Vest", "Headgear"]) }
|
||||
) then {
|
||||
private _typeLabel = [_kind, "Item"] select (_kind isEqualTo "");
|
||||
_items pushBack (_self call ["buildItem", [_cfg, _typeLabel, "Live utility entry generated from the game inventory."]]);
|
||||
};
|
||||
} forEach ("true" configClasses (configFile >> "CfgWeapons"));
|
||||
};
|
||||
case "primary": {
|
||||
_items = _self call ["appendCfgWeaponsByType", [_items, 1, "Primary Weapon", "Live primary weapon entry generated from the game inventory."]];
|
||||
};
|
||||
case "handgun": {
|
||||
_items = _self call ["appendCfgWeaponsByType", [_items, 2, "Handgun", "Live sidearm entry generated from the game inventory."]];
|
||||
};
|
||||
case "secondary": {
|
||||
_items = _self call ["appendCfgWeaponsByType", [_items, 4, "Launcher", "Live launcher entry generated from the game inventory."]];
|
||||
};
|
||||
case "cars": {
|
||||
_items = _self call ["appendCfgVehiclesByKind", [_items, "Car", "Vehicle", "Live wheeled vehicle entry generated from the game inventory."]];
|
||||
};
|
||||
case "armor": {
|
||||
_items = _self call ["appendCfgVehiclesByKind", [_items, "Tank", "Vehicle", "Live armored vehicle entry generated from the game inventory."]];
|
||||
};
|
||||
case "helis": {
|
||||
_items = _self call ["appendCfgVehiclesByKind", [_items, "Helicopter", "Aircraft", "Live helicopter entry generated from the game inventory."]];
|
||||
};
|
||||
case "planes": {
|
||||
_items = _self call ["appendCfgVehiclesByKind", [_items, "Plane", "Aircraft", "Live fixed-wing entry generated from the game inventory."]];
|
||||
};
|
||||
case "naval": {
|
||||
_items = _self call ["appendCfgVehiclesByKind", [_items, "Ship", "Naval", "Live naval vehicle entry generated from the game inventory."]];
|
||||
};
|
||||
case "other": {
|
||||
{
|
||||
private _cfg = _x;
|
||||
private _className = configName _cfg;
|
||||
private _isSupportedVehicle = _className isKindOf ["AllVehicles", configFile >> "CfgVehicles"];
|
||||
private _isKnownCategory =
|
||||
_className isKindOf ["Car", configFile >> "CfgVehicles"]
|
||||
|| { _className isKindOf ["Tank", configFile >> "CfgVehicles"] }
|
||||
|| { _className isKindOf ["Helicopter", configFile >> "CfgVehicles"] }
|
||||
|| { _className isKindOf ["Plane", configFile >> "CfgVehicles"] }
|
||||
|| { _className isKindOf ["Ship", configFile >> "CfgVehicles"] };
|
||||
|
||||
if (
|
||||
_self call ["isVisibleConfig", [_cfg]]
|
||||
&& { _isSupportedVehicle }
|
||||
&& { !_isKnownCategory }
|
||||
&& { getNumber (_cfg >> "isBackpack") isEqualTo 0 }
|
||||
&& { !(_className isKindOf ["CAManBase", configFile >> "CfgVehicles"]) }
|
||||
&& { !(_className isKindOf ["StaticWeapon", configFile >> "CfgVehicles"]) }
|
||||
) then {
|
||||
_items pushBack (_self call ["buildItem", [
|
||||
_cfg,
|
||||
"Special Vehicle",
|
||||
"Live specialty vehicle entry generated from the game inventory.",
|
||||
"editorPreview",
|
||||
true
|
||||
]]);
|
||||
};
|
||||
} forEach ("true" configClasses (configFile >> "CfgVehicles"));
|
||||
};
|
||||
};
|
||||
|
||||
private _sortedItems = _items apply { [toLowerANSI (_x getOrDefault ["name", ""]), _x] };
|
||||
|
||||
_sortedItems sort true;
|
||||
_sortedItems apply { _x select 1 }
|
||||
}],
|
||||
["isVehicleCategory", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
|
||||
(toLowerANSI _category) in ["cars", "armor", "helis", "planes", "naval", "other"]
|
||||
}],
|
||||
["buildPayloadCategory", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
|
||||
switch (toLowerANSI _category) do {
|
||||
case "ammo": { "magazine" };
|
||||
case "primary";
|
||||
case "secondary";
|
||||
case "handgun": { "weapon" };
|
||||
case "cars";
|
||||
case "armor";
|
||||
case "helis";
|
||||
case "planes";
|
||||
case "naval";
|
||||
case "other": { toLowerANSI _category };
|
||||
default { "item" };
|
||||
}
|
||||
}],
|
||||
["buildCategoryItems", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
|
||||
private _categoryKey = toLowerANSI _category;
|
||||
if (_categoryKey isEqualTo "") exitWith { [] };
|
||||
|
||||
private _catalogCache = _self getOrDefault ["catalogCache", createHashMap];
|
||||
if (_categoryKey in (keys _catalogCache)) exitWith { _catalogCache get _categoryKey };
|
||||
|
||||
private _items = _self call ["scanCategoryItems", [_categoryKey]];
|
||||
private _payloadCategory = _self call ["buildPayloadCategory", [_categoryKey]];
|
||||
private _entryKind = ["item", "vehicle"] select (_self call ["isVehicleCategory", [_categoryKey]]);
|
||||
|
||||
{
|
||||
_x set ["category", _payloadCategory];
|
||||
_x set ["entryKind", _entryKind];
|
||||
} forEach _items;
|
||||
|
||||
_catalogCache set [_categoryKey, _items];
|
||||
_self set ["catalogCache", _catalogCache];
|
||||
|
||||
_items
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(StoreCatalogService) = createHashMapObject [GVAR(StoreCatalogServiceBaseClass)];
|
||||
GVAR(StoreCatalogService)
|
||||
@ -37,7 +37,6 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
|
||||
private _targetControl = _control;
|
||||
if (isNull _targetControl) then { _targetControl = _self call ["getActiveBrowserControl", []]; };
|
||||
|
||||
if (isNull _targetControl) exitWith { false };
|
||||
|
||||
_self call ["execBridge", [_targetControl, "receive", createHashMapFromArray [
|
||||
@ -55,27 +54,29 @@ GVAR(StoreUIBridgeBaseClass) = compileFinal createHashMapFromArray [
|
||||
params [["_data", createHashMap, [createHashMap]]];
|
||||
|
||||
private _category = toLowerANSI (_data getOrDefault ["category", ""]);
|
||||
private _uid = getPlayerUID player;
|
||||
if (_category isEqualTo "") exitWith {
|
||||
_self call ["sendBridgeEvent", ["store::category::failure", createHashMapFromArray [
|
||||
["message", "No store category was provided."]
|
||||
]]];
|
||||
};
|
||||
|
||||
if (isNil QGVAR(StoreCatalogService)) exitWith {
|
||||
if (_uid isEqualTo "") exitWith {
|
||||
_self call ["sendBridgeEvent", ["store::category::failure", createHashMapFromArray [
|
||||
["category", _category],
|
||||
["message", "Store catalog is unavailable."]
|
||||
["message", "Store catalog request is unavailable."]
|
||||
]]];
|
||||
};
|
||||
|
||||
private _items = GVAR(StoreCatalogService) call ["buildCategoryItems", [_category]];
|
||||
diag_log format ["[FORGE:Client:Store] Category request forwarded to server: %1", _category];
|
||||
[SRPC(store,requestCategory), [_uid, _category]] call CFUNC(serverEvent);
|
||||
}],
|
||||
["handleCategoryResponse", compileFinal {
|
||||
params [["_payload", createHashMap, [createHashMap]]];
|
||||
|
||||
diag_log format ["[FORGE:Client:Store] Category request handled for %1 with %2 item(s).", _category, count _items];
|
||||
|
||||
_self call ["sendBridgeEvent", ["store::category::hydrate", createHashMapFromArray [
|
||||
["category", _category],
|
||||
["items", _items]
|
||||
]]];
|
||||
private _success = _payload getOrDefault ["success", false];
|
||||
private _bridgeEvent = ["store::category::failure", "store::category::hydrate"] select _success;
|
||||
_self call ["sendBridgeEvent", [_bridgeEvent, _payload]];
|
||||
}],
|
||||
["refreshStoreConfig", compileFinal {
|
||||
private _payload = call FUNC(buildUIPayload);
|
||||
|
||||
@ -1,64 +1 @@
|
||||
<!-- Generated by tools/build-webui.mjs for store UI index. Do not edit directly. -->
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>FORGE Supply Exchange</title>
|
||||
<script>
|
||||
window.ForgeSiteConfig = {
|
||||
addonName: "store",
|
||||
logLabel: "Store UI",
|
||||
styles: ["store-ui.css"],
|
||||
commonScripts: ["forge-webui.js"],
|
||||
scripts: ["store-ui.js"],
|
||||
};
|
||||
|
||||
(function loadForgeSiteLoader() {
|
||||
const armaLoaderPath =
|
||||
"forge\\forge_client\\addons\\common\\ui\\_site\\forge-site-loader.js";
|
||||
const browserLoaderPath =
|
||||
"../../../common/ui/_site/forge-site-loader.js";
|
||||
|
||||
function appendScript(js) {
|
||||
const script = document.createElement("script");
|
||||
script.text = js;
|
||||
document.head.appendChild(script);
|
||||
}
|
||||
|
||||
function requestLoader() {
|
||||
if (
|
||||
typeof A3API !== "undefined" &&
|
||||
A3API &&
|
||||
typeof A3API.RequestFile === "function"
|
||||
) {
|
||||
return A3API.RequestFile(armaLoaderPath);
|
||||
}
|
||||
|
||||
return fetch(browserLoaderPath).then((response) => {
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
"Failed to load " + browserLoaderPath,
|
||||
);
|
||||
}
|
||||
|
||||
return response.text();
|
||||
});
|
||||
}
|
||||
|
||||
requestLoader()
|
||||
.then(appendScript)
|
||||
.catch((error) => {
|
||||
console.error(
|
||||
"[Store UI] Failed to load Forge site loader.",
|
||||
error,
|
||||
);
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
</body>
|
||||
</html>
|
||||
<!doctype html><html lang="en"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>FORGE Supply Exchange</title><script>window.ForgeSiteConfig={addonName:"store",logLabel:"Store UI",styles:["store-ui.css"],commonScripts:["forge-webui.js"],scripts:["store-ui.js"]},function(){const e="../../../common/ui/_site/forge-site-loader.js";("undefined"!=typeof A3API&&A3API&&"function"==typeof A3API.RequestFile?A3API.RequestFile("forge\\forge_client\\addons\\common\\ui\\_site\\forge-site-loader.js"):fetch(e).then(o=>{if(!o.ok)throw new Error("Failed to load "+e);return o.text()})).then(function(e){const o=document.createElement("script");o.text=e,document.head.appendChild(o)}).catch(e=>{console.error("[Store UI] Failed to load Forge site loader.",e)})}()</script></head><body><div id="app"></div></body></html>
|
||||
@ -1,90 +1 @@
|
||||
/* Generated by tools/build-webui.mjs for Store UI styles. Do not edit directly. */
|
||||
:root {
|
||||
--store-shell-bg: #e4e3df;
|
||||
--store-surface: #f5f3ef;
|
||||
--store-surface-alt: #ece8e2;
|
||||
--store-surface-strong: #ffffff;
|
||||
--store-border: rgba(74, 91, 110, 0.2);
|
||||
--store-border-strong: rgba(20, 46, 79, 0.2);
|
||||
--store-text-main: #1f2d3d;
|
||||
--store-text-muted: #6a7787;
|
||||
--store-text-subtle: #8792a0;
|
||||
--store-accent: #12365d;
|
||||
--store-accent-soft: #dbe7f3;
|
||||
--store-accent-line: rgba(18, 54, 93, 0.12);
|
||||
--store-success: #2f7d5b;
|
||||
--store-danger: #8a3d3d;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Segoe UI", "Trebuchet MS", sans-serif;
|
||||
color: var(--store-text-main);
|
||||
background: var(--store-shell-bg);
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
cursor: not-allowed;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
:focus-visible {
|
||||
outline: 2px solid rgb(18 54 93 / 0.35);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
#app {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.store-btn {
|
||||
min-height: 2.75rem;
|
||||
padding: 0.72rem 1rem;
|
||||
border-radius: 0.8rem;
|
||||
border: 1px solid var(--store-border-strong);
|
||||
font-size: 0.82rem;
|
||||
font-weight: 700;
|
||||
letter-spacing: 0.08em;
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.store-btn.store-btn-primary {
|
||||
background: rgb(255 255 255 / 0.68);
|
||||
color: var(--store-accent);
|
||||
}
|
||||
|
||||
.store-btn.store-btn-primary:hover {
|
||||
background: rgb(219 231 243 / 0.88);
|
||||
}
|
||||
|
||||
.store-btn.store-btn-secondary {
|
||||
background: rgb(255 255 255 / 0.42);
|
||||
color: var(--store-text-muted);
|
||||
}
|
||||
|
||||
.store-btn.store-btn-secondary:hover {
|
||||
background: rgb(255 255 255 / 0.6);
|
||||
color: var(--store-text-main);
|
||||
}
|
||||
:root{--store-shell-bg:#e4e3df;--store-surface:#f5f3ef;--store-surface-alt:#ece8e2;--store-surface-strong:#fff;--store-border:#4a5b6e33;--store-border-strong:#142e4f33;--store-text-main:#1f2d3d;--store-text-muted:#6a7787;--store-text-subtle:#8792a0;--store-accent:#12365d;--store-accent-soft:#dbe7f3;--store-accent-line:#12365d1f;--store-success:#2f7d5b;--store-danger:#8a3d3d}*{box-sizing:border-box}html,body{width:100%;height:100%;margin:0;overflow:hidden}body{color:var(--store-text-main);background:var(--store-shell-bg);font-family:Segoe UI,Trebuchet MS,sans-serif}button,input,select{font:inherit}button{cursor:pointer}button:disabled{cursor:not-allowed;opacity:.7}:focus-visible{outline-offset:2px;outline:2px solid #12365d59}#app{width:100%;height:100%}.store-btn{border:1px solid var(--store-border-strong);letter-spacing:.08em;text-transform:uppercase;border-radius:.8rem;min-height:2.75rem;padding:.72rem 1rem;font-size:.82rem;font-weight:700}.store-btn.store-btn-primary{color:var(--store-accent);background:#ffffffad}.store-btn.store-btn-primary:hover{background:#dbe7f3e0}.store-btn.store-btn-secondary{color:var(--store-text-muted);background:#ffffff6b}.store-btn.store-btn-secondary:hover{color:var(--store-text-main);background:#fff9}
|
||||
File diff suppressed because one or more lines are too long
@ -75,9 +75,11 @@
|
||||
{ id: "headgear", label: "Headgear" },
|
||||
{ id: "facewear", label: "Facewear" },
|
||||
{ id: "vests", label: "Vests" },
|
||||
{ id: "backpacks", label: "Backpacks" },
|
||||
{ id: "attachments", label: "Attachments" },
|
||||
{ id: "weapons", label: "Weapons" },
|
||||
{ id: "ammo", label: "Ammo" },
|
||||
{ id: "items", label: "Items" },
|
||||
{ id: "misc", label: "Misc" },
|
||||
{ id: "vehicles", label: "Vehicles" },
|
||||
],
|
||||
vehicleCards: [
|
||||
@ -98,8 +100,10 @@
|
||||
headgear: [],
|
||||
facewear: [],
|
||||
vests: [],
|
||||
backpacks: [],
|
||||
attachments: [],
|
||||
ammo: [],
|
||||
items: [],
|
||||
misc: [],
|
||||
primary: [],
|
||||
secondary: [],
|
||||
handgun: [],
|
||||
|
||||
@ -37,6 +37,13 @@
|
||||
}
|
||||
|
||||
function formatTitle(value) {
|
||||
const normalizedValue = String(value || "")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (["items", "misc"].includes(normalizedValue)) {
|
||||
return "Misc";
|
||||
}
|
||||
|
||||
return String(value || "")
|
||||
.replace(/[-_]+/g, " ")
|
||||
.split(/\s+/)
|
||||
|
||||
@ -1 +1,2 @@
|
||||
PREP(initCatalogService);
|
||||
PREP(initStoreStore);
|
||||
|
||||
@ -6,6 +6,24 @@ PREP_RECOMPILE_END;
|
||||
|
||||
// private _category = [QUOTE(MOD_NAME), LLSTRING(displayName)];
|
||||
|
||||
[QGVAR(requestCategory), {
|
||||
params [["_uid", "", [""]], ["_category", "", [""]]];
|
||||
|
||||
if (_uid isEqualTo "" || { _category isEqualTo "" }) exitWith {
|
||||
diag_log "[FORGE:Server:Store] Invalid category request payload."
|
||||
};
|
||||
|
||||
private _player = [_uid] call EFUNC(common,getPlayer);
|
||||
if (_player isEqualTo objNull) exitWith {};
|
||||
|
||||
if (isNil QGVAR(StoreCatalogService)) exitWith {
|
||||
diag_log "[FORGE:Server:Store] Store catalog service is unavailable."
|
||||
};
|
||||
|
||||
private _result = GVAR(StoreCatalogService) call ["buildCategoryResponse", [_category]];
|
||||
[CRPC(store,responseCategory), [_result], _player] call CFUNC(targetEvent);
|
||||
}] call CFUNC(addEventHandler);
|
||||
|
||||
[QGVAR(requestCheckout), {
|
||||
params [["_uid", "", [""]], ["_payloadJson", "", [""]]];
|
||||
|
||||
|
||||
460
arma/server/addons/store/functions/fnc_initCatalogService.sqf
Normal file
460
arma/server/addons/store/functions/fnc_initCatalogService.sqf
Normal file
@ -0,0 +1,460 @@
|
||||
#include "..\script_component.hpp"
|
||||
|
||||
/*
|
||||
* File: fnc_initCatalogService.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-14
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the server-side store catalog service for authoritative category hydration and pricing.
|
||||
*/
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(StoreCatalogServiceBaseClass) = compileFinal createHashMapFromArray [
|
||||
["#type", "StoreCatalogServiceBaseClass"],
|
||||
["#create", compileFinal {
|
||||
_self set ["catalogCache", createHashMap];
|
||||
["INFO", "Store catalog service initialized!"] call EFUNC(common,log);
|
||||
}],
|
||||
["formatCurrency", compileFinal {
|
||||
params [["_amount", 0, [0]]];
|
||||
|
||||
format ["$%1", [_amount max 0] call BIS_fnc_numberText]
|
||||
}],
|
||||
["isVisibleConfig", compileFinal {
|
||||
params [["_cfg", configNull, [configNull]]];
|
||||
|
||||
isClass _cfg
|
||||
&& { getNumber (_cfg >> "scope") >= 2 }
|
||||
&& { (getText (_cfg >> "displayName")) isNotEqualTo "" }
|
||||
}],
|
||||
["buildDescription", compileFinal {
|
||||
params [["_cfg", configNull, [configNull]], ["_fallback", "", [""]]];
|
||||
|
||||
private _description = getText (_cfg >> "descriptionShort");
|
||||
if (_description isEqualTo "") then { _description = _fallback; };
|
||||
|
||||
_description
|
||||
}],
|
||||
["normalizeCategoryKey", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
|
||||
private _categoryKey = toLowerANSI _category;
|
||||
if (_categoryKey isEqualTo "items") exitWith { "misc" };
|
||||
|
||||
_categoryKey
|
||||
}],
|
||||
["calculateCatalogPriceValue", compileFinal {
|
||||
params [
|
||||
["_cfg", configNull, [configNull]],
|
||||
["_isVehicle", false, [false]]
|
||||
];
|
||||
|
||||
if (isNull _cfg) exitWith { 50 };
|
||||
|
||||
private _mass = 0;
|
||||
private _priceValue = 0;
|
||||
|
||||
if (_isVehicle) then {
|
||||
_priceValue = getNumber (_cfg >> "cost");
|
||||
} else {
|
||||
private _weaponType = getNumber (_cfg >> "type");
|
||||
if (_weaponType in [1, 2, 4]) then { _mass = getNumber (_cfg >> "WeaponSlotsInfo" >> "mass"); };
|
||||
if (_mass <= 0) then { _mass = getNumber (_cfg >> "ItemInfo" >> "mass"); };
|
||||
if (_mass <= 0) then { _mass = getNumber (_cfg >> "mass"); };
|
||||
|
||||
_priceValue = ceil ((_mass max 0) * 7.5);
|
||||
};
|
||||
|
||||
_priceValue max 50
|
||||
}],
|
||||
["buildCatalogItem", compileFinal {
|
||||
params [
|
||||
["_cfg", configNull, [configNull]],
|
||||
["_typeLabel", "", [""]],
|
||||
["_fallbackDescription", "", [""]],
|
||||
["_imageField", "picture", [""]],
|
||||
["_isVehicle", false, [false]]
|
||||
];
|
||||
|
||||
if (isNull _cfg) exitWith { createHashMap };
|
||||
|
||||
private _className = configName _cfg;
|
||||
private _displayName = getText (_cfg >> "displayName");
|
||||
private _picture = getText (_cfg >> _imageField);
|
||||
if (_picture isEqualTo "" && { _imageField isNotEqualTo "picture" }) then {
|
||||
_picture = getText (_cfg >> "picture");
|
||||
};
|
||||
|
||||
private _priceValue = _self call ["calculateCatalogPriceValue", [_cfg, _isVehicle]];
|
||||
|
||||
createHashMapFromArray [
|
||||
["className", _className],
|
||||
["code", _className],
|
||||
["name", _displayName],
|
||||
["description", _self call ["buildDescription", [_cfg, _fallbackDescription]]],
|
||||
["price", _self call ["formatCurrency", [_priceValue]]],
|
||||
["priceValue", _priceValue],
|
||||
["image", _picture],
|
||||
["type", _typeLabel]
|
||||
]
|
||||
}],
|
||||
["appendCfgWeaponsByItemInfoType", compileFinal {
|
||||
params [["_items", [], [[]]], ["_itemInfoType", -1, [0]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]];
|
||||
|
||||
{
|
||||
private _cfg = _x;
|
||||
if (
|
||||
_self call ["isVisibleConfig", [_cfg]]
|
||||
&& { getNumber (_cfg >> "ItemInfo" >> "type") isEqualTo _itemInfoType }
|
||||
) then {
|
||||
_items pushBack (_self call ["buildCatalogItem", [_cfg, _typeLabel, _fallbackDescription]]);
|
||||
};
|
||||
} forEach ("true" configClasses (configFile >> "CfgWeapons"));
|
||||
|
||||
_items
|
||||
}],
|
||||
["appendCfgWeaponsByType", compileFinal {
|
||||
params [["_items", [], [[]]], ["_weaponType", -1, [0]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]];
|
||||
|
||||
{
|
||||
private _cfg = _x;
|
||||
if (
|
||||
_self call ["isVisibleConfig", [_cfg]]
|
||||
&& { getNumber (_cfg >> "type") isEqualTo _weaponType }
|
||||
) then {
|
||||
_items pushBack (_self call ["buildCatalogItem", [_cfg, _typeLabel, _fallbackDescription]]);
|
||||
};
|
||||
} forEach ("true" configClasses (configFile >> "CfgWeapons"));
|
||||
|
||||
_items
|
||||
}],
|
||||
["isAceClassName", compileFinal {
|
||||
params [["_cfg", configNull, [configNull]]];
|
||||
|
||||
((toLowerANSI (configName _cfg)) select [0, 4]) isEqualTo "ace_"
|
||||
}],
|
||||
["isAttachmentConfig", compileFinal {
|
||||
params [["_cfg", configNull, [configNull]]];
|
||||
|
||||
if !(_self call ["isVisibleConfig", [_cfg]]) exitWith { false };
|
||||
if (_self call ["isAceClassName", [_cfg]]) exitWith { false };
|
||||
|
||||
private _className = configName _cfg;
|
||||
private _itemType = [_className] call BIS_fnc_itemType;
|
||||
private _group = toLowerANSI (_itemType param [0, ""]);
|
||||
private _kind = toLowerANSI (_itemType param [1, ""]);
|
||||
|
||||
(_group find "accessory") >= 0
|
||||
|| { (_kind find "accessory") >= 0 }
|
||||
|| { _kind in ["accessorymuzzle", "accessorypointer", "accessorysights", "accessorybipod"] }
|
||||
}],
|
||||
["resolveAttachmentTypeLabel", compileFinal {
|
||||
params [["_cfg", configNull, [configNull]]];
|
||||
|
||||
private _className = configName _cfg;
|
||||
private _itemType = [_className] call BIS_fnc_itemType;
|
||||
private _kind = toLowerANSI (_itemType param [1, ""]);
|
||||
|
||||
if ((_kind find "muzzle") >= 0) exitWith { "Muzzle Attachment" };
|
||||
if ((_kind find "optic") >= 0 || { (_kind find "sight") >= 0 }) exitWith { "Optic Attachment" };
|
||||
if ((_kind find "pointer") >= 0 || { (_kind find "flash") >= 0 } || { (_kind find "light") >= 0 }) exitWith { "Light Attachment" };
|
||||
if ((_kind find "bipod") >= 0) exitWith { "Bipod Attachment" };
|
||||
|
||||
"Attachment"
|
||||
}],
|
||||
["appendCfgAttachments", compileFinal {
|
||||
params [["_items", [], [[]]], ["_fallbackDescription", "", [""]]];
|
||||
|
||||
{
|
||||
private _cfg = _x;
|
||||
if (_self call ["isAttachmentConfig", [_cfg]]) then {
|
||||
private _typeLabel = _self call ["resolveAttachmentTypeLabel", [_cfg]];
|
||||
_items pushBack (_self call ["buildCatalogItem", [_cfg, _typeLabel, _fallbackDescription]]);
|
||||
};
|
||||
} forEach ("true" configClasses (configFile >> "CfgWeapons"));
|
||||
|
||||
_items
|
||||
}],
|
||||
["appendCfgVehiclesByKind", compileFinal {
|
||||
params [["_items", [], [[]]], ["_baseClass", "", [""]], ["_typeLabel", "", [""]], ["_fallbackDescription", "", [""]]];
|
||||
|
||||
{
|
||||
private _cfg = _x;
|
||||
private _className = configName _cfg;
|
||||
if (
|
||||
_self call ["isVisibleConfig", [_cfg]]
|
||||
&& { getNumber (_cfg >> "isBackpack") isEqualTo 0 }
|
||||
&& { !(_className isKindOf ["CAManBase", configFile >> "CfgVehicles"]) }
|
||||
&& { !(_className isKindOf ["StaticWeapon", configFile >> "CfgVehicles"]) }
|
||||
&& { _className isKindOf [_baseClass, configFile >> "CfgVehicles"] }
|
||||
) then {
|
||||
_items pushBack (_self call ["buildCatalogItem", [_cfg, _typeLabel, _fallbackDescription, "editorPreview", true]]);
|
||||
};
|
||||
} forEach ("true" configClasses (configFile >> "CfgVehicles"));
|
||||
|
||||
_items
|
||||
}],
|
||||
["isBackpackConfig", compileFinal {
|
||||
params [["_cfg", configNull, [configNull]]];
|
||||
|
||||
getNumber (_cfg >> "isBackpack") isEqualTo 1
|
||||
|| { getNumber (_cfg >> "ItemInfo" >> "type") isEqualTo TYPE_BACKPACK }
|
||||
}],
|
||||
["appendCfgBackpacks", compileFinal {
|
||||
params [["_items", [], [[]]], ["_typeLabel", "Backpack", [""]], ["_fallbackDescription", "", [""]]];
|
||||
|
||||
{
|
||||
private _cfg = _x;
|
||||
if (
|
||||
_self call ["isVisibleConfig", [_cfg]]
|
||||
&& { _self call ["isBackpackConfig", [_cfg]] }
|
||||
) then {
|
||||
_items pushBack (_self call ["buildCatalogItem", [_cfg, _typeLabel, _fallbackDescription]]);
|
||||
};
|
||||
} forEach ("true" configClasses (configFile >> "CfgVehicles"));
|
||||
|
||||
_items
|
||||
}],
|
||||
["scanCategoryItems", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
|
||||
private _categoryKey = _self call ["normalizeCategoryKey", [_category]];
|
||||
if (_categoryKey isEqualTo "") exitWith { [] };
|
||||
|
||||
private _items = [];
|
||||
|
||||
switch (_categoryKey) do {
|
||||
case "uniforms": { _items = _self call ["appendCfgWeaponsByItemInfoType", [_items, TYPE_UNIFORM, "Uniform", "Live uniform entry generated from the game inventory."]]; };
|
||||
case "headgear": { _items = _self call ["appendCfgWeaponsByItemInfoType", [_items, TYPE_HEADGEAR, "Headgear", "Live headgear entry generated from the game inventory."]]; };
|
||||
case "vests": { _items = _self call ["appendCfgWeaponsByItemInfoType", [_items, TYPE_VEST, "Vest", "Live vest entry generated from the game inventory."]]; };
|
||||
case "backpacks": { _items = _self call ["appendCfgBackpacks", [_items, "Backpack", "Live backpack entry generated from the game inventory."]]; };
|
||||
case "attachments": {
|
||||
_items = _self call ["appendCfgAttachments", [_items, "Live attachment entry generated from the game inventory."]];
|
||||
};
|
||||
case "facewear": {
|
||||
{ if (_self call ["isVisibleConfig", [_x]]) then { _items pushBack (_self call ["buildCatalogItem", [_x, "Facewear", "Live facewear entry generated from the game inventory."]]); }; } forEach ("true" configClasses (configFile >> "CfgGlasses"));
|
||||
};
|
||||
case "ammo": {
|
||||
{ if (_self call ["isVisibleConfig", [_x]]) then { _items pushBack (_self call ["buildCatalogItem", [_x, "Magazine", "Live ammunition entry generated from the game inventory."]]); }; } forEach ("true" configClasses (configFile >> "CfgMagazines"));
|
||||
};
|
||||
case "misc": {
|
||||
{
|
||||
private _cfg = _x;
|
||||
private _className = configName _cfg;
|
||||
private _itemType = [_className] call BIS_fnc_itemType;
|
||||
private _group = _itemType param [0, ""];
|
||||
private _kind = _itemType param [1, ""];
|
||||
private _weaponType = getNumber (_cfg >> "type");
|
||||
private _isAceClass = _self call ["isAceClassName", [_cfg]];
|
||||
|
||||
if (
|
||||
_self call ["isVisibleConfig", [_cfg]]
|
||||
&& { !(_weaponType in [1, 2, 4]) }
|
||||
&& { (_group in ["Item", "Equipment"]) || { _isAceClass } }
|
||||
&& { !(_kind in ["Uniform", "Vest", "Headgear"]) }
|
||||
&& { !(_self call ["isAttachmentConfig", [_cfg]]) }
|
||||
&& { (getNumber (_cfg >> "ItemInfo" >> "type") isNotEqualTo TYPE_BACKPACK) }
|
||||
) then {
|
||||
private _typeLabel = [_kind, "Item"] select (_kind isEqualTo "");
|
||||
_items pushBack (_self call ["buildCatalogItem", [_cfg, _typeLabel, "Live utility entry generated from the game inventory."]]);
|
||||
};
|
||||
} forEach ("true" configClasses (configFile >> "CfgWeapons"));
|
||||
};
|
||||
case "primary": { _items = _self call ["appendCfgWeaponsByType", [_items, 1, "Primary Weapon", "Live primary weapon entry generated from the game inventory."]]; };
|
||||
case "handgun": { _items = _self call ["appendCfgWeaponsByType", [_items, 2, "Handgun", "Live sidearm entry generated from the game inventory."]]; };
|
||||
case "secondary": { _items = _self call ["appendCfgWeaponsByType", [_items, 4, "Launcher", "Live launcher entry generated from the game inventory."]]; };
|
||||
case "cars": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Car", "Vehicle", "Live wheeled vehicle entry generated from the game inventory."]]; };
|
||||
case "armor": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Tank", "Vehicle", "Live armored vehicle entry generated from the game inventory."]]; };
|
||||
case "helis": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Helicopter", "Aircraft", "Live helicopter entry generated from the game inventory."]]; };
|
||||
case "planes": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Plane", "Aircraft", "Live fixed-wing entry generated from the game inventory."]]; };
|
||||
case "naval": { _items = _self call ["appendCfgVehiclesByKind", [_items, "Ship", "Naval", "Live naval vehicle entry generated from the game inventory."]]; };
|
||||
case "other": {
|
||||
{
|
||||
private _cfg = _x;
|
||||
private _className = configName _cfg;
|
||||
private _isSupportedVehicle = _className isKindOf ["AllVehicles", configFile >> "CfgVehicles"];
|
||||
private _isKnownCategory =
|
||||
_className isKindOf ["Car", configFile >> "CfgVehicles"]
|
||||
|| { _className isKindOf ["Tank", configFile >> "CfgVehicles"] }
|
||||
|| { _className isKindOf ["Helicopter", configFile >> "CfgVehicles"] }
|
||||
|| { _className isKindOf ["Plane", configFile >> "CfgVehicles"] }
|
||||
|| { _className isKindOf ["Ship", configFile >> "CfgVehicles"] };
|
||||
|
||||
if (
|
||||
_self call ["isVisibleConfig", [_cfg]]
|
||||
&& { _isSupportedVehicle }
|
||||
&& { !_isKnownCategory }
|
||||
&& { getNumber (_cfg >> "isBackpack") isEqualTo 0 }
|
||||
&& { !(_className isKindOf ["CAManBase", configFile >> "CfgVehicles"]) }
|
||||
&& { !(_className isKindOf ["StaticWeapon", configFile >> "CfgVehicles"]) }
|
||||
) then {
|
||||
_items pushBack (_self call ["buildCatalogItem", [_cfg, "Special Vehicle", "Live specialty vehicle entry generated from the game inventory.", "editorPreview", true]]);
|
||||
};
|
||||
} forEach ("true" configClasses (configFile >> "CfgVehicles"));
|
||||
};
|
||||
};
|
||||
|
||||
private _sortedItems = _items apply { [toLowerANSI (_x getOrDefault ["name", ""]), _x] };
|
||||
_sortedItems sort true;
|
||||
_sortedItems apply { _x select 1 }
|
||||
}],
|
||||
["isVehicleCategory", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
|
||||
(toLowerANSI _category) in ["cars", "armor", "helis", "planes", "naval", "other"]
|
||||
}],
|
||||
["buildPayloadCategory", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
|
||||
switch (toLowerANSI _category) do {
|
||||
case "backpacks": { "backpack" };
|
||||
case "attachments": { "attachment" };
|
||||
case "ammo": { "magazine" };
|
||||
case "primary";
|
||||
case "secondary";
|
||||
case "handgun": { "weapon" };
|
||||
case "cars";
|
||||
case "armor";
|
||||
case "helis";
|
||||
case "planes";
|
||||
case "naval";
|
||||
case "other": { toLowerANSI _category };
|
||||
default { "item" };
|
||||
}
|
||||
}],
|
||||
["isSupportedCategory", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
|
||||
(_self call ["normalizeCategoryKey", [_category]]) in ["uniforms", "headgear", "vests", "backpacks", "attachments", "facewear", "ammo", "misc", "primary", "handgun", "secondary", "cars", "armor", "helis", "planes", "naval", "other"]
|
||||
}],
|
||||
["buildCategoryItems", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
|
||||
private _categoryKey = _self call ["normalizeCategoryKey", [_category]];
|
||||
if (_categoryKey isEqualTo "") exitWith { [] };
|
||||
|
||||
private _catalogCache = _self getOrDefault ["catalogCache", createHashMap];
|
||||
if (_categoryKey in (keys _catalogCache)) exitWith { _catalogCache get _categoryKey };
|
||||
|
||||
private _items = _self call ["scanCategoryItems", [_categoryKey]];
|
||||
private _payloadCategory = _self call ["buildPayloadCategory", [_categoryKey]];
|
||||
private _entryKind = ["item", "vehicle"] select (_self call ["isVehicleCategory", [_categoryKey]]);
|
||||
|
||||
{
|
||||
_x set ["category", _payloadCategory];
|
||||
_x set ["entryKind", _entryKind];
|
||||
} forEach _items;
|
||||
|
||||
_catalogCache set [_categoryKey, _items];
|
||||
_self set ["catalogCache", _catalogCache];
|
||||
|
||||
_items
|
||||
}],
|
||||
["buildCategoryResponse", compileFinal {
|
||||
params [["_category", "", [""]]];
|
||||
|
||||
private _categoryKey = _self call ["normalizeCategoryKey", [_category]];
|
||||
private _response = createHashMapFromArray [["success", false], ["category", _categoryKey], ["items", []], ["message", "No store category was provided."]];
|
||||
|
||||
if (_categoryKey isEqualTo "") exitWith { _response };
|
||||
if !(_self call ["isSupportedCategory", [_categoryKey]]) exitWith {
|
||||
_response set ["message", format ["Unsupported store category: %1", _categoryKey]];
|
||||
_response
|
||||
};
|
||||
|
||||
_response set ["success", true];
|
||||
_response set ["message", ""];
|
||||
_response set ["items", _self call ["buildCategoryItems", [_categoryKey]]];
|
||||
_response
|
||||
}],
|
||||
["resolveCheckoutCategories", compileFinal {
|
||||
params [["_entry", createHashMap, [createHashMap]]];
|
||||
|
||||
private _entryKind = toLowerANSI (_entry getOrDefault ["entryKind", "item"]);
|
||||
private _category = toLowerANSI (_entry getOrDefault ["category", ""]);
|
||||
|
||||
if (_entryKind isEqualTo "vehicle") exitWith { ["cars", "armor", "helis", "planes", "naval", "other"] };
|
||||
if (_category isEqualTo "weapon") exitWith { ["primary", "handgun", "secondary"] };
|
||||
if (_category isEqualTo "backpack") exitWith { ["backpacks"] };
|
||||
if (_category isEqualTo "attachment") exitWith { ["attachments"] };
|
||||
if (_category isEqualTo "magazine") exitWith { ["ammo"] };
|
||||
|
||||
["uniforms", "headgear", "vests", "facewear", "misc", "attachments", "backpacks"]
|
||||
}],
|
||||
["resolveCheckoutCatalogEntry", compileFinal {
|
||||
params [["_entry", createHashMap, [createHashMap]]];
|
||||
|
||||
private _className = toLowerANSI (_entry getOrDefault ["classname", ""]);
|
||||
if (_className isEqualTo "") exitWith { createHashMap };
|
||||
|
||||
private _resolved = createHashMap;
|
||||
{
|
||||
private _catalogEntries = _self call ["buildCategoryItems", [_x]];
|
||||
private _match = _catalogEntries select { (toLowerANSI (_x getOrDefault ["className", ""])) isEqualTo _className };
|
||||
|
||||
if (_match isNotEqualTo []) exitWith { _resolved = _match select 0; };
|
||||
} forEach (_self call ["resolveCheckoutCategories", [_entry]]);
|
||||
|
||||
_resolved
|
||||
}],
|
||||
["calculateCheckoutTotal", compileFinal {
|
||||
params [["_items", [], [[]]], ["_vehicles", [], [[]]]];
|
||||
|
||||
private _result = createHashMapFromArray [["success", false], ["total", 0], ["message", "Checkout total must be greater than zero."]];
|
||||
private _total = 0;
|
||||
private _message = "";
|
||||
|
||||
{
|
||||
if (_message isEqualTo "") then {
|
||||
private _className = _x getOrDefault ["classname", ""];
|
||||
private _quantity = floor ((_x getOrDefault ["quantity", 1]) max 0);
|
||||
|
||||
if (_className isEqualTo "" || { _quantity <= 0 }) then {
|
||||
_message = "Checkout contains an invalid item entry.";
|
||||
} else {
|
||||
private _catalogEntry = _self call ["resolveCheckoutCatalogEntry", [createHashMapFromArray [["classname", _className], ["category", _x getOrDefault ["category", "item"]], ["entryKind", "item"]]]];
|
||||
|
||||
if (_catalogEntry isEqualTo createHashMap) then {
|
||||
_message = format ["Unsupported store item: %1", _className];
|
||||
} else {
|
||||
_total = _total + ((_catalogEntry getOrDefault ["priceValue", 0]) * _quantity);
|
||||
};
|
||||
};
|
||||
};
|
||||
} forEach _items;
|
||||
|
||||
{
|
||||
if (_message isEqualTo "") then {
|
||||
private _className = _x getOrDefault ["classname", ""];
|
||||
if (_className isEqualTo "") then {
|
||||
_message = "Checkout contains an invalid vehicle entry.";
|
||||
} else {
|
||||
private _catalogEntry = _self call ["resolveCheckoutCatalogEntry", [createHashMapFromArray [["classname", _className], ["category", _x getOrDefault ["category", ""]], ["entryKind", "vehicle"]]]];
|
||||
|
||||
if (_catalogEntry isEqualTo createHashMap) then {
|
||||
_message = format ["Unsupported store vehicle: %1", _className];
|
||||
} else {
|
||||
_total = _total + (_catalogEntry getOrDefault ["priceValue", 0]);
|
||||
};
|
||||
};
|
||||
};
|
||||
} forEach _vehicles;
|
||||
|
||||
if (_message isNotEqualTo "") exitWith {
|
||||
_result set ["message", _message];
|
||||
_result
|
||||
};
|
||||
|
||||
if (_total <= 0) exitWith { _result };
|
||||
|
||||
_result set ["success", true];
|
||||
_result set ["total", floor _total];
|
||||
_result set ["message", ""];
|
||||
_result
|
||||
}]
|
||||
];
|
||||
|
||||
GVAR(StoreCatalogService) = createHashMapObject [GVAR(StoreCatalogServiceBaseClass)];
|
||||
GVAR(StoreCatalogService)
|
||||
@ -4,13 +4,15 @@
|
||||
* File: fnc_initStoreStore.sqf
|
||||
* Author: IDSolutions
|
||||
* Date: 2026-03-12
|
||||
* Last Update: 2026-03-12
|
||||
* Last Update: 2026-03-14
|
||||
* Public: No
|
||||
*
|
||||
* Description:
|
||||
* Initializes the server-side store checkout flow.
|
||||
*/
|
||||
|
||||
if (isNil QGVAR(StoreCatalogService)) then { call FUNC(initCatalogService); };
|
||||
|
||||
#pragma hemtt ignore_variables ["_self"]
|
||||
GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
|
||||
["#type", "StoreBaseStore"],
|
||||
@ -82,11 +84,23 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
|
||||
|
||||
private _result = _self call ["buildResult", ["Checkout failed.", "cash"]];
|
||||
private _payload = fromJSON _payloadJson;
|
||||
if !(_payload isEqualType createHashMap) exitWith {
|
||||
_result set ["message", "Checkout request payload is invalid."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _paymentMethod = toLowerANSI (_payload getOrDefault ["paymentMethod", "cash"]);
|
||||
private _totalPrice = floor ((_payload getOrDefault ["totalPrice", 0]) max 0);
|
||||
private _items = _payload getOrDefault ["items", []];
|
||||
private _vehicles = _payload getOrDefault ["vehicles", []];
|
||||
|
||||
if (isNil QGVAR(StoreCatalogService)) exitWith {
|
||||
_result set ["message", "Store catalog service is unavailable."];
|
||||
_result
|
||||
};
|
||||
|
||||
private _priceResult = GVAR(StoreCatalogService) call ["calculateCheckoutTotal", [_items, _vehicles]];
|
||||
private _totalPrice = _priceResult getOrDefault ["total", 0];
|
||||
|
||||
_result set ["paymentMethod", _paymentMethod];
|
||||
_result set ["chargedTotal", _totalPrice];
|
||||
|
||||
@ -95,8 +109,8 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
|
||||
_result
|
||||
};
|
||||
|
||||
if (_totalPrice <= 0) exitWith {
|
||||
_result set ["message", "Checkout total must be greater than zero."];
|
||||
if !(_priceResult getOrDefault ["success", false]) exitWith {
|
||||
_result set ["message", _priceResult getOrDefault ["message", "Checkout total must be greater than zero."]];
|
||||
_result
|
||||
};
|
||||
|
||||
@ -144,10 +158,10 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
|
||||
if (_paymentMethod isEqualTo "org_funds" && { _vehicles isNotEqualTo [] }) then {
|
||||
_orgFleetResult = EGVAR(org,OrgStore) call ["addFleetVehicles", [_uid, _vehicles, true]];
|
||||
};
|
||||
|
||||
private _lockerPatch = _lockerResult getOrDefault ["patch", createHashMap];
|
||||
private _vaPatch = _vaResult getOrDefault ["patch", createHashMap];
|
||||
private _vgPatch = _vgResult getOrDefault ["patch", createHashMap];
|
||||
|
||||
if (keys _lockerPatch isNotEqualTo []) then { [CRPC(locker,responseSyncLocker), [_lockerPatch], _player] call CFUNC(targetEvent); };
|
||||
if (keys _vaPatch isNotEqualTo []) then { [CRPC(locker,responseSyncVA), [_vaPatch], _player] call CFUNC(targetEvent); };
|
||||
if (keys _vgPatch isNotEqualTo []) then { [CRPC(garage,responseSyncVG), [_vgPatch], _player] call CFUNC(targetEvent); };
|
||||
@ -157,9 +171,7 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
|
||||
|
||||
private _orgPatch = _payment getOrDefault ["orgPatch", createHashMap];
|
||||
private _orgFleetPatch = _orgFleetResult getOrDefault ["patch", createHashMap];
|
||||
if (keys _orgFleetPatch isNotEqualTo []) then {
|
||||
{ _orgPatch set [_x, _y]; } forEach _orgFleetPatch;
|
||||
};
|
||||
if (keys _orgFleetPatch isNotEqualTo []) then { { _orgPatch set [_x, _y]; } forEach _orgFleetPatch; };
|
||||
if (keys _orgPatch isNotEqualTo []) then {
|
||||
private _orgTargetUids = _payment getOrDefault ["orgTargetUids", []];
|
||||
{
|
||||
@ -168,9 +180,7 @@ GVAR(StoreBaseStore) = compileFinal createHashMapFromArray [
|
||||
|
||||
{
|
||||
private _memberPlayer = [_x] call EFUNC(common,getPlayer);
|
||||
if (_memberPlayer isNotEqualTo objNull) then {
|
||||
[CRPC(org,responseSyncOrg), [_orgPatch], _memberPlayer] call CFUNC(targetEvent);
|
||||
};
|
||||
if (_memberPlayer isNotEqualTo objNull) then { [CRPC(org,responseSyncOrg), [_orgPatch], _memberPlayer] call CFUNC(targetEvent); };
|
||||
} forEach _orgTargetUids;
|
||||
};
|
||||
|
||||
|
||||
699
package-lock.json
generated
Normal file
699
package-lock.json
generated
Normal file
@ -0,0 +1,699 @@
|
||||
{
|
||||
"name": "forge-webui",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "forge-webui",
|
||||
"devDependencies": {
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"lightningcss": "^1.29.3",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"prettier": "^3.6.2",
|
||||
"terser": "^5.44.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/gen-mapping": {
|
||||
"version": "0.3.13",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
|
||||
"integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
"@jridgewell/trace-mapping": "^0.3.24"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
|
||||
"integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/source-map": {
|
||||
"version": "0.3.11",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
|
||||
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/gen-mapping": "^0.3.5",
|
||||
"@jridgewell/trace-mapping": "^0.3.25"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/sourcemap-codec": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
|
||||
"integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@jridgewell/trace-mapping": {
|
||||
"version": "0.3.31",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
|
||||
"integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@jridgewell/resolve-uri": "^3.1.0",
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/acorn": {
|
||||
"version": "8.16.0",
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
|
||||
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/buffer-from": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/camel-case": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz",
|
||||
"integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pascal-case": "^3.1.2",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/clean-css": {
|
||||
"version": "5.3.3",
|
||||
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz",
|
||||
"integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"source-map": "~0.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "10.0.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz",
|
||||
"integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
}
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
"integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"cssesc": "bin/cssesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/detect-libc": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
|
||||
"integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dot-case": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz",
|
||||
"integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"no-case": "^3.0.4",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/entities": {
|
||||
"version": "4.5.0",
|
||||
"resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
|
||||
"integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.12"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/fb55/entities?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/html-minifier-terser": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz",
|
||||
"integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"camel-case": "^4.1.2",
|
||||
"clean-css": "~5.3.2",
|
||||
"commander": "^10.0.0",
|
||||
"entities": "^4.4.0",
|
||||
"param-case": "^3.0.4",
|
||||
"relateurl": "^0.2.7",
|
||||
"terser": "^5.15.1"
|
||||
},
|
||||
"bin": {
|
||||
"html-minifier-terser": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^14.13.1 || >=16.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz",
|
||||
"integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==",
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"lightningcss-android-arm64": "1.32.0",
|
||||
"lightningcss-darwin-arm64": "1.32.0",
|
||||
"lightningcss-darwin-x64": "1.32.0",
|
||||
"lightningcss-freebsd-x64": "1.32.0",
|
||||
"lightningcss-linux-arm-gnueabihf": "1.32.0",
|
||||
"lightningcss-linux-arm64-gnu": "1.32.0",
|
||||
"lightningcss-linux-arm64-musl": "1.32.0",
|
||||
"lightningcss-linux-x64-gnu": "1.32.0",
|
||||
"lightningcss-linux-x64-musl": "1.32.0",
|
||||
"lightningcss-win32-arm64-msvc": "1.32.0",
|
||||
"lightningcss-win32-x64-msvc": "1.32.0"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-android-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-arm64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz",
|
||||
"integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-darwin-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-freebsd-x64": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz",
|
||||
"integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm-gnueabihf": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz",
|
||||
"integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-arm64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-gnu": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz",
|
||||
"integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"glibc"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-linux-x64-musl": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz",
|
||||
"integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"libc": [
|
||||
"musl"
|
||||
],
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-arm64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lightningcss-win32-x64-msvc": {
|
||||
"version": "1.32.0",
|
||||
"resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz",
|
||||
"integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MPL-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">= 12.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/parcel"
|
||||
}
|
||||
},
|
||||
"node_modules/lower-case": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz",
|
||||
"integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/nanoid": {
|
||||
"version": "3.3.11",
|
||||
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
|
||||
"integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"nanoid": "bin/nanoid.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/no-case": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz",
|
||||
"integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"lower-case": "^2.0.2",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/param-case": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz",
|
||||
"integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"dot-case": "^3.0.4",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/pascal-case": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz",
|
||||
"integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"no-case": "^3.0.4",
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/picocolors": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
|
||||
"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
|
||||
"dev": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/postcss": {
|
||||
"version": "8.5.8",
|
||||
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz",
|
||||
"integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "tidelift",
|
||||
"url": "https://tidelift.com/funding/github/npm/postcss"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
"source-map-js": "^1.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^10 || ^12 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-nested": {
|
||||
"version": "7.0.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-nested/-/postcss-nested-7.0.2.tgz",
|
||||
"integrity": "sha512-5osppouFc0VR9/VYzYxO03VaDa3e8F23Kfd6/9qcZTUI8P58GIYlArOET2Wq0ywSl2o2PjELhYOFI4W7l5QHKw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/postcss/"
|
||||
},
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ai"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"postcss-selector-parser": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"postcss": "^8.2.14"
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-selector-parser": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz",
|
||||
"integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cssesc": "^3.0.0",
|
||||
"util-deprecate": "^1.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/prettier": {
|
||||
"version": "3.8.1",
|
||||
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.1.tgz",
|
||||
"integrity": "sha512-UOnG6LftzbdaHZcKoPFtOcCKztrQ57WkHDeRD9t/PTQtmT0NHSeWWepj6pS0z/N7+08BHFDQVUrfmfMRcZwbMg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/prettier/prettier?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/relateurl": {
|
||||
"version": "0.2.7",
|
||||
"resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz",
|
||||
"integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map": {
|
||||
"version": "0.6.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
|
||||
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
||||
"dev": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-support": {
|
||||
"version": "0.5.21",
|
||||
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
|
||||
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"buffer-from": "^1.0.0",
|
||||
"source-map": "^0.6.0"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.46.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz",
|
||||
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
|
||||
"dev": true,
|
||||
"license": "BSD-2-Clause",
|
||||
"dependencies": {
|
||||
"@jridgewell/source-map": "^0.3.3",
|
||||
"acorn": "^8.15.0",
|
||||
"commander": "^2.20.0",
|
||||
"source-map-support": "~0.5.20"
|
||||
},
|
||||
"bin": {
|
||||
"terser": "bin/terser"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/terser/node_modules/commander": {
|
||||
"version": "2.20.3",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
|
||||
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"dev": true,
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/util-deprecate": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
|
||||
"integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,14 @@
|
||||
{
|
||||
"name": "forge-webui",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"html-minifier-terser": "^7.2.0",
|
||||
"lightningcss": "^1.29.3",
|
||||
"postcss": "^8.5.6",
|
||||
"postcss-nested": "^7.0.2",
|
||||
"prettier": "^3.6.2",
|
||||
"terser": "^5.44.0"
|
||||
},
|
||||
"scripts": {
|
||||
"build:webui": "node tools/build-webui.mjs"
|
||||
}
|
||||
|
||||
@ -2,6 +2,11 @@ import { mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises"
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import { minify as minifyHtml } from "html-minifier-terser";
|
||||
import { transform as transformCss } from "lightningcss";
|
||||
import postcss from "postcss";
|
||||
import postcssNested from "postcss-nested";
|
||||
import { minify as minifyJs } from "terser";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const rootDir = path.resolve(__dirname, "..");
|
||||
@ -70,16 +75,39 @@ async function cleanOutputDirs(outputDirs) {
|
||||
}
|
||||
|
||||
async function buildJsBundle({ name, output, sources }) {
|
||||
const banner = `/* Generated by tools/build-webui.mjs for ${name}. Do not edit directly. */\n`;
|
||||
const chunks = await Promise.all(sources.map(readSource));
|
||||
await writeBundle(output, banner + chunks.join("\n\n"));
|
||||
const bundleSource = chunks.join("\n\n");
|
||||
const result = await minifyJs(bundleSource, {
|
||||
compress: true,
|
||||
mangle: true,
|
||||
format: {
|
||||
comments: false,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result?.code) {
|
||||
throw new Error(`Failed to minify JavaScript bundle for ${name}.`);
|
||||
}
|
||||
|
||||
await writeBundle(output, result.code);
|
||||
console.log(`Built ${output}`);
|
||||
}
|
||||
|
||||
async function buildCssBundle({ name, output, sources }) {
|
||||
const banner = `/* Generated by tools/build-webui.mjs for ${name}. Do not edit directly. */\n`;
|
||||
const chunks = await Promise.all(sources.map(readSource));
|
||||
await writeBundle(output, banner + chunks.join("\n\n"));
|
||||
const nestedResult = await postcss([postcssNested]).process(
|
||||
chunks.join("\n\n"),
|
||||
{
|
||||
from: undefined,
|
||||
},
|
||||
);
|
||||
const result = transformCss({
|
||||
filename: output,
|
||||
code: Buffer.from(nestedResult.css),
|
||||
minify: true,
|
||||
});
|
||||
|
||||
await writeBundle(output, result.code.toString("utf8"));
|
||||
console.log(`Built ${output}`);
|
||||
}
|
||||
|
||||
@ -149,8 +177,17 @@ function renderSiteIndex({ title, siteConfig }) {
|
||||
}
|
||||
|
||||
async function buildHtmlPage({ name, output, title, siteConfig }) {
|
||||
const banner = `<!-- Generated by tools/build-webui.mjs for ${name}. Do not edit directly. -->\n`;
|
||||
await writeBundle(output, banner + renderSiteIndex({ title, siteConfig }));
|
||||
const html = renderSiteIndex({ title, siteConfig });
|
||||
const minifiedHtml = await minifyHtml(html, {
|
||||
collapseBooleanAttributes: true,
|
||||
collapseWhitespace: true,
|
||||
minifyCSS: true,
|
||||
minifyJS: true,
|
||||
removeComments: true,
|
||||
removeRedundantAttributes: true,
|
||||
});
|
||||
|
||||
await writeBundle(output, minifiedHtml);
|
||||
console.log(`Built ${output}`);
|
||||
}
|
||||
|
||||
@ -277,11 +314,6 @@ async function loadUiConfig(absoluteConfigPath) {
|
||||
cssBundles,
|
||||
htmlPage,
|
||||
formatSourceTargets,
|
||||
formatGeneratedTargets: [
|
||||
...jsBundles.map((bundle) => bundle.output),
|
||||
...cssBundles.map((bundle) => bundle.output),
|
||||
htmlPage.output,
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
@ -297,15 +329,11 @@ async function collectUiBuildArtifacts() {
|
||||
formatSourceTargets: uiConfigs.flatMap(
|
||||
(config) => config.formatSourceTargets,
|
||||
),
|
||||
formatGeneratedTargets: uiConfigs.flatMap(
|
||||
(config) => config.formatGeneratedTargets,
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function build() {
|
||||
const uiArtifacts = await collectUiBuildArtifacts();
|
||||
const commonGeneratedTargets = commonJsBundles.map((bundle) => bundle.output);
|
||||
const commonOutputDirs = [resolveFromRoot(commonUiSiteDir)];
|
||||
|
||||
await runPrettier([
|
||||
@ -321,11 +349,6 @@ async function build() {
|
||||
]);
|
||||
await Promise.all(uiArtifacts.cssBundles.map(buildCssBundle));
|
||||
await Promise.all(uiArtifacts.htmlPages.map(buildHtmlPage));
|
||||
|
||||
await runPrettier([
|
||||
...commonGeneratedTargets,
|
||||
...uiArtifacts.formatGeneratedTargets,
|
||||
]);
|
||||
}
|
||||
|
||||
build().catch((error) => {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user