Some checks are pending
CI / check (push) Waiting to run
- 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
94 lines
3.8 KiB
TypeScript
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);
|
|
}
|
|
}
|