563 lines
19 KiB
TypeScript
563 lines
19 KiB
TypeScript
/**
|
|
* <folk-pubs-publish-panel> — Unified sidebar for sharing, DIY printing, and ordering.
|
|
*
|
|
* Three tabs: Share, DIY Print, Order.
|
|
* Replaces the old bare "Download PDF" link.
|
|
*
|
|
* Attributes:
|
|
* pdf-url — Blob URL of the generated PDF
|
|
* format-id — Selected format (a7, a6, quarter-letter, digest)
|
|
* format-name — Display name of the format
|
|
* page-count — Number of pages in the generated PDF
|
|
* space-slug — Current space slug for API calls
|
|
*/
|
|
|
|
export class FolkPubsPublishPanel extends HTMLElement {
|
|
private _pdfUrl = "";
|
|
private _formatId = "";
|
|
private _formatName = "";
|
|
private _pageCount = 0;
|
|
private _spaceSlug = "personal";
|
|
private _activeTab: "share" | "diy" | "order" = "share";
|
|
private _printers: any[] = [];
|
|
private _printersLoading = false;
|
|
private _printersError: string | null = null;
|
|
private _selectedProvider: any = null;
|
|
private _emailSending = false;
|
|
private _emailSent = false;
|
|
private _emailError: string | null = null;
|
|
private _impositionLoading = false;
|
|
private _orderStatus: string | null = null;
|
|
private _batchStatus: any = null;
|
|
|
|
static get observedAttributes() {
|
|
return ["pdf-url", "format-id", "format-name", "page-count", "space-slug"];
|
|
}
|
|
|
|
attributeChangedCallback(name: string, _old: string, val: string) {
|
|
if (name === "pdf-url") this._pdfUrl = val;
|
|
else if (name === "format-id") this._formatId = val;
|
|
else if (name === "format-name") this._formatName = val;
|
|
else if (name === "page-count") this._pageCount = parseInt(val) || 0;
|
|
else if (name === "space-slug") this._spaceSlug = val;
|
|
if (this.shadowRoot) this.render();
|
|
}
|
|
|
|
connectedCallback() {
|
|
this._pdfUrl = this.getAttribute("pdf-url") || "";
|
|
this._formatId = this.getAttribute("format-id") || "";
|
|
this._formatName = this.getAttribute("format-name") || "";
|
|
this._pageCount = parseInt(this.getAttribute("page-count") || "0") || 0;
|
|
this._spaceSlug = this.getAttribute("space-slug") || "personal";
|
|
if (!this.shadowRoot) this.attachShadow({ mode: "open" });
|
|
this.render();
|
|
}
|
|
|
|
private render() {
|
|
if (!this.shadowRoot) return;
|
|
this.shadowRoot.innerHTML = `
|
|
${this.getStyles()}
|
|
<div class="panel">
|
|
<div class="tabs">
|
|
<button class="tab ${this._activeTab === 'share' ? 'active' : ''}" data-tab="share">Share</button>
|
|
<button class="tab ${this._activeTab === 'diy' ? 'active' : ''}" data-tab="diy">DIY Print</button>
|
|
<button class="tab ${this._activeTab === 'order' ? 'active' : ''}" data-tab="order">Order</button>
|
|
</div>
|
|
<div class="tab-content">
|
|
${this._activeTab === 'share' ? this.renderShareTab() : ''}
|
|
${this._activeTab === 'diy' ? this.renderDiyTab() : ''}
|
|
${this._activeTab === 'order' ? this.renderOrderTab() : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
this.bindEvents();
|
|
}
|
|
|
|
private renderShareTab(): string {
|
|
return `
|
|
<div class="section">
|
|
<a class="action-btn primary" href="${this._pdfUrl}" download>Download PDF</a>
|
|
<button class="action-btn" data-action="copy-link">Copy Flipbook Link</button>
|
|
<div class="email-row">
|
|
<input type="email" class="email-input" placeholder="Email address" />
|
|
<button class="action-btn small" data-action="email-pdf" ${this._emailSending ? 'disabled' : ''}>
|
|
${this._emailSending ? 'Sending...' : 'Send'}
|
|
</button>
|
|
</div>
|
|
${this._emailSent ? '<div class="msg success">PDF sent!</div>' : ''}
|
|
${this._emailError ? `<div class="msg error">${this.esc(this._emailError)}</div>` : ''}
|
|
</div>
|
|
<div class="meta">
|
|
${this._formatName} · ${this._pageCount} pages
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderDiyTab(): string {
|
|
return `
|
|
<div class="section">
|
|
<button class="action-btn primary" data-action="download-imposition" ${this._impositionLoading ? 'disabled' : ''}>
|
|
${this._impositionLoading ? 'Generating...' : 'Download Imposition PDF'}
|
|
</button>
|
|
<p class="hint">Pre-arranged pages for double-sided printing & folding.</p>
|
|
<div class="guide-placeholder" data-guide-target></div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderOrderTab(): string {
|
|
if (this._selectedProvider) {
|
|
return this.renderProviderDetail();
|
|
}
|
|
|
|
return `
|
|
<div class="section">
|
|
<button class="action-btn primary" data-action="find-printers" ${this._printersLoading ? 'disabled' : ''}>
|
|
${this._printersLoading ? 'Searching...' : 'Find Nearby Printers'}
|
|
</button>
|
|
${this._printersError ? `<div class="msg error">${this.esc(this._printersError)}</div>` : ''}
|
|
${this._printers.length > 0 ? this.renderPrinterList() : ''}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderPrinterList(): string {
|
|
return `
|
|
<div class="printer-list">
|
|
${this._printers.map((p) => `
|
|
<button class="printer-card" data-provider-id="${this.esc(p.id)}">
|
|
<div class="printer-name">${this.esc(p.name)}</div>
|
|
<div class="printer-meta">
|
|
${this.esc(p.city)} · ${p.distance_km} km
|
|
${p.source === 'curated' ? '<span class="badge">curated</span>' : ''}
|
|
</div>
|
|
${p.tags?.length ? `<div class="printer-tags">${p.tags.map((t: string) => `<span class="tag">${this.esc(t)}</span>`).join('')}</div>` : ''}
|
|
${p.capabilities?.length ? `<div class="printer-caps">${p.capabilities.join(', ')}</div>` : ''}
|
|
</button>
|
|
`).join('')}
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private renderProviderDetail(): string {
|
|
const p = this._selectedProvider;
|
|
return `
|
|
<div class="section">
|
|
<button class="back-btn" data-action="back-to-list">← Back to results</button>
|
|
<div class="provider-detail">
|
|
<h4>${this.esc(p.name)}</h4>
|
|
<div class="provider-info">
|
|
${p.address ? `<div>${this.esc(p.address)}</div>` : ''}
|
|
${p.website ? `<div><a href="${this.esc(p.website)}" target="_blank" rel="noopener">${this.esc(p.website)}</a></div>` : ''}
|
|
${p.email ? `<div>${this.esc(p.email)}</div>` : ''}
|
|
${p.phone ? `<div>${this.esc(p.phone)}</div>` : ''}
|
|
${p.description ? `<div class="provider-desc">${this.esc(p.description)}</div>` : ''}
|
|
</div>
|
|
<button class="action-btn primary" data-action="place-order">Place Order</button>
|
|
<button class="action-btn" data-action="join-batch">Join Group Buy</button>
|
|
${this._orderStatus ? `<div class="msg success">${this.esc(this._orderStatus)}</div>` : ''}
|
|
${this._batchStatus ? `<div class="msg info">Batch: ${this._batchStatus.action} · ${this._batchStatus.participants || '?'} participants</div>` : ''}
|
|
</div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private bindEvents() {
|
|
if (!this.shadowRoot) return;
|
|
|
|
// Tab switching
|
|
this.shadowRoot.querySelectorAll(".tab").forEach((btn) => {
|
|
btn.addEventListener("click", () => {
|
|
this._activeTab = (btn as HTMLElement).dataset.tab as any;
|
|
this.render();
|
|
});
|
|
});
|
|
|
|
// Share tab actions
|
|
this.shadowRoot.querySelector('[data-action="copy-link"]')?.addEventListener("click", () => {
|
|
const url = `${window.location.origin}${window.location.pathname}`;
|
|
navigator.clipboard.writeText(url).then(() => {
|
|
const btn = this.shadowRoot!.querySelector('[data-action="copy-link"]')!;
|
|
btn.textContent = "Copied!";
|
|
setTimeout(() => { btn.textContent = "Copy Flipbook Link"; }, 2000);
|
|
});
|
|
});
|
|
|
|
this.shadowRoot.querySelector('[data-action="email-pdf"]')?.addEventListener("click", () => {
|
|
this.sendEmailPdf();
|
|
});
|
|
|
|
// DIY tab
|
|
this.shadowRoot.querySelector('[data-action="download-imposition"]')?.addEventListener("click", () => {
|
|
this.downloadImposition();
|
|
});
|
|
|
|
// Load guide content
|
|
const guideTarget = this.shadowRoot.querySelector('[data-guide-target]');
|
|
if (guideTarget && this._activeTab === 'diy') {
|
|
this.loadGuide(guideTarget as HTMLElement);
|
|
}
|
|
|
|
// Order tab
|
|
this.shadowRoot.querySelector('[data-action="find-printers"]')?.addEventListener("click", () => {
|
|
this.findPrinters();
|
|
});
|
|
|
|
this.shadowRoot.querySelectorAll(".printer-card").forEach((card) => {
|
|
card.addEventListener("click", () => {
|
|
const id = (card as HTMLElement).dataset.providerId;
|
|
this._selectedProvider = this._printers.find((p) => p.id === id) || null;
|
|
this.render();
|
|
});
|
|
});
|
|
|
|
this.shadowRoot.querySelector('[data-action="back-to-list"]')?.addEventListener("click", () => {
|
|
this._selectedProvider = null;
|
|
this._orderStatus = null;
|
|
this._batchStatus = null;
|
|
this.render();
|
|
});
|
|
|
|
this.shadowRoot.querySelector('[data-action="place-order"]')?.addEventListener("click", () => {
|
|
this.placeOrder();
|
|
});
|
|
|
|
this.shadowRoot.querySelector('[data-action="join-batch"]')?.addEventListener("click", () => {
|
|
this.joinBatch();
|
|
});
|
|
}
|
|
|
|
private async sendEmailPdf() {
|
|
const input = this.shadowRoot?.querySelector(".email-input") as HTMLInputElement;
|
|
const email = input?.value?.trim();
|
|
if (!email || !email.includes("@")) {
|
|
this._emailError = "Enter a valid email address";
|
|
this.render();
|
|
return;
|
|
}
|
|
|
|
this._emailSending = true;
|
|
this._emailError = null;
|
|
this._emailSent = false;
|
|
this.render();
|
|
|
|
try {
|
|
const res = await fetch(`/${this._spaceSlug}/rpubs/api/email-pdf`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
email,
|
|
format: this._formatId,
|
|
// Content will be re-read from the editor via a custom event
|
|
...this.getEditorContent(),
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json();
|
|
throw new Error(err.error || "Failed to send email");
|
|
}
|
|
|
|
this._emailSent = true;
|
|
} catch (e: any) {
|
|
this._emailError = e.message;
|
|
} finally {
|
|
this._emailSending = false;
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
private async downloadImposition() {
|
|
this._impositionLoading = true;
|
|
this.render();
|
|
|
|
try {
|
|
const res = await fetch(`/${this._spaceSlug}/rpubs/api/imposition`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
format: this._formatId,
|
|
...this.getEditorContent(),
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json();
|
|
throw new Error(err.error || "Imposition generation failed");
|
|
}
|
|
|
|
const blob = await res.blob();
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement("a");
|
|
a.href = url;
|
|
a.download = `imposition-${this._formatId}.pdf`;
|
|
a.click();
|
|
URL.revokeObjectURL(url);
|
|
} catch (e: any) {
|
|
console.error("[rpubs] Imposition error:", e);
|
|
} finally {
|
|
this._impositionLoading = false;
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
private async loadGuide(target: HTMLElement) {
|
|
if (!this._formatId) return;
|
|
|
|
// Dynamically import the guide
|
|
const { getGuide, recommendedBinding, paddedPageCount } = await import("../print-guides");
|
|
const guide = getGuide(this._formatId);
|
|
if (!guide) { target.innerHTML = '<p class="hint">No guide available for this format.</p>'; return; }
|
|
|
|
const binding = recommendedBinding(this._formatId, this._pageCount);
|
|
const padded = paddedPageCount(this._pageCount);
|
|
const sheets = Math.ceil(padded / guide.pagesPerSheet);
|
|
|
|
target.innerHTML = `
|
|
<div class="guide">
|
|
<h4>${guide.formatName} — DIY Guide</h4>
|
|
<div class="guide-stat">Sheets needed: ${sheets} (${guide.parentSheet})</div>
|
|
<div class="guide-stat">Binding: ${binding}</div>
|
|
<div class="guide-stat">Paper: ${guide.paperRecommendation}</div>
|
|
|
|
<h5>Tools</h5>
|
|
<ul>${guide.tools.map((t: string) => `<li>${t}</li>`).join('')}</ul>
|
|
|
|
<h5>Folding</h5>
|
|
<ol>${guide.foldInstructions.map((s: string) => `<li>${s}</li>`).join('')}</ol>
|
|
|
|
<h5>Binding</h5>
|
|
<ol>${guide.bindingInstructions.filter((s: string) => s).map((s: string) => `<li>${s.replace(/^\s+/, '')}</li>`).join('')}</ol>
|
|
|
|
<h5>Tips</h5>
|
|
<ul>${guide.tips.map((t: string) => `<li>${t}</li>`).join('')}</ul>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
private async findPrinters() {
|
|
this._printersLoading = true;
|
|
this._printersError = null;
|
|
this.render();
|
|
|
|
try {
|
|
const pos = await new Promise<GeolocationPosition>((resolve, reject) => {
|
|
navigator.geolocation.getCurrentPosition(resolve, reject, { timeout: 10000 });
|
|
});
|
|
|
|
const { latitude: lat, longitude: lng } = pos.coords;
|
|
const res = await fetch(
|
|
`/${this._spaceSlug}/rpubs/api/printers?lat=${lat}&lng=${lng}&radius=100&format=${this._formatId}`,
|
|
);
|
|
|
|
if (!res.ok) throw new Error("Failed to search printers");
|
|
const data = await res.json();
|
|
this._printers = data.providers || [];
|
|
} catch (e: any) {
|
|
if (e.code === 1) {
|
|
this._printersError = "Location access denied. Enable location to find nearby printers.";
|
|
} else {
|
|
this._printersError = e.message || "Search failed";
|
|
}
|
|
} finally {
|
|
this._printersLoading = false;
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
private async placeOrder() {
|
|
if (!this._selectedProvider) return;
|
|
try {
|
|
const res = await fetch(`/${this._spaceSlug}/rpubs/api/order`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
provider_id: this._selectedProvider.id,
|
|
provider_name: this._selectedProvider.name,
|
|
provider_distance_km: this._selectedProvider.distance_km,
|
|
total_price: 0,
|
|
currency: "USD",
|
|
format: this._formatId,
|
|
...this.getEditorContent(),
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json();
|
|
throw new Error(err.error || "Order failed");
|
|
}
|
|
|
|
const order = await res.json();
|
|
this._orderStatus = `Order created: ${order.id || 'confirmed'}`;
|
|
this.render();
|
|
} catch (e: any) {
|
|
this._orderStatus = `Error: ${e.message}`;
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
private async joinBatch() {
|
|
if (!this._selectedProvider) return;
|
|
try {
|
|
const res = await fetch(`/${this._spaceSlug}/rpubs/api/batch`, {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({
|
|
provider_id: this._selectedProvider.id,
|
|
provider_name: this._selectedProvider.name,
|
|
format: this._formatId,
|
|
...this.getEditorContent(),
|
|
}),
|
|
});
|
|
|
|
if (!res.ok) {
|
|
const err = await res.json();
|
|
throw new Error(err.error || "Batch operation failed");
|
|
}
|
|
|
|
this._batchStatus = await res.json();
|
|
this.render();
|
|
} catch (e: any) {
|
|
this._batchStatus = { action: "error", error: e.message };
|
|
this.render();
|
|
}
|
|
}
|
|
|
|
/** Get content from the parent editor by reading its textarea */
|
|
private getEditorContent(): { content: string; title?: string; author?: string } {
|
|
const editor = this.closest("folk-pubs-editor") || document.querySelector("folk-pubs-editor");
|
|
if (!editor?.shadowRoot) return { content: "" };
|
|
|
|
const textarea = editor.shadowRoot.querySelector(".content-area") as HTMLTextAreaElement;
|
|
const titleInput = editor.shadowRoot.querySelector(".title-input") as HTMLInputElement;
|
|
const authorInput = editor.shadowRoot.querySelector(".author-input") as HTMLInputElement;
|
|
|
|
return {
|
|
content: textarea?.value || "",
|
|
title: titleInput?.value?.trim() || undefined,
|
|
author: authorInput?.value?.trim() || undefined,
|
|
};
|
|
}
|
|
|
|
private esc(s: string): string {
|
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
}
|
|
|
|
private getStyles(): string {
|
|
return `<style>
|
|
:host { display: block; }
|
|
.panel { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
|
|
.tabs {
|
|
display: flex; gap: 0; border-bottom: 1px solid var(--rs-border-subtle, #333);
|
|
}
|
|
.tab {
|
|
flex: 1; padding: 0.4rem 0.5rem;
|
|
border: none; border-bottom: 2px solid transparent;
|
|
background: transparent;
|
|
color: var(--rs-text-secondary, #aaa);
|
|
font-size: 0.75rem; font-weight: 500; cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
.tab:hover { color: var(--rs-text-primary, #eee); }
|
|
.tab.active {
|
|
color: var(--rs-primary, #3b82f6);
|
|
border-bottom-color: var(--rs-primary, #3b82f6);
|
|
}
|
|
|
|
.tab-content { padding: 0.5rem 0; }
|
|
.section { display: flex; flex-direction: column; gap: 0.5rem; }
|
|
|
|
.action-btn {
|
|
display: block; width: 100%; text-align: center;
|
|
padding: 0.5rem; border-radius: 0.375rem;
|
|
border: 1px solid var(--rs-border, #444);
|
|
background: var(--rs-bg-surface, #2a2a2a);
|
|
color: var(--rs-text-primary, #eee);
|
|
font-size: 0.8rem; cursor: pointer;
|
|
text-decoration: none;
|
|
transition: all 0.15s;
|
|
}
|
|
.action-btn:hover { border-color: var(--rs-primary, #3b82f6); }
|
|
.action-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
.action-btn.primary {
|
|
background: var(--rs-primary, #3b82f6);
|
|
border-color: var(--rs-primary, #3b82f6);
|
|
color: #fff; font-weight: 600;
|
|
}
|
|
.action-btn.primary:hover { opacity: 0.9; }
|
|
.action-btn.small { width: auto; flex-shrink: 0; padding: 0.4rem 0.75rem; }
|
|
|
|
.email-row { display: flex; gap: 0.375rem; }
|
|
.email-input {
|
|
flex: 1; padding: 0.4rem 0.5rem;
|
|
border: 1px solid var(--rs-input-border, #444);
|
|
border-radius: 0.375rem;
|
|
background: var(--rs-input-bg, #1a1a2e);
|
|
color: var(--rs-input-text, #eee);
|
|
font-size: 0.8rem;
|
|
}
|
|
.email-input:focus { outline: none; border-color: var(--rs-primary, #3b82f6); }
|
|
.email-input::placeholder { color: var(--rs-text-muted, #666); }
|
|
|
|
.msg { font-size: 0.75rem; padding: 0.375rem 0.5rem; border-radius: 0.25rem; }
|
|
.msg.success { background: rgba(34, 197, 94, 0.15); color: var(--rs-success, #22c55e); }
|
|
.msg.error { background: rgba(248, 113, 113, 0.1); color: #f87171; }
|
|
.msg.info { background: rgba(59, 130, 246, 0.1); color: #60a5fa; }
|
|
|
|
.hint { font-size: 0.7rem; color: var(--rs-text-muted, #666); margin: 0; }
|
|
.meta { font-size: 0.7rem; color: var(--rs-text-secondary, #aaa); text-align: center; }
|
|
|
|
/* DIY guide */
|
|
.guide h4 { margin: 0.5rem 0 0.25rem; font-size: 0.85rem; color: var(--rs-text-primary, #eee); }
|
|
.guide h5 { margin: 0.75rem 0 0.25rem; font-size: 0.75rem; color: var(--rs-text-secondary, #aaa); text-transform: uppercase; letter-spacing: 0.05em; }
|
|
.guide ul, .guide ol { margin: 0; padding-left: 1.25rem; font-size: 0.75rem; color: var(--rs-text-primary, #ddd); line-height: 1.5; }
|
|
.guide li { margin-bottom: 0.25rem; }
|
|
.guide-stat { font-size: 0.75rem; color: var(--rs-text-secondary, #aaa); }
|
|
|
|
/* Printer list */
|
|
.printer-list { display: flex; flex-direction: column; gap: 0.375rem; max-height: 300px; overflow-y: auto; }
|
|
.printer-card {
|
|
text-align: left; padding: 0.5rem;
|
|
border: 1px solid var(--rs-border, #444);
|
|
border-radius: 0.375rem;
|
|
background: var(--rs-bg-surface, #2a2a2a);
|
|
color: var(--rs-text-primary, #eee);
|
|
cursor: pointer; transition: border-color 0.15s;
|
|
}
|
|
.printer-card:hover { border-color: var(--rs-primary, #3b82f6); }
|
|
.printer-name { font-size: 0.8rem; font-weight: 600; }
|
|
.printer-meta { font-size: 0.7rem; color: var(--rs-text-secondary, #aaa); }
|
|
.printer-tags { display: flex; gap: 0.25rem; flex-wrap: wrap; margin-top: 0.25rem; }
|
|
.tag {
|
|
font-size: 0.6rem; padding: 0.1rem 0.375rem;
|
|
border-radius: 1rem;
|
|
background: rgba(34, 197, 94, 0.15);
|
|
color: var(--rs-success, #22c55e);
|
|
}
|
|
.badge {
|
|
font-size: 0.6rem; padding: 0.1rem 0.375rem;
|
|
border-radius: 1rem;
|
|
background: rgba(59, 130, 246, 0.15);
|
|
color: #60a5fa;
|
|
}
|
|
.printer-caps { font-size: 0.65rem; color: var(--rs-text-muted, #666); margin-top: 0.2rem; }
|
|
|
|
/* Provider detail */
|
|
.back-btn {
|
|
background: none; border: none;
|
|
color: var(--rs-text-secondary, #aaa);
|
|
font-size: 0.75rem; cursor: pointer;
|
|
padding: 0; text-align: left;
|
|
}
|
|
.back-btn:hover { color: var(--rs-text-primary, #eee); }
|
|
.provider-detail h4 { margin: 0.5rem 0 0.375rem; font-size: 0.9rem; }
|
|
.provider-info { font-size: 0.75rem; color: var(--rs-text-secondary, #aaa); display: flex; flex-direction: column; gap: 0.2rem; margin-bottom: 0.5rem; }
|
|
.provider-info a { color: var(--rs-primary, #3b82f6); }
|
|
.provider-desc { font-size: 0.7rem; color: var(--rs-text-muted, #666); margin-top: 0.25rem; }
|
|
</style>`;
|
|
}
|
|
}
|
|
|
|
customElements.define("folk-pubs-publish-panel", FolkPubsPublishPanel);
|