/** * — Full-featured merch design tool. * * 4-tab layout: * - Browse: Product catalog with mockups, filtering, add-to-cart * - Create: AI generate, upload, manage designs * - HitherDither: Dithering tools + screen-print separations * - Orders: rCart order status + fulfillment tracking * * Demo mode: 4-step interactive flow (preserved from original). */ // --- 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: "305x406mm", 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: "210x297mm", baseCost: "$1.20-$1.50", printful: true, }, { id: "poster", name: "Poster (A3)", printArea: "297x420mm", baseCost: "$4.50-$7.00", printful: false, }, { id: "hoodie", name: "Hoodie", printArea: "356x406mm", 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 - 297x420mm - 300 DPI `; } // --- Component --- import { TourEngine } from "../../../shared/tour-engine"; import { SwagLocalFirstClient } from "../local-first-client"; import type { SwagDoc, SwagDesign } from "../schemas"; // Auth helpers function getSession(): { accessToken: string; claims: { sub: string; did?: string; username?: string } } | null { try { const raw = localStorage.getItem("encryptid_session"); if (!raw) return null; const s = JSON.parse(raw); return s?.accessToken ? s : null; } catch { return null; } } function getMyDid(): string | null { const s = getSession(); if (!s) return null; return (s.claims as any).did || s.claims.sub; } // Dithering algorithms for the HitherDither tab const DITHER_ALGORITHMS = [ { id: "floyd-steinberg", name: "Floyd-Steinberg", group: "Error Diffusion" }, { id: "atkinson", name: "Atkinson", group: "Error Diffusion" }, { id: "stucki", name: "Stucki", group: "Error Diffusion" }, { id: "burkes", name: "Burkes", group: "Error Diffusion" }, { id: "sierra", name: "Sierra", group: "Error Diffusion" }, { id: "sierra-two-row", name: "Sierra Two-Row", group: "Error Diffusion" }, { id: "sierra-lite", name: "Sierra Lite", group: "Error Diffusion" }, { id: "jarvis-judice-ninke", name: "Jarvis-Judice-Ninke", group: "Error Diffusion" }, { id: "bayer", name: "Bayer (Ordered)", group: "Ordered" }, { id: "ordered", name: "Ordered", group: "Ordered" }, { id: "cluster-dot", name: "Cluster Dot", group: "Ordered" }, ]; type TabId = "browse" | "create" | "dither" | "orders"; class FolkSwagDesigner extends HTMLElement { private shadow: ShadowRoot; private space = ""; // Tab state private activeTab: TabId = "browse"; // Demo state 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; // Browse tab state private catalogProducts: any[] = []; private catalogLoading = false; private catalogSearch = ""; private catalogCategory = ""; // Create tab state private createMode: "ai" | "upload" | "designs" = "designs"; private aiConcept = ""; private aiName = ""; private aiTags = ""; private aiGenerating = false; private uploadFile: File | null = null; private uploadPreview = ""; private uploadName = ""; private uploadDescription = ""; private myDesigns: any[] = []; // Dither tab state private ditherDesignSlug = ""; private ditherAlgorithm = "floyd-steinberg"; private ditherNumColors = 8; private ditherPreviewUrl = ""; private ditherLoading = false; private ditherColors: string[] = []; private separationData: any = null; // Orders tab state private orders: any[] = []; // Multiplayer state private lfClient: SwagLocalFirstClient | null = null; private _lfcUnsub: (() => void) | null = null; private sharedDesigns: SwagDesign[] = []; private _tour!: TourEngine; private static readonly TOUR_STEPS = [ { target: '.product', title: "Choose Product", message: "Select a product type - tee, sticker, poster, or hoodie.", advanceOnClick: true }, { target: '.steps-bar', title: "Design Flow", message: "Follow the 4-step flow: Product, Design, Generate, Pipeline.", advanceOnClick: false }, { target: '.sample-btn', title: "Sample Design", message: "Try the demo with a pre-made sample design to see the full pipeline.", advanceOnClick: true }, { target: '.generate-btn', title: "Generate", message: "Generate print-ready files and see provider matching + revenue splits.", advanceOnClick: false }, ]; constructor() { super(); this.shadow = this.attachShadow({ mode: "open" }); this._tour = new TourEngine( this.shadow, FolkSwagDesigner.TOUR_STEPS, "rswag_tour_done", () => this.shadow.host as HTMLElement, ); } 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(); } else { this.initMultiplayer(); this.loadCatalog(); this.loadMyDesigns(); this.render(); } if (!localStorage.getItem("rswag_tour_done")) { setTimeout(() => this._tour.start(), 1200); } } disconnectedCallback() { this._lfcUnsub?.(); this._lfcUnsub = null; this.lfClient?.disconnect(); } private async initMultiplayer() { try { this.lfClient = new SwagLocalFirstClient(this.space); await this.lfClient.init(); await this.lfClient.subscribe(); this._lfcUnsub = this.lfClient.onChange((doc) => { this.extractDesigns(doc); this.render(); }); const doc = this.lfClient.getDoc(); if (doc) this.extractDesigns(doc); this.render(); } catch (err) { console.warn('[rSwag] Local-first init failed:', err); } } private extractDesigns(doc: SwagDoc) { this.sharedDesigns = doc.designs ? Object.values(doc.designs).sort((a, b) => b.updatedAt - a.updatedAt) : []; } private saveDesignToSync(artifactId: string) { if (!this.lfClient) return; const design: SwagDesign = { id: crypto.randomUUID(), title: this.designTitle || 'Untitled Design', productType: this.selectedProduct as SwagDesign['productType'], artifactId, source: 'artifact', status: 'active', imageUrl: null, products: [], slug: null, description: null, tags: [], createdBy: getMyDid(), createdAt: Date.now(), updatedAt: Date.now(), }; this.lfClient.saveDesign(design); } private deleteSharedDesign(designId: string) { if (!this.lfClient) return; if (confirm('Delete this design?')) { this.lfClient.deleteDesign(designId); } } private getApiBase(): string { const path = window.location.pathname; const match = path.match(/^(\/[^/]+)?\/rswag/); return match ? match[0] : "/rswag"; } // ── Data loading ── private async loadCatalog() { this.catalogLoading = true; try { const params = new URLSearchParams(); if (this.catalogSearch) params.set("q", this.catalogSearch); if (this.catalogCategory) params.set("category", this.catalogCategory); const resp = await fetch(`${this.getApiBase()}/api/storefront?${params}`); if (resp.ok) { const data = await resp.json(); this.catalogProducts = data.products || []; } } catch (e) { console.warn("[rSwag] Failed to load catalog:", e); } this.catalogLoading = false; this.render(); } private async loadMyDesigns() { try { const resp = await fetch(`${this.getApiBase()}/api/designs`); if (resp.ok) { const data = await resp.json(); this.myDesigns = data.designs || []; } } catch (e) { console.warn("[rSwag] Failed to load designs:", e); } } // ── Demo mode methods (preserved) ── 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: "/rcart?tab=catalog" }, { 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); } // ── Generate (full mode) ── 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(); if (this.artifact?.id) this.saveDesignToSync(this.artifact.id); } catch (e) { this.error = e instanceof Error ? e.message : "Generation failed"; } finally { this.generating = false; this.render(); } } // ── AI Generate ── private async aiGenerate() { if (this.aiGenerating || !this.aiConcept || !this.aiName) return; this.aiGenerating = true; this.error = ""; this.render(); try { const resp = await fetch(`${this.getApiBase()}/api/design/generate`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ concept: this.aiConcept, name: this.aiName, tags: this.aiTags ? this.aiTags.split(",").map(t => t.trim()) : [], product_type: "sticker", }), }); if (!resp.ok) { const err = await resp.json(); throw new Error(err.error || `Failed: ${resp.status}`); } const result = await resp.json(); this.error = ""; this.aiConcept = ""; this.aiName = ""; this.aiTags = ""; this.createMode = "designs"; await this.loadMyDesigns(); } catch (e) { this.error = e instanceof Error ? e.message : "AI generation failed"; } finally { this.aiGenerating = false; this.render(); } } // ── Upload ── private async uploadDesign() { if (!this.uploadFile) return; this.generating = true; this.error = ""; this.render(); try { const formData = new FormData(); formData.append("image", this.uploadFile); formData.append("name", this.uploadName || "Untitled Upload"); formData.append("description", this.uploadDescription || ""); formData.append("product_type", "sticker"); const resp = await fetch(`${this.getApiBase()}/api/design/upload`, { method: "POST", body: formData }); if (!resp.ok) { const err = await resp.json(); throw new Error(err.error || `Failed: ${resp.status}`); } this.uploadFile = null; this.uploadPreview = ""; this.uploadName = ""; this.uploadDescription = ""; this.createMode = "designs"; await this.loadMyDesigns(); } catch (e) { this.error = e instanceof Error ? e.message : "Upload failed"; } finally { this.generating = false; this.render(); } } // ── Dither ── private async applyDither() { if (!this.ditherDesignSlug) return; this.ditherLoading = true; this.render(); try { const params = new URLSearchParams({ algorithm: this.ditherAlgorithm, num_colors: String(this.ditherNumColors), format: "json", }); const resp = await fetch(`${this.getApiBase()}/api/designs/${this.ditherDesignSlug}/dither?${params}`); if (resp.ok) { const data = await resp.json(); this.ditherPreviewUrl = data.image_url; this.ditherColors = data.colors_used || []; } } catch (e) { console.warn("[rSwag] Dither error:", e); } this.ditherLoading = false; this.render(); } private async generateSeparations() { if (!this.ditherDesignSlug) return; this.ditherLoading = true; this.render(); try { const resp = await fetch(`${this.getApiBase()}/api/designs/${this.ditherDesignSlug}/screen-print`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ num_colors: this.ditherNumColors, algorithm: this.ditherAlgorithm, }), }); if (resp.ok) { this.separationData = await resp.json(); } } catch (e) { console.warn("[rSwag] Separation error:", e); } this.ditherLoading = false; this.render(); } // ── Render ── private render() { if (this.space === "demo") { this.renderDemo(); } else { this.renderFull(); } this._tour.renderOverlay(); } startTour() { this._tour.start(); } // ──── Full mode (4-tab layout) ──── private renderFull() { const isLive = this.lfClient?.isConnected ?? false; this.shadow.innerHTML = ` ${isLive ? `
LIVE
` : ''}
${(["browse", "create", "dither", "orders"] as TabId[]).map(tab => ` `).join("")}
${this.activeTab === "browse" ? this.renderBrowseTab() : ""} ${this.activeTab === "create" ? this.renderCreateTab() : ""} ${this.activeTab === "dither" ? this.renderDitherTab() : ""} ${this.activeTab === "orders" ? this.renderOrdersTab() : ""}
`; this.bindFullEvents(); } // ── Browse Tab ── private renderBrowseTab(): string { return `
${this.catalogLoading ? '
Loading catalog...
' : ''} ${this.catalogProducts.length === 0 && !this.catalogLoading ? `

No products yet. Switch to the Create tab to add designs!

` : ''}
${this.catalogProducts.map(p => `
${this.esc(p.name)}
${p.category} $${p.basePrice.toFixed(2)}
${p.variants?.length ? `
${p.variants.map((v: string) => `${v}`).join("")}
` : ""}
`).join("")}
`; } // ── Create Tab ── private renderCreateTab(): string { return `
${this.error ? `
${this.esc(this.error)}
` : ""} ${this.createMode === "ai" ? this.renderAiCreate() : ""} ${this.createMode === "upload" ? this.renderUploadCreate() : ""} ${this.createMode === "designs" ? this.renderMyDesigns() : ""} `; } private renderAiCreate(): string { return `

