feat: make SLA Campaign Clock movable, resizable, and minimizable popped-out window app

This commit is contained in:
2026-05-24 22:20:18 +01:00
parent 18a1c0bd72
commit 8e5e0556bf
3 changed files with 138 additions and 47 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
"id": "sla-shift-ledger",
"title": "SLA Shift Ledger",
"description": "Tracks SLA campaign day shifts, actor duty schedules, housing tiers, and weekly living-cost ledgers.",
"version": "0.1.0",
"version": "0.2.0",
"url": "https://github.com/slaguru666/sla-shift-ledger",
"manifest": "https://raw.githubusercontent.com/slaguru666/sla-shift-ledger/main/module.json",
"download": "https://github.com/slaguru666/sla-shift-ledger/releases/latest/download/sla-shift-ledger.zip",
+81 -26
View File
@@ -211,49 +211,92 @@ async function advanceShift(delta = 1) {
ui.notifications.info(`Campaign time advanced to ${buildCampaignStamp(nextState)}.`);
}
function renderCampaignBanner() {
let host = document.querySelector("#sla-shift-ledger-banner");
if (!host) {
host = document.createElement("section");
host.id = "sla-shift-ledger-banner";
host.className = "sla-shift-ledger-banner";
document.body.appendChild(host);
class SlaCampaignClockApp extends Application {
static get defaultOptions() {
return foundry.utils.mergeObject(super.defaultOptions, {
id: "sla-campaign-clock-app",
title: "SLA Campaign Clock",
classes: ["sla-campaign-clock-window", "mosh"],
width: 580,
height: "auto",
resizable: true,
minimizable: true,
popOut: true
});
}
async _renderInner(data) {
const state = getState();
const weekNumber = getWeekFromDay(state.day);
const currentShift = getShift(state.shift);
host.innerHTML = `
<div class="sla-shift-ledger-banner-inner">
const html = `
<div class="sla-shift-ledger-window-inner">
<div class="sla-shift-ledger-banner-meta">
<div class="sla-shift-ledger-banner-kicker">SLA Campaign Clock</div>
<div class="sla-shift-ledger-banner-title">Day ${state.day} · Week ${weekNumber}</div>
<div class="sla-shift-ledger-banner-subtitle">${currentShift.label}</div>
<div class="sla-shift-ledger-banner-kicker" style="color: #7cc4ff; font-size: 0.72rem; letter-spacing: 0.18em; text-transform: uppercase;">SLA Campaign Clock</div>
<div class="sla-shift-ledger-banner-title" style="font-size: 1.2rem; font-weight: 700; color: #f4f8fd; margin: 4px 0;">Day ${state.day} · Week ${weekNumber}</div>
<div class="sla-shift-ledger-banner-subtitle" style="font-size: 0.85rem; color: #9eb5cf; margin-top: 2px;">${currentShift.label}</div>
</div>
<div class="sla-shift-ledger-banner-main">
<div class="sla-shift-ledger-shifts">
<div class="sla-shift-ledger-banner-main" style="margin-top: 15px;">
<div class="sla-shift-ledger-shifts" style="display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px;">
${SHIFTS.map((shift, index) => `
<div class="sla-shift-ledger-shift ${index === Number(state.shift) ? "is-active" : ""}">
<div class="sla-shift-ledger-shift ${index === Number(state.shift) ? "is-active" : ""}" style="border-radius: 12px; border: 1px solid ${index === Number(state.shift) ? "rgba(124, 196, 255, 0.4)" : "rgba(255, 255, 255, 0.1)"}; background: ${index === Number(state.shift) ? "rgba(31, 93, 156, 0.3)" : "rgba(255, 255, 255, 0.04)"}; min-height: 48px; display: flex; align-items: center; justify-content: center; font-weight: bold; font-size: 0.9rem;">
${shift.short}
</div>
`).join("")}
</div>
<div class="sla-shift-ledger-banner-controls">
<div class="sla-shift-ledger-banner-controls" style="margin-top: 15px; display: flex; gap: 8px; justify-content: flex-end;">
${game.user?.isGM ? `
<button type="button" class="sla-shift-ledger-button" data-action="prev">Prev</button>
<button type="button" class="sla-shift-ledger-button" data-action="next">Next</button>
<button type="button" class="sla-shift-ledger-button" data-action="open">Open</button>
<button type="button" class="sla-shift-ledger-button" data-action="prev"><i class="fas fa-arrow-left"></i> Prev</button>
<button type="button" class="sla-shift-ledger-button" data-action="next">Next <i class="fas fa-arrow-right"></i></button>
<button type="button" class="sla-shift-ledger-button" data-action="open"><i class="fas fa-cog"></i> Config</button>
` : ""}
</div>
</div>
</div>
`;
return $(html);
}
activateListeners(html) {
super.activateListeners(html);
if (game.user?.isGM) {
host.querySelector("[data-action='prev']")?.addEventListener("click", () => advanceShift(-1));
host.querySelector("[data-action='next']")?.addEventListener("click", () => advanceShift(1));
host.querySelector("[data-action='open']")?.addEventListener("click", () => SlaShiftLedgerApp.open());
html.find("[data-action='prev']").on("click", () => advanceShift(-1));
html.find("[data-action='next']").on("click", () => advanceShift(1));
html.find("[data-action='open']").on("click", () => SlaShiftLedgerApp.open());
}
}
static get activeApp() {
return Object.values(ui.windows).find(app => app.constructor === SlaCampaignClockApp) ?? null;
}
static open() {
const active = SlaCampaignClockApp.activeApp;
if (active) {
active.render(true);
active.maximize();
return active;
}
const app = new SlaCampaignClockApp();
app.render(true);
return app;
}
static toggle() {
const active = SlaCampaignClockApp.activeApp;
if (active) {
active.close();
} else {
SlaCampaignClockApp.open();
}
}
}
function renderCampaignBanner() {
const active = SlaCampaignClockApp.activeApp;
if (active) {
active.render(false);
}
}
@@ -325,7 +368,6 @@ class SlaShiftLedgerApp extends FormApplication {
}
function installSceneButton(_app, html) {
if (!game.user?.isGM) return;
const root = getHtmlRoot(html);
const directoryHeader = root?.querySelector(".directory-header");
if (!directoryHeader) return;
@@ -335,7 +377,18 @@ function installSceneButton(_app, html) {
header.className = "header-actions sla-shift-ledger-fallback-actions";
directoryHeader.appendChild(header);
}
if (!header || header.querySelector(".sla-shift-ledger-open")) return;
if (!header.querySelector(".sla-campaign-clock-open")) {
const clockBtn = document.createElement("button");
clockBtn.type = "button";
clockBtn.className = "sla-campaign-clock-open";
clockBtn.style.marginRight = "4px";
clockBtn.innerHTML = `<i class="fas fa-clock"></i> Campaign Clock`;
clockBtn.addEventListener("click", () => SlaCampaignClockApp.toggle());
header.appendChild(clockBtn);
}
if (game.user?.isGM && !header.querySelector(".sla-shift-ledger-open")) {
const button = document.createElement("button");
button.type = "button";
button.className = "sla-shift-ledger-open";
@@ -343,6 +396,7 @@ function installSceneButton(_app, html) {
button.addEventListener("click", () => SlaShiftLedgerApp.open());
header.appendChild(button);
}
}
function installSceneButtonFromSidebarTab(app, html) {
const root = getHtmlRoot(html);
@@ -564,9 +618,10 @@ Hooks.once("ready", () => {
getState,
getConfig,
advanceShift,
applyWeeklyBilling
applyWeeklyBilling,
openClock: () => SlaCampaignClockApp.open()
};
renderCampaignBanner();
SlaCampaignClockApp.open();
});
Hooks.on("renderSceneDirectory", installSceneButton);
+36
View File
@@ -332,3 +332,39 @@
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
/* Premium SLA Campaign Clock Window styling */
.sla-campaign-clock-window.window-app {
background: linear-gradient(180deg, rgba(17, 22, 31, 0.98), rgba(12, 16, 23, 0.96)) !important;
border: 1px solid rgba(124, 196, 255, 0.25) !important;
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.5) !important;
border-radius: 16px !important;
overflow: hidden !important;
}
.sla-campaign-clock-window .window-header {
border-bottom: 1px solid rgba(124, 196, 255, 0.15) !important;
background: rgba(17, 22, 31, 0.5) !important;
color: #7cc4ff !important;
font-family: "Josefin Sans", sans-serif !important;
letter-spacing: 0.05em !important;
padding: 6px 12px !important;
}
.sla-campaign-clock-window .window-content {
background: transparent !important;
padding: 18px !important;
color: #d9e7f7 !important;
}
.sla-campaign-clock-window .sla-shift-ledger-window-inner {
display: flex;
flex-direction: column;
}
.sla-campaign-clock-window .sla-shift-ledger-shift.is-active {
background: linear-gradient(180deg, rgba(77, 149, 221, 0.38), rgba(31, 93, 156, 0.42)) !important;
color: #ffffff !important;
border-color: rgba(146, 207, 255, 0.7) !important;
box-shadow: 0 0 10px rgba(124, 196, 255, 0.3) !important;
}