import { FolkShape } from "./folk-shape"; import { css, html } from "./tags"; const styles = css` :host { background: white; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); min-width: 260px; min-height: 200px; } .header { display: flex; align-items: center; justify-content: space-between; padding: 8px 12px; background: #7c3aed; color: white; border-radius: 8px 8px 0 0; font-size: 12px; font-weight: 600; cursor: move; } .header-title { display: flex; align-items: center; gap: 6px; } .header-actions button { background: transparent; border: none; color: white; cursor: pointer; padding: 2px 6px; border-radius: 4px; font-size: 14px; } .header-actions button:hover { background: rgba(255, 255, 255, 0.2); } .progress-info { padding: 8px 12px; font-size: 11px; color: #64748b; display: flex; justify-content: space-between; align-items: center; border-bottom: 1px solid #e2e8f0; } .progress-bar { width: 80px; height: 6px; background: #e2e8f0; border-radius: 3px; overflow: hidden; } .progress-fill { height: 100%; background: #7c3aed; border-radius: 3px; transition: width 0.3s ease; } .packing-list { padding: 8px 12px; max-height: 300px; overflow-y: auto; } .category-label { font-size: 10px; font-weight: 600; color: #7c3aed; text-transform: uppercase; letter-spacing: 0.5px; padding: 6px 0 3px; } .item { display: flex; align-items: center; gap: 8px; padding: 4px 0; font-size: 12px; cursor: pointer; } .item:hover { background: #f8fafc; border-radius: 4px; padding: 4px 6px; margin: 0 -6px; } .checkbox { width: 16px; height: 16px; border: 2px solid #cbd5e1; border-radius: 4px; display: flex; align-items: center; justify-content: center; flex-shrink: 0; cursor: pointer; } .checkbox.checked { background: #7c3aed; border-color: #7c3aed; } .checkbox.checked::after { content: "\u2713"; color: white; font-size: 10px; font-weight: 700; } .item-name { color: #1e293b; } .item-name.packed { text-decoration: line-through; color: #94a3b8; } .item-qty { font-size: 10px; color: #94a3b8; margin-left: auto; } .add-form { padding: 8px 12px; border-top: 1px solid #e2e8f0; } .add-form input { width: 100%; padding: 6px 8px; border: 1px solid #e2e8f0; border-radius: 4px; font-size: 12px; outline: none; } .add-form input:focus { border-color: #7c3aed; } `; export interface PackingEntry { id: string; name: string; category?: string; packed: boolean; quantity: number; } declare global { interface HTMLElementTagNameMap { "folk-packing-list": FolkPackingList; } } export class FolkPackingList extends FolkShape { static override tagName = "folk-packing-list"; static { const sheet = new CSSStyleSheet(); const parentRules = Array.from(FolkShape.styles.cssRules) .map((r) => r.cssText) .join("\n"); const childRules = Array.from(styles.cssRules) .map((r) => r.cssText) .join("\n"); sheet.replaceSync(`${parentRules}\n${childRules}`); this.styles = sheet; } #items: PackingEntry[] = []; #listEl: HTMLElement | null = null; #progressTextEl: HTMLElement | null = null; #progressFillEl: HTMLElement | null = null; get items() { return this.#items; } set items(v: PackingEntry[]) { this.#items = v; this.#render(); this.dispatchEvent(new CustomEvent("content-change")); } get packedCount() { return this.#items.filter((i) => i.packed).length; } addItem(item: PackingEntry) { this.#items.push(item); this.#render(); this.dispatchEvent(new CustomEvent("content-change")); } toggleItem(id: string) { const item = this.#items.find((i) => i.id === id); if (item) { item.packed = !item.packed; this.#render(); this.dispatchEvent(new CustomEvent("content-change")); } } override createRenderRoot() { const root = super.createRenderRoot(); const wrapper = document.createElement("div"); wrapper.innerHTML = html`
\uD83C\uDF92 Packing List
`; const slot = root.querySelector("slot"); const containerDiv = slot?.parentElement as HTMLElement; if (containerDiv) { containerDiv.replaceWith(wrapper); } this.#listEl = wrapper.querySelector(".packing-list"); this.#progressTextEl = wrapper.querySelector(".progress-text"); this.#progressFillEl = wrapper.querySelector(".progress-fill"); const closeBtn = wrapper.querySelector(".close-btn") as HTMLButtonElement; const addInput = wrapper.querySelector(".add-input") as HTMLInputElement; closeBtn.addEventListener("click", (e) => { e.stopPropagation(); this.dispatchEvent(new CustomEvent("close")); }); addInput.addEventListener("keydown", (e) => { if (e.key === "Enter" && addInput.value.trim()) { e.stopPropagation(); this.addItem({ id: `pack-${Date.now()}`, name: addInput.value.trim(), packed: false, quantity: 1, }); addInput.value = ""; } }); addInput.addEventListener("click", (e) => e.stopPropagation()); this.#render(); return root; } #escapeHtml(text: string): string { const div = document.createElement("div"); div.textContent = text; return div.innerHTML; } #render() { if (!this.#listEl || !this.#progressTextEl || !this.#progressFillEl) return; const total = this.#items.length; const packed = this.packedCount; const pct = total > 0 ? (packed / total) * 100 : 0; this.#progressTextEl.textContent = `${packed}/${total} packed`; this.#progressFillEl.style.width = `${pct}%`; if (total === 0) { this.#listEl.innerHTML = '
No items yet
'; return; } // Group by category const groups = new Map(); for (const item of this.#items) { const cat = item.category || "Other"; if (!groups.has(cat)) groups.set(cat, []); groups.get(cat)!.push(item); } let result = ""; for (const [cat, items] of groups) { result += `
${this.#escapeHtml(cat)}
`; for (const item of items) { result += `
${this.#escapeHtml(item.name)} ${item.quantity > 1 ? `x${item.quantity}` : ""}
`; } } this.#listEl.innerHTML = result; // Add click handlers for checkboxes this.#listEl.querySelectorAll(".item").forEach((el) => { el.addEventListener("click", (e) => { e.stopPropagation(); const id = (el as HTMLElement).dataset.id; if (id) this.toggleItem(id); }); }); } override toJSON() { return { ...super.toJSON(), type: "folk-packing-list", items: this.#items, }; } }