AI Design Generator

Describe your design concept and let Gemini create it.

`; } private renderUploadCreate(): string { return `

Upload Artwork

Upload PNG, JPEG, or WebP (min 500x500, max 10MB).

${this.uploadPreview ? `Preview` : `
Click or drag to upload artwork
`}
`; } private renderMyDesigns(): string { return `

My Designs

${this.myDesigns.length === 0 ? `

No designs yet. Use AI Generate or Upload to create one.

` : ""}
${this.myDesigns.map(d => `
${this.esc(d.name)}
${d.status} ${d.source}
${d.status === "draft" ? `` : ""} ${d.status === "draft" ? `` : ""}
`).join("")}
${this.sharedDesigns.length > 0 ? `

Space Designs (Multiplayer)

${this.sharedDesigns.map(d => `
${this.productIcon(d.productType)}
${this.esc(d.title)}
${d.productType}${d.artifactId ? ' - ready' : ''}
${d.artifactId ? `Download` : ''} ${d.createdBy === getMyDid() ? `` : ''}
`).join("")}
` : ""}`; } // ── HitherDither Tab ── private renderDitherTab(): string { const designOptions = this.myDesigns.map(d => ``).join(""); return `

HitherDither

Apply dithering algorithms for screen printing and artistic effects.

${this.ditherNumColors}
${this.ditherPreviewUrl ? `

Dithered Preview

Original Original
Dithered ${this.ditherAlgorithm}
${this.ditherColors.length ? `
Palette: ${this.ditherColors.map(c => ``).join("")}
` : ""} Download Dithered PNG
` : ""} ${this.separationData ? `

Screen-Print Separations

Composite Composite
${(this.separationData.colors || []).map((color: string) => `
${color} #${color}
`).join("")}
` : ""}
`; } // ── Orders Tab ── private renderOrdersTab(): string { return `

