From 8e5e0556bf8248f41247a7eff1a301aeb7ce4e05 Mon Sep 17 00:00:00 2001 From: "Tim Evans (slaguru666)" Date: Sun, 24 May 2026 22:20:18 +0100 Subject: [PATCH] feat: make SLA Campaign Clock movable, resizable, and minimizable popped-out window app --- module.json | 2 +- module/sla-shift-ledger.mjs | 147 +++++++++++++++++++++++++----------- styles/sla-shift-ledger.css | 36 +++++++++ 3 files changed, 138 insertions(+), 47 deletions(-) diff --git a/module.json b/module.json index 370ba44..432a8b8 100644 --- a/module.json +++ b/module.json @@ -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", diff --git a/module/sla-shift-ledger.mjs b/module/sla-shift-ledger.mjs index e6fe153..3c767cd 100644 --- a/module/sla-shift-ledger.mjs +++ b/module/sla-shift-ledger.mjs @@ -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 = ` -
-
-
SLA Campaign Clock
-
Day ${state.day} · Week ${weekNumber}
-
${currentShift.label}
-
-
-
- ${SHIFTS.map((shift, index) => ` -
- ${shift.short} -
- `).join("")} + const html = ` +
+
+
SLA Campaign Clock
+
Day ${state.day} · Week ${weekNumber}
+
${currentShift.label}
-
- ${game.user?.isGM ? ` - - - - ` : ""} +
+
+ ${SHIFTS.map((shift, index) => ` +
+ ${shift.short} +
+ `).join("")} +
+
+ ${game.user?.isGM ? ` + + + + ` : ""} +
-
- `; + `; + 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 = ` 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 = ` 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 = ` 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); diff --git a/styles/sla-shift-ledger.css b/styles/sla-shift-ledger.css index e4ad11d..2c5733f 100644 --- a/styles/sla-shift-ledger.css +++ b/styles/sla-shift-ledger.css @@ -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; +}