Mats Bosson bf576905a7
Some checks are pending
CI / check (push) Waiting to run
All components, schemas, tests, MCP, and showcase
- 51 headless Web Components (45 core + 6 animation)
- Shared helpers: emit(), part(), listen(), wireLabel(), initialValue()
- Zero `new CustomEvent` or `static #counter` — all use shared utils
- Zod schemas for all 44 core components
- MCP package with discover, inspect, compose, validate tools
- Showcase with Aperture Science theme, M3 Expressive motion
- 81 tests passing, TypeScript strict mode clean
- Signals (~500B), SPA router (~400B), zero dependencies
2026-03-31 20:21:41 +07:00

94 lines
3.8 KiB
TypeScript

import { signal, effect } from "../../signals";
import { emit } from "../../shared/helpers";
/** PettyCalendar — month grid with day selection and month navigation. */
export class PettyCalendar extends HTMLElement {
static observedAttributes = ["value", "min", "max"];
readonly #month = signal(new Date().getMonth());
readonly #year = signal(new Date().getFullYear());
readonly #selected = signal("");
#stopEffect: (() => void) | null = null;
get value(): string { return this.#selected.get(); }
set value(v: string) { this.#selected.set(v); }
connectedCallback(): void {
const init = this.getAttribute("value") ?? "";
if (init) { this.#selected.set(init); this.#parseMonth(init); }
this.#stopEffect = effect(() => this.#render());
this.querySelector("[data-part=prev-month]")?.addEventListener("click", this.#onPrev);
this.querySelector("[data-part=next-month]")?.addEventListener("click", this.#onNext);
this.addEventListener("click", this.#onDayClick);
}
disconnectedCallback(): void {
this.#stopEffect = null;
this.querySelector("[data-part=prev-month]")?.removeEventListener("click", this.#onPrev);
this.querySelector("[data-part=next-month]")?.removeEventListener("click", this.#onNext);
this.removeEventListener("click", this.#onDayClick);
}
attributeChangedCallback(name: string, _old: string | null, next: string | null): void {
if (name === "value" && next) { this.#selected.set(next); this.#parseMonth(next); }
}
#parseMonth(dateStr: string): void {
const d = new Date(dateStr);
if (!Number.isNaN(d.getTime())) { this.#month.set(d.getMonth()); this.#year.set(d.getFullYear()); }
}
#onPrev = (): void => {
if (this.#month.get() === 0) { this.#month.set(11); this.#year.set(this.#year.get() - 1); }
else this.#month.set(this.#month.get() - 1);
};
#onNext = (): void => {
if (this.#month.get() === 11) { this.#month.set(0); this.#year.set(this.#year.get() + 1); }
else this.#month.set(this.#month.get() + 1);
};
#onDayClick = (e: Event): void => {
const btn = (e.target as HTMLElement).closest("[data-date]");
if (!btn || btn.hasAttribute("data-disabled")) return;
const date = (btn as HTMLElement).dataset.date ?? "";
this.#selected.set(date);
emit(this, "change", { value: date });
};
#createDayCell(day: number, iso: string, sel: string, today: string): HTMLTableCellElement {
const td = document.createElement("td");
const btn = document.createElement("button");
btn.dataset.date = iso;
btn.dataset.part = "day";
btn.setAttribute("role", "gridcell");
btn.textContent = String(day);
if (iso === sel) btn.dataset.state = "selected";
if (iso === today) btn.dataset.state = btn.dataset.state ? `${btn.dataset.state} today` : "today";
td.appendChild(btn);
return td;
}
#render(): void {
const m = this.#month.get();
const y = this.#year.get();
const sel = this.#selected.get();
const title = this.querySelector("[data-part=title]");
if (title) title.textContent = `${new Date(y, m).toLocaleString("default", { month: "long" })} ${y}`;
const body = this.querySelector("[data-part=body]");
if (!body) return;
const today = new Date().toISOString().slice(0, 10);
const firstDay = new Date(y, m, 1).getDay();
const daysInMonth = new Date(y, m + 1, 0).getDate();
body.replaceChildren();
let row = document.createElement("tr");
for (let i = 0; i < firstDay; i++) row.appendChild(document.createElement("td"));
for (let d = 1; d <= daysInMonth; d++) {
const iso = `${y}-${String(m + 1).padStart(2, "0")}-${String(d).padStart(2, "0")}`;
row.appendChild(this.#createDayCell(d, iso, sel, today));
if ((firstDay + d) % 7 === 0) { body.appendChild(row); row = document.createElement("tr"); }
}
if (row.children.length > 0) body.appendChild(row);
}
}