Orders

Orders containing rSwag products are shown here via rCart.

${this.orders.length === 0 ? `

No orders yet. Browse products and add them to your cart to get started.

` : `
${this.orders.map(o => `
${o.id}
${o.status}
${o.tracking ? `Track shipment` : ""}
`).join("")}
`}
`; } // ── Event binding (full mode) ── private bindFullEvents() { // Tab switching this.shadow.querySelectorAll(".tab-btn").forEach(btn => { btn.addEventListener("click", () => { this.activeTab = btn.dataset.tab as TabId; this.render(); }); }); // Orders tab button in empty state this.shadow.querySelectorAll("[data-tab]").forEach(btn => { if (!btn.classList.contains("tab-btn")) { btn.addEventListener("click", () => { this.activeTab = btn.dataset.tab as TabId; this.render(); }); } }); // Browse tab this.shadow.querySelector(".search-input")?.addEventListener("input", (e) => { this.catalogSearch = (e.target as HTMLInputElement).value; clearTimeout((this as any)._searchTimeout); (this as any)._searchTimeout = setTimeout(() => this.loadCatalog(), 300); }); this.shadow.querySelector(".category-select")?.addEventListener("change", (e) => { this.catalogCategory = (e.target as HTMLSelectElement).value; this.loadCatalog(); }); this.shadow.querySelectorAll(".add-to-cart-btn").forEach(btn => { btn.addEventListener("click", () => { const slug = btn.dataset.slug; // TODO: integrate with rCart catalog ingest alert(`Added ${slug} to cart (rCart integration pending)`); }); }); // Create tab this.shadow.querySelectorAll(".mode-btn").forEach(btn => { btn.addEventListener("click", () => { this.createMode = btn.dataset.mode as "ai" | "upload" | "designs"; this.error = ""; this.render(); }); }); // AI Generate this.shadow.querySelector(".ai-name-input")?.addEventListener("input", (e) => { this.aiName = (e.target as HTMLInputElement).value; }); this.shadow.querySelector(".ai-concept-input")?.addEventListener("input", (e) => { this.aiConcept = (e.target as HTMLTextAreaElement).value; }); this.shadow.querySelector(".ai-tags-input")?.addEventListener("input", (e) => { this.aiTags = (e.target as HTMLInputElement).value; }); this.shadow.querySelector(".ai-generate-btn")?.addEventListener("click", () => this.aiGenerate()); // Upload const uploadArea = this.shadow.querySelector(".upload-area"); const uploadInput = this.shadow.querySelector(".upload-file-input") as HTMLInputElement; uploadArea?.addEventListener("click", () => uploadInput?.click()); uploadInput?.addEventListener("change", () => { const file = uploadInput.files?.[0]; if (file) { this.uploadFile = file; this.uploadPreview = URL.createObjectURL(file); this.render(); } }); this.shadow.querySelector(".upload-name-input")?.addEventListener("input", (e) => { this.uploadName = (e.target as HTMLInputElement).value; }); this.shadow.querySelector(".upload-desc-input")?.addEventListener("input", (e) => { this.uploadDescription = (e.target as HTMLInputElement).value; }); this.shadow.querySelector(".upload-submit-btn")?.addEventListener("click", () => this.uploadDesign()); // My designs actions this.shadow.querySelectorAll(".activate-btn").forEach(btn => { btn.addEventListener("click", async () => { const slug = btn.dataset.slug; await fetch(`${this.getApiBase()}/api/design/${slug}/activate`, { method: "POST" }); await this.loadMyDesigns(); this.render(); }); }); this.shadow.querySelectorAll(".delete-design-btn").forEach(btn => { btn.addEventListener("click", async () => { const slug = btn.dataset.slug; if (!confirm(`Delete design "${slug}"?`)) return; await fetch(`${this.getApiBase()}/api/design/${slug}`, { method: "DELETE" }); await this.loadMyDesigns(); this.render(); }); }); this.shadow.querySelectorAll(".dither-btn").forEach(btn => { btn.addEventListener("click", () => { this.ditherDesignSlug = btn.dataset.slug || ""; this.activeTab = "dither"; this.render(); }); }); this.shadow.querySelectorAll(".shared-delete-btn").forEach(btn => { btn.addEventListener("click", () => { const id = btn.dataset.id; if (id) this.deleteSharedDesign(id); }); }); // Dither tab this.shadow.querySelector(".dither-design-select")?.addEventListener("change", (e) => { this.ditherDesignSlug = (e.target as HTMLSelectElement).value; this.ditherPreviewUrl = ""; this.separationData = null; this.render(); }); this.shadow.querySelector(".dither-algo-select")?.addEventListener("change", (e) => { this.ditherAlgorithm = (e.target as HTMLSelectElement).value; }); this.shadow.querySelector(".dither-colors-range")?.addEventListener("input", (e) => { this.ditherNumColors = parseInt((e.target as HTMLInputElement).value, 10); const span = this.shadow.querySelector(".range-value"); if (span) span.textContent = String(this.ditherNumColors); }); this.shadow.querySelector(".apply-dither-btn")?.addEventListener("click", () => this.applyDither()); this.shadow.querySelector(".generate-separations-btn")?.addEventListener("click", () => this.generateSeparations()); } // ──── Demo mode rendering (preserved from original) ──── 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.usedSampleDesign && this.demoStep < 3 ? `
` : ""} ${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 = `
`; html += `
${progressLabels.map((label, i) => `${this.progressStep > i + 1 ? '✓ ' : ''}${label}`).join("")}
`; 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); html += `

