/** * Parse onboarding media manifests into guide groups. * * WHY: keeping manifest shaping outside the view makes guide behavior * testable without mounting the whole onboarding page. * * @param {string[]} files - Manifest file paths relative to the onboarding media root. * @returns {object} Guide groups keyed by service, step, and variant. */ export function parseManifest(files) { const grouped = {}; for (const path of files) { if (typeof path !== "string") continue; const cleaned = path.replace(/^\/+/, "").replace(/\\/g, "/"); const parts = cleaned.split("/"); if (parts.length < 3) continue; const service = parts[0]; const step = parts[1]; const rest = parts.slice(2); let variant = "default"; let filename = rest.join("/"); if (rest.length > 1) { variant = rest[0]; filename = rest.slice(1).join("/"); } const order = guideOrder(filename); const label = guideLabel(filename); const url = `/media/onboarding/${cleaned}`; grouped[service] = grouped[service] || {}; grouped[service][step] = grouped[service][step] || {}; grouped[service][step][variant] = grouped[service][step][variant] || { id: variant, title: variant === "default" ? "" : variant, shots: [] }; grouped[service][step][variant].shots.push({ url, order, label, file: filename }); } Object.values(grouped).forEach((serviceSteps) => { Object.values(serviceSteps).forEach((variants) => { Object.values(variants).forEach((group) => { group.shots.sort((a, b) => (a.order - b.order) || a.file.localeCompare(b.file)); }); }); }); return grouped; } function guideOrder(filename) { const prefix = filename.match(/^(\d{1,3})/); if (prefix) return Number(prefix[1]); const step = filename.match(/step[-_ ]?(\d{1,3})/i); if (step) return Number(step[1]); return Number.MAX_SAFE_INTEGER; } function guideLabel(filename) { const base = filename.replace(/\.(png|jpe?g|webp)$/i, ""); return base.replace(/^\d+[-_]?/, "").replace(/[-_]/g, " ").trim(); }