843 lines
41 KiB
TypeScript
843 lines
41 KiB
TypeScript
/**
|
||
* <folk-swag-designer> — upload artwork → generate print-ready files.
|
||
* Product selector (sticker, poster, tee, hoodie), image upload with preview,
|
||
* generate button, artifact result display with download link.
|
||
*
|
||
* Demo mode: 4-step interactive flow with inline SVG mockups,
|
||
* provider matching, revenue splits, and pipeline visualization.
|
||
*/
|
||
|
||
// --- Demo data (self-contained, zero API calls in demo mode) ---
|
||
|
||
interface DemoProduct {
|
||
id: string;
|
||
name: string;
|
||
printArea: string;
|
||
baseCost: string;
|
||
printful: boolean;
|
||
sizes?: string[];
|
||
colors?: { id: string; name: string; hex: string }[];
|
||
}
|
||
|
||
interface DemoProvider {
|
||
name: string;
|
||
type: "cosmolocal" | "global";
|
||
city: string;
|
||
lat: number;
|
||
lng: number;
|
||
capabilities: string[];
|
||
unitCost: number;
|
||
turnaround: string;
|
||
}
|
||
|
||
const DEMO_PRODUCTS: DemoProduct[] = [
|
||
{
|
||
id: "tee", name: "T-Shirt", printArea: "305×406mm", baseCost: "$9.25–$13.25",
|
||
printful: true,
|
||
sizes: ["S", "M", "L", "XL", "2XL", "3XL"],
|
||
colors: [
|
||
{ id: "black", name: "Black", hex: "#0a0a0a" },
|
||
{ id: "white", name: "White", hex: "#ffffff" },
|
||
{ id: "forest_green", name: "Forest Green", hex: "#2d4a3e" },
|
||
{ id: "heather_charcoal", name: "Heather Charcoal", hex: "#4a4a4a" },
|
||
{ id: "maroon", name: "Maroon", hex: "#5a2d2d" },
|
||
],
|
||
},
|
||
{
|
||
id: "sticker", name: "Sticker Sheet", printArea: "210×297mm", baseCost: "$1.20–$1.50",
|
||
printful: true,
|
||
},
|
||
{
|
||
id: "poster", name: "Poster (A3)", printArea: "297×420mm", baseCost: "$4.50–$7.00",
|
||
printful: false,
|
||
},
|
||
{
|
||
id: "hoodie", name: "Hoodie", printArea: "356×406mm", baseCost: "$23.95–$27.95",
|
||
printful: true,
|
||
sizes: ["S", "M", "L", "XL", "2XL"],
|
||
colors: [
|
||
{ id: "black", name: "Black", hex: "#0a0a0a" },
|
||
{ id: "dark_grey_heather", name: "Dark Grey Heather", hex: "#3a3a3a" },
|
||
],
|
||
},
|
||
];
|
||
|
||
const DEMO_PROVIDERS: DemoProvider[] = [
|
||
{ name: "De Drukker Collective", type: "cosmolocal", city: "Amsterdam", lat: 52.37, lng: 4.90, capabilities: ["dtg-print", "vinyl-cut", "screen-print"], unitCost: 8.50, turnaround: "3-4 days" },
|
||
{ name: "Kuona Print Collective", type: "cosmolocal", city: "Nairobi", lat: -1.29, lng: 36.82, capabilities: ["dtg-print", "screen-print", "vinyl-cut"], unitCost: 5.50, turnaround: "5-7 days" },
|
||
{ name: "Grafica Popular", type: "cosmolocal", city: "Sao Paulo", lat: -23.55, lng: -46.63, capabilities: ["screen-print", "risograph", "inkjet-print"], unitCost: 6.00, turnaround: "4-6 days" },
|
||
{ name: "Printful", type: "global", city: "Global", lat: 0, lng: 0, capabilities: ["dtg-print", "vinyl-cut", "inkjet-print"], unitCost: 9.25, turnaround: "5-7 days" },
|
||
];
|
||
|
||
const DEMO_BUYER = { lat: 52.52, lng: 13.41 }; // Berlin
|
||
|
||
function haversineKm(lat1: number, lng1: number, lat2: number, lng2: number): number {
|
||
const R = 6371;
|
||
const dLat = (lat2 - lat1) * Math.PI / 180;
|
||
const dLng = (lng2 - lng1) * Math.PI / 180;
|
||
const a = Math.sin(dLat / 2) ** 2 + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * Math.sin(dLng / 2) ** 2;
|
||
return R * 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
|
||
}
|
||
|
||
// --- SVG generators ---
|
||
|
||
function cosmoDesignSvg(): string {
|
||
return `<svg viewBox="0 0 200 200" xmlns="http://www.w3.org/2000/svg">
|
||
<rect width="200" height="200" fill="#0f172a"/>
|
||
<circle cx="100" cy="100" r="70" fill="none" stroke="#6366f1" stroke-width="1.5" opacity="0.4"/>
|
||
<circle cx="100" cy="100" r="50" fill="none" stroke="#818cf8" stroke-width="1.5" opacity="0.6"/>
|
||
<circle cx="100" cy="100" r="30" fill="none" stroke="#a5b4fc" stroke-width="1.5" opacity="0.8"/>
|
||
<circle cx="100" cy="100" r="10" fill="#6366f1"/>
|
||
${Array.from({length: 12}, (_, i) => {
|
||
const a = i * 30 * Math.PI / 180;
|
||
return `<line x1="${100 + 15 * Math.cos(a)}" y1="${100 + 15 * Math.sin(a)}" x2="${100 + 75 * Math.cos(a)}" y2="${100 + 75 * Math.sin(a)}" stroke="#818cf8" stroke-width="1" opacity="0.3"/>`;
|
||
}).join("")}
|
||
<circle cx="100" cy="100" r="5" fill="#e2e8f0"/>
|
||
<text x="100" y="160" text-anchor="middle" fill="#94a3b8" font-size="8" font-family="system-ui">COSMOLOCAL</text>
|
||
<text x="100" y="170" text-anchor="middle" fill="#64748b" font-size="6" font-family="system-ui">NETWORK</text>
|
||
</svg>`;
|
||
}
|
||
|
||
function teeMockupSvg(color: string): string {
|
||
return `<svg viewBox="0 0 240 300" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M60,40 L30,60 L10,120 L40,130 L50,90 L50,280 L190,280 L190,90 L200,130 L230,120 L210,60 L180,40 L160,55 Q140,70 120,70 Q100,70 80,55 Z" fill="${color}" stroke="#475569" stroke-width="1.5"/>
|
||
<defs><clipPath id="tee-clip"><rect x="80" y="90" width="80" height="107" rx="4"/></clipPath></defs>
|
||
<g clip-path="url(#tee-clip)" transform="translate(80,90)">
|
||
<svg viewBox="0 0 200 200" width="80" height="80" y="13">${cosmoDesignSvg().replace(/<svg[^>]*>/, "").replace("</svg>", "")}</svg>
|
||
</g>
|
||
<rect x="80" y="90" width="80" height="107" rx="4" fill="none" stroke="#6366f1" stroke-width="0.75" stroke-dasharray="3,2" opacity="0.5"/>
|
||
</svg>`;
|
||
}
|
||
|
||
function hoodieMockupSvg(color: string): string {
|
||
return `<svg viewBox="0 0 260 310" xmlns="http://www.w3.org/2000/svg">
|
||
<path d="M65,50 L30,70 L5,140 L40,150 L55,100 L55,290 L205,290 L205,100 L220,150 L255,140 L230,70 L195,50 L175,60 Q155,80 130,85 Q105,80 85,60 Z" fill="${color}" stroke="#475569" stroke-width="1.5"/>
|
||
<path d="M85,60 Q105,30 130,25 Q155,30 175,60 Q155,80 130,85 Q105,80 85,60 Z" fill="${color}" stroke="#475569" stroke-width="1"/>
|
||
<ellipse cx="130" cy="55" rx="20" ry="15" fill="#1e293b" stroke="#475569" stroke-width="1"/>
|
||
<defs><clipPath id="hoodie-clip"><rect x="80" y="100" width="100" height="115" rx="4"/></clipPath></defs>
|
||
<g clip-path="url(#hoodie-clip)" transform="translate(80,100)">
|
||
<svg viewBox="0 0 200 200" width="100" height="100" y="8">${cosmoDesignSvg().replace(/<svg[^>]*>/, "").replace("</svg>", "")}</svg>
|
||
</g>
|
||
<rect x="80" y="100" width="100" height="115" rx="4" fill="none" stroke="#6366f1" stroke-width="0.75" stroke-dasharray="3,2" opacity="0.5"/>
|
||
</svg>`;
|
||
}
|
||
|
||
function stickerMockupSvg(): string {
|
||
return `<svg viewBox="0 0 240 240" xmlns="http://www.w3.org/2000/svg">
|
||
<rect x="20" y="20" width="200" height="200" rx="16" fill="#1e293b" stroke="#475569" stroke-width="1.5"/>
|
||
<rect x="30" y="30" width="180" height="180" rx="12" fill="none" stroke="#6366f1" stroke-width="1" stroke-dasharray="4,3" opacity="0.5"/>
|
||
<g transform="translate(40,40)">
|
||
<svg viewBox="0 0 200 200" width="160" height="160">${cosmoDesignSvg().replace(/<svg[^>]*>/, "").replace("</svg>", "")}</svg>
|
||
</g>
|
||
<text x="120" y="232" text-anchor="middle" fill="#64748b" font-size="7" font-family="system-ui">kiss-cut border</text>
|
||
</svg>`;
|
||
}
|
||
|
||
function posterMockupSvg(): string {
|
||
return `<svg viewBox="0 0 220 300" xmlns="http://www.w3.org/2000/svg">
|
||
<rect x="15" y="15" width="190" height="270" fill="#1e293b" stroke="#475569" stroke-width="2"/>
|
||
<rect x="25" y="25" width="170" height="250" fill="none" stroke="#334155" stroke-width="1"/>
|
||
<g transform="translate(40,50)">
|
||
<svg viewBox="0 0 200 200" width="140" height="140">${cosmoDesignSvg().replace(/<svg[^>]*>/, "").replace("</svg>", "")}</svg>
|
||
</g>
|
||
<text x="110" y="230" text-anchor="middle" fill="#94a3b8" font-size="10" font-family="system-ui" font-weight="600">COSMOLOCAL NETWORK</text>
|
||
<text x="110" y="245" text-anchor="middle" fill="#64748b" font-size="7" font-family="system-ui">A3 — 297×420mm — 300 DPI</text>
|
||
</svg>`;
|
||
}
|
||
|
||
// --- Component ---
|
||
|
||
class FolkSwagDesigner extends HTMLElement {
|
||
private shadow: ShadowRoot;
|
||
private space = "";
|
||
private selectedProduct = "tee";
|
||
private selectedSize = "M";
|
||
private selectedColor = "black";
|
||
private imageFile: File | null = null;
|
||
private imagePreview = "";
|
||
private designTitle = "";
|
||
private generating = false;
|
||
private artifact: any = null;
|
||
private error = "";
|
||
private demoStep: 1 | 2 | 3 | 4 = 1;
|
||
private progressStep = 0;
|
||
private usedSampleDesign = false;
|
||
|
||
constructor() {
|
||
super();
|
||
this.shadow = this.attachShadow({ mode: "open" });
|
||
}
|
||
|
||
connectedCallback() {
|
||
this.space = this.getAttribute("space") || "";
|
||
if (this.space === "demo") {
|
||
this.selectedProduct = "tee";
|
||
this.selectedSize = "M";
|
||
this.selectedColor = "black";
|
||
this.designTitle = "Cosmolocal Network Tee";
|
||
this.demoStep = 1;
|
||
this.render();
|
||
return;
|
||
}
|
||
this.render();
|
||
}
|
||
|
||
private getApiBase(): string {
|
||
const path = window.location.pathname;
|
||
const parts = path.split("/").filter(Boolean);
|
||
return parts.length >= 2 ? `/${parts[0]}/swag` : "/demo/swag";
|
||
}
|
||
|
||
private getDemoProduct(): DemoProduct {
|
||
return DEMO_PRODUCTS.find(p => p.id === this.selectedProduct) || DEMO_PRODUCTS[0];
|
||
}
|
||
|
||
private getAutoTitle(): string {
|
||
const p = this.getDemoProduct();
|
||
return `Cosmolocal Network ${p.name}`;
|
||
}
|
||
|
||
private demoSelectProduct(id: string) {
|
||
this.selectedProduct = id;
|
||
const p = this.getDemoProduct();
|
||
this.selectedSize = p.sizes?.[1] || "";
|
||
this.selectedColor = p.colors?.[0]?.id || "";
|
||
this.designTitle = this.getAutoTitle();
|
||
this.demoStep = 1;
|
||
this.usedSampleDesign = false;
|
||
this.artifact = null;
|
||
this.render();
|
||
}
|
||
|
||
private demoUseSample() {
|
||
this.usedSampleDesign = true;
|
||
this.demoStep = 2;
|
||
this.render();
|
||
}
|
||
|
||
private demoGenerate() {
|
||
this.demoStep = 3;
|
||
this.progressStep = 0;
|
||
this.render();
|
||
|
||
const steps = [1, 2, 3, 4];
|
||
const delays = [400, 400, 400, 300];
|
||
let elapsed = 0;
|
||
for (const s of steps) {
|
||
elapsed += delays[s - 1];
|
||
setTimeout(() => {
|
||
this.progressStep = s;
|
||
if (s === 4) {
|
||
this.buildDemoArtifact();
|
||
this.demoStep = 4;
|
||
}
|
||
this.render();
|
||
}, elapsed);
|
||
}
|
||
}
|
||
|
||
private buildDemoArtifact() {
|
||
const p = this.getDemoProduct();
|
||
const dims: Record<string, { w: number; h: number }> = {
|
||
tee: { w: 305, h: 406 }, sticker: { w: 210, h: 297 },
|
||
poster: { w: 297, h: 420 }, hoodie: { w: 356, h: 406 },
|
||
};
|
||
const d = dims[p.id] || dims.tee;
|
||
const wpx = Math.round((d.w / 25.4) * 300);
|
||
const hpx = Math.round((d.h / 25.4) * 300);
|
||
|
||
const capabilities: Record<string, string[]> = {
|
||
tee: ["dtg-print"], sticker: ["vinyl-cut"], poster: ["inkjet-print"], hoodie: ["dtg-print"],
|
||
};
|
||
const substrates: Record<string, string[]> = {
|
||
tee: ["cotton-standard", "cotton-organic"], sticker: ["vinyl-matte", "vinyl-gloss"],
|
||
poster: ["paper-160gsm-cover", "paper-100gsm"], hoodie: ["cotton-polyester-blend"],
|
||
};
|
||
|
||
this.artifact = {
|
||
title: this.designTitle || this.getAutoTitle(),
|
||
product: p.id,
|
||
spec: {
|
||
product_type: p.id === "sticker" ? "sticker-sheet" : p.id,
|
||
dimensions: { width_mm: d.w, height_mm: d.h },
|
||
dpi: 300,
|
||
color_space: "sRGB",
|
||
required_capabilities: capabilities[p.id],
|
||
substrates: substrates[p.id],
|
||
},
|
||
render_targets: {
|
||
print: { format: "png", label: "Print-Ready PNG", dimensions: `${wpx}x${hpx} px`, url: "#demo-print" },
|
||
preview: { format: "png", label: "Preview PNG", dimensions: `${Math.round(wpx / 4)}x${Math.round(hpx / 4)} px`, url: "#demo-preview" },
|
||
},
|
||
pricing: { creator_share: "35%", community_share: "15%", provider_share: "50%" },
|
||
next_actions: [
|
||
{ action: "ingest_to_rcart", label: "Send to rCart", url: "/demo/cart" },
|
||
{ action: "edit_design", label: "Edit Design" },
|
||
{ action: "save_to_rfiles", label: "Save to rFiles" },
|
||
],
|
||
...(this.selectedSize ? { size: this.selectedSize } : {}),
|
||
...(this.selectedColor ? { color: this.selectedColor } : {}),
|
||
};
|
||
}
|
||
|
||
private getMatchedProviders(): (DemoProvider & { distance: number })[] {
|
||
const p = this.getDemoProduct();
|
||
const cap = p.id === "sticker" ? "vinyl-cut" : p.id === "poster" ? "inkjet-print" : "dtg-print";
|
||
return DEMO_PROVIDERS
|
||
.filter(prov => prov.capabilities.includes(cap))
|
||
.map(prov => ({
|
||
...prov,
|
||
distance: prov.type === "global" ? Infinity : Math.round(haversineKm(DEMO_BUYER.lat, DEMO_BUYER.lng, prov.lat, prov.lng)),
|
||
}))
|
||
.sort((a, b) => a.distance - b.distance);
|
||
}
|
||
|
||
private async generate() {
|
||
if (this.space === "demo") {
|
||
this.demoGenerate();
|
||
return;
|
||
}
|
||
if (!this.imageFile || this.generating) return;
|
||
this.generating = true;
|
||
this.error = "";
|
||
this.artifact = null;
|
||
this.render();
|
||
|
||
try {
|
||
const formData = new FormData();
|
||
formData.append("image", this.imageFile);
|
||
formData.append("product", this.selectedProduct);
|
||
formData.append("title", this.designTitle || "Untitled Design");
|
||
|
||
const res = await fetch(`${this.getApiBase()}/api/artifact`, {
|
||
method: "POST",
|
||
body: formData,
|
||
});
|
||
|
||
if (!res.ok) {
|
||
const err = await res.json();
|
||
throw new Error(err.error || `Failed: ${res.status}`);
|
||
}
|
||
|
||
this.artifact = await res.json();
|
||
} catch (e) {
|
||
this.error = e instanceof Error ? e.message : "Generation failed";
|
||
} finally {
|
||
this.generating = false;
|
||
this.render();
|
||
}
|
||
}
|
||
|
||
private render() {
|
||
if (this.space === "demo") {
|
||
this.renderDemo();
|
||
} else {
|
||
this.renderFull();
|
||
}
|
||
}
|
||
|
||
// ---- Demo mode rendering (4-step flow) ----
|
||
|
||
private renderDemo() {
|
||
const p = this.getDemoProduct();
|
||
const isApparel = p.id === "tee" || p.id === "hoodie";
|
||
|
||
this.shadow.innerHTML = `
|
||
<style>${this.getDemoStyles()}</style>
|
||
|
||
<!-- Step indicators -->
|
||
<div class="steps-bar">
|
||
${[
|
||
{ n: 1, label: "Product" },
|
||
{ n: 2, label: "Design" },
|
||
{ n: 3, label: "Generate" },
|
||
{ n: 4, label: "Pipeline" },
|
||
].map(s => `<div class="step-dot ${this.demoStep >= s.n ? 'active' : ''} ${this.demoStep === s.n ? 'current' : ''}">
|
||
<span class="step-num">${this.demoStep > s.n ? '✓' : s.n}</span>
|
||
<span class="step-label">${s.label}</span>
|
||
</div>`).join('<div class="step-line"></div>')}
|
||
</div>
|
||
|
||
<!-- Step 1: Product Selection -->
|
||
<section class="step-section ${this.demoStep >= 1 ? 'visible' : ''}">
|
||
<div class="products">
|
||
${DEMO_PRODUCTS.map(dp => `
|
||
<div class="product ${this.selectedProduct === dp.id ? 'active' : ''}" data-product="${dp.id}">
|
||
<div class="product-icon">${this.productIcon(dp.id)}</div>
|
||
<div class="product-name">${dp.name}</div>
|
||
<div class="product-specs">${dp.printArea}</div>
|
||
<div class="product-cost">${dp.baseCost}</div>
|
||
${dp.printful ? '<span class="badge badge-printful">Printful</span>' : '<span class="badge badge-local">cosmolocal only</span>'}
|
||
</div>
|
||
`).join("")}
|
||
</div>
|
||
|
||
${isApparel && p.sizes ? `
|
||
<div class="option-row">
|
||
<span class="option-label">Size</span>
|
||
<div class="pills">
|
||
${p.sizes.map(s => `<button class="pill ${this.selectedSize === s ? 'active' : ''}" data-size="${s}">${s}</button>`).join("")}
|
||
</div>
|
||
</div>` : ""}
|
||
|
||
${isApparel && p.colors ? `
|
||
<div class="option-row">
|
||
<span class="option-label">Color</span>
|
||
<div class="color-swatches">
|
||
${p.colors.map(c => `<button class="swatch ${this.selectedColor === c.id ? 'active' : ''}" data-color="${c.id}" style="background:${c.hex}" title="${c.name}"></button>`).join("")}
|
||
</div>
|
||
</div>` : ""}
|
||
</section>
|
||
|
||
<!-- Step 2: Design Mockup -->
|
||
<section class="step-section ${this.demoStep >= 2 ? 'visible' : ''}">
|
||
<div class="mockup-area">
|
||
<div class="mockup-svg">${this.getMockupSvg()}</div>
|
||
<div class="mockup-info">
|
||
<h3 class="mockup-title">${this.esc(this.designTitle || this.getAutoTitle())}</h3>
|
||
<div class="mockup-meta">${p.name} ${this.selectedSize ? `/ ${this.selectedSize}` : ""} ${this.selectedColor && p.colors ? `/ ${p.colors.find(c => c.id === this.selectedColor)?.name || ""}` : ""}</div>
|
||
${!this.usedSampleDesign ? `<button class="btn btn-secondary sample-btn">Use Sample Design</button>` : '<span class="badge badge-ok">Sample design loaded</span>'}
|
||
</div>
|
||
</div>
|
||
</section>
|
||
|
||
<!-- Generate button (between step 2 and 3) -->
|
||
${this.demoStep === 2 && this.usedSampleDesign ? `
|
||
<div class="generate-row">
|
||
<button class="btn btn-primary generate-btn">Generate Print-Ready Files</button>
|
||
</div>` : ""}
|
||
|
||
<!-- Step 3: Progress + Artifact + Providers -->
|
||
${this.demoStep >= 3 ? this.renderStep3() : ""}
|
||
|
||
<!-- Step 4: Pipeline Visualization -->
|
||
${this.demoStep >= 4 ? this.renderStep4() : ""}
|
||
`;
|
||
|
||
this.bindDemoEvents();
|
||
}
|
||
|
||
private renderStep3(): string {
|
||
const progressLabels = ["Processing image...", "Generating artifact...", "Matching providers...", "Done!"];
|
||
|
||
let html = `<section class="step-section visible">`;
|
||
|
||
// Progress bar (always show)
|
||
html += `<div class="progress-bar">
|
||
<div class="progress-fill" style="width:${this.progressStep * 25}%"></div>
|
||
</div>
|
||
<div class="progress-steps">
|
||
${progressLabels.map((label, i) => `<span class="prog-label ${this.progressStep > i ? 'done' : this.progressStep === i + 1 ? 'active' : ''}">${this.progressStep > i + 1 ? '✓ ' : ''}${label}</span>`).join("")}
|
||
</div>`;
|
||
|
||
// Only show artifact + providers once done
|
||
if (this.demoStep >= 4 && this.artifact) {
|
||
const providers = this.getMatchedProviders();
|
||
const selectedProvider = providers[0];
|
||
const unitCost = selectedProvider?.unitCost || 9.25;
|
||
const provAmt = (unitCost * 0.5).toFixed(2);
|
||
const creatorAmt = (unitCost * 0.35).toFixed(2);
|
||
const communityAmt = (unitCost * 0.15).toFixed(2);
|
||
|
||
// Artifact card
|
||
html += `
|
||
<div class="artifact-card">
|
||
<h3 class="artifact-heading">Artifact Envelope</h3>
|
||
<div class="artifact-grid">
|
||
<div class="artifact-field"><span class="af-label">Product</span><span class="af-value">${this.esc(this.artifact.spec.product_type)}</span></div>
|
||
<div class="artifact-field"><span class="af-label">Dimensions</span><span class="af-value">${this.artifact.spec.dimensions.width_mm}×${this.artifact.spec.dimensions.height_mm}mm</span></div>
|
||
<div class="artifact-field"><span class="af-label">DPI</span><span class="af-value">${this.artifact.spec.dpi}</span></div>
|
||
<div class="artifact-field"><span class="af-label">Color Space</span><span class="af-value">${this.artifact.spec.color_space}</span></div>
|
||
<div class="artifact-field"><span class="af-label">Capabilities</span><span class="af-value">${(this.artifact.spec.required_capabilities || []).join(", ")}</span></div>
|
||
<div class="artifact-field"><span class="af-label">Substrates</span><span class="af-value">${(this.artifact.spec.substrates || []).join(", ")}</span></div>
|
||
</div>
|
||
<div class="artifact-targets">
|
||
${Object.values(this.artifact.render_targets).map((t: any) => `<span class="target-chip">${t.label}: ${t.dimensions}</span>`).join("")}
|
||
</div>
|
||
<div class="artifact-actions">
|
||
${(this.artifact.next_actions || []).map((a: any) => `<span class="action-chip">${a.label}</span>`).join("")}
|
||
</div>
|
||
</div>`;
|
||
|
||
// Provider match table
|
||
html += `
|
||
<div class="provider-section">
|
||
<h3 class="provider-heading">Provider Matching <span class="buyer-loc">(buyer: Berlin)</span></h3>
|
||
<div class="provider-table">
|
||
<div class="pt-header">
|
||
<span>Provider</span><span>Type</span><span>City</span><span>Distance</span><span>Cost</span><span>Turnaround</span>
|
||
</div>
|
||
${providers.map((prov, i) => `
|
||
<div class="pt-row ${i === 0 ? 'nearest' : ''}">
|
||
<span class="pt-name">${prov.name}</span>
|
||
<span><span class="badge ${prov.type === 'cosmolocal' ? 'badge-cosmo' : 'badge-global'}">${prov.type}</span></span>
|
||
<span>${prov.city}</span>
|
||
<span>${prov.distance === Infinity ? '--' : `~${prov.distance.toLocaleString()} km`}</span>
|
||
<span class="pt-cost">$${prov.unitCost.toFixed(2)}</span>
|
||
<span>${prov.turnaround}</span>
|
||
</div>`).join("")}
|
||
</div>
|
||
</div>`;
|
||
|
||
// Revenue split bar
|
||
html += `
|
||
<div class="split-section">
|
||
<h4 class="split-heading">Revenue Split <span class="split-total">(from $${unitCost.toFixed(2)} unit cost)</span></h4>
|
||
<div class="split-bar">
|
||
<div class="split-seg split-provider" style="flex:50">Provider 50% <span class="split-amt">$${provAmt}</span></div>
|
||
<div class="split-seg split-creator" style="flex:35">Creator 35% <span class="split-amt">$${creatorAmt}</span></div>
|
||
<div class="split-seg split-community" style="flex:15"><span class="split-amt">$${communityAmt}</span></div>
|
||
</div>
|
||
<div class="split-legend"><span>Community 15%</span></div>
|
||
</div>`;
|
||
}
|
||
|
||
html += `</section>`;
|
||
return html;
|
||
}
|
||
|
||
private renderStep4(): string {
|
||
return `
|
||
<section class="step-section visible">
|
||
<h3 class="pipeline-heading">Print-on-Demand Pipeline</h3>
|
||
<div class="pipeline">
|
||
<div class="pipe-node done"><div class="pipe-icon">${this.productIcon("tee")}</div><div class="pipe-label">rSwag Design</div><span class="pipe-check">✓</span></div>
|
||
<div class="pipe-arrow">→</div>
|
||
<div class="pipe-node done"><div class="pipe-icon"><svg viewBox="0 0 20 20" width="24" height="24"><rect x="2" y="4" width="16" height="12" rx="2" fill="none" stroke="#818cf8" stroke-width="1.5"/><line x1="6" y1="8" x2="14" y2="8" stroke="#818cf8" stroke-width="1"/><line x1="6" y1="11" x2="12" y2="11" stroke="#818cf8" stroke-width="1"/></svg></div><div class="pipe-label">Artifact Spec</div><span class="pipe-check">✓</span></div>
|
||
<div class="pipe-arrow">→</div>
|
||
<div class="pipe-node done"><div class="pipe-icon"><svg viewBox="0 0 20 20" width="24" height="24"><rect x="3" y="3" width="14" height="14" rx="3" fill="none" stroke="#818cf8" stroke-width="1.5"/><circle cx="10" cy="10" r="3" fill="#818cf8"/></svg></div><div class="pipe-label">rCart Catalog</div><span class="pipe-check">✓</span></div>
|
||
<div class="pipe-arrow">→</div>
|
||
<div class="pipe-node done"><div class="pipe-icon"><svg viewBox="0 0 20 20" width="24" height="24"><circle cx="10" cy="8" r="4" fill="none" stroke="#818cf8" stroke-width="1.5"/><path d="M4,18 Q4,13 10,13 Q16,13 16,18" fill="none" stroke="#818cf8" stroke-width="1.5"/></svg></div><div class="pipe-label">Provider Match</div><span class="pipe-check">✓</span></div>
|
||
<div class="pipe-arrow">→</div>
|
||
<div class="pipe-node done"><div class="pipe-icon"><svg viewBox="0 0 20 20" width="24" height="24"><rect x="3" y="5" width="14" height="10" rx="2" fill="none" stroke="#818cf8" stroke-width="1.5"/><path d="M3,8 L10,12 L17,8" fill="none" stroke="#818cf8" stroke-width="1.5"/></svg></div><div class="pipe-label">Local Print</div><span class="pipe-check">✓</span></div>
|
||
</div>
|
||
<div class="pipeline-actions">
|
||
<a class="btn btn-primary" href="/demo/cart">Send to rCart</a>
|
||
<button class="btn btn-secondary json-toggle-btn">View Artifact JSON</button>
|
||
</div>
|
||
<pre class="json-pre">${this.esc(JSON.stringify(this.artifact, null, 2))}</pre>
|
||
</section>`;
|
||
}
|
||
|
||
private productIcon(id: string): string {
|
||
const icons: Record<string, string> = {
|
||
tee: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M7,4 L4,6 L2,12 L5,13 L6,9 L6,22 L18,22 L18,9 L19,13 L22,12 L20,6 L17,4 L15,6 Q14,8 12,8 Q10,8 9,6 Z" fill="none" stroke="#818cf8" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
|
||
sticker: `<svg viewBox="0 0 24 24" width="28" height="28"><rect x="3" y="3" width="18" height="18" rx="4" fill="none" stroke="#818cf8" stroke-width="1.5"/><circle cx="12" cy="12" r="5" fill="none" stroke="#818cf8" stroke-width="1.5"/></svg>`,
|
||
poster: `<svg viewBox="0 0 24 24" width="28" height="28"><rect x="4" y="2" width="16" height="20" rx="2" fill="none" stroke="#818cf8" stroke-width="1.5"/><line x1="8" y1="7" x2="16" y2="7" stroke="#818cf8" stroke-width="1"/><line x1="8" y1="10" x2="16" y2="10" stroke="#818cf8" stroke-width="1"/><rect x="8" y="13" width="8" height="5" rx="1" fill="none" stroke="#818cf8" stroke-width="1"/></svg>`,
|
||
hoodie: `<svg viewBox="0 0 24 24" width="28" height="28"><path d="M7,5 L3,7 L1,14 L4,15 L5,10 L5,22 L19,22 L19,10 L20,15 L23,14 L21,7 L17,5 L15,7 Q14,9 12,9 Q10,9 9,7 Z" fill="none" stroke="#818cf8" stroke-width="1.5" stroke-linejoin="round"/><ellipse cx="12" cy="6" rx="3" ry="2" fill="none" stroke="#818cf8" stroke-width="1"/></svg>`,
|
||
};
|
||
return icons[id] || icons.tee;
|
||
}
|
||
|
||
private getMockupSvg(): string {
|
||
const colorHex = this.getDemoProduct().colors?.find(c => c.id === this.selectedColor)?.hex || "#0a0a0a";
|
||
switch (this.selectedProduct) {
|
||
case "tee": return teeMockupSvg(colorHex);
|
||
case "hoodie": return hoodieMockupSvg(colorHex);
|
||
case "sticker": return stickerMockupSvg();
|
||
case "poster": return posterMockupSvg();
|
||
default: return teeMockupSvg(colorHex);
|
||
}
|
||
}
|
||
|
||
private bindDemoEvents() {
|
||
// Product selection
|
||
this.shadow.querySelectorAll(".product").forEach(el => {
|
||
el.addEventListener("click", () => this.demoSelectProduct((el as HTMLElement).dataset.product || "tee"));
|
||
});
|
||
// Size pills
|
||
this.shadow.querySelectorAll(".pill[data-size]").forEach(el => {
|
||
el.addEventListener("click", () => { this.selectedSize = (el as HTMLElement).dataset.size || "M"; this.render(); });
|
||
});
|
||
// Color swatches
|
||
this.shadow.querySelectorAll(".swatch[data-color]").forEach(el => {
|
||
el.addEventListener("click", () => { this.selectedColor = (el as HTMLElement).dataset.color || "black"; this.render(); });
|
||
});
|
||
// Sample design button
|
||
this.shadow.querySelector(".sample-btn")?.addEventListener("click", () => this.demoUseSample());
|
||
// Generate button
|
||
this.shadow.querySelector(".generate-btn")?.addEventListener("click", () => this.demoGenerate());
|
||
// JSON toggle
|
||
this.shadow.querySelector(".json-toggle-btn")?.addEventListener("click", () => {
|
||
const pre = this.shadow.querySelector(".json-pre");
|
||
pre?.classList.toggle("visible");
|
||
});
|
||
}
|
||
|
||
// ---- Full (non-demo) rendering ----
|
||
|
||
private renderFull() {
|
||
const products = [
|
||
{ id: "sticker", name: "Sticker Sheet", icon: "📋", desc: "A4 vinyl stickers" },
|
||
{ id: "poster", name: "Poster (A3)", icon: "🖼", desc: "A3 art print" },
|
||
{ id: "tee", name: "T-Shirt", icon: "👕", desc: '12x16" DTG print' },
|
||
{ id: "hoodie", name: "Hoodie", icon: "🧥", desc: '14x16" DTG print' },
|
||
];
|
||
|
||
this.shadow.innerHTML = `
|
||
<style>
|
||
:host { display: block; padding: 1.5rem; max-width: 900px; margin: 0 auto; }
|
||
.products { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; margin-bottom: 1.5rem; }
|
||
.product { padding: 1rem; border-radius: 12px; border: 2px solid #334155; background: #1e293b; cursor: pointer; text-align: center; transition: all 0.15s; }
|
||
.product:hover { border-color: #475569; }
|
||
.product.active { border-color: #6366f1; background: rgba(99,102,241,0.1); }
|
||
.product-icon { font-size: 2rem; margin-bottom: 0.375rem; }
|
||
.product-name { color: #f1f5f9; font-weight: 600; font-size: 0.875rem; }
|
||
.product-desc { color: #64748b; font-size: 0.75rem; margin-top: 0.25rem; }
|
||
.upload-area { border: 2px dashed #334155; border-radius: 12px; padding: 2rem; text-align: center; margin-bottom: 1rem; cursor: pointer; transition: border-color 0.15s; background: #1e293b; }
|
||
.upload-area:hover { border-color: #6366f1; }
|
||
.upload-area.has-image { border-style: solid; border-color: #475569; }
|
||
.upload-label { color: #94a3b8; font-size: 0.875rem; }
|
||
.preview-img { max-width: 200px; max-height: 200px; border-radius: 8px; }
|
||
.title-input { width: 100%; padding: 0.625rem 0.75rem; border: 1px solid #334155; border-radius: 8px; background: #1e293b; color: #f1f5f9; font-size: 0.875rem; margin-bottom: 1rem; box-sizing: border-box; }
|
||
.title-input:focus { outline: none; border-color: #6366f1; }
|
||
.generate-btn { width: 100%; padding: 0.75rem; border: none; border-radius: 8px; background: #4f46e5; color: #fff; font-size: 1rem; font-weight: 600; cursor: pointer; margin-bottom: 1rem; }
|
||
.generate-btn:hover { background: #4338ca; }
|
||
.generate-btn:disabled { opacity: 0.5; cursor: not-allowed; }
|
||
.error { background: rgba(239,68,68,0.1); border: 1px solid #ef4444; border-radius: 8px; padding: 0.75rem; color: #fca5a5; font-size: 0.875rem; margin-bottom: 1rem; }
|
||
.result { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 1.25rem; }
|
||
.result-title { color: #f1f5f9; font-weight: 600; margin: 0 0 0.5rem; }
|
||
.result-meta { color: #94a3b8; font-size: 0.8125rem; margin-bottom: 1rem; }
|
||
.result-actions { display: flex; gap: 0.75rem; flex-wrap: wrap; }
|
||
.result-btn { padding: 0.5rem 1rem; border-radius: 8px; font-size: 0.875rem; text-decoration: none; font-weight: 500; cursor: pointer; border: none; }
|
||
.result-btn-primary { background: #4f46e5; color: #fff; }
|
||
.result-btn-primary:hover { background: #4338ca; }
|
||
.result-btn-secondary { background: #334155; color: #f1f5f9; }
|
||
.result-btn-secondary:hover { background: #475569; }
|
||
.json-toggle { color: #818cf8; cursor: pointer; font-size: 0.75rem; margin-top: 0.75rem; display: inline-block; }
|
||
.json-pre { background: #0f172a; border-radius: 8px; padding: 0.75rem; overflow-x: auto; font-size: 0.6875rem; color: #94a3b8; margin-top: 0.5rem; max-height: 300px; display: none; }
|
||
.json-pre.visible { display: block; }
|
||
input[type="file"] { display: none; }
|
||
@media (max-width: 768px) {
|
||
.products { grid-template-columns: repeat(2, 1fr); }
|
||
}
|
||
</style>
|
||
|
||
<div class="products">
|
||
${products.map((p) => `
|
||
<div class="product ${this.selectedProduct === p.id ? 'active' : ''}" data-product="${p.id}">
|
||
<div class="product-icon">${p.icon}</div>
|
||
<div class="product-name">${p.name}</div>
|
||
<div class="product-desc">${p.desc}</div>
|
||
</div>
|
||
`).join("")}
|
||
</div>
|
||
|
||
<div class="upload-area ${this.imagePreview ? 'has-image' : ''}">
|
||
${this.imagePreview
|
||
? `<img class="preview-img" src="${this.imagePreview}" alt="Preview">`
|
||
: `<div class="upload-label">Click or drag to upload artwork (PNG, JPG, SVG)</div>`}
|
||
<input type="file" accept="image/*">
|
||
</div>
|
||
|
||
<input class="title-input" type="text" placeholder="Design title" value="${this.esc(this.designTitle)}">
|
||
|
||
<button class="generate-btn" ${!this.imageFile || this.generating ? 'disabled' : ''}>
|
||
${this.generating ? 'Generating...' : 'Generate Print-Ready Files'}
|
||
</button>
|
||
|
||
${this.error ? `<div class="error">${this.esc(this.error)}</div>` : ""}
|
||
|
||
${this.artifact ? `
|
||
<div class="result">
|
||
<h3 class="result-title">${this.esc(this.artifact.payload?.title || this.artifact.title || "Artifact")}</h3>
|
||
<div class="result-meta">
|
||
${this.esc(this.artifact.spec?.product_type || "")} •
|
||
${this.artifact.spec?.dimensions?.width_mm}x${this.artifact.spec?.dimensions?.height_mm}mm •
|
||
${this.artifact.spec?.dpi}dpi
|
||
</div>
|
||
<div class="result-actions">
|
||
${Object.entries(this.artifact.render_targets || {}).map(([key, target]: [string, any]) => `
|
||
<a class="result-btn result-btn-primary" href="${target.url}" target="_blank">Download ${target.format.toUpperCase()}</a>
|
||
`).join("")}
|
||
<button class="result-btn result-btn-secondary" data-action="copy-json">Copy Artifact JSON</button>
|
||
</div>
|
||
<span class="json-toggle">Show artifact envelope</span>
|
||
<pre class="json-pre">${this.esc(JSON.stringify(this.artifact, null, 2))}</pre>
|
||
</div>` : ""}
|
||
`;
|
||
|
||
// Event listeners
|
||
this.shadow.querySelectorAll(".product").forEach((el) => {
|
||
el.addEventListener("click", () => {
|
||
this.selectedProduct = (el as HTMLElement).dataset.product || "sticker";
|
||
this.render();
|
||
});
|
||
});
|
||
|
||
const uploadArea = this.shadow.querySelector(".upload-area");
|
||
const fileInput = this.shadow.querySelector('input[type="file"]') as HTMLInputElement;
|
||
uploadArea?.addEventListener("click", () => fileInput?.click());
|
||
fileInput?.addEventListener("change", () => {
|
||
const file = fileInput.files?.[0];
|
||
if (file) {
|
||
this.imageFile = file;
|
||
this.imagePreview = URL.createObjectURL(file);
|
||
this.render();
|
||
}
|
||
});
|
||
|
||
this.shadow.querySelector(".title-input")?.addEventListener("input", (e) => {
|
||
this.designTitle = (e.target as HTMLInputElement).value;
|
||
});
|
||
|
||
this.shadow.querySelector(".generate-btn")?.addEventListener("click", () => this.generate());
|
||
|
||
this.shadow.querySelector(".json-toggle")?.addEventListener("click", () => {
|
||
const pre = this.shadow.querySelector(".json-pre");
|
||
pre?.classList.toggle("visible");
|
||
});
|
||
|
||
this.shadow.querySelector('[data-action="copy-json"]')?.addEventListener("click", () => {
|
||
navigator.clipboard.writeText(JSON.stringify(this.artifact, null, 2));
|
||
});
|
||
}
|
||
|
||
private getDemoStyles(): string {
|
||
return `
|
||
:host { display: block; padding: 1.5rem; max-width: 960px; margin: 0 auto; }
|
||
*, *::before, *::after { box-sizing: border-box; }
|
||
|
||
/* Step bar */
|
||
.steps-bar { display: flex; align-items: center; justify-content: center; gap: 0; margin-bottom: 2rem; }
|
||
.step-dot { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; }
|
||
.step-num { width: 28px; height: 28px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.75rem; font-weight: 600; border: 2px solid #334155; color: #64748b; background: #1e293b; transition: all 0.2s; }
|
||
.step-dot.active .step-num { border-color: #6366f1; color: #fff; background: #4f46e5; }
|
||
.step-dot.current .step-num { box-shadow: 0 0 0 3px rgba(99,102,241,0.3); }
|
||
.step-label { font-size: 0.6875rem; color: #64748b; }
|
||
.step-dot.active .step-label { color: #a5b4fc; }
|
||
.step-line { width: 40px; height: 2px; background: #334155; margin: 0 0.5rem; margin-bottom: 1rem; }
|
||
|
||
/* Product grid */
|
||
.products { display: grid; grid-template-columns: repeat(4, 1fr); gap: 0.75rem; margin-bottom: 1.5rem; }
|
||
.product { padding: 0.875rem 0.5rem; border-radius: 12px; border: 2px solid #334155; background: #1e293b; cursor: pointer; text-align: center; transition: all 0.15s; }
|
||
.product:hover { border-color: #475569; }
|
||
.product.active { border-color: #6366f1; background: rgba(99,102,241,0.1); }
|
||
.product-icon { margin-bottom: 0.375rem; display: flex; justify-content: center; }
|
||
.product-name { color: #f1f5f9; font-weight: 600; font-size: 0.8125rem; }
|
||
.product-specs { color: #64748b; font-size: 0.6875rem; margin-top: 0.125rem; }
|
||
.product-cost { color: #94a3b8; font-size: 0.75rem; margin-top: 0.25rem; }
|
||
|
||
/* Badges */
|
||
.badge { display: inline-block; padding: 0.125rem 0.5rem; border-radius: 999px; font-size: 0.625rem; font-weight: 600; margin-top: 0.375rem; }
|
||
.badge-printful { background: rgba(56,189,248,0.15); color: #38bdf8; }
|
||
.badge-local { background: rgba(34,197,94,0.15); color: #4ade80; }
|
||
.badge-cosmo { background: rgba(34,197,94,0.15); color: #4ade80; }
|
||
.badge-global { background: rgba(56,189,248,0.15); color: #38bdf8; }
|
||
.badge-ok { background: rgba(34,197,94,0.15); color: #4ade80; padding: 0.25rem 0.75rem; font-size: 0.75rem; }
|
||
|
||
/* Options */
|
||
.option-row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; }
|
||
.option-label { color: #94a3b8; font-size: 0.8125rem; font-weight: 500; min-width: 40px; }
|
||
.pills { display: flex; gap: 0.375rem; flex-wrap: wrap; }
|
||
.pill { padding: 0.375rem 0.75rem; border-radius: 999px; border: 1px solid #334155; background: #1e293b; color: #94a3b8; font-size: 0.75rem; cursor: pointer; transition: all 0.15s; }
|
||
.pill:hover { border-color: #475569; color: #f1f5f9; }
|
||
.pill.active { border-color: #6366f1; background: rgba(99,102,241,0.15); color: #fff; }
|
||
.color-swatches { display: flex; gap: 0.5rem; }
|
||
.swatch { width: 28px; height: 28px; border-radius: 50%; border: 2px solid #334155; cursor: pointer; transition: all 0.15s; }
|
||
.swatch:hover { border-color: #475569; }
|
||
.swatch.active { border-color: #6366f1; box-shadow: 0 0 0 3px rgba(99,102,241,0.3); }
|
||
|
||
/* Mockup */
|
||
.mockup-area { display: flex; gap: 1.5rem; align-items: center; background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 1.5rem; margin-bottom: 1.5rem; }
|
||
.mockup-svg { flex-shrink: 0; }
|
||
.mockup-svg svg { width: 180px; height: auto; }
|
||
.mockup-info { flex: 1; }
|
||
.mockup-title { color: #f1f5f9; font-weight: 600; font-size: 1rem; margin: 0 0 0.25rem; }
|
||
.mockup-meta { color: #94a3b8; font-size: 0.8125rem; margin-bottom: 0.75rem; }
|
||
|
||
/* Buttons */
|
||
.btn { padding: 0.625rem 1.25rem; border-radius: 8px; font-size: 0.875rem; font-weight: 600; cursor: pointer; border: none; text-decoration: none; display: inline-block; transition: all 0.15s; }
|
||
.btn-primary { background: #4f46e5; color: #fff; }
|
||
.btn-primary:hover { background: #4338ca; }
|
||
.btn-secondary { background: #334155; color: #f1f5f9; }
|
||
.btn-secondary:hover { background: #475569; }
|
||
.generate-row { text-align: center; margin-bottom: 1.5rem; }
|
||
|
||
/* Step sections */
|
||
.step-section { margin-bottom: 1.5rem; }
|
||
.step-section:not(.visible) { display: none; }
|
||
|
||
/* Progress bar */
|
||
.progress-bar { height: 6px; background: #1e293b; border-radius: 3px; overflow: hidden; margin-bottom: 0.5rem; }
|
||
.progress-fill { height: 100%; background: linear-gradient(90deg, #6366f1, #818cf8); border-radius: 3px; transition: width 0.3s ease; }
|
||
.progress-steps { display: flex; justify-content: space-between; margin-bottom: 1.5rem; }
|
||
.prog-label { font-size: 0.6875rem; color: #475569; transition: color 0.2s; }
|
||
.prog-label.active { color: #818cf8; font-weight: 600; }
|
||
.prog-label.done { color: #4ade80; }
|
||
|
||
/* Artifact card */
|
||
.artifact-card { background: #1e293b; border: 1px solid #334155; border-radius: 12px; padding: 1.25rem; margin-bottom: 1rem; }
|
||
.artifact-heading { color: #f1f5f9; font-weight: 600; font-size: 0.9375rem; margin: 0 0 0.75rem; }
|
||
.artifact-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 0.5rem 1rem; margin-bottom: 0.75rem; }
|
||
.artifact-field { display: flex; flex-direction: column; }
|
||
.af-label { color: #64748b; font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.05em; }
|
||
.af-value { color: #e2e8f0; font-size: 0.8125rem; }
|
||
.artifact-targets { display: flex; gap: 0.5rem; flex-wrap: wrap; margin-bottom: 0.5rem; }
|
||
.target-chip { background: rgba(99,102,241,0.1); color: #a5b4fc; padding: 0.25rem 0.625rem; border-radius: 6px; font-size: 0.6875rem; }
|
||
.artifact-actions { display: flex; gap: 0.5rem; flex-wrap: wrap; }
|
||
.action-chip { background: #0f172a; color: #94a3b8; padding: 0.25rem 0.625rem; border-radius: 6px; font-size: 0.6875rem; border: 1px solid #334155; }
|
||
|
||
/* Provider table */
|
||
.provider-section { margin-bottom: 1rem; }
|
||
.provider-heading { color: #f1f5f9; font-weight: 600; font-size: 0.9375rem; margin: 0 0 0.75rem; }
|
||
.buyer-loc { color: #64748b; font-weight: 400; font-size: 0.75rem; }
|
||
.provider-table { background: #1e293b; border: 1px solid #334155; border-radius: 12px; overflow: hidden; }
|
||
.pt-header, .pt-row { display: grid; grid-template-columns: 2fr 1fr 1fr 1.2fr 0.8fr 1fr; padding: 0.625rem 1rem; font-size: 0.8125rem; align-items: center; }
|
||
.pt-header { background: #0f172a; color: #64748b; font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; }
|
||
.pt-row { color: #e2e8f0; border-top: 1px solid #1e293b; }
|
||
.pt-row.nearest { background: rgba(99,102,241,0.05); border-left: 3px solid #6366f1; }
|
||
.pt-name { font-weight: 500; }
|
||
.pt-cost { color: #4ade80; font-weight: 600; }
|
||
|
||
/* Split bar */
|
||
.split-section { margin-bottom: 1.5rem; }
|
||
.split-heading { color: #94a3b8; font-size: 0.8125rem; font-weight: 500; margin: 0 0 0.5rem; }
|
||
.split-total { color: #64748b; font-weight: 400; }
|
||
.split-bar { display: flex; height: 32px; border-radius: 8px; overflow: hidden; font-size: 0.6875rem; font-weight: 600; }
|
||
.split-seg { display: flex; align-items: center; justify-content: center; gap: 0.25rem; color: #fff; }
|
||
.split-provider { background: #16a34a; }
|
||
.split-creator { background: #4f46e5; }
|
||
.split-community { background: #d97706; }
|
||
.split-amt { opacity: 0.85; }
|
||
.split-legend { text-align: right; margin-top: 0.25rem; }
|
||
.split-legend span { color: #64748b; font-size: 0.6875rem; }
|
||
|
||
/* Pipeline */
|
||
.pipeline-heading { color: #f1f5f9; font-weight: 600; font-size: 0.9375rem; margin: 0 0 1rem; text-align: center; }
|
||
.pipeline { display: flex; align-items: center; justify-content: center; gap: 0; flex-wrap: wrap; margin-bottom: 1.5rem; }
|
||
.pipe-node { display: flex; flex-direction: column; align-items: center; gap: 0.25rem; padding: 0.75rem; background: #1e293b; border: 1px solid #334155; border-radius: 10px; position: relative; min-width: 90px; }
|
||
.pipe-node.done { border-color: #22c55e; }
|
||
.pipe-icon { display: flex; align-items: center; justify-content: center; }
|
||
.pipe-label { color: #e2e8f0; font-size: 0.6875rem; font-weight: 500; text-align: center; }
|
||
.pipe-check { position: absolute; top: -6px; right: -6px; background: #16a34a; color: #fff; width: 16px; height: 16px; border-radius: 50%; display: flex; align-items: center; justify-content: center; font-size: 0.5625rem; }
|
||
.pipe-arrow { color: #475569; font-size: 1.25rem; margin: 0 0.25rem; margin-bottom: 0.75rem; }
|
||
|
||
/* Pipeline actions */
|
||
.pipeline-actions { display: flex; gap: 0.75rem; justify-content: center; margin-bottom: 1rem; }
|
||
|
||
/* JSON */
|
||
.json-pre { background: #0f172a; border: 1px solid #334155; border-radius: 8px; padding: 0.75rem; overflow-x: auto; font-size: 0.6875rem; color: #94a3b8; max-height: 300px; display: none; white-space: pre-wrap; word-break: break-all; }
|
||
.json-pre.visible { display: block; }
|
||
|
||
@media (max-width: 768px) {
|
||
.products { grid-template-columns: repeat(2, 1fr); }
|
||
.mockup-area { flex-direction: column; text-align: center; }
|
||
.pt-header, .pt-row { grid-template-columns: 1.5fr 0.8fr 0.8fr 1fr 0.7fr 0.8fr; font-size: 0.6875rem; padding: 0.5rem; }
|
||
.artifact-grid { grid-template-columns: repeat(2, 1fr); }
|
||
.pipeline { gap: 0.25rem; }
|
||
.pipe-node { min-width: 70px; padding: 0.5rem; }
|
||
.pipe-label { font-size: 0.5625rem; }
|
||
}
|
||
`;
|
||
}
|
||
|
||
private esc(s: string): string {
|
||
const d = document.createElement("div");
|
||
d.textContent = s;
|
||
return d.innerHTML;
|
||
}
|
||
}
|
||
|
||
customElements.define("folk-swag-designer", FolkSwagDesigner);
|