Artifact Envelope

Product${this.esc(this.artifact.spec.product_type)}
Dimensions${this.artifact.spec.dimensions.width_mm}x${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("")}
`; 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("")}
`; 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() { this.shadow.querySelector("#btn-tour")?.addEventListener("click", () => this.startTour()); this.shadow.querySelectorAll(".product").forEach(el => { el.addEventListener("click", () => this.demoSelectProduct((el as HTMLElement).dataset.product || "tee")); }); this.shadow.querySelectorAll(".pill[data-size]").forEach(el => { el.addEventListener("click", () => { this.selectedSize = (el as HTMLElement).dataset.size || "M"; this.render(); }); }); this.shadow.querySelectorAll(".swatch[data-color]").forEach(el => { el.addEventListener("click", () => { this.selectedColor = (el as HTMLElement).dataset.color || "black"; this.render(); }); }); this.shadow.querySelector(".sample-btn")?.addEventListener("click", () => this.demoUseSample()); this.shadow.querySelector(".generate-btn")?.addEventListener("click", () => this.demoGenerate()); this.shadow.querySelector(".json-toggle-btn")?.addEventListener("click", () => { this.shadow.querySelector(".json-pre")?.classList.toggle("visible"); }); } // ── Styles ── private getFullStyles(): string { return ` :host { display: block; padding: 1.5rem; max-width: 960px; margin: 0 auto; } *, *::before, *::after { box-sizing: border-box; } /* Live indicator */ .live-header { display: flex; align-items: center; gap: 8px; margin-bottom: 0.5rem; } .live-dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; background: #22c55e; animation: pulse-dot 2s infinite; } @keyframes pulse-dot { 0%,100% { opacity: 1; } 50% { opacity: 0.4; } } .live-badge { font-size: 0.7rem; padding: 2px 8px; border-radius: 999px; background: rgba(34,197,94,0.15); color: #22c55e; font-weight: 500; display: flex; align-items: center; gap: 2px; } /* Tab bar */ .tab-bar { display: flex; gap: 0; border-bottom: 2px solid var(--rs-border, #334155); margin-bottom: 1.5rem; } .tab-btn { padding: 0.625rem 1.25rem; border: none; background: none; color: var(--rs-text-secondary, #94a3b8); font-size: 0.875rem; font-weight: 500; cursor: pointer; border-bottom: 2px solid transparent; margin-bottom: -2px; transition: all 0.15s; font-family: inherit; } .tab-btn:hover { color: var(--rs-text-primary, #e2e8f0); } .tab-btn.active { color: var(--rs-primary, #6366f1); border-bottom-color: var(--rs-primary, #6366f1); font-weight: 600; } /* Common */ .section-title { color: var(--rs-text-primary); font-weight: 600; font-size: 1rem; margin: 0 0 0.375rem; } .section-desc { color: var(--rs-text-secondary); font-size: 0.8125rem; margin: 0 0 1rem; } .loading { text-align: center; color: var(--rs-text-muted); padding: 2rem; } .empty-state { text-align: center; padding: 3rem 1rem; color: var(--rs-text-secondary); } .empty-state p { margin: 0 0 1rem; } .error { background: rgba(239,68,68,0.1); border: 1px solid var(--rs-error, #ef4444); border-radius: 8px; padding: 0.75rem; color: #fca5a5; font-size: 0.875rem; margin-bottom: 1rem; } /* Badges */ .badge { display: inline-block; padding: 0.125rem 0.5rem; border-radius: 999px; font-size: 0.625rem; font-weight: 600; } .badge-stickers { background: rgba(34,197,94,0.15); color: #4ade80; } .badge-apparel { background: rgba(56,189,248,0.15); color: #38bdf8; } .badge-prints { background: rgba(249,115,22,0.15); color: #fb923c; } .badge-draft { background: rgba(100,116,139,0.15); color: #94a3b8; } .badge-active { background: rgba(34,197,94,0.15); color: #4ade80; } .badge-source { background: rgba(99,102,241,0.15); color: #a5b4fc; } .badge-cosmo { background: rgba(34,197,94,0.15); color: #4ade80; } .badge-global { background: rgba(56,189,248,0.15); color: #38bdf8; } .badge-printful { background: rgba(56,189,248,0.15); color: #38bdf8; } .badge-local { background: rgba(34,197,94,0.15); color: #4ade80; } .badge-ok { background: rgba(34,197,94,0.15); color: #4ade80; padding: 0.25rem 0.75rem; font-size: 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; font-family: inherit; } .btn:disabled { opacity: 0.5; cursor: not-allowed; } .btn-primary { background: var(--rs-primary-hover, #4f46e5); color: #fff; } .btn-primary:hover:not(:disabled) { background: #4338ca; } .btn-secondary { background: var(--rs-bg-surface-raised, #1e293b); color: var(--rs-text-primary, #e2e8f0); } .btn-secondary:hover { background: var(--rs-bg-hover, #334155); } .btn-danger { background: rgba(239,68,68,0.15); color: #fca5a5; } .btn-danger:hover { background: rgba(239,68,68,0.25); } .btn-sm { padding: 0.375rem 0.75rem; font-size: 0.75rem; } /* Browse tab */ .browse-controls { display: flex; gap: 0.75rem; margin-bottom: 1rem; } .search-input, .category-select { padding: 0.5rem 0.75rem; border: 1px solid var(--rs-input-border, #334155); border-radius: 8px; background: var(--rs-input-bg, #0f172a); color: var(--rs-input-text, #e2e8f0); font-size: 0.875rem; font-family: inherit; } .search-input { flex: 1; } .search-input:focus, .category-select:focus { outline: none; border-color: var(--rs-primary, #6366f1); } .product-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 1rem; } .catalog-card { background: var(--rs-bg-surface, #1e293b); border: 1px solid var(--rs-border, #334155); border-radius: 12px; overflow: hidden; transition: border-color 0.15s; } .catalog-card:hover { border-color: var(--rs-primary, #6366f1); } .catalog-img { height: 180px; background-size: cover; background-position: center; background-color: var(--rs-bg-page, #0f172a); } .catalog-body { padding: 0.75rem; } .catalog-name { font-weight: 600; font-size: 0.875rem; color: var(--rs-text-primary); margin-bottom: 0.375rem; } .catalog-meta { display: flex; align-items: center; gap: 0.5rem; margin-bottom: 0.375rem; } .catalog-price { font-weight: 600; color: #4ade80; font-size: 0.875rem; } .catalog-variants { display: flex; gap: 0.25rem; flex-wrap: wrap; margin-bottom: 0.5rem; } .variant-chip { padding: 0.125rem 0.375rem; border-radius: 4px; font-size: 0.625rem; background: var(--rs-bg-page); color: var(--rs-text-muted); border: 1px solid var(--rs-border); } /* Create tab */ .create-modes { display: flex; gap: 0.5rem; margin-bottom: 1rem; } .mode-btn { padding: 0.5rem 1rem; border-radius: 8px; border: 1px solid var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-secondary); font-size: 0.8125rem; cursor: pointer; transition: all 0.15s; font-family: inherit; } .mode-btn:hover { border-color: var(--rs-border-strong); } .mode-btn.active { border-color: var(--rs-primary); background: rgba(99,102,241,0.1); color: var(--rs-text-primary); } .create-section { margin-bottom: 1.5rem; } .input-field { width: 100%; padding: 0.625rem 0.75rem; border: 1px solid var(--rs-input-border, #334155); border-radius: 8px; background: var(--rs-input-bg, #0f172a); color: var(--rs-input-text, #e2e8f0); font-size: 0.875rem; margin-bottom: 0.75rem; box-sizing: border-box; font-family: inherit; } .input-field:focus { outline: none; border-color: var(--rs-primary); } textarea.input-field { resize: vertical; min-height: 80px; } .upload-area { border: 2px dashed var(--rs-border); border-radius: 12px; padding: 2rem; text-align: center; margin-bottom: 0.75rem; cursor: pointer; transition: border-color 0.15s; background: var(--rs-bg-surface); } .upload-area:hover { border-color: var(--rs-primary); } .upload-area.has-image { border-style: solid; border-color: var(--rs-border-strong); } .upload-label { color: var(--rs-text-secondary); font-size: 0.875rem; } .preview-img { max-width: 200px; max-height: 200px; border-radius: 8px; } input[type="file"] { display: none; } /* Designs grid */ .designs-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(220px, 1fr)); gap: 0.75rem; } .design-card { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 10px; overflow: hidden; transition: border-color 0.15s; } .design-card:hover { border-color: var(--rs-primary); } .design-img { height: 140px; background-size: cover; background-position: center; background-color: var(--rs-bg-page); } .design-card-icon { font-size: 1.5rem; padding: 1rem; text-align: center; } .design-body { padding: 0.75rem; } .design-name { font-weight: 600; font-size: 0.8125rem; color: var(--rs-text-primary); margin-bottom: 0.25rem; } .design-meta { display: flex; gap: 0.375rem; margin-bottom: 0.5rem; } .design-actions { display: flex; gap: 0.375rem; flex-wrap: wrap; } /* Dither tab */ .dither-section { } .dither-controls { display: grid; grid-template-columns: 1fr 1fr; gap: 0.75rem; margin-bottom: 1rem; } .control-group { display: flex; flex-direction: column; gap: 0.25rem; } .control-group label { font-size: 0.75rem; color: var(--rs-text-muted); font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; } .control-group select { padding: 0.5rem; border: 1px solid var(--rs-input-border); border-radius: 6px; background: var(--rs-input-bg); color: var(--rs-input-text); font-size: 0.8125rem; font-family: inherit; } .control-group select:focus { outline: none; border-color: var(--rs-primary); } .control-group input[type="range"] { width: 100%; accent-color: var(--rs-primary); } .range-value { font-size: 0.75rem; color: var(--rs-text-secondary); font-weight: 600; } .dither-actions { display: flex; gap: 0.75rem; margin-bottom: 1.5rem; } .dither-preview, .separations-preview { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 12px; padding: 1rem; margin-bottom: 1rem; } .dither-preview h4, .separations-preview h4 { margin: 0 0 0.75rem; color: var(--rs-text-primary); font-size: 0.9375rem; } .preview-row { display: flex; align-items: center; gap: 1rem; margin-bottom: 0.75rem; } .preview-original, .preview-dithered { text-align: center; } .preview-original img, .preview-dithered img { max-width: 200px; max-height: 200px; border-radius: 8px; border: 1px solid var(--rs-border); } .preview-original span, .preview-dithered span { display: block; font-size: 0.6875rem; color: var(--rs-text-muted); margin-top: 0.25rem; } .preview-arrow { font-size: 1.5rem; color: var(--rs-text-muted); } .palette-row { display: flex; align-items: center; gap: 0.375rem; margin-bottom: 0.75rem; } .palette-label { font-size: 0.75rem; color: var(--rs-text-muted); } .palette-swatch { display: inline-block; width: 20px; height: 20px; border-radius: 4px; border: 1px solid rgba(255,255,255,0.2); } .sep-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 0.5rem; } .sep-card { text-align: center; } .sep-card img { width: 100%; border-radius: 6px; border: 1px solid var(--rs-border); } .sep-card span { display: block; font-size: 0.6875rem; color: var(--rs-text-muted); margin-top: 0.25rem; } .sep-color { display: flex; align-items: center; justify-content: center; gap: 0.25rem; } /* Orders tab */ .orders-list { display: flex; flex-direction: column; gap: 0.5rem; } .order-card { display: flex; align-items: center; gap: 1rem; background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 8px; padding: 0.75rem; } .order-id { font-weight: 600; font-size: 0.875rem; color: var(--rs-text-primary); } .order-tracking { font-size: 0.8125rem; color: var(--rs-primary); } @media (max-width: 768px) { .tab-btn { padding: 0.5rem 0.75rem; font-size: 0.75rem; } .product-grid, .designs-grid { grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); } .dither-controls { grid-template-columns: 1fr; } .preview-row { flex-direction: column; } .browse-controls { flex-direction: column; } } `; } private getDemoStyles(): string { return ` :host { display: block; padding: 1.5rem; max-width: 960px; margin: 0 auto; } *, *::before, *::after { box-sizing: border-box; } .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 var(--rs-border); color: var(--rs-text-muted); background: var(--rs-bg-surface); transition: all 0.2s; } .step-dot.active .step-num { border-color: var(--rs-primary); color: #fff; background: var(--rs-primary-hover); } .step-dot.current .step-num { box-shadow: 0 0 0 3px rgba(99,102,241,0.3); } .step-label { font-size: 0.6875rem; color: var(--rs-text-muted); } .step-dot.active .step-label { color: #a5b4fc; } .step-line { width: 40px; height: 2px; background: var(--rs-border); margin: 0 0.5rem; margin-bottom: 1rem; } .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 var(--rs-border); background: var(--rs-bg-surface); cursor: pointer; text-align: center; transition: all 0.15s; } .product:hover { border-color: var(--rs-border-strong); } .product.active { border-color: var(--rs-primary); background: rgba(99,102,241,0.1); } .product-icon { margin-bottom: 0.375rem; display: flex; justify-content: center; } .product-name { color: var(--rs-text-primary); font-weight: 600; font-size: 0.8125rem; } .product-specs { color: var(--rs-text-muted); font-size: 0.6875rem; margin-top: 0.125rem; } .product-cost { color: var(--rs-text-secondary); font-size: 0.75rem; margin-top: 0.25rem; } .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; } .option-row { display: flex; align-items: center; gap: 0.75rem; margin-bottom: 0.75rem; } .option-label { color: var(--rs-text-secondary); 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 var(--rs-border); background: var(--rs-bg-surface); color: var(--rs-text-secondary); font-size: 0.75rem; cursor: pointer; transition: all 0.15s; } .pill:hover { border-color: var(--rs-border-strong); color: var(--rs-text-primary); } .pill.active { border-color: var(--rs-primary); 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 var(--rs-border); cursor: pointer; transition: all 0.15s; } .swatch:hover { border-color: var(--rs-border-strong); } .swatch.active { border-color: var(--rs-primary); box-shadow: 0 0 0 3px rgba(99,102,241,0.3); } .mockup-area { display: flex; gap: 1.5rem; align-items: center; background: var(--rs-bg-surface); border: 1px solid var(--rs-border); 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: var(--rs-text-primary); font-weight: 600; font-size: 1rem; margin: 0 0 0.25rem; } .mockup-meta { color: var(--rs-text-secondary); font-size: 0.8125rem; margin-bottom: 0.75rem; } .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; font-family: inherit; } .btn-primary { background: var(--rs-primary-hover); color: #fff; } .btn-primary:hover { background: #4338ca; } .btn-secondary { background: var(--rs-bg-surface-raised); color: var(--rs-text-primary); } .btn-secondary:hover { background: var(--rs-bg-hover); } .generate-row { text-align: center; margin-bottom: 1.5rem; } .step-section { margin-bottom: 1.5rem; } .step-section:not(.visible) { display: none; } .progress-bar { height: 6px; background: var(--rs-bg-surface); border-radius: 3px; overflow: hidden; margin-bottom: 0.5rem; } .progress-fill { height: 100%; background: linear-gradient(90deg, var(--rs-primary), var(--rs-primary-hover)); 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: var(--rs-text-muted); transition: color 0.2s; } .prog-label.active { color: var(--rs-primary-hover); font-weight: 600; } .prog-label.done { color: #4ade80; } .artifact-card { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); border-radius: 12px; padding: 1.25rem; margin-bottom: 1rem; } .artifact-heading { color: var(--rs-text-primary); 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: var(--rs-text-muted); font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.05em; } .af-value { color: var(--rs-text-primary); 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: var(--rs-bg-page); color: var(--rs-text-secondary); padding: 0.25rem 0.625rem; border-radius: 6px; font-size: 0.6875rem; border: 1px solid var(--rs-border); } .provider-section { margin-bottom: 1rem; } .provider-heading { color: var(--rs-text-primary); font-weight: 600; font-size: 0.9375rem; margin: 0 0 0.75rem; } .buyer-loc { color: var(--rs-text-muted); font-weight: 400; font-size: 0.75rem; } .provider-table { background: var(--rs-bg-surface); border: 1px solid var(--rs-border); 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: var(--rs-bg-page); color: var(--rs-text-muted); font-size: 0.6875rem; text-transform: uppercase; letter-spacing: 0.05em; font-weight: 600; } .pt-row { color: var(--rs-text-primary); border-top: 1px solid var(--rs-border-subtle); } .pt-row.nearest { background: rgba(99,102,241,0.05); border-left: 3px solid var(--rs-primary); } .pt-name { font-weight: 500; } .pt-cost { color: #4ade80; font-weight: 600; } .split-section { margin-bottom: 1.5rem; } .split-heading { color: var(--rs-text-secondary); font-size: 0.8125rem; font-weight: 500; margin: 0 0 0.5rem; } .split-total { color: var(--rs-text-muted); 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: var(--rs-text-muted); font-size: 0.6875rem; } .pipeline-heading { color: var(--rs-text-primary); 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: var(--rs-bg-surface); border: 1px solid var(--rs-border); 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: var(--rs-text-primary); 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: var(--rs-text-muted); font-size: 1.25rem; margin: 0 0.25rem; margin-bottom: 0.75rem; } .pipeline-actions { display: flex; gap: 0.75rem; justify-content: center; margin-bottom: 1rem; } .json-pre { background: var(--rs-bg-page); border: 1px solid var(--rs-border); border-radius: 8px; padding: 0.75rem; overflow-x: auto; font-size: 0.6875rem; color: var(--rs-text-secondary); 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);