/** * — Markdown editor with format selector and PDF generation. * * Drop in markdown text, pick a pocket-book format, generate a print-ready PDF. * Supports file drag-and-drop, sample content, and PDF preview/download. */ interface BookFormat { id: string; name: string; widthMm: number; heightMm: number; description: string; minPages: number; maxPages: number; } const SAMPLE_CONTENT = `# The Commons ## What Are Commons? Commons are shared resources managed by communities. They can be natural resources like forests, fisheries, and water systems, or digital resources like open-source software, Wikipedia, and creative works. The concept of the commons has deep historical roots. In medieval England, common land was shared among villagers for grazing animals, gathering firewood, and growing food. These weren't unmanaged free-for-alls — they operated under sophisticated rules developed over generations. ## Elinor Ostrom's Design Principles Elinor Ostrom, who won the Nobel Prize in Economics in 2009, identified eight design principles for successful commons governance: 1. Clearly defined boundaries 2. Rules adapted to local conditions 3. Collective-choice arrangements 4. Monitoring by community members 5. Graduated sanctions for rule violators 6. Accessible conflict-resolution mechanisms 7. Recognition of the right to organize 8. Nested enterprises for larger-scale resources > The tragedy of the commons is not inevitable. Communities around the world have managed shared resources sustainably for centuries when given the tools and authority to do so. ## The Digital Commons The internet has created entirely new forms of commons. Open-source software, Creative Commons licensing, and collaborative platforms demonstrate that the commons model scales beyond physical resources. --- These ideas matter because they challenge the assumption that only private ownership or government control can manage resources effectively. The commons represent a third way — community governance of shared wealth.`; export class FolkPubsEditor extends HTMLElement { private _formats: BookFormat[] = []; private _spaceSlug = "personal"; private _selectedFormat = "digest"; private _loading = false; private _error: string | null = null; private _pdfUrl: string | null = null; private _pdfInfo: string | null = null; set formats(val: BookFormat[]) { this._formats = val; if (this.shadowRoot) this.render(); } set spaceSlug(val: string) { this._spaceSlug = val; } connectedCallback() { this.attachShadow({ mode: "open" }); this.render(); } disconnectedCallback() { if (this._pdfUrl) URL.revokeObjectURL(this._pdfUrl); } private render() { if (!this.shadowRoot) return; this.shadowRoot.innerHTML = ` ${this.getStyles()}
`; this.bindEvents(); } private bindEvents() { if (!this.shadowRoot) return; const textarea = this.shadowRoot.querySelector(".content-area") as HTMLTextAreaElement; const titleInput = this.shadowRoot.querySelector(".title-input") as HTMLInputElement; const authorInput = this.shadowRoot.querySelector(".author-input") as HTMLInputElement; const generateBtn = this.shadowRoot.querySelector(".btn-generate") as HTMLButtonElement; const sampleBtn = this.shadowRoot.querySelector(".btn-sample"); const fileInput = this.shadowRoot.querySelector('input[type="file"]') as HTMLInputElement; // Format buttons this.shadowRoot.querySelectorAll(".format-btn").forEach((btn) => { btn.addEventListener("click", () => { this._selectedFormat = (btn as HTMLElement).dataset.format!; // Clear previous PDF on format change if (this._pdfUrl) { URL.revokeObjectURL(this._pdfUrl); this._pdfUrl = null; this._pdfInfo = null; } this._error = null; this.render(); }); }); // Sample content sampleBtn?.addEventListener("click", () => { textarea.value = SAMPLE_CONTENT; titleInput.value = ""; authorInput.value = ""; }); // File upload fileInput?.addEventListener("change", () => { const file = fileInput.files?.[0]; if (file) { const reader = new FileReader(); reader.onload = () => { textarea.value = reader.result as string; }; reader.readAsText(file); } }); // Drag-and-drop textarea?.addEventListener("dragover", (e) => { e.preventDefault(); textarea.classList.add("dragover"); }); textarea?.addEventListener("dragleave", () => { textarea.classList.remove("dragover"); }); textarea?.addEventListener("drop", (e) => { e.preventDefault(); textarea.classList.remove("dragover"); const file = (e as DragEvent).dataTransfer?.files[0]; if (file && (file.type.startsWith("text/") || file.name.match(/\.(md|txt|markdown)$/))) { const reader = new FileReader(); reader.onload = () => { textarea.value = reader.result as string; }; reader.readAsText(file); } }); // Generate PDF generateBtn?.addEventListener("click", async () => { const content = textarea.value.trim(); if (!content) { this._error = "Please enter some content first."; this.render(); return; } this._loading = true; this._error = null; if (this._pdfUrl) URL.revokeObjectURL(this._pdfUrl); this._pdfUrl = null; this.render(); try { const res = await fetch(`/${this._spaceSlug}/pubs/api/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content, title: titleInput.value.trim() || undefined, author: authorInput.value.trim() || undefined, format: this._selectedFormat, }), }); if (!res.ok) { const err = await res.json(); throw new Error(err.error || "Generation failed"); } const blob = await res.blob(); const pageCount = res.headers.get("X-Page-Count") || "?"; const format = this._formats.find((f) => f.id === this._selectedFormat); this._pdfUrl = URL.createObjectURL(blob); this._pdfInfo = `${pageCount} pages · ${format?.name || this._selectedFormat}`; this._loading = false; this.render(); } catch (e: any) { this._loading = false; this._error = e.message; this.render(); } }); } private getStyles(): string { return ``; } private escapeHtml(s: string): string { return s.replace(/&/g, "&").replace(//g, ">"); } } customElements.define("folk-pubs-editor", FolkPubsEditor);