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", "id": "sla-shift-ledger",
"title": "SLA Shift Ledger", "title": "SLA Shift Ledger",
"description": "Tracks SLA campaign day shifts, actor duty schedules, housing tiers, and weekly living-cost ledgers.", "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", "url": "https://github.com/slaguru666/sla-shift-ledger",
"manifest": "https://raw.githubusercontent.com/slaguru666/sla-shift-ledger/main/module.json", "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", "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)}.`); ui.notifications.info(`Campaign time advanced to ${buildCampaignStamp(nextState)}.`);
} }
function renderCampaignBanner() { class SlaCampaignClockApp extends Application {
let host = document.querySelector("#sla-shift-ledger-banner"); static get defaultOptions() {
if (!host) { return foundry.utils.mergeObject(super.defaultOptions, {
host = document.createElement("section"); id: "sla-campaign-clock-app",
host.id = "sla-shift-ledger-banner"; title: "SLA Campaign Clock",
host.className = "sla-shift-ledger-banner"; classes: ["sla-campaign-clock-window", "mosh"],
document.body.appendChild(host); width: 580,
height: "auto",
resizable: true,
minimizable: true,
popOut: true
});
} }
async _renderInner(data) {
const state = getState(); const state = getState();
const weekNumber = getWeekFromDay(state.day); const weekNumber = getWeekFromDay(state.day);
const currentShift = getShift(state.shift); const currentShift = getShift(state.shift);
host.innerHTML = ` const html = `
<div class="sla-shift-ledger-banner-inner"> <div class="sla-shift-ledger-window-inner">
<div class="sla-shift-ledger-banner-meta"> <div class="sla-shift-ledger-banner-meta">
<div class="sla-shift-ledger-banner-kicker">SLA Campaign Clock</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">Day ${state.day} · Week ${weekNumber}</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">${currentShift.label}</div> <div class="sla-shift-ledger-banner-subtitle" style="font-size: 0.85rem; color: #9eb5cf; margin-top: 2px;">${currentShift.label}</div>
</div> </div>
<div class="sla-shift-ledger-banner-main"> <div class="sla-shift-ledger-banner-main" style="margin-top: 15px;">
<div class="sla-shift-ledger-shifts"> <div class="sla-shift-ledger-shifts" style="display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 8px;">
${SHIFTS.map((shift, index) => ` ${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} ${shift.short}
</div> </div>
`).join("")} `).join("")}
</div> </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 ? ` ${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="prev"><i class="fas fa-arrow-left"></i> 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="next">Next <i class="fas fa-arrow-right"></i></button>
<button type="button" class="sla-shift-ledger-button" data-action="open">Open</button> <button type="button" class="sla-shift-ledger-button" data-action="open"><i class="fas fa-cog"></i> Config</button>
` : ""} ` : ""}
</div> </div>
</div> </div>
</div> </div>
`; `;
return $(html);
}
activateListeners(html) {
super.activateListeners(html);
if (game.user?.isGM) { if (game.user?.isGM) {
host.querySelector("[data-action='prev']")?.addEventListener("click", () => advanceShift(-1)); html.find("[data-action='prev']").on("click", () => advanceShift(-1));
host.querySelector("[data-action='next']")?.addEventListener("click", () => advanceShift(1)); html.find("[data-action='next']").on("click", () => advanceShift(1));
host.querySelector("[data-action='open']")?.addEventListener("click", () => SlaShiftLedgerApp.open()); 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) { function installSceneButton(_app, html) {
if (!game.user?.isGM) return;
const root = getHtmlRoot(html); const root = getHtmlRoot(html);
const directoryHeader = root?.querySelector(".directory-header"); const directoryHeader = root?.querySelector(".directory-header");
if (!directoryHeader) return; if (!directoryHeader) return;
@@ -335,7 +377,18 @@ function installSceneButton(_app, html) {
header.className = "header-actions sla-shift-ledger-fallback-actions"; header.className = "header-actions sla-shift-ledger-fallback-actions";
directoryHeader.appendChild(header); 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"); const button = document.createElement("button");
button.type = "button"; button.type = "button";
button.className = "sla-shift-ledger-open"; button.className = "sla-shift-ledger-open";
@@ -343,6 +396,7 @@ function installSceneButton(_app, html) {
button.addEventListener("click", () => SlaShiftLedgerApp.open()); button.addEventListener("click", () => SlaShiftLedgerApp.open());
header.appendChild(button); header.appendChild(button);
} }
}
function installSceneButtonFromSidebarTab(app, html) { function installSceneButtonFromSidebarTab(app, html) {
const root = getHtmlRoot(html); const root = getHtmlRoot(html);
@@ -564,9 +618,10 @@ Hooks.once("ready", () => {
getState, getState,
getConfig, getConfig,
advanceShift, advanceShift,
applyWeeklyBilling applyWeeklyBilling,
openClock: () => SlaCampaignClockApp.open()
}; };
renderCampaignBanner(); SlaCampaignClockApp.open();
}); });
Hooks.on("renderSceneDirectory", installSceneButton); Hooks.on("renderSceneDirectory", installSceneButton);
+36
View File
@@ -332,3 +332,39 @@
grid-template-columns: repeat(2, minmax(0, 1fr)); 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;
}