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
+101 -46
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
});
}
const state = getState();
const weekNumber = getWeekFromDay(state.day);
const currentShift = getShift(state.shift);
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">
<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>
<div class="sla-shift-ledger-banner-main">
<div class="sla-shift-ledger-shifts">
${SHIFTS.map((shift, index) => `
<div class="sla-shift-ledger-shift ${index === Number(state.shift) ? "is-active" : ""}">
${shift.short}
</div>
`).join("")}
const html = `
<div class="sla-shift-ledger-window-inner">
<div class="sla-shift-ledger-banner-meta">
<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-controls">
${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>
` : ""}
<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" : ""}" 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" 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"><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>
</div>
`;
`;
return $(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());
activateListeners(html) {
super.activateListeners(html);
if (game.user?.isGM) {
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,13 +377,25 @@ 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;
const button = document.createElement("button");
button.type = "button";
button.className = "sla-shift-ledger-open";
button.innerHTML = `<i class="fas fa-calendar-days"></i> Shift Ledger`;
button.addEventListener("click", () => SlaShiftLedgerApp.open());
header.appendChild(button);
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";
button.innerHTML = `<i class="fas fa-calendar-days"></i> Shift Ledger`;
button.addEventListener("click", () => SlaShiftLedgerApp.open());
header.appendChild(button);
}
}
function installSceneButtonFromSidebarTab(app, html) {
@@ -564,9 +618,10 @@ Hooks.once("ready", () => {
getState,
getConfig,
advanceShift,
applyWeeklyBilling
applyWeeklyBilling,
openClock: () => SlaCampaignClockApp.open()
};
renderCampaignBanner();
SlaCampaignClockApp.open();
});
Hooks.on("renderSceneDirectory", installSceneButton);