feat(rpubs): polish publish flow with teal accents, SVG icons, and card layouts

Visual overhaul of the rpubs editor and publish panel to match rpubs.online/press
design language: circular step indicators with checkmarks, SVG stroke icons on all
buttons, card-based publish panel with teal accent colors, pricing tier cards,
numbered DIY guide steps, group buy progress bar, and format info chips.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-21 17:04:19 -07:00
parent 0067369af1
commit 2ab620fcc5
3 changed files with 761 additions and 139 deletions

View File

@ -64,6 +64,14 @@ These ideas matter because they challenge the assumption that only private owner
const SYNC_DEBOUNCE_MS = 800; const SYNC_DEBOUNCE_MS = 800;
// ── SVG Icons ──
const SVG_CHECK = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`;
const SVG_ARROW_RIGHT = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="5" y1="12" x2="19" y2="12"/><polyline points="12 5 19 12 12 19"/></svg>`;
const SVG_ARROW_LEFT = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><line x1="19" y1="12" x2="5" y2="12"/><polyline points="12 19 5 12 12 5"/></svg>`;
const SVG_EXPAND = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 3 21 3 21 9"/><polyline points="9 21 3 21 3 15"/><line x1="21" y1="3" x2="14" y2="10"/><line x1="3" y1="21" x2="10" y2="14"/></svg>`;
const SVG_DOWNLOAD = `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`;
const SVG_SPINNER = `<span class="btn-spinner"></span>`;
export class FolkPubsEditor extends HTMLElement { export class FolkPubsEditor extends HTMLElement {
private _formats: BookFormat[] = []; private _formats: BookFormat[] = [];
private _spaceSlug = "personal"; private _spaceSlug = "personal";
@ -451,18 +459,18 @@ export class FolkPubsEditor extends HTMLElement {
<!-- Step indicator --> <!-- Step indicator -->
<div class="step-indicator"> <div class="step-indicator">
<div class="steps"> <div class="steps">
<button class="step ${this._view === 'write' ? 'active' : ''} ${this._pdfUrl ? 'completed' : ''}" data-step="write"> <button class="step ${this._view === 'write' ? 'active' : ''} ${this._pdfUrl && this._view !== 'write' ? 'done' : ''}" data-step="write">
<span class="step-num">${this._pdfUrl ? '&#10003;' : '1'}</span> <span class="step-circle">${this._pdfUrl && this._view !== 'write' ? SVG_CHECK : '1'}</span>
<span class="step-label">Create</span> <span class="step-label">Create</span>
</button> </button>
<div class="step-line ${this._view !== 'write' ? 'filled' : ''}"></div> <div class="step-line ${this._view !== 'write' ? 'filled' : ''}"></div>
<button class="step ${this._view === 'preview' ? 'active' : ''} ${this._view === 'publish' ? 'completed' : ''}" data-step="preview" ${!this._pdfUrl ? 'disabled' : ''}> <button class="step ${this._view === 'preview' ? 'active' : ''} ${this._view === 'publish' ? 'done' : ''}" data-step="preview" ${!this._pdfUrl ? 'disabled' : ''}>
<span class="step-num">${this._view === 'publish' ? '&#10003;' : '2'}</span> <span class="step-circle">${this._view === 'publish' ? SVG_CHECK : '2'}</span>
<span class="step-label">Preview</span> <span class="step-label">Preview</span>
</button> </button>
<div class="step-line ${this._view === 'publish' ? 'filled' : ''}"></div> <div class="step-line ${this._view === 'publish' ? 'filled' : ''}"></div>
<button class="step ${this._view === 'publish' ? 'active' : ''}" data-step="publish" ${!this._pdfUrl ? 'disabled' : ''}> <button class="step ${this._view === 'publish' ? 'active' : ''}" data-step="publish" ${!this._pdfUrl ? 'disabled' : ''}>
<span class="step-num">3</span> <span class="step-circle">3</span>
<span class="step-label">Publish</span> <span class="step-label">Publish</span>
</button> </button>
</div> </div>
@ -499,7 +507,9 @@ export class FolkPubsEditor extends HTMLElement {
</div> </div>
${this._error ? `<div class="inline-error">${this.escapeHtml(this._error)}</div>` : ''} ${this._error ? `<div class="inline-error">${this.escapeHtml(this._error)}</div>` : ''}
<button class="btn-generate" ${this._loading ? "disabled" : ""}> <button class="btn-generate" ${this._loading ? "disabled" : ""}>
${this._loading ? "Generating..." : "Generate Preview \u2192"} ${this._loading
? `${SVG_SPINNER} Generating...`
: `Generate Preview ${SVG_ARROW_RIGHT}`}
</button> </button>
</div> </div>
</div> </div>
@ -507,19 +517,22 @@ export class FolkPubsEditor extends HTMLElement {
} }
private renderPreviewStep(): string { private renderPreviewStep(): string {
const currentFormat = this._formats.find(f => f.id === this._selectedFormat);
const dims = currentFormat ? `${currentFormat.widthMm}\u00D7${currentFormat.heightMm}mm` : '';
return ` return `
<div class="preview-step"> <div class="preview-step">
<div class="preview-actions"> <div class="preview-actions">
<button class="action-btn" data-action="back-to-write">\u2190 Edit</button> <button class="action-btn" data-action="back-to-write">${SVG_ARROW_LEFT} Edit</button>
<button class="action-btn" data-action="fullscreen-toggle">Fullscreen</button> <button class="action-btn" data-action="fullscreen-toggle">${SVG_EXPAND} Fullscreen</button>
<a class="action-btn" href="${this._pdfUrl}" download>Download PDF</a> <a class="action-btn" href="${this._pdfUrl}" download>${SVG_DOWNLOAD} Download</a>
${currentFormat ? `<span class="format-chip">${this.escapeHtml(currentFormat.name)} \u00B7 ${dims}</span>` : ''}
</div> </div>
<div class="preview-flipbook"> <div class="preview-flipbook">
<folk-pubs-flipbook pdf-url="${this._pdfUrl}"></folk-pubs-flipbook> <folk-pubs-flipbook pdf-url="${this._pdfUrl}"></folk-pubs-flipbook>
</div> </div>
<div class="preview-bottom-bar"> <div class="preview-bottom-bar">
<span class="preview-info">${this._pdfInfo || ''}</span> <span class="preview-info">${this._pdfInfo || ''}</span>
<button class="btn-publish-next">Publish \u2192</button> <button class="btn-publish-next">Publish ${SVG_ARROW_RIGHT}</button>
</div> </div>
</div> </div>
`; `;
@ -942,7 +955,7 @@ export class FolkPubsEditor extends HTMLElement {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0.5rem 1rem; padding: 0.625rem 1rem;
border-bottom: 1px solid var(--rs-border-subtle); border-bottom: 1px solid var(--rs-border-subtle);
background: var(--rs-bg-surface); background: var(--rs-bg-surface);
} }
@ -956,39 +969,56 @@ export class FolkPubsEditor extends HTMLElement {
.step { .step {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.375rem; gap: 0.5rem;
padding: 0.35rem 0.75rem; padding: 0.25rem 0.5rem;
border: 1px solid var(--rs-border); border: none;
border-radius: 2rem; border-radius: 0;
background: transparent; background: transparent;
color: var(--rs-text-muted); color: var(--rs-text-muted);
font-size: 0.78rem; font-size: 0.78rem;
cursor: pointer; cursor: pointer;
transition: all 0.15s; transition: all 0.15s;
} }
.step:disabled { cursor: not-allowed; opacity: 0.4; } .step:disabled { cursor: not-allowed; opacity: 0.35; }
.step:not(:disabled):hover { border-color: var(--rs-primary); color: var(--rs-text-primary); } .step:not(:disabled):hover { color: var(--rs-text-primary); }
.step.active { .step:not(:disabled):hover .step-circle { border-color: var(--rs-accent, #14b8a6); }
background: var(--rs-primary); .step.active .step-circle {
border-color: var(--rs-primary); background: var(--rs-accent, #14b8a6);
border-color: var(--rs-accent, #14b8a6);
color: #fff; color: #fff;
font-weight: 600;
} }
.step.completed:not(.active) { .step.active { color: var(--rs-text-primary); font-weight: 600; }
border-color: var(--rs-success, #22c55e); .step.done .step-circle {
color: var(--rs-success, #22c55e); background: var(--rs-accent, #14b8a6);
border-color: var(--rs-accent, #14b8a6);
color: #fff;
} }
.step.done { color: var(--rs-accent, #14b8a6); }
.step-num { font-weight: 700; font-size: 0.75rem; } .step-circle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
border-radius: 50%;
border: 2px solid var(--rs-border, #444);
background: transparent;
color: var(--rs-text-muted);
font-size: 0.75rem;
font-weight: 700;
transition: all 0.15s;
flex-shrink: 0;
}
.step-line { .step-line {
width: 24px; width: 32px;
height: 2px; height: 2px;
background: var(--rs-border); background: var(--rs-border);
margin: 0 0.25rem; margin: 0 0.125rem;
transition: background 0.15s; transition: background 0.15s;
} }
.step-line.filled { background: var(--rs-primary); } .step-line.filled { background: var(--rs-accent, #14b8a6); }
.step-info { .step-info {
font-size: 0.75rem; font-size: 0.75rem;
@ -1052,8 +1082,8 @@ export class FolkPubsEditor extends HTMLElement {
.format-badge { .format-badge {
padding: 0.15rem 0.5rem; padding: 0.15rem 0.5rem;
border-radius: 1rem; border-radius: 1rem;
background: rgba(59, 130, 246, 0.1); background: rgba(20, 184, 166, 0.1);
color: var(--rs-primary); color: var(--rs-accent, #14b8a6);
font-size: 0.65rem; font-size: 0.65rem;
font-weight: 500; font-weight: 500;
} }
@ -1067,17 +1097,30 @@ export class FolkPubsEditor extends HTMLElement {
padding: 0.5rem 1.25rem; padding: 0.5rem 1.25rem;
border: none; border: none;
border-radius: 0.5rem; border-radius: 0.5rem;
background: var(--rs-primary); background: var(--rs-accent, #14b8a6);
color: #fff; color: #fff;
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: background 0.15s; transition: background 0.15s;
white-space: nowrap; white-space: nowrap;
display: inline-flex;
align-items: center;
gap: 0.375rem;
} }
.btn-generate:hover { background: var(--rs-primary-hover); } .btn-generate:hover { background: var(--rs-accent-hover, #0d9488); }
.btn-generate:disabled { opacity: 0.5; cursor: not-allowed; } .btn-generate:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-spinner {
display: inline-block;
width: 14px; height: 14px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
animation: btn-spin 0.6s linear infinite;
}
@keyframes btn-spin { to { transform: rotate(360deg); } }
/* ── Preview step ── */ /* ── Preview step ── */
.preview-step { .preview-step {
@ -1105,8 +1148,21 @@ export class FolkPubsEditor extends HTMLElement {
text-decoration: none; text-decoration: none;
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 0.375rem;
}
.action-btn:hover { border-color: var(--rs-accent, #14b8a6); color: var(--rs-text-primary); }
.action-btn:hover svg { color: var(--rs-accent, #14b8a6); }
.format-chip {
margin-left: auto;
padding: 0.25rem 0.625rem;
border-radius: 1rem;
background: rgba(20, 184, 166, 0.1);
color: var(--rs-accent, #14b8a6);
font-size: 0.7rem;
font-weight: 500;
white-space: nowrap;
} }
.action-btn:hover { border-color: var(--rs-primary); color: var(--rs-text-primary); }
.preview-flipbook { .preview-flipbook {
flex: 1; flex: 1;
@ -1140,14 +1196,17 @@ export class FolkPubsEditor extends HTMLElement {
padding: 0.5rem 1.25rem; padding: 0.5rem 1.25rem;
border: none; border: none;
border-radius: 0.5rem; border-radius: 0.5rem;
background: var(--rs-primary); background: var(--rs-accent, #14b8a6);
color: #fff; color: #fff;
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 600; font-weight: 600;
cursor: pointer; cursor: pointer;
transition: background 0.15s; transition: background 0.15s;
display: inline-flex;
align-items: center;
gap: 0.375rem;
} }
.btn-publish-next:hover { background: var(--rs-primary-hover); } .btn-publish-next:hover { background: var(--rs-accent-hover, #0d9488); }
/* ── Publish step ── */ /* ── Publish step ── */

View File

@ -169,9 +169,19 @@ export class FolkPubsFlipbook extends HTMLElement {
const container = this.shadowRoot.querySelector(".flipbook-container") as HTMLElement; const container = this.shadowRoot.querySelector(".flipbook-container") as HTMLElement;
if (!container) return; if (!container) return;
try {
await this.loadStPageFlip(); await this.loadStPageFlip();
} catch (e) {
console.warn('[folk-pubs-flipbook] StPageFlip failed to load, using fallback:', e);
this.renderFallback();
return;
}
const PageFlip = (window as any).St?.PageFlip; const PageFlip = (window as any).St?.PageFlip;
if (!PageFlip) return; if (!PageFlip) {
console.warn('[folk-pubs-flipbook] StPageFlip not available, using fallback');
this.renderFallback();
return;
}
this._flipBook = new PageFlip(container, { this._flipBook = new PageFlip(container, {
width: Math.round(pageW), width: Math.round(pageW),
@ -241,9 +251,26 @@ export class FolkPubsFlipbook extends HTMLElement {
}); });
} }
private renderFallback() {
if (!this.shadowRoot) return;
this.shadowRoot.innerHTML = `
${this.getStyles()}
<div class="reader">
<div class="fallback-pages">
${this._pageImages.map((src, i) => `<img src="${src}" alt="Page ${i + 1}" />`).join('')}
</div>
<div class="page-info">${this._numPages} pages</div>
</div>
`;
}
private getStyles(): string { private getStyles(): string {
return `<style> return `<style>
:host { display: block; } :host {
display: block;
position: relative;
z-index: 0; /* stacking context — keeps StPageFlip elements above backgrounds */
}
.loading { .loading {
display: flex; flex-direction: column; align-items: center; display: flex; flex-direction: column; align-items: center;
@ -252,21 +279,32 @@ export class FolkPubsFlipbook extends HTMLElement {
.loading-spinner { .loading-spinner {
width: 32px; height: 32px; width: 32px; height: 32px;
border: 3px solid var(--rs-border-strong, #444); border: 3px solid var(--rs-border-strong, #444);
border-top-color: #60a5fa; border-radius: 50%; border-top-color: var(--rs-accent, #14b8a6); border-radius: 50%;
animation: spin 0.8s linear infinite; animation: spin 0.8s linear infinite;
} }
@keyframes spin { to { transform: rotate(360deg); } } @keyframes spin { to { transform: rotate(360deg); } }
.loading-status { color: var(--rs-text-secondary, #aaa); font-size: 0.8rem; } .loading-status { color: var(--rs-text-secondary, #aaa); font-size: 0.8rem; }
.loading-bar { width: 160px; height: 3px; background: var(--rs-bg-surface, #333); border-radius: 2px; overflow: hidden; } .loading-bar { width: 160px; height: 3px; background: var(--rs-bg-surface, #333); border-radius: 2px; overflow: hidden; }
.loading-fill { height: 100%; background: #60a5fa; transition: width 0.3s; border-radius: 2px; } .loading-fill { height: 100%; background: var(--rs-accent, #14b8a6); transition: width 0.3s; border-radius: 2px; }
.error { padding: 1rem; color: #f87171; font-size: 0.8rem; text-align: center; } .error { padding: 1rem; color: #f87171; font-size: 0.8rem; text-align: center; }
.reader { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; } .reader { display: flex; flex-direction: column; align-items: center; gap: 0.5rem; }
.flipbook-row { display: flex; align-items: center; gap: 0.375rem; } .flipbook-row { display: flex; align-items: center; gap: 0.375rem; }
.flipbook-container { .flipbook-container {
position: relative;
overflow: hidden; border-radius: 3px; overflow: hidden; border-radius: 3px;
box-shadow: 0 4px 20px rgba(0,0,0,0.35); box-shadow: 0 4px 20px rgba(0,0,0,0.35);
background: #fff;
}
/* Fallback scroll view when StPageFlip fails to load */
.fallback-pages {
display: flex; flex-direction: column; align-items: center; gap: 1rem;
padding: 1rem; max-height: 500px; overflow-y: auto;
}
.fallback-pages img {
max-width: 100%; border-radius: 2px;
box-shadow: 0 2px 8px rgba(0,0,0,0.2);
} }
.nav-btn { .nav-btn {
width: 32px; height: 60px; width: 32px; height: 60px;

View File

@ -30,6 +30,24 @@ export class FolkPubsPublishPanel extends HTMLElement {
private _orderStatus: string | null = null; private _orderStatus: string | null = null;
private _batchStatus: any = null; private _batchStatus: any = null;
private static readonly ICONS = {
download: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>`,
copy: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="9" y="9" width="13" height="13" rx="2" ry="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>`,
mail: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/></svg>`,
book: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z"/></svg>`,
scissors: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="6" cy="6" r="3"/><circle cx="6" cy="18" r="3"/><line x1="20" y1="4" x2="8.12" y2="15.88"/><line x1="14.47" y1="14.48" x2="20" y2="20"/><line x1="8.12" y1="8.12" x2="12" y2="12"/></svg>`,
printer: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="6 9 6 2 18 2 18 9"/><path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"/><rect x="6" y="14" width="12" height="8"/></svg>`,
location: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M21 10c0 7-9 13-9 13s-9-6-9-13a9 9 0 0 1 18 0z"/><circle cx="12" cy="10" r="3"/></svg>`,
users: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2"/><circle cx="9" cy="7" r="4"/><path d="M23 21v-2a4 4 0 0 0-3-3.87"/><path d="M16 3.13a4 4 0 0 1 0 7.75"/></svg>`,
check: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="20 6 9 17 4 12"/></svg>`,
send: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><line x1="22" y1="2" x2="11" y2="13"/><polygon points="22 2 15 22 11 13 2 9 22 2"/></svg>`,
share: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="18" cy="5" r="3"/><circle cx="6" cy="12" r="3"/><circle cx="18" cy="19" r="3"/><line x1="8.59" y1="13.51" x2="15.42" y2="17.49"/><line x1="15.41" y1="6.51" x2="8.59" y2="10.49"/></svg>`,
info: `<svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="12" y1="16" x2="12" y2="12"/><line x1="12" y1="8" x2="12.01" y2="8"/></svg>`,
phone: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><path d="M22 16.92v3a2 2 0 0 1-2.18 2 19.79 19.79 0 0 1-8.63-3.07 19.5 19.5 0 0 1-6-6 19.79 19.79 0 0 1-3.07-8.67A2 2 0 0 1 4.11 2h3a2 2 0 0 1 2 1.72c.127.96.361 1.903.7 2.81a2 2 0 0 1-.45 2.11L8.09 9.91a16 16 0 0 0 6 6l1.27-1.27a2 2 0 0 1 2.11-.45c.907.339 1.85.573 2.81.7A2 2 0 0 1 22 16.92z"/></svg>`,
globe: `<svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="10"/><line x1="2" y1="12" x2="22" y2="12"/><path d="M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10 15.3 15.3 0 0 1 4-10z"/></svg>`,
verified: `<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z"/></svg>`,
};
static get observedAttributes() { static get observedAttributes() {
return ["pdf-url", "format-id", "format-name", "page-count", "space-slug"]; return ["pdf-url", "format-id", "format-name", "page-count", "space-slug"];
} }
@ -59,9 +77,18 @@ export class FolkPubsPublishPanel extends HTMLElement {
${this.getStyles()} ${this.getStyles()}
<div class="panel"> <div class="panel">
<div class="tabs"> <div class="tabs">
<button class="tab ${this._activeTab === 'share' ? 'active' : ''}" data-tab="share">Share</button> <button class="tab ${this._activeTab === 'share' ? 'active' : ''}" data-tab="share">
<button class="tab ${this._activeTab === 'diy' ? 'active' : ''}" data-tab="diy">DIY Print</button> ${FolkPubsPublishPanel.ICONS.share}
<button class="tab ${this._activeTab === 'order' ? 'active' : ''}" data-tab="order">Order</button> <span>Share</span>
</button>
<button class="tab ${this._activeTab === 'diy' ? 'active' : ''}" data-tab="diy">
${FolkPubsPublishPanel.ICONS.scissors}
<span>DIY Print</span>
</button>
<button class="tab ${this._activeTab === 'order' ? 'active' : ''}" data-tab="order">
${FolkPubsPublishPanel.ICONS.printer}
<span>Order</span>
</button>
</div> </div>
<div class="tab-content"> <div class="tab-content">
${this._activeTab === 'share' ? this.renderShareTab() : ''} ${this._activeTab === 'share' ? this.renderShareTab() : ''}
@ -76,28 +103,64 @@ export class FolkPubsPublishPanel extends HTMLElement {
private renderShareTab(): string { private renderShareTab(): string {
return ` return `
<div class="section"> <div class="section">
<a class="action-btn primary" href="${this._pdfUrl}" download>Download PDF</a> <div class="info-card">
<button class="action-btn" data-action="copy-link">Copy Flipbook Link</button> ${FolkPubsPublishPanel.ICONS.book}
<div class="info-card-text">
<span class="info-card-title">${this.esc(this._formatName)}</span>
<span class="info-card-detail">${this._pageCount} pages</span>
</div>
</div>
<a class="action-btn primary" href="${this._pdfUrl}" download>
${FolkPubsPublishPanel.ICONS.download}
<span>Download PDF</span>
</a>
<button class="action-btn secondary" data-action="copy-link">
${FolkPubsPublishPanel.ICONS.copy}
<span>Copy Flipbook Link</span>
</button>
<div class="divider-row">
<span class="divider-line"></span>
<span class="divider-text">or send by email</span>
<span class="divider-line"></span>
</div>
<div class="email-row"> <div class="email-row">
<input type="email" class="email-input" placeholder="Email address" /> <div class="email-input-wrap">
<button class="action-btn small" data-action="email-pdf" ${this._emailSending ? 'disabled' : ''}> ${FolkPubsPublishPanel.ICONS.mail}
${this._emailSending ? 'Sending...' : 'Send'} <input type="email" class="email-input" placeholder="recipient@email.com" />
</div>
<button class="action-btn accent send-btn" data-action="email-pdf" ${this._emailSending ? 'disabled' : ''}>
${this._emailSending
? '<span class="spinner"></span>'
: FolkPubsPublishPanel.ICONS.send}
<span>${this._emailSending ? 'Sending...' : 'Send'}</span>
</button> </button>
</div> </div>
${this._emailSent ? '<div class="msg success">PDF sent!</div>' : ''} ${this._emailSent ? `<div class="msg success">${FolkPubsPublishPanel.ICONS.check} PDF sent!</div>` : ''}
${this._emailError ? `<div class="msg error">${this.esc(this._emailError)}</div>` : ''} ${this._emailError ? `<div class="msg error">${this.esc(this._emailError)}</div>` : ''}
</div> </div>
<div class="meta">
${this._formatName} &middot; ${this._pageCount} pages
</div>
`; `;
} }
private renderDiyTab(): string { private renderDiyTab(): string {
return ` return `
<div class="section"> <div class="section">
<div class="section-header">
${FolkPubsPublishPanel.ICONS.scissors}
<div>
<div class="section-title">DIY Printing Guide</div>
<div class="section-subtitle">Print, fold &amp; bind at home</div>
</div>
</div>
<button class="action-btn primary" data-action="download-imposition" ${this._impositionLoading ? 'disabled' : ''}> <button class="action-btn primary" data-action="download-imposition" ${this._impositionLoading ? 'disabled' : ''}>
${this._impositionLoading ? 'Generating...' : 'Download Imposition PDF'} ${this._impositionLoading
? '<span class="spinner"></span>'
: FolkPubsPublishPanel.ICONS.download}
<span>${this._impositionLoading ? 'Generating...' : 'Download Imposition PDF'}</span>
</button> </button>
<p class="hint">Pre-arranged pages for double-sided printing &amp; folding.</p> <p class="hint">Pre-arranged pages for double-sided printing &amp; folding.</p>
<div class="guide-placeholder" data-guide-target></div> <div class="guide-placeholder" data-guide-target></div>
@ -112,8 +175,30 @@ export class FolkPubsPublishPanel extends HTMLElement {
return ` return `
<div class="section"> <div class="section">
<div class="pricing-tiers">
<div class="tier-card">
<div class="tier-qty">25+</div>
<div class="tier-binding">Saddle stitch</div>
<div class="tier-price">~$1.20/ea</div>
</div>
<div class="tier-card popular">
<div class="tier-badge">popular</div>
<div class="tier-qty">50+</div>
<div class="tier-binding">Perfect bind</div>
<div class="tier-price">~$0.85/ea</div>
</div>
<div class="tier-card">
<div class="tier-qty">100+</div>
<div class="tier-binding">Perfect bind</div>
<div class="tier-price">~$0.60/ea</div>
</div>
</div>
<button class="action-btn primary" data-action="find-printers" ${this._printersLoading ? 'disabled' : ''}> <button class="action-btn primary" data-action="find-printers" ${this._printersLoading ? 'disabled' : ''}>
${this._printersLoading ? 'Searching...' : 'Find Nearby Printers'} ${this._printersLoading
? '<span class="spinner"></span>'
: FolkPubsPublishPanel.ICONS.location}
<span>${this._printersLoading ? 'Searching...' : 'Find Nearby Printers'}</span>
</button> </button>
${this._printersError ? `<div class="msg error">${this.esc(this._printersError)}</div>` : ''} ${this._printersError ? `<div class="msg error">${this.esc(this._printersError)}</div>` : ''}
${this._printers.length > 0 ? this.renderPrinterList() : ''} ${this._printers.length > 0 ? this.renderPrinterList() : ''}
@ -126,10 +211,13 @@ export class FolkPubsPublishPanel extends HTMLElement {
<div class="printer-list"> <div class="printer-list">
${this._printers.map((p) => ` ${this._printers.map((p) => `
<button class="printer-card" data-provider-id="${this.esc(p.id)}"> <button class="printer-card" data-provider-id="${this.esc(p.id)}">
<div class="printer-name">${this.esc(p.name)}</div> <div class="printer-header">
<span class="printer-name">${this.esc(p.name)}</span>
<span class="printer-distance">${p.distance_km} km</span>
</div>
<div class="printer-meta"> <div class="printer-meta">
${this.esc(p.city)} &middot; ${p.distance_km} km ${this.esc(p.city)}
${p.source === 'curated' ? '<span class="badge">curated</span>' : ''} ${p.source === 'curated' ? `<span class="verified-badge">${FolkPubsPublishPanel.ICONS.verified} verified</span>` : ''}
</div> </div>
${p.tags?.length ? `<div class="printer-tags">${p.tags.map((t: string) => `<span class="tag">${this.esc(t)}</span>`).join('')}</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>` : ''} ${p.capabilities?.length ? `<div class="printer-caps">${p.capabilities.join(', ')}</div>` : ''}
@ -145,18 +233,57 @@ export class FolkPubsPublishPanel extends HTMLElement {
<div class="section"> <div class="section">
<button class="back-btn" data-action="back-to-list">&larr; Back to results</button> <button class="back-btn" data-action="back-to-list">&larr; Back to results</button>
<div class="provider-detail"> <div class="provider-detail">
<div class="provider-card-header">
${FolkPubsPublishPanel.ICONS.printer}
<h4>${this.esc(p.name)}</h4> <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> </div>
<button class="action-btn primary" data-action="place-order">Place Order</button> <div class="provider-info-grid">
<button class="action-btn" data-action="join-batch">Join Group Buy</button> ${p.address ? `<div class="provider-info-row">${FolkPubsPublishPanel.ICONS.location} <span>${this.esc(p.address)}</span></div>` : ''}
${this._orderStatus ? `<div class="msg success">${this.esc(this._orderStatus)}</div>` : ''} ${p.phone ? `<div class="provider-info-row">${FolkPubsPublishPanel.ICONS.phone} <span>${this.esc(p.phone)}</span></div>` : ''}
${this._batchStatus ? `<div class="msg info">Batch: ${this._batchStatus.action} &middot; ${this._batchStatus.participants || '?'} participants</div>` : ''} ${p.website ? `<div class="provider-info-row">${FolkPubsPublishPanel.ICONS.globe} <a href="${this.esc(p.website)}" target="_blank" rel="noopener">${this.esc(p.website)}</a></div>` : ''}
${p.email ? `<div class="provider-info-row">${FolkPubsPublishPanel.ICONS.mail} <span>${this.esc(p.email)}</span></div>` : ''}
</div>
${p.description ? `<div class="provider-desc">${this.esc(p.description)}</div>` : ''}
<button class="action-btn primary" data-action="place-order">
${FolkPubsPublishPanel.ICONS.printer}
<span>Place Order</span>
</button>
<button class="action-btn secondary" data-action="join-batch">
${FolkPubsPublishPanel.ICONS.users}
<span>Join Group Buy</span>
</button>
${this._orderStatus ? `<div class="msg success">${FolkPubsPublishPanel.ICONS.check} ${this.esc(this._orderStatus)}</div>` : ''}
${this._batchStatus ? this.renderBatchProgress() : ''}
</div>
</div>
`;
}
private renderBatchProgress(): string {
const b = this._batchStatus;
if (b.action === 'error') return `<div class="msg error">${this.esc(b.error)}</div>`;
const participants = b.participants || 1;
const threshold = 25;
const pct = Math.min(Math.round((participants / threshold) * 100), 100);
return `
<div class="batch-card">
<div class="batch-header">
${FolkPubsPublishPanel.ICONS.users}
<span>Group Buy Progress</span>
</div>
<div class="batch-bar-wrap">
<div class="batch-bar">
<div class="batch-bar-fill" style="width:${pct}%"></div>
</div>
<div class="batch-bar-label">${participants} / ${threshold} participants</div>
</div>
<div class="batch-tiers">
<span class="${participants >= 25 ? 'tier-unlocked' : 'tier-locked'}">25+ unlocked</span>
<span class="${participants >= 50 ? 'tier-unlocked' : 'tier-locked'}">50+ ${participants >= 50 ? 'unlocked' : 'locked'}</span>
</div> </div>
</div> </div>
`; `;
@ -178,8 +305,12 @@ export class FolkPubsPublishPanel extends HTMLElement {
const url = `${window.location.origin}${window.location.pathname}`; const url = `${window.location.origin}${window.location.pathname}`;
navigator.clipboard.writeText(url).then(() => { navigator.clipboard.writeText(url).then(() => {
const btn = this.shadowRoot!.querySelector('[data-action="copy-link"]')!; const btn = this.shadowRoot!.querySelector('[data-action="copy-link"]')!;
btn.textContent = "Copied!"; const span = btn.querySelector('span');
setTimeout(() => { btn.textContent = "Copy Flipbook Link"; }, 2000); if (span) span.textContent = "Copied!";
btn.innerHTML = `${FolkPubsPublishPanel.ICONS.check}<span>Copied!</span>`;
setTimeout(() => {
btn.innerHTML = `${FolkPubsPublishPanel.ICONS.copy}<span>Copy Flipbook Link</span>`;
}, 2000);
}); });
}); });
@ -315,22 +446,61 @@ export class FolkPubsPublishPanel extends HTMLElement {
target.innerHTML = ` target.innerHTML = `
<div class="guide"> <div class="guide">
<h4>${guide.formatName} DIY Guide</h4> <div class="guide-card">
<div class="guide-stat">Sheets needed: ${sheets} (${guide.parentSheet})</div> <div class="guide-stats">
<div class="guide-stat">Binding: ${binding}</div> <div class="guide-stat-col">
<div class="guide-stat">Paper: ${guide.paperRecommendation}</div> <span class="guide-stat-value">${sheets}</span>
<span class="guide-stat-label">Sheets</span>
</div>
<div class="guide-stat-col">
<span class="guide-stat-value">${guide.parentSheet}</span>
<span class="guide-stat-label">Paper</span>
</div>
<div class="guide-stat-col">
<span class="guide-stat-value">${binding}</span>
<span class="guide-stat-label">Binding</span>
</div>
</div>
<h5>Tools</h5> <div class="guide-info-banner">
<ul>${guide.tools.map((t: string) => `<li>${t}</li>`).join('')}</ul> ${FolkPubsPublishPanel.ICONS.info}
<span>${guide.paperRecommendation}</span>
</div>
<h5>Folding</h5> <div class="guide-section">
<ol>${guide.foldInstructions.map((s: string) => `<li>${s}</li>`).join('')}</ol> <div class="guide-section-title">Tools needed</div>
<ul class="guide-list">${guide.tools.map((t: string) => `<li>${t}</li>`).join('')}</ul>
</div>
<h5>Binding</h5> <div class="guide-section">
<ol>${guide.bindingInstructions.filter((s: string) => s).map((s: string) => `<li>${s.replace(/^\s+/, '')}</li>`).join('')}</ol> <div class="guide-section-title">Folding</div>
<div class="guide-numbered-steps">
${guide.foldInstructions.map((s: string, i: number) => `
<div class="guide-step">
<span class="guide-step-num">${i + 1}</span>
<span>${s}</span>
</div>
`).join('')}
</div>
</div>
<h5>Tips</h5> <div class="guide-section">
<ul>${guide.tips.map((t: string) => `<li>${t}</li>`).join('')}</ul> <div class="guide-section-title">Binding</div>
<div class="guide-numbered-steps">
${guide.bindingInstructions.filter((s: string) => s).map((s: string, i: number) => `
<div class="guide-step">
<span class="guide-step-num">${i + 1}</span>
<span>${s.replace(/^\s+/, '')}</span>
</div>
`).join('')}
</div>
</div>
<div class="guide-section">
<div class="guide-section-title">Tips</div>
<ul class="guide-tips">${guide.tips.map((t: string) => `<li>\u2605 ${t}</li>`).join('')}</ul>
</div>
</div>
</div> </div>
`; `;
} }
@ -446,104 +616,378 @@ export class FolkPubsPublishPanel extends HTMLElement {
private getStyles(): string { private getStyles(): string {
return `<style> return `<style>
:host { display: block; max-width: 32rem; margin: 0 auto; } :host { display: block; max-width: 32rem; margin: 0 auto; }
.panel { display: flex; flex-direction: column; gap: 0.5rem; }
.panel {
display: flex;
flex-direction: column;
background: var(--rs-bg-surface, #1e1e2e);
border: 1px solid var(--rs-border, #333);
border-radius: 0.75rem;
box-shadow: var(--rs-shadow-md, 0 4px 16px rgba(0,0,0,0.25));
overflow: hidden;
}
/* ── Tabs ── */
.tabs { .tabs {
display: flex; gap: 0; border-bottom: 1px solid var(--rs-border-subtle, #333); display: flex;
gap: 0;
border-bottom: 1px solid var(--rs-border-subtle, #333);
background: var(--rs-bg-page, #181825);
} }
.tab { .tab {
flex: 1; padding: 0.4rem 0.5rem; flex: 1;
border: none; border-bottom: 2px solid transparent; padding: 0.625rem 0.5rem;
border: none;
border-bottom: 2px solid transparent;
background: transparent; background: transparent;
color: var(--rs-text-secondary, #aaa); color: var(--rs-text-secondary, #aaa);
font-size: 0.75rem; font-weight: 500; cursor: pointer; font-size: 0.78rem;
font-weight: 500;
cursor: pointer;
transition: all 0.15s; transition: all 0.15s;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.375rem;
} }
.tab:hover { color: var(--rs-text-primary, #eee); } .tab:hover { color: var(--rs-text-primary, #eee); }
.tab.active { .tab.active {
color: var(--rs-primary, #3b82f6); color: var(--rs-accent, #14b8a6);
border-bottom-color: var(--rs-primary, #3b82f6); border-bottom-color: var(--rs-accent, #14b8a6);
} }
.tab svg { opacity: 0.7; }
.tab.active svg { opacity: 1; }
.tab-content { padding: 0.5rem 0; } /* ── Tab Content ── */
.section { display: flex; flex-direction: column; gap: 0.5rem; }
.tab-content { padding: 1rem; }
.section { display: flex; flex-direction: column; gap: 0.75rem; }
/* ── Info Card ── */
.info-card {
display: flex;
align-items: center;
gap: 0.625rem;
padding: 0.625rem 0.75rem;
background: var(--rs-card-bg, #232336);
border: 1px solid var(--rs-card-border, #2a2a3e);
border-radius: 0.5rem;
}
.info-card svg { color: var(--rs-accent, #14b8a6); flex-shrink: 0; }
.info-card-text { display: flex; flex-direction: column; gap: 0.125rem; }
.info-card-title { font-size: 0.8rem; font-weight: 600; color: var(--rs-text-primary, #eee); }
.info-card-detail { font-size: 0.7rem; color: var(--rs-text-muted, #666); }
/* ── Section Header ── */
.section-header {
display: flex;
align-items: center;
gap: 0.625rem;
margin-bottom: 0.25rem;
}
.section-header svg { color: var(--rs-accent, #14b8a6); flex-shrink: 0; }
.section-title { font-size: 0.85rem; font-weight: 600; color: var(--rs-text-primary, #eee); }
.section-subtitle { font-size: 0.7rem; color: var(--rs-text-muted, #666); }
/* ── Action Buttons ── */
.action-btn { .action-btn {
display: block; width: 100%; text-align: center; display: inline-flex;
padding: 0.5rem; border-radius: 0.375rem; align-items: center;
justify-content: center;
gap: 0.375rem;
width: 100%;
text-align: center;
padding: 0.5rem 0.75rem;
border-radius: 0.5rem;
border: 1px solid var(--rs-border, #444); border: 1px solid var(--rs-border, #444);
background: var(--rs-bg-surface, #2a2a2a); background: var(--rs-bg-surface, #2a2a2a);
color: var(--rs-text-primary, #eee); color: var(--rs-text-primary, #eee);
font-size: 0.8rem; cursor: pointer; font-size: 0.8rem;
cursor: pointer;
text-decoration: none; text-decoration: none;
transition: all 0.15s; transition: all 0.15s;
} }
.action-btn:hover { border-color: var(--rs-primary, #3b82f6); } .action-btn:hover { border-color: var(--rs-accent, #14b8a6); }
.action-btn:disabled { opacity: 0.5; cursor: not-allowed; } .action-btn:disabled { opacity: 0.5; cursor: not-allowed; }
.action-btn svg { flex-shrink: 0; }
.action-btn.primary { .action-btn.primary {
background: var(--rs-primary, #3b82f6); background: var(--rs-accent, #14b8a6);
border-color: var(--rs-primary, #3b82f6); border-color: var(--rs-accent, #14b8a6);
color: #fff; font-weight: 600; color: #fff;
font-weight: 600;
} }
.action-btn.primary:hover { opacity: 0.9; } .action-btn.primary:hover { background: var(--rs-accent-hover, #0d9488); }
.action-btn.small { width: auto; flex-shrink: 0; padding: 0.4rem 0.75rem; }
.action-btn.secondary {
background: transparent;
border: 1px solid var(--rs-border, #444);
}
.action-btn.secondary:hover { border-color: var(--rs-accent, #14b8a6); color: var(--rs-accent, #14b8a6); }
.action-btn.accent {
background: var(--rs-accent, #14b8a6);
border-color: var(--rs-accent, #14b8a6);
color: #fff;
font-weight: 600;
}
.action-btn.accent:hover { background: var(--rs-accent-hover, #0d9488); }
.send-btn { width: auto; flex-shrink: 0; padding: 0.5rem 0.875rem; }
/* ── Spinner ── */
.spinner {
display: inline-block;
width: 14px; height: 14px;
border: 2px solid rgba(255,255,255,0.3);
border-top-color: #fff;
border-radius: 50%;
animation: spin 0.6s linear infinite;
}
@keyframes spin { to { transform: rotate(360deg); } }
/* ── Divider ── */
.divider-row {
display: flex;
align-items: center;
gap: 0.625rem;
margin: 0.25rem 0;
}
.divider-line { flex: 1; height: 1px; background: var(--rs-border-subtle, #333); }
.divider-text { font-size: 0.7rem; color: var(--rs-text-muted, #666); white-space: nowrap; }
/* ── Email ── */
.email-row { display: flex; gap: 0.375rem; } .email-row { display: flex; gap: 0.375rem; }
.email-input { .email-input-wrap {
flex: 1; padding: 0.4rem 0.5rem; flex: 1;
display: flex;
align-items: center;
gap: 0.375rem;
padding: 0 0.5rem;
border: 1px solid var(--rs-input-border, #444); border: 1px solid var(--rs-input-border, #444);
border-radius: 0.375rem; border-radius: 0.5rem;
background: var(--rs-input-bg, #1a1a2e); background: var(--rs-input-bg, #1a1a2e);
transition: border-color 0.15s;
}
.email-input-wrap:focus-within { border-color: var(--rs-accent, #14b8a6); }
.email-input-wrap svg { color: var(--rs-text-muted, #666); flex-shrink: 0; }
.email-input {
flex: 1;
padding: 0.5rem 0;
border: none;
background: transparent;
color: var(--rs-input-text, #eee); color: var(--rs-input-text, #eee);
font-size: 0.8rem; font-size: 0.8rem;
outline: none;
} }
.email-input:focus { outline: none; border-color: var(--rs-primary, #3b82f6); }
.email-input::placeholder { color: var(--rs-text-muted, #666); } .email-input::placeholder { color: var(--rs-text-muted, #666); }
.msg { font-size: 0.75rem; padding: 0.375rem 0.5rem; border-radius: 0.25rem; } /* ── Messages ── */
.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 {
.msg.info { background: rgba(59, 130, 246, 0.1); color: #60a5fa; } font-size: 0.75rem;
padding: 0.5rem 0.625rem;
border-radius: 0.5rem;
display: inline-flex;
align-items: center;
gap: 0.375rem;
}
.msg.success {
background: rgba(20, 184, 166, 0.1);
border: 1px solid rgba(20, 184, 166, 0.2);
color: var(--rs-accent, #14b8a6);
}
.msg.error {
background: rgba(248, 113, 113, 0.1);
border: 1px solid rgba(248, 113, 113, 0.2);
color: #f87171;
}
.msg.info {
background: rgba(20, 184, 166, 0.08);
border: 1px solid rgba(20, 184, 166, 0.15);
color: var(--rs-accent, #14b8a6);
}
.hint { font-size: 0.7rem; color: var(--rs-text-muted, #666); margin: 0; } .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 */ /* ── 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-card {
.guide ul, .guide ol { margin: 0; padding-left: 1.25rem; font-size: 0.75rem; color: var(--rs-text-primary, #ddd); line-height: 1.5; } background: var(--rs-card-bg, #232336);
.guide li { margin-bottom: 0.25rem; } border: 1px solid var(--rs-card-border, #2a2a3e);
.guide-stat { font-size: 0.75rem; color: var(--rs-text-secondary, #aaa); } border-radius: 0.5rem;
padding: 0.875rem;
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.guide-stats {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 0.5rem;
text-align: center;
}
.guide-stat-col { display: flex; flex-direction: column; gap: 0.125rem; }
.guide-stat-value { font-size: 0.85rem; font-weight: 700; color: var(--rs-accent, #14b8a6); }
.guide-stat-label { font-size: 0.65rem; color: var(--rs-text-muted, #666); text-transform: uppercase; letter-spacing: 0.04em; }
.guide-info-banner {
display: flex;
align-items: flex-start;
gap: 0.5rem;
padding: 0.5rem 0.625rem;
background: rgba(20, 184, 166, 0.08);
border: 1px solid rgba(20, 184, 166, 0.15);
border-radius: 0.375rem;
font-size: 0.72rem;
color: var(--rs-text-secondary, #aaa);
line-height: 1.4;
}
.guide-info-banner svg { color: var(--rs-accent, #14b8a6); flex-shrink: 0; margin-top: 0.05rem; }
.guide-section { display: flex; flex-direction: column; gap: 0.375rem; }
.guide-section-title {
font-size: 0.72rem;
font-weight: 600;
color: var(--rs-text-secondary, #aaa);
text-transform: uppercase;
letter-spacing: 0.04em;
}
.guide-list {
margin: 0; padding-left: 1.25rem;
font-size: 0.75rem; color: var(--rs-text-primary, #ddd); line-height: 1.5;
}
.guide-list li { margin-bottom: 0.2rem; }
.guide-numbered-steps { display: flex; flex-direction: column; gap: 0.375rem; }
.guide-step {
display: flex;
align-items: flex-start;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--rs-text-primary, #ddd);
line-height: 1.4;
}
.guide-step-num {
display: inline-flex;
align-items: center;
justify-content: center;
width: 20px; height: 20px;
border-radius: 50%;
background: var(--rs-accent, #14b8a6);
color: #fff;
font-size: 0.65rem;
font-weight: 700;
flex-shrink: 0;
margin-top: 0.05rem;
}
.guide-tips {
margin: 0; padding: 0; list-style: none;
font-size: 0.75rem; color: var(--rs-text-primary, #ddd); line-height: 1.5;
}
.guide-tips li { margin-bottom: 0.2rem; }
/* ── Pricing Tiers ── */
.pricing-tiers {
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 0.5rem;
margin-bottom: 0.25rem;
}
.tier-card {
display: flex;
flex-direction: column;
align-items: center;
gap: 0.125rem;
padding: 0.625rem 0.5rem;
border: 1px solid var(--rs-card-border, #2a2a3e);
border-radius: 0.5rem;
background: var(--rs-card-bg, #232336);
text-align: center;
position: relative;
}
.tier-card.popular {
border-color: var(--rs-accent, #14b8a6);
background: rgba(20, 184, 166, 0.06);
}
.tier-badge {
position: absolute;
top: -8px;
font-size: 0.55rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.05em;
padding: 0.1rem 0.5rem;
border-radius: 1rem;
background: var(--rs-accent, #14b8a6);
color: #fff;
}
.tier-qty { font-size: 0.9rem; font-weight: 700; color: var(--rs-accent, #14b8a6); }
.tier-binding { font-size: 0.65rem; color: var(--rs-text-muted, #666); }
.tier-price { font-size: 0.75rem; font-weight: 600; color: var(--rs-text-primary, #eee); }
/* ── Printer List ── */
/* Printer list */
.printer-list { display: flex; flex-direction: column; gap: 0.375rem; max-height: 300px; overflow-y: auto; } .printer-list { display: flex; flex-direction: column; gap: 0.375rem; max-height: 300px; overflow-y: auto; }
.printer-card { .printer-card {
text-align: left; padding: 0.5rem; text-align: left;
border: 1px solid var(--rs-border, #444); padding: 0.625rem 0.75rem;
border-radius: 0.375rem; border: 1px solid var(--rs-card-border, #2a2a3e);
background: var(--rs-bg-surface, #2a2a2a); border-radius: 0.5rem;
background: var(--rs-card-bg, #232336);
color: var(--rs-text-primary, #eee); color: var(--rs-text-primary, #eee);
cursor: pointer; transition: border-color 0.15s; cursor: pointer;
transition: all 0.15s;
}
.printer-card:hover {
border-color: var(--rs-accent, #14b8a6);
background: rgba(20, 184, 166, 0.04);
}
.printer-header {
display: flex;
justify-content: space-between;
align-items: center;
} }
.printer-card:hover { border-color: var(--rs-primary, #3b82f6); }
.printer-name { font-size: 0.8rem; font-weight: 600; } .printer-name { font-size: 0.8rem; font-weight: 600; }
.printer-meta { font-size: 0.7rem; color: var(--rs-text-secondary, #aaa); } .printer-distance { font-size: 0.7rem; color: var(--rs-text-muted, #666); }
.printer-tags { display: flex; gap: 0.25rem; flex-wrap: wrap; margin-top: 0.25rem; } .printer-meta {
font-size: 0.7rem;
color: var(--rs-text-secondary, #aaa);
display: flex;
align-items: center;
gap: 0.375rem;
margin-top: 0.125rem;
}
.verified-badge {
display: inline-flex;
align-items: center;
gap: 0.2rem;
font-size: 0.6rem;
color: var(--rs-accent, #14b8a6);
}
.printer-tags { display: flex; gap: 0.25rem; flex-wrap: wrap; margin-top: 0.375rem; }
.tag { .tag {
font-size: 0.6rem; padding: 0.1rem 0.375rem; font-size: 0.6rem;
padding: 0.15rem 0.5rem;
border-radius: 1rem; border-radius: 1rem;
background: rgba(34, 197, 94, 0.15); background: rgba(20, 184, 166, 0.1);
color: var(--rs-success, #22c55e); color: var(--rs-accent, #14b8a6);
} }
.badge { .printer-caps { font-size: 0.65rem; color: var(--rs-text-muted, #666); margin-top: 0.25rem; }
font-size: 0.6rem; padding: 0.1rem 0.375rem;
border-radius: 1rem; /* ── Provider Detail ── */
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 { .back-btn {
background: none; border: none; background: none; border: none;
color: var(--rs-text-secondary, #aaa); color: var(--rs-text-secondary, #aaa);
@ -551,10 +995,91 @@ export class FolkPubsPublishPanel extends HTMLElement {
padding: 0; text-align: left; padding: 0; text-align: left;
} }
.back-btn:hover { color: var(--rs-text-primary, #eee); } .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-detail {
.provider-info a { color: var(--rs-primary, #3b82f6); } display: flex;
.provider-desc { font-size: 0.7rem; color: var(--rs-text-muted, #666); margin-top: 0.25rem; } flex-direction: column;
gap: 0.625rem;
}
.provider-card-header {
display: flex;
align-items: center;
gap: 0.5rem;
}
.provider-card-header svg { color: var(--rs-accent, #14b8a6); }
.provider-detail h4 { margin: 0; font-size: 0.9rem; }
.provider-info-grid {
display: flex;
flex-direction: column;
gap: 0.375rem;
padding: 0.625rem 0.75rem;
background: var(--rs-card-bg, #232336);
border: 1px solid var(--rs-card-border, #2a2a3e);
border-radius: 0.5rem;
}
.provider-info-row {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
color: var(--rs-text-secondary, #aaa);
}
.provider-info-row svg { color: var(--rs-text-muted, #666); flex-shrink: 0; }
.provider-info-row a { color: var(--rs-accent, #14b8a6); text-decoration: none; }
.provider-info-row a:hover { text-decoration: underline; }
.provider-desc { font-size: 0.72rem; color: var(--rs-text-muted, #666); line-height: 1.4; }
/* ── Batch / Group Buy ── */
.batch-card {
display: flex;
flex-direction: column;
gap: 0.5rem;
padding: 0.75rem;
background: var(--rs-card-bg, #232336);
border: 1px solid var(--rs-card-border, #2a2a3e);
border-radius: 0.5rem;
}
.batch-header {
display: flex;
align-items: center;
gap: 0.375rem;
font-size: 0.78rem;
font-weight: 600;
color: var(--rs-text-primary, #eee);
}
.batch-header svg { color: var(--rs-accent, #14b8a6); }
.batch-bar-wrap { display: flex; flex-direction: column; gap: 0.25rem; }
.batch-bar {
height: 6px;
border-radius: 3px;
background: var(--rs-border-subtle, #333);
overflow: hidden;
}
.batch-bar-fill {
height: 100%;
border-radius: 3px;
background: linear-gradient(90deg, var(--rs-accent, #14b8a6), var(--rs-accent-hover, #0d9488));
transition: width 0.4s ease;
}
.batch-bar-label { font-size: 0.68rem; color: var(--rs-text-muted, #666); }
.batch-tiers {
display: flex;
gap: 0.75rem;
font-size: 0.68rem;
}
.tier-unlocked { color: var(--rs-accent, #14b8a6); font-weight: 500; }
.tier-locked { color: var(--rs-text-muted, #666); }
/* ── Responsive ── */
@media (max-width: 480px) {
.tab-content { padding: 0.75rem; }
.pricing-tiers { grid-template-columns: 1fr; }
.guide-stats { grid-template-columns: 1fr; }
}
</style>`; </style>`;
} }
} }