/** * — 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 ` ${Array.from({length: 12}, (_, i) => { const a = i * 30 * Math.PI / 180; return ``; }).join("")} COSMOLOCAL NETWORK `; } function teeMockupSvg(color: string): string { return ` ${cosmoDesignSvg().replace(/]*>/, "").replace("", "")} `; } function hoodieMockupSvg(color: string): string { return ` ${cosmoDesignSvg().replace(/]*>/, "").replace("", "")} `; } function stickerMockupSvg(): string { return ` ${cosmoDesignSvg().replace(/]*>/, "").replace("", "")} kiss-cut border `; } function posterMockupSvg(): string { return ` ${cosmoDesignSvg().replace(/]*>/, "").replace("", "")} COSMOLOCAL NETWORK A3 — 297×420mm — 300 DPI `; } // --- 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 = { 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 = { tee: ["dtg-print"], sticker: ["vinyl-cut"], poster: ["inkjet-print"], hoodie: ["dtg-print"], }; const substrates: Record = { 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 = `
${[ { n: 1, label: "Product" }, { n: 2, label: "Design" }, { n: 3, label: "Generate" }, { n: 4, label: "Pipeline" }, ].map(s => `
${this.demoStep > s.n ? '✓' : s.n} ${s.label}
`).join('
')}
${DEMO_PRODUCTS.map(dp => `
${this.productIcon(dp.id)}
${dp.name}
${dp.printArea}
${dp.baseCost}
${dp.printful ? 'Printful' : 'cosmolocal only'}
`).join("")}
${isApparel && p.sizes ? `
Size
${p.sizes.map(s => ``).join("")}
` : ""} ${isApparel && p.colors ? `
Color
${p.colors.map(c => ``).join("")}
` : ""}
${this.getMockupSvg()}

${this.esc(this.designTitle || this.getAutoTitle())}

${p.name} ${this.selectedSize ? `/ ${this.selectedSize}` : ""} ${this.selectedColor && p.colors ? `/ ${p.colors.find(c => c.id === this.selectedColor)?.name || ""}` : ""}
${!this.usedSampleDesign ? `` : 'Sample design loaded'}
${this.demoStep === 2 && this.usedSampleDesign ? `
` : ""} ${this.demoStep >= 3 ? this.renderStep3() : ""} ${this.demoStep >= 4 ? this.renderStep4() : ""} `; this.bindDemoEvents(); } private renderStep3(): string { const progressLabels = ["Processing image...", "Generating artifact...", "Matching providers...", "Done!"]; let html = `
`; // Progress bar (always show) html += `
${progressLabels.map((label, i) => `${this.progressStep > i + 1 ? '✓ ' : ''}${label}`).join("")}
`; // 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 += `

Artifact Envelope

Product${this.esc(this.artifact.spec.product_type)}
Dimensions${this.artifact.spec.dimensions.width_mm}×${this.artifact.spec.dimensions.height_mm}mm
DPI${this.artifact.spec.dpi}
Color Space${this.artifact.spec.color_space}
Capabilities${(this.artifact.spec.required_capabilities || []).join(", ")}
Substrates${(this.artifact.spec.substrates || []).join(", ")}
${Object.values(this.artifact.render_targets).map((t: any) => `${t.label}: ${t.dimensions}`).join("")}
${(this.artifact.next_actions || []).map((a: any) => `${a.label}`).join("")}
`; // Provider match table html += `

Provider Matching (buyer: Berlin)

ProviderTypeCityDistanceCostTurnaround
${providers.map((prov, i) => `
${prov.name} ${prov.type} ${prov.city} ${prov.distance === Infinity ? '--' : `~${prov.distance.toLocaleString()} km`} $${prov.unitCost.toFixed(2)} ${prov.turnaround}
`).join("")}
`; // Revenue split bar html += `

Revenue Split (from $${unitCost.toFixed(2)} unit cost)

Provider 50% $${provAmt}
Creator 35% $${creatorAmt}
$${communityAmt}
Community 15%
`; } html += `
`; return html; } private renderStep4(): string { return `

Print-on-Demand Pipeline

${this.productIcon("tee")}
rSwag Design
Artifact Spec
rCart Catalog
Provider Match
Local Print
Send to rCart
${this.esc(JSON.stringify(this.artifact, null, 2))}
`; } private productIcon(id: string): string { const icons: Record = { tee: ``, sticker: ``, poster: ``, hoodie: ``, }; 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 = `
${products.map((p) => `
${p.icon}
${p.name}
${p.desc}
`).join("")}
${this.imagePreview ? `Preview` : `
Click or drag to upload artwork (PNG, JPG, SVG)
`}
${this.error ? `
${this.esc(this.error)}
` : ""} ${this.artifact ? `

${this.esc(this.artifact.payload?.title || this.artifact.title || "Artifact")}

${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
${Object.entries(this.artifact.render_targets || {}).map(([key, target]: [string, any]) => ` Download ${target.format.toUpperCase()} `).join("")}
Show artifact envelope
${this.esc(JSON.stringify(this.artifact, null, 2))}
` : ""} `; // 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);