First commit, done on M6 from the PLAN.MD

This commit is contained in:
gauvainboiche
2026-10-25 17:34:44 +01:00
parent d5dc2a3901
commit 04193e2013
86 changed files with 8396 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
// Centre folder: the weekly decision card as a paper document with two rubber
// stamps. Also shows the idle and end-of-term messages.
import { t, tx, dateParts } from "../i18n/i18n.js";
import { dateOfDay } from "../engine/index.js";
import { $, esc } from "./desk.js";
const STAMP_MS = 750; // how long the stamp animation plays before the answer is applied
export function createDecision(root, { cardsById, characters, onAnswer, onProfile, keysEnabled = () => true }) {
root.innerHTML = `
<h2 class="inbox__tab">${t("inbox.title")}</h2>
<div class="inbox__body"></div>`;
const body = $(root, ".inbox__body");
let shown = "";
let locked = false;
let latest = null;
function choose(choice) {
if (locked) return;
const card = $(body, ".card");
if (!card) return;
locked = true;
card.dataset.stamped = choice;
for (const b of card.querySelectorAll("button")) b.disabled = true;
const stamp = document.createElement("div");
stamp.className = `card__stamp card__stamp--${choice}`;
stamp.textContent = t(choice === "yes" ? "card.approved" : "card.rejected");
card.append(stamp);
setTimeout(() => onAnswer(choice), STAMP_MS);
}
body.addEventListener("click", (e) => {
const b = e.target.closest("button[data-choice]");
if (b) choose(b.dataset.choice);
const p = e.target.closest("button[data-pronouns]");
if (p) onProfile(p.dataset.pronouns);
});
// The panel is rebuilt on language changes and new games, so its key listener must be removable.
const onKey = (e) => {
if (!keysEnabled() || !latest?.pendingCard) return;
if (latest.pending.review || latest.pending.profile || latest.pending.alert) return;
if (e.ctrlKey || e.metaKey || e.altKey) return;
if (e.target.closest?.("input, textarea, select")) return;
const k = e.key.toLowerCase();
if (k === "y") choose("yes");
else if (k === "n") choose("no");
};
document.addEventListener("keydown", onKey);
function option(card, side) {
const label = side === "yes" ? t("card.approved") : t("card.rejected");
return `
<button class="stamp-btn stamp-btn--${side}" data-choice="${side}">
<span class="stamp-btn__label">${esc(tx(card[side].label))}</span>
<span class="stamp-btn__ink">${esc(label)}</span>
</button>`;
}
function cardHtml(card, state) {
const from = characters[card.sender];
const date = dateParts(dateOfDay(state.startDate, state.pendingCard.day).iso);
const paragraphs = tx(card.body).split("\n\n").map((p) => `<p>${esc(p)}</p>`).join("");
return `
<article class="card card--${esc(card.doc)}">
<header class="card__head">
<span class="card__kind">${t("kind." + card.doc)}</span>
<span class="card__date">${esc(date.day)} ${esc(date.month)} ${esc(date.year)}</span>
</header>
<div class="card__from">
<span class="seal">${esc(from.seal)}</span>
<span><b>${esc(tx(from.name))}</b><small>${esc(tx(from.role))}</small></span>
</div>
<h3 class="card__title">${esc(tx(card.title))}</h3>
<div class="card__body">${paragraphs}</div>
<div class="card__choices">${option(card, "yes")}${option(card, "no")}</div>
<p class="card__keys">${t("card.keys")}</p>
</article>`;
}
function update(state) {
latest = state;
let kind = "idle";
if (state.over) kind = "over";
else if (state.pending.profile) kind = "profile";
else if (state.pendingCard) kind = `card:${state.pendingCard.id}`;
if (kind === shown) return;
shown = kind;
locked = false;
body.classList.toggle("is-card", kind.startsWith("card:"));
if (kind === "profile") {
body.innerHTML = `
<h3>${t("profile.title")}</h3>
<p>${t("profile.lead")}</p>
<p class="profile__ask">${t("profile.body")}</p>
<div class="profile__choices">
${["he", "she", "they"].map((p) => `<button class="brass brass--wide" data-pronouns="${p}">${t("profile." + p)}</button>`).join("")}
</div>
<p class="profile__note">${t("profile.note")}</p>`;
} else if (kind === "idle") {
body.innerHTML = `<p class="inbox__idle">${t("inbox.idle")}</p>`;
} else if (kind === "over") {
body.innerHTML = `<h3>${t("inbox.overTitle")}</h3><p>${t("inbox.overBody")}</p>`;
} else {
body.innerHTML = cardHtml(cardsById.get(state.pendingCard.id), state);
}
}
return { update, destroy: () => document.removeEventListener("keydown", onKey) };
}