From da5435afbc96fcf4b109c22e8ed22294808f099e Mon Sep 17 00:00:00 2001
From: Christian
Date: Thu, 15 Oct 2026 23:49:46 +0200
Subject: [PATCH] feat(ui): evenly distribute sector pills with icons and
compact rates, add 10s tender cooldown, and sync ready tender counter
---
frontend/css/style.css | 33 +++++++++++++++++++++++---------
frontend/js/app.js | 19 +++++++++++++++---
frontend/js/core/engine.js | 28 ++++++++++++++++++++++++---
frontend/js/ui/contractsView.js | 18 +++++++++++++++--
frontend/js/ui/productionView.js | 17 ++++++++++------
5 files changed, 92 insertions(+), 23 deletions(-)
diff --git a/frontend/css/style.css b/frontend/css/style.css
index 7af0fdc..e9ce1d9 100644
--- a/frontend/css/style.css
+++ b/frontend/css/style.css
@@ -385,10 +385,12 @@ body {
}
.sector-pill-rate {
- font-size: 0.7rem;
+ font-size: 0.72rem;
font-weight: 700;
color: var(--emerald-dark);
margin-left: 2px;
+ letter-spacing: -0.02em;
+ white-space: nowrap;
}
/* Sector Output Summary Banner */
@@ -577,30 +579,35 @@ body {
background: var(--bg-surface);
border: 1px solid var(--border-color);
border-radius: var(--radius-md);
- padding: 8px 12px;
+ padding: 6px 8px;
}
.sector-tabs-scroll {
display: flex;
- gap: 8px;
- overflow-x: auto;
- padding-bottom: 2px;
+ gap: 6px;
+ width: 100%;
}
.sector-pill {
+ flex: 1 1 0;
+ min-width: 0;
display: inline-flex;
align-items: center;
+ justify-content: center;
gap: 6px;
background: var(--bg-surface-sub);
border: 1px solid var(--border-color);
- padding: 6px 12px;
+ padding: 8px 6px;
border-radius: var(--radius-sm);
- font-size: 0.78rem;
- font-weight: 600;
- color: var(--text-secondary);
cursor: pointer;
white-space: nowrap;
transition: all 0.15s;
+ box-sizing: border-box;
+}
+
+.sector-pill iconify-icon {
+ font-size: 1.15rem;
+ flex-shrink: 0;
}
.sector-pill:hover {
@@ -613,6 +620,14 @@ body {
border-color: var(--text-primary);
}
+.sector-pill.active iconify-icon {
+ color: #ffffff !important;
+}
+
+.sector-pill.active .sector-pill-rate {
+ color: #a7f3d0;
+}
+
.production-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(380px, 1fr));
diff --git a/frontend/js/app.js b/frontend/js/app.js
index 0b67738..9126b8c 100644
--- a/frontend/js/app.js
+++ b/frontend/js/app.js
@@ -256,6 +256,16 @@ class ProductionApp {
state.stats.contractsCompleted = (state.stats.contractsCompleted || 0) + 1;
state.activeContracts.splice(idx, 1);
+ state.contractCooldown = 10.0; // 10 second cooldown before new tender RFP appears
+
+ // Recompute readyContractsCount
+ let readyCount = 0;
+ for (const actC of state.activeContracts) {
+ const ps = state.products[actC.productId];
+ if (ps && (ps.stock || 0) >= actC.targetAmount) readyCount++;
+ }
+ state.readyContractsCount = readyCount;
+
this.showToast(`Contract Fulfilled! Earned ${fmtCurrency(totalPayout)} + ${c.rewardRep} Rep!`, 'success');
this.sync.queueAction(action);
this.mountActiveView();
@@ -342,9 +352,12 @@ class ProductionApp {
const elContractsBadge = document.getElementById('contracts-badge');
if (elContractsBadge) {
- const count = (state.activeContracts || []).length;
- elContractsBadge.textContent = count;
- elContractsBadge.style.display = count > 0 ? 'inline-flex' : 'none';
+ const readyCount = state.readyContractsCount || 0;
+ const textVal = String(readyCount);
+ if (elContractsBadge.textContent !== textVal) {
+ elContractsBadge.textContent = textVal;
+ }
+ elContractsBadge.style.display = readyCount > 0 ? 'inline-flex' : 'none';
}
const userBadge = document.getElementById('user-badge');
diff --git a/frontend/js/core/engine.js b/frontend/js/core/engine.js
index f761251..6c97f1b 100644
--- a/frontend/js/core/engine.js
+++ b/frontend/js/core/engine.js
@@ -193,18 +193,35 @@ export class SimulationEngine {
updateContracts(dt) {
const state = this.sm.state;
if (!state.activeContracts) state.activeContracts = [];
+ if (state.contractCooldown === undefined) state.contractCooldown = 0;
for (let i = state.activeContracts.length - 1; i >= 0; i--) {
const c = state.activeContracts[i];
c.remainingSeconds -= dt;
if (c.remainingSeconds <= 0) {
- // Expired contract, remove
+ // Expired contract, remove and trigger a cooldown before next tender
state.activeContracts.splice(i, 1);
+ state.contractCooldown = Math.max(state.contractCooldown, 10.0);
}
}
- // Replenish up to 3 open contracts
- if (state.activeContracts.length < 3) {
+ // Count how many active tenders are fulfilled and ready to collect
+ let readyCount = 0;
+ for (const c of state.activeContracts) {
+ const pState = state.products[c.productId];
+ if (pState && (pState.stock || 0) >= c.targetAmount) {
+ readyCount++;
+ }
+ }
+ state.readyContractsCount = readyCount;
+
+ // Process replenishment cooldown
+ if (state.contractCooldown > 0) {
+ state.contractCooldown = Math.max(0, state.contractCooldown - dt);
+ }
+
+ // Replenish up to 3 open contracts only when cooldown is over
+ if (state.activeContracts.length < 3 && state.contractCooldown <= 0) {
const unlockedProducts = PRODUCTS.filter(p => state.products[p.id]?.unlocked);
if (unlockedProducts.length > 0) {
const availableTemplates = CONTRACT_TEMPLATES.filter(tmpl =>
@@ -226,6 +243,11 @@ export class SimulationEngine {
rewardCash: tmpl.rewardCash,
rewardRep: tmpl.rewardRep
});
+
+ // If still under 3, stagger next replenish with a slight pause
+ if (state.activeContracts.length < 3) {
+ state.contractCooldown = 10.0;
+ }
}
}
}
diff --git a/frontend/js/ui/contractsView.js b/frontend/js/ui/contractsView.js
index b22a065..538ebc3 100644
--- a/frontend/js/ui/contractsView.js
+++ b/frontend/js/ui/contractsView.js
@@ -23,8 +23,10 @@ export function mountContractsBoard(container, state, onAction) {
- Completed Tenders:
- ${state.stats.contractsCompleted || 0}
+ Ready to Deliver:
+ ${state.readyContractsCount || 0}
+ Completed:
+ ${state.stats.contractsCompleted || 0}
@@ -125,6 +127,18 @@ export function updateContractsBoard(container, state, onAction) {
return;
}
+ const elStatsReady = document.getElementById('contract-stats-ready');
+ if (elStatsReady) {
+ const val = String(state.readyContractsCount || 0);
+ if (elStatsReady.textContent !== val) elStatsReady.textContent = val;
+ }
+
+ const elStatsCompleted = document.getElementById('contract-stats-completed');
+ if (elStatsCompleted) {
+ const val = String(state.stats?.contractsCompleted || 0);
+ if (elStatsCompleted.textContent !== val) elStatsCompleted.textContent = val;
+ }
+
for (const c of contracts) {
const currentStock = state.products[c.productId]?.stock || 0;
const canFulfill = currentStock >= c.targetAmount;
diff --git a/frontend/js/ui/productionView.js b/frontend/js/ui/productionView.js
index e0a2939..952aaab 100644
--- a/frontend/js/ui/productionView.js
+++ b/frontend/js/ui/productionView.js
@@ -69,17 +69,16 @@ export function mountProductionFloor(container, state, onAction, engine) {
let html = `