@sonordev/agency-site-kit 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/dist/{chunk-4IES23GV.js → chunk-2RW4C42Y.js} +16 -4
  2. package/dist/chunk-2RW4C42Y.js.map +1 -0
  3. package/dist/{chunk-QGSW43TM.cjs → chunk-2Y2RJB3A.cjs} +12 -12
  4. package/dist/{chunk-QGSW43TM.cjs.map → chunk-2Y2RJB3A.cjs.map} +1 -1
  5. package/dist/{chunk-UXUU7FVT.cjs → chunk-6OWFFV6N.cjs} +4 -65
  6. package/dist/chunk-6OWFFV6N.cjs.map +1 -0
  7. package/dist/{chunk-NLGUPJZA.js → chunk-7FFZX7C5.js} +4 -4
  8. package/dist/{chunk-NLGUPJZA.js.map → chunk-7FFZX7C5.js.map} +1 -1
  9. package/dist/{chunk-7ZHRMWJU.cjs → chunk-A5WCMHAE.cjs} +2 -2
  10. package/dist/{chunk-7ZHRMWJU.cjs.map → chunk-A5WCMHAE.cjs.map} +1 -1
  11. package/dist/chunk-FM25LB67.js +50 -0
  12. package/dist/chunk-FM25LB67.js.map +1 -0
  13. package/dist/{chunk-KXLROAO5.js → chunk-FT2SNEYP.js} +2 -2
  14. package/dist/{chunk-KXLROAO5.js.map → chunk-FT2SNEYP.js.map} +1 -1
  15. package/dist/{chunk-5MEZU3OG.cjs → chunk-OG5RBKQJ.cjs} +15 -3
  16. package/dist/chunk-OG5RBKQJ.cjs.map +1 -0
  17. package/dist/chunk-X76U7VLF.cjs +57 -0
  18. package/dist/chunk-X76U7VLF.cjs.map +1 -0
  19. package/dist/{chunk-RWQ675N6.js → chunk-X7UCJ45A.js} +4 -61
  20. package/dist/chunk-X7UCJ45A.js.map +1 -0
  21. package/dist/format-C-axf13G.d.cts +68 -0
  22. package/dist/format-poUpLtQ2.d.ts +68 -0
  23. package/dist/index.cjs +25 -24
  24. package/dist/index.cjs.map +1 -1
  25. package/dist/index.d.cts +5 -3
  26. package/dist/index.d.ts +5 -3
  27. package/dist/index.js +5 -4
  28. package/dist/index.js.map +1 -1
  29. package/dist/layout/index.cjs +2 -2
  30. package/dist/layout/index.d.cts +12 -2
  31. package/dist/layout/index.d.ts +12 -2
  32. package/dist/layout/index.js +1 -1
  33. package/dist/portfolio/client.cjs +2 -17
  34. package/dist/portfolio/client.cjs.map +1 -1
  35. package/dist/portfolio/client.js +1 -16
  36. package/dist/portfolio/client.js.map +1 -1
  37. package/dist/portfolio/index.cjs +18 -17
  38. package/dist/portfolio/index.d.cts +5 -74
  39. package/dist/portfolio/index.d.ts +5 -74
  40. package/dist/portfolio/index.js +4 -3
  41. package/dist/portfolio/sections/ChallengesSection.d.cts +1 -1
  42. package/dist/portfolio/sections/ChallengesSection.d.ts +1 -1
  43. package/dist/portfolio/sections/DetailsSection.d.cts +1 -1
  44. package/dist/portfolio/sections/DetailsSection.d.ts +1 -1
  45. package/dist/portfolio/sections/HeroSection.d.cts +1 -1
  46. package/dist/portfolio/sections/HeroSection.d.ts +1 -1
  47. package/dist/portfolio/sections/ResultsSection.cjs +4 -3
  48. package/dist/portfolio/sections/ResultsSection.cjs.map +1 -1
  49. package/dist/portfolio/sections/ResultsSection.d.cts +1 -1
  50. package/dist/portfolio/sections/ResultsSection.d.ts +1 -1
  51. package/dist/portfolio/sections/ResultsSection.js +2 -1
  52. package/dist/portfolio/sections/ResultsSection.js.map +1 -1
  53. package/dist/portfolio/sections/ServicesSection.d.cts +1 -1
  54. package/dist/portfolio/sections/ServicesSection.d.ts +1 -1
  55. package/dist/portfolio/sections/TechStackSection.d.cts +1 -1
  56. package/dist/portfolio/sections/TechStackSection.d.ts +1 -1
  57. package/dist/portfolio/sections/TestimonialSection.d.cts +1 -1
  58. package/dist/portfolio/sections/TestimonialSection.d.ts +1 -1
  59. package/dist/portfolio/server.cjs +40 -14
  60. package/dist/portfolio/server.d.cts +3 -1
  61. package/dist/portfolio/server.d.ts +3 -1
  62. package/dist/portfolio/server.js +3 -1
  63. package/dist/{types-0tVPy4pU.d.ts → types-He5A9okM.d.cts} +1 -1
  64. package/dist/{types-0tVPy4pU.d.cts → types-He5A9okM.d.ts} +1 -1
  65. package/package.json +3 -3
  66. package/dist/chunk-4IES23GV.js.map +0 -1
  67. package/dist/chunk-5MEZU3OG.cjs.map +0 -1
  68. package/dist/chunk-RWQ675N6.js.map +0 -1
  69. package/dist/chunk-UXUU7FVT.cjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/portfolio/components/PortfolioPage.tsx","../src/portfolio/components/PortfolioSchema.tsx","../src/portfolio/components/PortfolioCard.tsx","../src/portfolio/components/PortfolioGrid.tsx","../src/portfolio/components/PortfolioIndex.tsx","../src/portfolio/components/PortfolioSkeletons.tsx","../src/types.ts"],"names":["jsx","jsxs"],"mappings":"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","file":"chunk-NLGUPJZA.js","sourcesContent":["/**\n * PortfolioPage — Server component that renders a full portfolio case study.\n *\n * Maps each section in item.sections to the correct renderer component.\n * All sections below the hero are loaded lazily via next/dynamic.\n *\n * NOTE: This file is intentionally NOT a client component — it is designed\n * to be used as an RSC in a Next.js app directory page.\n *\n * Sections MUST be imported via `@sonordev/agency-site-kit/portfolio/sections/*`\n * (self-referencing subpaths, external in tsup's server build), never via\n * relative paths: a relative import would bundle the client sections into\n * this server chunk, the 'use client' boundary would be lost, and the whole\n * PortfolioItemFull would serialize into the RSC flight payload again.\n *\n * Whether a section is a server or client component is decided by the\n * section file itself (its 'use client' directive; tsup routes it to the\n * matching build). Server sections are imported statically — they ship zero\n * client JS, so there is nothing to lazy-load. Client sections load through\n * next/dynamic so each becomes its own browser chunk, fetched only when the\n * case study actually renders that section type.\n */\n\nimport React from 'react';\nimport dynamic from 'next/dynamic';\nimport type {\n PortfolioItemFull,\n PortfolioSection,\n PortfolioHeroData,\n PortfolioChallengesData,\n PortfolioStrategyData,\n PortfolioResultsData,\n PortfolioTechStackData,\n PortfolioServicesData,\n PortfolioTestimonialData,\n PortfolioGalleryData,\n PortfolioVideoData,\n PortfolioTeamData,\n PortfolioFeatureSpotlightData,\n PortfolioBeforeAfterData,\n PortfolioMetricsTimelineData,\n PortfolioConversionFunnelData,\n PortfolioDetailsData,\n PortfolioCTAData,\n} from '../../types';\n\n// Hero is loaded eagerly (above the fold, client — it animates)\nimport HeroSection from '@sonordev/agency-site-kit/portfolio/sections/HeroSection';\n\n// Server sections — no 'use client' in their files, so they render on the\n// server and ship ZERO component JS to the browser. Static imports: there is\n// no client chunk to defer.\nimport ChallengesSection from '@sonordev/agency-site-kit/portfolio/sections/ChallengesSection';\nimport ResultsSection from '@sonordev/agency-site-kit/portfolio/sections/ResultsSection';\nimport TechStackSection from '@sonordev/agency-site-kit/portfolio/sections/TechStackSection';\nimport ServicesSection from '@sonordev/agency-site-kit/portfolio/sections/ServicesSection';\nimport TestimonialSection from '@sonordev/agency-site-kit/portfolio/sections/TestimonialSection';\nimport SpeedComparisonSection from '@sonordev/agency-site-kit/portfolio/sections/SpeedComparisonSection';\nimport DesignSystemSection from '@sonordev/agency-site-kit/portfolio/sections/DesignSystemSection';\nimport DetailsSection from '@sonordev/agency-site-kit/portfolio/sections/DetailsSection';\n\n// Client sections — animated/interactive; lazily loaded so each becomes its\n// own browser chunk, fetched only when rendered.\nconst StrategySection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/StrategySection'), { ssr: true });\nconst GallerySection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/GallerySection'), { ssr: true });\nconst VideoSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/VideoSection'), { ssr: true });\nconst TeamSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/TeamSection'), { ssr: true });\nconst FeatureSpotlightSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/FeatureSpotlightSection'), { ssr: true });\nconst BeforeAfterSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/BeforeAfterSection'), { ssr: true });\nconst MetricsTimelineSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/MetricsTimelineSection'), { ssr: true });\nconst ConversionFunnelSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/ConversionFunnelSection'), { ssr: true });\nconst PerformanceSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/PerformanceSection'), { ssr: true });\nconst SiteArchitectureSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/SiteArchitectureSection'), { ssr: true });\nconst CTASection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/CTASection'), { ssr: true });\n\ninterface PortfolioPageProps {\n item: PortfolioItemFull;\n}\n\n// Defense-in-depth only — the enforcing sanitize pass runs in\n// getPortfolioItem (portfolio/server.ts), the last genuinely-server-side hop.\n// See src/portfolio/sanitize.ts for the single source of truth and the story.\nimport { sanitizeSiteArchitectureData } from '../sanitize';\n\n// Display policy for proof points (Lighthouse dedupe, results curation) —\n// see src/portfolio/curate-proof.ts for the single source of truth.\nimport { curatePortfolioProof } from '../curate-proof';\n\n// Engagement-tracking island (client) — crosses the boundary via subpath.\nimport PortfolioAnalytics from '@sonordev/agency-site-kit/portfolio/components/PortfolioAnalytics';\n\nfunction renderSection(section: PortfolioSection, item: PortfolioItemFull, index: number) {\n const key = `${section.sectionType}-${index}`;\n\n switch (section.sectionType) {\n case 'portfolioHero':\n return (\n <HeroSection\n key={key}\n data={section.data as PortfolioHeroData}\n screenshots={item.hero_screenshots ?? undefined}\n liveUrl={item.live_url ?? undefined}\n />\n );\n case 'portfolioChallenges':\n return <ChallengesSection key={key} data={section.data as PortfolioChallengesData} />;\n case 'portfolioStrategy':\n return <StrategySection key={key} data={section.data as PortfolioStrategyData} />;\n case 'portfolioResults':\n return (\n <ResultsSection\n key={key}\n data={section.data as PortfolioResultsData}\n metricsDelta={item.metricsDelta}\n metricsRefreshedAt={item.metrics_last_refreshed_at}\n />\n );\n case 'portfolioTechStack':\n return <TechStackSection key={key} data={section.data as PortfolioTechStackData} />;\n case 'portfolioServices':\n return <ServicesSection key={key} data={section.data as PortfolioServicesData} />;\n case 'portfolioTestimonial':\n return <TestimonialSection key={key} data={section.data as PortfolioTestimonialData} />;\n case 'portfolioGallery':\n return <GallerySection key={key} data={section.data as PortfolioGalleryData} />;\n case 'portfolioVideo':\n return <VideoSection key={key} data={section.data as PortfolioVideoData} />;\n case 'portfolioTeam':\n return <TeamSection key={key} data={section.data as PortfolioTeamData} />;\n case 'portfolioFeatureSpotlight':\n return <FeatureSpotlightSection key={key} data={section.data as PortfolioFeatureSpotlightData} />;\n case 'portfolioBeforeAfter':\n return <BeforeAfterSection key={key} data={section.data as PortfolioBeforeAfterData} />;\n case 'portfolioMetricsTimeline':\n return <MetricsTimelineSection key={key} data={section.data as PortfolioMetricsTimelineData} />;\n case 'portfolioConversionFunnel':\n return <ConversionFunnelSection key={key} data={section.data as PortfolioConversionFunnelData} />;\n case 'portfolioPerformance':\n return <PerformanceSection key={key} data={section.data as any} />;\n case 'portfolioSpeedComparison':\n return <SpeedComparisonSection key={key} data={section.data as any} />;\n case 'portfolioSiteArchitecture':\n return <SiteArchitectureSection key={key} data={sanitizeSiteArchitectureData(section.data) as any} />;\n case 'portfolioDesignSystem':\n return <DesignSystemSection key={key} data={section.data as any} />;\n case 'portfolioDetails':\n return <DetailsSection key={key} data={section.data as PortfolioDetailsData} />;\n case 'portfolioCTA':\n return <CTASection key={key} data={section.data as PortfolioCTAData} />;\n case 'portfolioSeo':\n // SEO section is metadata-only; nothing to render visually.\n return null;\n default:\n return null;\n }\n}\n\n// Canonical display order — ensures consistent rendering regardless of API\n// order. Keys must match PortfolioSectionType exactly (note 'portfolioCTA' —\n// a lowercased 'portfolioCta' key silently fell through to the ?? 99 fallback\n// for years). Unknown/future types sort last via the fallback.\nconst SECTION_ORDER: Record<string, number> = {\n portfolioHero: 0,\n portfolioChallenges: 1,\n portfolioStrategy: 2,\n portfolioResults: 3,\n portfolioPerformance: 4,\n portfolioSpeedComparison: 5,\n portfolioSiteArchitecture: 6,\n portfolioTechStack: 7,\n portfolioDesignSystem: 8,\n portfolioServices: 9,\n portfolioBeforeAfter: 10,\n portfolioTestimonial: 11,\n portfolioMetricsTimeline: 12,\n portfolioConversionFunnel: 13,\n portfolioDetails: 14,\n portfolioSeo: 15,\n portfolioCTA: 16,\n};\n\nexport default function PortfolioPage({ item: rawItem }: PortfolioPageProps) {\n const item = curatePortfolioProof(rawItem);\n\n // Section order: once an agency has deliberately reordered in the\n // dashboard (item.sectionsOrdered), the array order IS the narrative —\n // respect it, with the hero pinned first (it's the LCP shell). Items\n // never reordered keep the canonical order below, so nothing shifts\n // out from under existing case studies.\n const sortedSections = item.sectionsOrdered\n ? [...item.sections].sort(\n (a, b) =>\n (a.sectionType === 'portfolioHero' ? 0 : 1) -\n (b.sectionType === 'portfolioHero' ? 0 : 1),\n )\n : [...item.sections].sort(\n (a, b) => (SECTION_ORDER[a.sectionType] ?? 99) - (SECTION_ORDER[b.sectionType] ?? 99)\n );\n\n return (\n <article className=\"flex w-full min-w-0 max-w-full flex-col overflow-x-clip\">\n {sortedSections.map((section, index) => {\n const rendered = renderSection(section, item, index);\n if (!rendered) return null;\n return (\n <div\n key={`wrapper-${section.sectionType}-${index}`}\n className=\"w-full\"\n data-portfolio-section={section.sectionType}\n >\n {rendered}\n </div>\n );\n })}\n {/* Engagement tracking — childless idle-deferred sibling, never a wrapper */}\n <PortfolioAnalytics slug={item.slug} />\n </article>\n );\n}\n","/**\n * PortfolioSchema — JSON-LD structured data for a case study page.\n *\n * Server component: renders a single <script type=\"application/ld+json\">\n * describing the case study as a schema.org Article (there is no dedicated\n * CaseStudy type; Article is what Google documents for editorial content).\n * Ships zero client JS and adds rich-result eligibility + entity clarity for\n * the agency's portfolio pages.\n *\n * Usage (in the case study page, next to <PortfolioPage />):\n *\n * ```tsx\n * <PortfolioSchema\n * item={item}\n * url={`https://youragency.com/work/${item.slug}`}\n * publisher={{ name: 'Your Agency', url: 'https://youragency.com' }}\n * />\n * ```\n *\n * Only reads fields from the already-sanitized PortfolioItemFull (see\n * getPortfolioItem / sanitize.ts) — never section internals.\n */\n\nimport React from 'react';\nimport type { PortfolioItemFull } from '../../types';\n\nexport interface PortfolioSchemaProps {\n item: PortfolioItemFull;\n /** Canonical URL of this case study page. */\n url: string;\n /** The agency, emitted as author + publisher Organization. */\n publisher?: {\n name: string;\n url?: string;\n /** Absolute URL to a logo image. */\n logo?: string;\n };\n}\n\n/** JSON-LD-safe serialization — escape `<` so `</script>` can never break out. */\nfunction jsonLd(value: unknown): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c');\n}\n\nexport default function PortfolioSchema({ item, url, publisher }: PortfolioSchemaProps) {\n const headline = item.seo?.metaTitle || item.title;\n const description = item.seo?.metaDescription || item.description;\n\n const keywords = [\n ...(item.seo?.keywords ?? []),\n item.category,\n ...item.services,\n ].filter(Boolean);\n\n const org = publisher\n ? {\n '@type': 'Organization',\n name: publisher.name,\n ...(publisher.url ? { url: publisher.url } : {}),\n ...(publisher.logo\n ? { logo: { '@type': 'ImageObject', url: publisher.logo } }\n : {}),\n }\n : undefined;\n\n const schema = {\n '@context': 'https://schema.org',\n '@type': 'Article',\n headline,\n ...(description ? { description } : {}),\n ...(item.hero_image ? { image: [item.hero_image] } : {}),\n url,\n mainEntityOfPage: { '@type': 'WebPage', '@id': url },\n ...(item.published_at ? { datePublished: item.published_at } : {}),\n ...(item.metrics_last_refreshed_at || item.published_at\n ? { dateModified: item.metrics_last_refreshed_at || item.published_at }\n : {}),\n ...(keywords.length ? { keywords: keywords.join(', ') } : {}),\n ...(org ? { author: org, publisher: org } : {}),\n ...(item.details?.industry ? { about: item.details.industry } : {}),\n };\n\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: jsonLd(schema) }}\n />\n );\n}\n","// Shared component — no directive on purpose: hook-free, so it renders\n// server-side (zero JS) from PortfolioIndex and stays usable from client\n// code via the /portfolio/client barrel.\nimport React from 'react';\nimport Image from 'next/image';\nimport Link from 'next/link';\nimport type { PortfolioItem } from '../../types';\nimport GlassCard from '@sonordev/agency-site-kit/portfolio/primitives/GlassCard';\nimport { curateHeroKpis } from '../curate-proof';\nimport { formatCategoryLabel, formatServiceTag } from '../format';\n\ninterface PortfolioCardProps {\n item: PortfolioItem;\n className?: string;\n /** Base path for portfolio detail links (default: '/work') */\n basePath?: string;\n}\n\nexport default function PortfolioCard({ item, className = '', basePath = '/work' }: PortfolioCardProps) {\n // Use hero_screenshots desktop → hero_image → trifolio screenshot convention\n const thumbnailSrc =\n item.hero_screenshots?.desktop ||\n item.hero_image ||\n (item.project_id ? `/portfolio-previews/${item.slug}.png` : undefined);\n const kpis = Array.isArray(item.kpis) ? item.kpis : [];\n const services = Array.isArray(item.services) ? item.services : [];\n // Collapse per-category Lighthouse KPIs so a card never reads\n // \"100/100 · 100/100 · 100/100\" — business outcomes lead the preview.\n const kpiPreview = curateHeroKpis(kpis).slice(0, 3);\n const servicePreview = services.slice(0, 3);\n\n return (\n <Link href={`${basePath}/${item.slug}`} className={`block group ${className}`}>\n <GlassCard padding=\"sm\" hover>\n <div className=\"flex flex-col gap-4\">\n {/* Thumbnail */}\n {thumbnailSrc && (\n <div\n className=\"relative w-full overflow-hidden rounded-xl\"\n style={{ aspectRatio: '16/10' }}\n >\n <Image\n src={thumbnailSrc}\n alt={item.hero_image_alt || item.title}\n fill\n sizes=\"(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw\"\n className=\"object-cover object-top transition-transform duration-500 group-hover:scale-105\"\n />\n {/* Category badge */}\n <span\n className=\"absolute top-3 left-3 px-3 py-1 rounded-full text-[11px] uppercase tracking-wider font-semibold\"\n style={{\n background: 'color-mix(in srgb, var(--sk-primary, #6366f1) 80%, transparent)',\n color: '#ffffff',\n backdropFilter: 'blur(8px)',\n }}\n >\n {formatCategoryLabel(item.category)}\n </span>\n </div>\n )}\n\n {/* Text content */}\n <div className=\"flex flex-col gap-2 px-2 pb-2\">\n <h3\n className=\"text-lg font-semibold line-clamp-1\"\n style={{ color: 'var(--sk-text-primary, #ffffff)' }}\n >\n {item.title}\n </h3>\n\n {item.subtitle && (\n <p\n className=\"text-sm line-clamp-2\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {item.subtitle}\n </p>\n )}\n\n {/* KPI preview */}\n {kpiPreview.length > 0 && (\n <div className=\"flex items-center gap-4 mt-1\">\n {kpiPreview.map((kpi, i) => (\n <div key={i} className=\"flex flex-col\">\n <span\n className=\"text-base font-bold\"\n style={{ color: 'var(--sk-primary, #6366f1)' }}\n >\n {kpi.prefix || ''}\n {kpi.value.toLocaleString()}\n {kpi.suffix}\n </span>\n <span\n className=\"text-[10px] uppercase tracking-wider font-medium\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {kpi.label}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Services tags */}\n {servicePreview.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5 mt-2\">\n {servicePreview.map((service) => (\n <span\n key={service}\n className=\"px-2.5 py-0.5 rounded-full text-[11px] font-medium\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-secondary, #a1a1aa)',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n {formatServiceTag(service)}\n </span>\n ))}\n {services.length > 3 && (\n <span\n className=\"px-2.5 py-0.5 rounded-full text-[11px] font-medium\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-tertiary, #71717a)',\n }}\n >\n +{item.services.length - 3}\n </span>\n )}\n </div>\n )}\n </div>\n </div>\n </GlassCard>\n </Link>\n );\n}\n","// Shared component — no directive on purpose: hook-free, so it renders\n// server-side (zero JS) from PortfolioIndex and stays usable from client\n// code via the /portfolio/client barrel.\nimport React from 'react';\nimport type { PortfolioItem } from '../../types';\nimport ScrollReveal from '@sonordev/agency-site-kit/portfolio/primitives/ScrollReveal';\nimport PortfolioCard from './PortfolioCard';\n\ninterface PortfolioGridProps {\n items: PortfolioItem[];\n className?: string;\n /** Base path for portfolio detail links (default: '/work') */\n basePath?: string;\n}\n\nexport default function PortfolioGrid({ items, className = '', basePath = '/work' }: PortfolioGridProps) {\n return (\n <ScrollReveal stagger={0.1}>\n <div\n className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 ${className}`}\n style={{\n gap: 'var(--sk-grid-gap, 24px)',\n }}\n >\n {items.map((item) => (\n <PortfolioCard key={item.id} item={item} basePath={basePath} />\n ))}\n </div>\n </ScrollReveal>\n );\n}\n","/**\n * PortfolioIndex — turnkey, server-rendered /work index.\n *\n * Fetches items + categories and renders: category nav (real links —\n * crawlable, no client state), the portfolio grid, pagination, and an\n * intentional empty state. Ships zero component JS.\n *\n * Category filtering is route-based for SEO:\n * /work → all items\n * /work/category/[category] → one category (slug via slugifyCategory)\n * Pagination via ?page= on either route.\n */\n\nimport React from 'react';\nimport {\n getPortfolioCategories,\n getPortfolioItems,\n slugifyCategory,\n} from '../server';\nimport PortfolioGrid from './PortfolioGrid';\n\nexport interface PortfolioIndexProps {\n /** Route prefix for case studies + category links. Defaults to \"/work\". */\n basePath?: string;\n /** Active category SLUG (from the [category] route param). */\n category?: string | null;\n /** 1-based page number (from ?page=). */\n page?: number;\n /** Items per page. Defaults to 12. */\n pageSize?: number;\n /** Optional heading block rendered above the category nav. */\n heading?: React.ReactNode;\n /** Override the empty state. */\n emptyState?: React.ReactNode;\n /** Hide the category nav (e.g. single-category portfolios). */\n showCategories?: boolean;\n className?: string;\n}\n\nfunction CategoryLink({\n href,\n label,\n active,\n}: {\n href: string;\n label: string;\n active: boolean;\n}) {\n return (\n <a\n href={href}\n aria-current={active ? 'page' : undefined}\n className=\"px-4 py-1.5 rounded-full text-sm font-medium transition-colors\"\n style={{\n background: active\n ? 'color-mix(in srgb, var(--sk-primary, #6366f1) 15%, transparent)'\n : 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: active ? 'var(--sk-primary, #6366f1)' : 'var(--sk-text-secondary, #a1a1aa)',\n border: `1px solid ${\n active\n ? 'color-mix(in srgb, var(--sk-primary, #6366f1) 25%, transparent)'\n : 'var(--sk-border, rgba(255,255,255,0.1))'\n }`,\n }}\n >\n {label}\n </a>\n );\n}\n\nfunction PageLink({ href, label, disabled }: { href: string; label: string; disabled: boolean }) {\n if (disabled) {\n return (\n <span\n aria-disabled=\"true\"\n className=\"px-4 py-2 rounded-xl text-sm font-medium opacity-40\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-tertiary, #71717a)',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n {label}\n </span>\n );\n }\n return (\n <a\n href={href}\n className=\"px-4 py-2 rounded-xl text-sm font-medium transition-colors\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-secondary, #a1a1aa)',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n {label}\n </a>\n );\n}\n\nexport default async function PortfolioIndex({\n basePath = '/work',\n category = null,\n page = 1,\n pageSize = 12,\n heading,\n emptyState,\n showCategories = true,\n className = '',\n}: PortfolioIndexProps) {\n const currentPage = Math.max(1, Math.floor(page));\n const categories = await getPortfolioCategories();\n\n // Route param is a slug; the API filter wants the original category value.\n const activeCategory = category\n ? categories.find((c) => slugifyCategory(c) === category) ?? null\n : null;\n\n const { items, total } = await getPortfolioItems({\n category: activeCategory ?? undefined,\n limit: pageSize,\n offset: (currentPage - 1) * pageSize,\n });\n\n const totalPages = Math.max(1, Math.ceil(total / pageSize));\n const pageHref = (p: number) => {\n const base = activeCategory\n ? `${basePath}/category/${slugifyCategory(activeCategory)}`\n : basePath;\n return p <= 1 ? base : `${base}?page=${p}`;\n };\n\n return (\n <section className={`w-full ${className}`}>\n <div className=\"max-w-7xl mx-auto px-6\">\n {heading}\n\n {/* Category nav — real links, crawlable */}\n {showCategories && categories.length > 1 && (\n <nav aria-label=\"Portfolio categories\" className=\"flex flex-wrap gap-2 mb-10\">\n <CategoryLink href={basePath} label=\"All\" active={!activeCategory} />\n {categories.map((c) => (\n <CategoryLink\n key={c}\n href={`${basePath}/category/${slugifyCategory(c)}`}\n label={c}\n active={activeCategory === c}\n />\n ))}\n </nav>\n )}\n\n {items.length > 0 ? (\n <PortfolioGrid items={items} basePath={basePath} />\n ) : (\n emptyState ?? (\n <div\n className=\"flex flex-col items-center gap-3 py-24 rounded-2xl text-center\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.03))',\n border: '1px dashed var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n <span\n className=\"text-lg font-semibold\"\n style={{ color: 'var(--sk-text-primary, #ffffff)' }}\n >\n Case studies coming soon\n </span>\n <span className=\"text-sm\" style={{ color: 'var(--sk-text-tertiary, #71717a)' }}>\n {activeCategory\n ? `Nothing published in ${activeCategory} yet.`\n : \"We're writing up our latest work right now.\"}\n </span>\n </div>\n )\n )}\n\n {/* Pagination */}\n {totalPages > 1 && (\n <nav\n aria-label=\"Portfolio pages\"\n className=\"flex items-center justify-center gap-3 mt-12\"\n >\n <PageLink\n href={pageHref(currentPage - 1)}\n label=\"← Previous\"\n disabled={currentPage <= 1}\n />\n <span className=\"text-sm\" style={{ color: 'var(--sk-text-tertiary, #71717a)' }}>\n Page {currentPage} of {totalPages}\n </span>\n <PageLink\n href={pageHref(currentPage + 1)}\n label=\"Next →\"\n disabled={currentPage >= totalPages}\n />\n </nav>\n )}\n </div>\n </section>\n );\n}\n","/**\n * Loading skeletons for portfolio routes — pure CSS pulse, server-safe,\n * zero JS. Drop into `loading.tsx`:\n *\n * ```tsx\n * export { PortfolioIndexSkeleton as default } from '@sonordev/agency-site-kit/portfolio';\n * ```\n */\n\nimport React from 'react';\n\nconst PULSE_CSS = `\n.sk-skeleton{animation:sk-skeleton-pulse 1.6s ease-in-out infinite;background:var(--sk-surface,rgba(255,255,255,0.06));border:1px solid var(--sk-border,rgba(255,255,255,0.08));border-radius:16px}\n@keyframes sk-skeleton-pulse{0%,100%{opacity:.55}50%{opacity:1}}\n@media (prefers-reduced-motion:reduce){.sk-skeleton{animation:none}}\n`;\n\nfunction SkeletonStyles() {\n return (\n <style href=\"sk-skeletons\" precedence=\"default\">\n {PULSE_CSS}\n </style>\n );\n}\n\n/** Grid of pulsing card placeholders for the /work index. */\nexport function PortfolioIndexSkeleton({ cards = 6 }: { cards?: number }) {\n return (\n <section className=\"w-full py-16\">\n <SkeletonStyles />\n <div className=\"max-w-7xl mx-auto px-6\">\n <div className=\"sk-skeleton h-9 w-56 mb-4\" />\n <div className=\"sk-skeleton h-5 w-80 mb-10\" />\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {Array.from({ length: cards }).map((_, i) => (\n <div key={i} className=\"flex flex-col gap-3\">\n <div className=\"sk-skeleton w-full\" style={{ aspectRatio: '16/10' }} />\n <div className=\"sk-skeleton h-5 w-3/4\" />\n <div className=\"sk-skeleton h-4 w-1/2\" />\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n\n/** Hero-shaped placeholder for /work/[slug] case study pages. */\nexport function PortfolioPageSkeleton() {\n return (\n <article className=\"w-full\">\n <SkeletonStyles />\n <div className=\"max-w-7xl mx-auto px-6 py-24\">\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-12 items-start\">\n <div className=\"flex flex-col gap-5\">\n <div className=\"sk-skeleton h-7 w-32\" style={{ borderRadius: '9999px' }} />\n <div className=\"sk-skeleton h-12 w-full\" />\n <div className=\"sk-skeleton h-12 w-4/5\" />\n <div className=\"sk-skeleton h-5 w-full\" />\n <div className=\"sk-skeleton h-5 w-2/3\" />\n </div>\n <div className=\"sk-skeleton w-full\" style={{ aspectRatio: '11/5' }} />\n </div>\n </div>\n </article>\n );\n}\n","/**\n * @sonordev/agency-site-kit — Type definitions\n *\n * These types mirror the Portal API's cms/types.ts definitions for portfolio\n * content. Keep them in sync when modifying the API response shapes.\n */\n\n// ---------------------------------------------------------------------------\n// Brand Configuration\n// ---------------------------------------------------------------------------\n\nexport interface BrandConfig {\n /** Primary brand color (hex) */\n primary: string;\n /** Secondary/accent color (hex) */\n secondary: string;\n /** Background color (hex) */\n background: string;\n /** Elevated background — cards, modals (hex) */\n backgroundElevated: string;\n /** Surface color — interactive containers (hex) */\n surface: string;\n /** Surface hover state (hex) */\n surfaceHover: string;\n /** Surface border color (hex) */\n surfaceBorder: string;\n /** Primary text color (hex) */\n textPrimary: string;\n /** Secondary text color (hex) */\n textSecondary: string;\n /** Tertiary/muted text color (hex) */\n textTertiary: string;\n /** Heading font family */\n fontHeading: string;\n /** Body font family */\n fontBody: string;\n /** Logo URL (absolute or relative) */\n logoUrl?: string;\n /** Logo alt text */\n logoAlt?: string;\n /** Dark mode overrides — partial, merged with base values */\n darkMode?: Partial<Omit<BrandConfig, 'fontHeading' | 'fontBody' | 'logoUrl' | 'logoAlt' | 'darkMode'>>;\n /** Border radius tokens */\n radius?: {\n sm: string;\n md: string;\n lg: string;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Sanity Image Reference\n// ---------------------------------------------------------------------------\n\nexport interface SanityImageRef {\n _type: 'image';\n asset: { _type: 'reference'; _ref: string };\n hotspot?: { x: number; y: number; height: number; width: number };\n crop?: { top: number; bottom: number; left: number; right: number };\n alt?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Metrics\n// ---------------------------------------------------------------------------\n\nexport type MetricSource = 'measured' | 'estimated';\n\nexport interface PortfolioKPI {\n label: string;\n value: number;\n suffix: string;\n prefix?: string;\n description: string;\n source: MetricSource;\n}\n\nexport interface MetricsDelta {\n metric: string;\n baseline: number;\n current: number;\n delta: number;\n deltaPercent: number;\n direction: 'up' | 'down' | 'flat';\n timespan: string;\n}\n\nexport interface MetricsSnapshot {\n seo: {\n clicks_28d?: number;\n impressions_28d?: number;\n avg_position?: number;\n ctr?: number;\n };\n cwv: {\n lcp_ms?: number;\n inp_ms?: number;\n cls?: number;\n performance_score?: number;\n };\n analytics: {\n sessions_28d?: number;\n page_views_28d?: number;\n conversion_rate?: number;\n bounce_rate?: number;\n };\n pages: {\n total_indexed?: number;\n total_crawled?: number;\n total_pages?: number;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Portfolio Section Data Types\n// ---------------------------------------------------------------------------\n\nexport interface PortfolioHeroData {\n headline: string;\n subheadline: string;\n description: string;\n category: string;\n services: string[];\n liveUrl?: string;\n screenshots: {\n desktop?: string;\n tablet?: string;\n mobile?: string;\n };\n kpis: PortfolioKPI[];\n}\n\nexport interface PortfolioChallengeItem {\n title: string;\n description: string;\n solution: string;\n result?: string;\n icon?: string;\n}\n\nexport interface PortfolioChallengesData {\n items: PortfolioChallengeItem[];\n}\n\nexport interface PortfolioStrategyPhase {\n number: number;\n title: string;\n description: string;\n deliverables?: string[];\n icon?: string;\n timeline?: string;\n}\n\nexport interface PortfolioStrategyData {\n phases: PortfolioStrategyPhase[];\n}\n\nexport interface PortfolioResultItem {\n title: string;\n description: string;\n metric?: {\n value: number;\n suffix: string;\n prefix?: string;\n };\n source: MetricSource;\n icon?: string;\n}\n\nexport interface PortfolioResultsData {\n items: PortfolioResultItem[];\n}\n\nexport interface PortfolioTechItem {\n name: string;\n category: string;\n icon?: string;\n description?: string;\n}\n\nexport interface PortfolioTechStackData {\n technologies: PortfolioTechItem[];\n}\n\nexport interface PortfolioServiceItem {\n title: string;\n description: string;\n features: string[];\n icon?: string;\n}\n\nexport interface PortfolioServicesData {\n items: PortfolioServiceItem[];\n}\n\nexport interface PortfolioTestimonialData {\n quote: string;\n author: string;\n title: string;\n company: string;\n avatar?: SanityImageRef;\n rating?: number;\n}\n\nexport interface PortfolioGalleryImage {\n image: SanityImageRef;\n caption?: string;\n type?: 'screenshot' | 'before' | 'after' | 'design';\n}\n\nexport interface PortfolioGalleryData {\n images: PortfolioGalleryImage[];\n layout?: 'grid' | 'masonry';\n}\n\nexport interface PortfolioVideoData {\n url?: string;\n embedUrl?: string;\n thumbnail: SanityImageRef;\n title: string;\n duration?: string;\n platform?: 'youtube' | 'vimeo' | 'custom';\n}\n\nexport interface PortfolioTeamMember {\n name: string;\n role: string;\n avatar?: SanityImageRef;\n}\n\nexport interface PortfolioTeamData {\n members: PortfolioTeamMember[];\n}\n\nexport interface PortfolioAnnotation {\n x: number;\n y: number;\n label: string;\n description: string;\n icon?: string;\n}\n\nexport interface PortfolioFeatureSpotlightData {\n image: SanityImageRef;\n title: string;\n description: string;\n layout: 'left' | 'right' | 'full';\n annotations: PortfolioAnnotation[];\n}\n\nexport interface PortfolioBeforeAfterData {\n before: SanityImageRef;\n after: SanityImageRef;\n title?: string;\n description?: string;\n defaultPosition?: number;\n}\n\nexport interface PortfolioMetricsDataPoint {\n month: string;\n label: string;\n metrics: {\n traffic?: number;\n conversions?: number;\n revenue?: number;\n rankings?: number;\n };\n}\n\nexport interface PortfolioMetricsTimelineData {\n dataPoints: PortfolioMetricsDataPoint[];\n baselineDate: string;\n title?: string;\n description?: string;\n}\n\nexport interface PortfolioFunnelStage {\n label: string;\n value: number;\n icon?: string;\n color?: string;\n}\n\nexport interface PortfolioConversionFunnelData {\n stages: PortfolioFunnelStage[];\n title?: string;\n description?: string;\n}\n\nexport interface PortfolioDetailsData {\n industry: string;\n location?: string;\n website?: string;\n timeline?: string;\n launchDate?: string;\n budgetRange?: string;\n}\n\nexport interface PortfolioSeoData {\n metaTitle: string;\n metaDescription: string;\n keywords: string[];\n ogImage?: SanityImageRef;\n}\n\nexport interface PortfolioCTAData {\n headline: string;\n description: string;\n buttonText: string;\n buttonUrl: string;\n style?: 'primary' | 'glass';\n}\n\n// ---------------------------------------------------------------------------\n// Portfolio Sections\n// ---------------------------------------------------------------------------\n\nexport const PORTFOLIO_SECTION_TYPES = [\n 'portfolioHero',\n 'portfolioChallenges',\n 'portfolioStrategy',\n 'portfolioResults',\n 'portfolioTechStack',\n 'portfolioServices',\n 'portfolioTestimonial',\n 'portfolioGallery',\n 'portfolioVideo',\n 'portfolioTeam',\n 'portfolioFeatureSpotlight',\n 'portfolioBeforeAfter',\n 'portfolioMetricsTimeline',\n 'portfolioConversionFunnel',\n 'portfolioPerformance',\n 'portfolioSpeedComparison',\n 'portfolioSiteArchitecture',\n 'portfolioDesignSystem',\n 'portfolioDetails',\n 'portfolioSeo',\n 'portfolioCTA',\n] as const;\n\nexport type PortfolioSectionType = (typeof PORTFOLIO_SECTION_TYPES)[number];\n\nexport type PortfolioSectionData =\n | PortfolioHeroData\n | PortfolioChallengesData\n | PortfolioStrategyData\n | PortfolioResultsData\n | PortfolioTechStackData\n | PortfolioServicesData\n | PortfolioTestimonialData\n | PortfolioGalleryData\n | PortfolioVideoData\n | PortfolioTeamData\n | PortfolioFeatureSpotlightData\n | PortfolioBeforeAfterData\n | PortfolioMetricsTimelineData\n | PortfolioConversionFunnelData\n | PortfolioDetailsData\n | PortfolioSeoData\n | PortfolioCTAData;\n\nexport interface PortfolioSectionDataMap {\n portfolioHero: PortfolioHeroData;\n portfolioChallenges: PortfolioChallengesData;\n portfolioStrategy: PortfolioStrategyData;\n portfolioResults: PortfolioResultsData;\n portfolioTechStack: PortfolioTechStackData;\n portfolioServices: PortfolioServicesData;\n portfolioTestimonial: PortfolioTestimonialData;\n portfolioGallery: PortfolioGalleryData;\n portfolioVideo: PortfolioVideoData;\n portfolioTeam: PortfolioTeamData;\n portfolioFeatureSpotlight: PortfolioFeatureSpotlightData;\n portfolioBeforeAfter: PortfolioBeforeAfterData;\n portfolioMetricsTimeline: PortfolioMetricsTimelineData;\n portfolioConversionFunnel: PortfolioConversionFunnelData;\n portfolioDetails: PortfolioDetailsData;\n portfolioSeo: PortfolioSeoData;\n portfolioCTA: PortfolioCTAData;\n}\n\nexport interface PortfolioSection<T extends PortfolioSectionType = PortfolioSectionType> {\n sectionType: T;\n displayName: string;\n data: T extends keyof PortfolioSectionDataMap ? PortfolioSectionDataMap[T] : PortfolioSectionData;\n}\n\n// ---------------------------------------------------------------------------\n// Portfolio Item (mirrors public API response)\n// ---------------------------------------------------------------------------\n\nexport interface PortfolioItem {\n id: string;\n slug: string;\n title: string;\n subtitle: string;\n category: string;\n services: string[];\n description: string;\n hero_image: string;\n hero_image_alt: string;\n live_url: string | null;\n kpis: PortfolioKPI[];\n details: PortfolioDetailsData | null;\n seo: PortfolioSeoData | null;\n featured: boolean;\n order?: number;\n published_at: string | null;\n /** Sonor project ID (present for items generated from a managed project) */\n project_id: string | null;\n hero_screenshots: {\n desktop?: string;\n tablet?: string;\n mobile?: string;\n } | null;\n baseline_metrics: MetricsSnapshot | null;\n current_metrics: MetricsSnapshot | null;\n metrics_last_refreshed_at: string | null;\n}\n\nexport interface PortfolioItemFull extends PortfolioItem {\n sections: PortfolioSection[];\n metricsDelta: MetricsDelta[];\n /**\n * True once the agency deliberately reordered sections in the dashboard.\n * PortfolioPage respects the sections array order when set (hero pinned\n * first); otherwise it applies the canonical order.\n */\n sectionsOrdered?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// API Responses\n// ---------------------------------------------------------------------------\n\nexport interface PortfolioListResponse {\n items: PortfolioItem[];\n total: number;\n limit: number;\n offset: number;\n}\n\nexport interface PortfolioConfigResponse {\n brand: BrandConfig;\n orgName: string;\n orgSlug: string;\n}\n"]}
1
+ {"version":3,"sources":["../src/portfolio/components/PortfolioPage.tsx","../src/portfolio/components/PortfolioSchema.tsx","../src/portfolio/components/PortfolioCard.tsx","../src/portfolio/components/PortfolioGrid.tsx","../src/portfolio/components/PortfolioIndex.tsx","../src/portfolio/components/PortfolioSkeletons.tsx","../src/types.ts"],"names":["jsx","jsxs"],"mappings":";;;;;;;;;;;;;;;;;;;;;AA+DA,IAAM,eAAA,GAAkB,QAAQ,MAAM,OAAO,8DAA8D,CAAA,EAAG,EAAE,GAAA,EAAK,IAAA,EAAM,CAAA;AAC3H,IAAM,cAAA,GAAiB,QAAQ,MAAM,OAAO,6DAA6D,CAAA,EAAG,EAAE,GAAA,EAAK,IAAA,EAAM,CAAA;AACzH,IAAM,YAAA,GAAe,QAAQ,MAAM,OAAO,2DAA2D,CAAA,EAAG,EAAE,GAAA,EAAK,IAAA,EAAM,CAAA;AACrH,IAAM,WAAA,GAAc,QAAQ,MAAM,OAAO,0DAA0D,CAAA,EAAG,EAAE,GAAA,EAAK,IAAA,EAAM,CAAA;AACnH,IAAM,uBAAA,GAA0B,QAAQ,MAAM,OAAO,sEAAsE,CAAA,EAAG,EAAE,GAAA,EAAK,IAAA,EAAM,CAAA;AAC3I,IAAM,kBAAA,GAAqB,QAAQ,MAAM,OAAO,iEAAiE,CAAA,EAAG,EAAE,GAAA,EAAK,IAAA,EAAM,CAAA;AACjI,IAAM,sBAAA,GAAyB,QAAQ,MAAM,OAAO,qEAAqE,CAAA,EAAG,EAAE,GAAA,EAAK,IAAA,EAAM,CAAA;AACzI,IAAM,uBAAA,GAA0B,QAAQ,MAAM,OAAO,sEAAsE,CAAA,EAAG,EAAE,GAAA,EAAK,IAAA,EAAM,CAAA;AAC3I,IAAM,kBAAA,GAAqB,QAAQ,MAAM,OAAO,iEAAiE,CAAA,EAAG,EAAE,GAAA,EAAK,IAAA,EAAM,CAAA;AACjI,IAAM,uBAAA,GAA0B,QAAQ,MAAM,OAAO,sEAAsE,CAAA,EAAG,EAAE,GAAA,EAAK,IAAA,EAAM,CAAA;AAC3I,IAAM,UAAA,GAAa,QAAQ,MAAM,OAAO,yDAAyD,CAAA,EAAG,EAAE,GAAA,EAAK,IAAA,EAAM,CAAA;AAkBjH,SAAS,aAAA,CAAc,OAAA,EAA2B,IAAA,EAAyB,KAAA,EAAe;AACxF,EAAA,MAAM,GAAA,GAAM,CAAA,EAAG,OAAA,CAAQ,WAAW,IAAI,KAAK,CAAA,CAAA;AAE3C,EAAA,QAAQ,QAAQ,WAAA;AAAa,IAC3B,KAAK,eAAA;AACH,MAAA,uBACE,GAAA;AAAA,QAAC,WAAA;AAAA,QAAA;AAAA,UAEC,MAAM,OAAA,CAAQ,IAAA;AAAA,UACd,WAAA,EAAa,KAAK,gBAAA,IAAoB,MAAA;AAAA,UACtC,OAAA,EAAS,KAAK,QAAA,IAAY;AAAA,SAAA;AAAA,QAHrB;AAAA,OAIP;AAAA,IAEJ,KAAK,qBAAA;AACH,MAAA,uBAAO,GAAA,CAAC,iBAAA,EAAA,EAA4B,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAAoD,CAAA;AAAA,IACrF,KAAK,mBAAA;AACH,MAAA,uBAAO,GAAA,CAAC,eAAA,EAAA,EAA0B,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAAkD,CAAA;AAAA,IACjF,KAAK,kBAAA;AACH,MAAA,uBACE,GAAA;AAAA,QAAC,cAAA;AAAA,QAAA;AAAA,UAEC,MAAM,OAAA,CAAQ,IAAA;AAAA,UACd,cAAc,IAAA,CAAK,YAAA;AAAA,UACnB,oBAAoB,IAAA,CAAK;AAAA,SAAA;AAAA,QAHpB;AAAA,OAIP;AAAA,IAEJ,KAAK,oBAAA;AACH,MAAA,uBAAO,GAAA,CAAC,gBAAA,EAAA,EAA2B,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAAmD,CAAA;AAAA,IACnF,KAAK,mBAAA;AACH,MAAA,uBAAO,GAAA,CAAC,eAAA,EAAA,EAA0B,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAAkD,CAAA;AAAA,IACjF,KAAK,sBAAA;AACH,MAAA,uBAAO,GAAA,CAAC,kBAAA,EAAA,EAA6B,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAAqD,CAAA;AAAA,IACvF,KAAK,kBAAA;AACH,MAAA,uBAAO,GAAA,CAAC,cAAA,EAAA,EAAyB,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAAiD,CAAA;AAAA,IAC/E,KAAK,gBAAA;AACH,MAAA,uBAAO,GAAA,CAAC,YAAA,EAAA,EAAuB,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAA+C,CAAA;AAAA,IAC3E,KAAK,eAAA;AACH,MAAA,uBAAO,GAAA,CAAC,WAAA,EAAA,EAAsB,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAA8C,CAAA;AAAA,IACzE,KAAK,2BAAA;AACH,MAAA,uBAAO,GAAA,CAAC,uBAAA,EAAA,EAAkC,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAA0D,CAAA;AAAA,IACjG,KAAK,sBAAA;AACH,MAAA,uBAAO,GAAA,CAAC,kBAAA,EAAA,EAA6B,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAAqD,CAAA;AAAA,IACvF,KAAK,0BAAA;AACH,MAAA,uBAAO,GAAA,CAAC,sBAAA,EAAA,EAAiC,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAAyD,CAAA;AAAA,IAC/F,KAAK,2BAAA;AACH,MAAA,uBAAO,GAAA,CAAC,uBAAA,EAAA,EAAkC,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAA0D,CAAA;AAAA,IACjG,KAAK,sBAAA;AACH,MAAA,uBAAO,GAAA,CAAC,kBAAA,EAAA,EAA6B,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAAgC,CAAA;AAAA,IAClE,KAAK,0BAAA;AACH,MAAA,uBAAO,GAAA,CAAC,sBAAA,EAAA,EAAiC,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAAgC,CAAA;AAAA,IACtE,KAAK,2BAAA;AACH,MAAA,2BAAQ,uBAAA,EAAA,EAAkC,IAAA,EAAM,6BAA6B,OAAA,CAAQ,IAAI,KAApD,GAA8D,CAAA;AAAA,IACrG,KAAK,uBAAA;AACH,MAAA,uBAAO,GAAA,CAAC,mBAAA,EAAA,EAA8B,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAAgC,CAAA;AAAA,IACnE,KAAK,kBAAA;AACH,MAAA,uBAAO,GAAA,CAAC,cAAA,EAAA,EAAyB,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAAiD,CAAA;AAAA,IAC/E,KAAK,cAAA;AACH,MAAA,uBAAO,GAAA,CAAC,UAAA,EAAA,EAAqB,IAAA,EAAM,OAAA,CAAQ,QAAnB,GAA6C,CAAA;AAAA,IACvE,KAAK,cAAA;AAEH,MAAA,OAAO,IAAA;AAAA,IACT;AACE,MAAA,OAAO,IAAA;AAAA;AAEb;AAMA,IAAM,aAAA,GAAwC;AAAA,EAC5C,aAAA,EAAe,CAAA;AAAA,EACf,mBAAA,EAAqB,CAAA;AAAA,EACrB,iBAAA,EAAmB,CAAA;AAAA,EACnB,gBAAA,EAAkB,CAAA;AAAA,EAClB,oBAAA,EAAsB,CAAA;AAAA,EACtB,wBAAA,EAA0B,CAAA;AAAA,EAC1B,yBAAA,EAA2B,CAAA;AAAA,EAC3B,kBAAA,EAAoB,CAAA;AAAA,EACpB,qBAAA,EAAuB,CAAA;AAAA,EACvB,iBAAA,EAAmB,CAAA;AAAA,EACnB,oBAAA,EAAsB,EAAA;AAAA,EACtB,oBAAA,EAAsB,EAAA;AAAA,EACtB,wBAAA,EAA0B,EAAA;AAAA,EAC1B,yBAAA,EAA2B,EAAA;AAAA,EAC3B,gBAAA,EAAkB,EAAA;AAAA,EAClB,YAAA,EAAc,EAAA;AAAA,EACd,YAAA,EAAc;AAChB,CAAA;AAEe,SAAR,aAAA,CAA+B,EAAE,IAAA,EAAM,OAAA,EAAQ,EAAuB;AAC3E,EAAA,MAAM,IAAA,GAAO,qBAAqB,OAAO,CAAA;AAOzC,EAAA,MAAM,iBAAiB,IAAA,CAAK,eAAA,GACxB,CAAC,GAAG,IAAA,CAAK,QAAQ,CAAA,CAAE,IAAA;AAAA,IACjB,CAAC,CAAA,EAAG,CAAA,KAAA,CACD,CAAA,CAAE,WAAA,KAAgB,eAAA,GAAkB,CAAA,GAAI,CAAA,KACxC,CAAA,CAAE,WAAA,KAAgB,eAAA,GAAkB,CAAA,GAAI,CAAA;AAAA,GAC7C,GACA,CAAC,GAAG,IAAA,CAAK,QAAQ,CAAA,CAAE,IAAA;AAAA,IACjB,CAAC,CAAA,EAAG,CAAA,KAAA,CAAO,aAAA,CAAc,CAAA,CAAE,WAAW,CAAA,IAAK,EAAA,KAAO,aAAA,CAAc,CAAA,CAAE,WAAW,CAAA,IAAK,EAAA;AAAA,GACpF;AAEJ,EAAA,uBACE,IAAA,CAAC,SAAA,EAAA,EAAQ,SAAA,EAAU,yDAAA,EAChB,QAAA,EAAA;AAAA,IAAA,cAAA,CAAe,GAAA,CAAI,CAAC,OAAA,EAAS,KAAA,KAAU;AACtC,MAAA,MAAM,QAAA,GAAW,aAAA,CAAc,OAAA,EAAS,IAAA,EAAM,KAAK,CAAA;AACnD,MAAA,IAAI,CAAC,UAAU,OAAO,IAAA;AACtB,MAAA,uBACE,GAAA;AAAA,QAAC,KAAA;AAAA,QAAA;AAAA,UAEC,SAAA,EAAU,QAAA;AAAA,UACV,0BAAwB,OAAA,CAAQ,WAAA;AAAA,UAE/B,QAAA,EAAA;AAAA,SAAA;AAAA,QAJI,CAAA,QAAA,EAAW,OAAA,CAAQ,WAAW,CAAA,CAAA,EAAI,KAAK,CAAA;AAAA,OAK9C;AAAA,IAEJ,CAAC,CAAA;AAAA,oBAED,GAAA,CAAC,kBAAA,EAAA,EAAmB,IAAA,EAAM,IAAA,CAAK,IAAA,EAAM;AAAA,GAAA,EACvC,CAAA;AAEJ;AClLA,SAAS,OAAO,KAAA,EAAwB;AACtC,EAAA,OAAO,KAAK,SAAA,CAAU,KAAK,CAAA,CAAE,OAAA,CAAQ,MAAM,SAAS,CAAA;AACtD;AAEe,SAAR,eAAA,CAAiC,EAAE,IAAA,EAAM,GAAA,EAAK,WAAU,EAAyB;AACtF,EAAA,MAAM,QAAA,GAAW,IAAA,CAAK,GAAA,EAAK,SAAA,IAAa,IAAA,CAAK,KAAA;AAC7C,EAAA,MAAM,WAAA,GAAc,IAAA,CAAK,GAAA,EAAK,eAAA,IAAmB,IAAA,CAAK,WAAA;AAEtD,EAAA,MAAM,QAAA,GAAW;AAAA,IACf,GAAI,IAAA,CAAK,GAAA,EAAK,QAAA,IAAY,EAAC;AAAA,IAC3B,IAAA,CAAK,QAAA;AAAA,IACL,GAAG,IAAA,CAAK;AAAA,GACV,CAAE,OAAO,OAAO,CAAA;AAEhB,EAAA,MAAM,MAAM,SAAA,GACR;AAAA,IACE,OAAA,EAAS,cAAA;AAAA,IACT,MAAM,SAAA,CAAU,IAAA;AAAA,IAChB,GAAI,UAAU,GAAA,GAAM,EAAE,KAAK,SAAA,CAAU,GAAA,KAAQ,EAAC;AAAA,IAC9C,GAAI,SAAA,CAAU,IAAA,GACV,EAAE,IAAA,EAAM,EAAE,OAAA,EAAS,aAAA,EAAe,GAAA,EAAK,SAAA,CAAU,IAAA,EAAK,KACtD;AAAC,GACP,GACA,MAAA;AAEJ,EAAA,MAAM,MAAA,GAAS;AAAA,IACb,UAAA,EAAY,oBAAA;AAAA,IACZ,OAAA,EAAS,SAAA;AAAA,IACT,QAAA;AAAA,IACA,GAAI,WAAA,GAAc,EAAE,WAAA,KAAgB,EAAC;AAAA,IACrC,GAAI,IAAA,CAAK,UAAA,GAAa,EAAE,KAAA,EAAO,CAAC,IAAA,CAAK,UAAU,CAAA,EAAE,GAAI,EAAC;AAAA,IACtD,GAAA;AAAA,IACA,gBAAA,EAAkB,EAAE,OAAA,EAAS,SAAA,EAAW,OAAO,GAAA,EAAI;AAAA,IACnD,GAAI,KAAK,YAAA,GAAe,EAAE,eAAe,IAAA,CAAK,YAAA,KAAiB,EAAC;AAAA,IAChE,GAAI,IAAA,CAAK,yBAAA,IAA6B,IAAA,CAAK,YAAA,GACvC,EAAE,YAAA,EAAc,IAAA,CAAK,yBAAA,IAA6B,IAAA,CAAK,YAAA,EAAa,GACpE,EAAC;AAAA,IACL,GAAI,QAAA,CAAS,MAAA,GAAS,EAAE,QAAA,EAAU,SAAS,IAAA,CAAK,IAAI,CAAA,EAAE,GAAI,EAAC;AAAA,IAC3D,GAAI,MAAM,EAAE,MAAA,EAAQ,KAAK,SAAA,EAAW,GAAA,KAAQ,EAAC;AAAA,IAC7C,GAAI,IAAA,CAAK,OAAA,EAAS,QAAA,GAAW,EAAE,OAAO,IAAA,CAAK,OAAA,CAAQ,QAAA,EAAS,GAAI;AAAC,GACnE;AAEA,EAAA,uBACEA,GAAAA;AAAA,IAAC,QAAA;AAAA,IAAA;AAAA,MACC,IAAA,EAAK,qBAAA;AAAA,MACL,uBAAA,EAAyB,EAAE,MAAA,EAAQ,MAAA,CAAO,MAAM,CAAA;AAAE;AAAA,GACpD;AAEJ;ACtEe,SAAR,cAA+B,EAAE,IAAA,EAAM,YAAY,EAAA,EAAI,QAAA,GAAW,SAAQ,EAAuB;AAEtG,EAAA,MAAM,YAAA,GACJ,IAAA,CAAK,gBAAA,EAAkB,OAAA,IACvB,IAAA,CAAK,UAAA,KACJ,IAAA,CAAK,UAAA,GAAa,CAAA,oBAAA,EAAuB,IAAA,CAAK,IAAI,CAAA,IAAA,CAAA,GAAS,MAAA,CAAA;AAC9D,EAAA,MAAM,IAAA,GAAO,MAAM,OAAA,CAAQ,IAAA,CAAK,IAAI,CAAA,GAAI,IAAA,CAAK,OAAO,EAAC;AACrD,EAAA,MAAM,QAAA,GAAW,MAAM,OAAA,CAAQ,IAAA,CAAK,QAAQ,CAAA,GAAI,IAAA,CAAK,WAAW,EAAC;AAGjE,EAAA,MAAM,aAAa,cAAA,CAAe,IAAI,CAAA,CAAE,KAAA,CAAM,GAAG,CAAC,CAAA;AAClD,EAAA,MAAM,cAAA,GAAiB,QAAA,CAAS,KAAA,CAAM,CAAA,EAAG,CAAC,CAAA;AAE1C,EAAA,uBACEA,GAAAA,CAAC,IAAA,EAAA,EAAK,IAAA,EAAM,CAAA,EAAG,QAAQ,CAAA,CAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAA,EAAI,SAAA,EAAW,CAAA,YAAA,EAAe,SAAS,CAAA,CAAA,EACzE,QAAA,kBAAAA,GAAAA,CAAC,SAAA,EAAA,EAAU,OAAA,EAAQ,IAAA,EAAK,KAAA,EAAK,IAAA,EAC3B,QAAA,kBAAAC,IAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,qBAAA,EAEZ,QAAA,EAAA;AAAA,IAAA,YAAA,oBACCA,IAAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,SAAA,EAAU,4CAAA;AAAA,QACV,KAAA,EAAO,EAAE,WAAA,EAAa,OAAA,EAAQ;AAAA,QAE9B,QAAA,EAAA;AAAA,0BAAAD,GAAAA;AAAA,YAAC,KAAA;AAAA,YAAA;AAAA,cACC,GAAA,EAAK,YAAA;AAAA,cACL,GAAA,EAAK,IAAA,CAAK,cAAA,IAAkB,IAAA,CAAK,KAAA;AAAA,cACjC,IAAA,EAAI,IAAA;AAAA,cACJ,KAAA,EAAM,0DAAA;AAAA,cACN,SAAA,EAAU;AAAA;AAAA,WACZ;AAAA,0BAEAA,GAAAA;AAAA,YAAC,MAAA;AAAA,YAAA;AAAA,cACC,SAAA,EAAU,iGAAA;AAAA,cACV,KAAA,EAAO;AAAA,gBACL,UAAA,EAAY,iEAAA;AAAA,gBACZ,KAAA,EAAO,SAAA;AAAA,gBACP,cAAA,EAAgB;AAAA,eAClB;AAAA,cAEC,QAAA,EAAA,mBAAA,CAAoB,KAAK,QAAQ;AAAA;AAAA;AACpC;AAAA;AAAA,KACF;AAAA,oBAIFC,IAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,+BAAA,EACb,QAAA,EAAA;AAAA,sBAAAD,GAAAA;AAAA,QAAC,IAAA;AAAA,QAAA;AAAA,UACC,SAAA,EAAU,oCAAA;AAAA,UACV,KAAA,EAAO,EAAE,KAAA,EAAO,iCAAA,EAAkC;AAAA,UAEjD,QAAA,EAAA,IAAA,CAAK;AAAA;AAAA,OACR;AAAA,MAEC,IAAA,CAAK,4BACJA,GAAAA;AAAA,QAAC,GAAA;AAAA,QAAA;AAAA,UACC,SAAA,EAAU,sBAAA;AAAA,UACV,KAAA,EAAO,EAAE,KAAA,EAAO,kCAAA,EAAmC;AAAA,UAElD,QAAA,EAAA,IAAA,CAAK;AAAA;AAAA,OACR;AAAA,MAID,WAAW,MAAA,GAAS,CAAA,oBACnBA,GAAAA,CAAC,SAAI,SAAA,EAAU,8BAAA,EACZ,QAAA,EAAA,UAAA,CAAW,GAAA,CAAI,CAAC,GAAA,EAAK,CAAA,qBACpBC,IAAAA,CAAC,KAAA,EAAA,EAAY,WAAU,eAAA,EACrB,QAAA,EAAA;AAAA,wBAAAA,IAAAA;AAAA,UAAC,MAAA;AAAA,UAAA;AAAA,YACC,SAAA,EAAU,qBAAA;AAAA,YACV,KAAA,EAAO,EAAE,KAAA,EAAO,4BAAA,EAA6B;AAAA,YAE5C,QAAA,EAAA;AAAA,cAAA,GAAA,CAAI,MAAA,IAAU,EAAA;AAAA,cACd,GAAA,CAAI,MAAM,cAAA,EAAe;AAAA,cACzB,GAAA,CAAI;AAAA;AAAA;AAAA,SACP;AAAA,wBACAD,GAAAA;AAAA,UAAC,MAAA;AAAA,UAAA;AAAA,YACC,SAAA,EAAU,kDAAA;AAAA,YACV,KAAA,EAAO,EAAE,KAAA,EAAO,kCAAA,EAAmC;AAAA,YAElD,QAAA,EAAA,GAAA,CAAI;AAAA;AAAA;AACP,OAAA,EAAA,EAdQ,CAeV,CACD,CAAA,EACH,CAAA;AAAA,MAID,eAAe,MAAA,GAAS,CAAA,oBACvBC,IAAAA,CAAC,KAAA,EAAA,EAAI,WAAU,6BAAA,EACZ,QAAA,EAAA;AAAA,QAAA,cAAA,CAAe,GAAA,CAAI,CAAC,OAAA,qBACnBD,GAAAA;AAAA,UAAC,MAAA;AAAA,UAAA;AAAA,YAEC,SAAA,EAAU,oDAAA;AAAA,YACV,KAAA,EAAO;AAAA,cACL,UAAA,EAAY,2CAAA;AAAA,cACZ,KAAA,EAAO,mCAAA;AAAA,cACP,MAAA,EAAQ;AAAA,aACV;AAAA,YAEC,2BAAiB,OAAO;AAAA,WAAA;AAAA,UARpB;AAAA,SAUR,CAAA;AAAA,QACA,QAAA,CAAS,MAAA,GAAS,CAAA,oBACjBC,IAAAA;AAAA,UAAC,MAAA;AAAA,UAAA;AAAA,YACC,SAAA,EAAU,oDAAA;AAAA,YACV,KAAA,EAAO;AAAA,cACL,UAAA,EAAY,2CAAA;AAAA,cACZ,KAAA,EAAO;AAAA,aACT;AAAA,YACD,QAAA,EAAA;AAAA,cAAA,GAAA;AAAA,cACG,IAAA,CAAK,SAAS,MAAA,GAAS;AAAA;AAAA;AAAA;AAC3B,OAAA,EAEJ;AAAA,KAAA,EAEJ;AAAA,GAAA,EACF,GACF,CAAA,EACF,CAAA;AAEJ;AC3He,SAAR,cAA+B,EAAE,KAAA,EAAO,YAAY,EAAA,EAAI,QAAA,GAAW,SAAQ,EAAuB;AACvG,EAAA,uBACED,GAAAA,CAAC,YAAA,EAAA,EAAa,OAAA,EAAS,KACrB,QAAA,kBAAAA,GAAAA;AAAA,IAAC,KAAA;AAAA,IAAA;AAAA,MACC,SAAA,EAAW,kDAAkD,SAAS,CAAA,CAAA;AAAA,MACtE,KAAA,EAAO;AAAA,QACL,GAAA,EAAK;AAAA,OACP;AAAA,MAEC,QAAA,EAAA,KAAA,CAAM,GAAA,CAAI,CAAC,IAAA,qBACVA,GAAAA,CAAC,aAAA,EAAA,EAA4B,IAAA,EAAY,QAAA,EAAA,EAArB,IAAA,CAAK,EAAoC,CAC9D;AAAA;AAAA,GACH,EACF,CAAA;AAEJ;ACSA,SAAS,YAAA,CAAa;AAAA,EACpB,IAAA;AAAA,EACA,KAAA;AAAA,EACA;AACF,CAAA,EAIG;AACD,EAAA,uBACEA,GAAAA;AAAA,IAAC,GAAA;AAAA,IAAA;AAAA,MACC,IAAA;AAAA,MACA,cAAA,EAAc,SAAS,MAAA,GAAS,MAAA;AAAA,MAChC,SAAA,EAAU,gEAAA;AAAA,MACV,KAAA,EAAO;AAAA,QACL,UAAA,EAAY,SACR,iEAAA,GACA,2CAAA;AAAA,QACJ,KAAA,EAAO,SAAS,4BAAA,GAA+B,mCAAA;AAAA,QAC/C,MAAA,EAAQ,CAAA,UAAA,EACN,MAAA,GACI,iEAAA,GACA,yCACN,CAAA;AAAA,OACF;AAAA,MAEC,QAAA,EAAA;AAAA;AAAA,GACH;AAEJ;AAEA,SAAS,QAAA,CAAS,EAAE,IAAA,EAAM,KAAA,EAAO,UAAS,EAAuD;AAC/F,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,uBACEA,GAAAA;AAAA,MAAC,MAAA;AAAA,MAAA;AAAA,QACC,eAAA,EAAc,MAAA;AAAA,QACd,SAAA,EAAU,qDAAA;AAAA,QACV,KAAA,EAAO;AAAA,UACL,UAAA,EAAY,2CAAA;AAAA,UACZ,KAAA,EAAO,kCAAA;AAAA,UACP,MAAA,EAAQ;AAAA,SACV;AAAA,QAEC,QAAA,EAAA;AAAA;AAAA,KACH;AAAA,EAEJ;AACA,EAAA,uBACEA,GAAAA;AAAA,IAAC,GAAA;AAAA,IAAA;AAAA,MACC,IAAA;AAAA,MACA,SAAA,EAAU,4DAAA;AAAA,MACV,KAAA,EAAO;AAAA,QACL,UAAA,EAAY,2CAAA;AAAA,QACZ,KAAA,EAAO,mCAAA;AAAA,QACP,MAAA,EAAQ;AAAA,OACV;AAAA,MAEC,QAAA,EAAA;AAAA;AAAA,GACH;AAEJ;AAEA,eAAO,cAAA,CAAsC;AAAA,EAC3C,QAAA,GAAW,OAAA;AAAA,EACX,QAAA,GAAW,IAAA;AAAA,EACX,IAAA,GAAO,CAAA;AAAA,EACP,QAAA,GAAW,EAAA;AAAA,EACX,OAAA;AAAA,EACA,UAAA;AAAA,EACA,cAAA,GAAiB,IAAA;AAAA,EACjB,SAAA,GAAY;AACd,CAAA,EAAwB;AACtB,EAAA,MAAM,cAAc,IAAA,CAAK,GAAA,CAAI,GAAG,IAAA,CAAK,KAAA,CAAM,IAAI,CAAC,CAAA;AAChD,EAAA,MAAM,UAAA,GAAa,MAAM,sBAAA,EAAuB;AAGhD,EAAA,MAAM,cAAA,GAAiB,QAAA,GACnB,UAAA,CAAW,IAAA,CAAK,CAAC,CAAA,KAAM,eAAA,CAAgB,CAAC,CAAA,KAAM,QAAQ,CAAA,IAAK,IAAA,GAC3D,IAAA;AAEJ,EAAA,MAAM,EAAE,KAAA,EAAO,KAAA,EAAM,GAAI,MAAM,iBAAA,CAAkB;AAAA,IAC/C,UAAU,cAAA,IAAkB,MAAA;AAAA,IAC5B,KAAA,EAAO,QAAA;AAAA,IACP,MAAA,EAAA,CAAS,cAAc,CAAA,IAAK;AAAA,GAC7B,CAAA;AAED,EAAA,MAAM,UAAA,GAAa,KAAK,GAAA,CAAI,CAAA,EAAG,KAAK,IAAA,CAAK,KAAA,GAAQ,QAAQ,CAAC,CAAA;AAC1D,EAAA,MAAM,QAAA,GAAW,CAAC,CAAA,KAAc;AAC9B,IAAA,MAAM,IAAA,GAAO,iBACT,CAAA,EAAG,QAAQ,aAAa,eAAA,CAAgB,cAAc,CAAC,CAAA,CAAA,GACvD,QAAA;AACJ,IAAA,OAAO,KAAK,CAAA,GAAI,IAAA,GAAO,CAAA,EAAG,IAAI,SAAS,CAAC,CAAA,CAAA;AAAA,EAC1C,CAAA;AAEA,EAAA,uBACEA,GAAAA,CAAC,SAAA,EAAA,EAAQ,SAAA,EAAW,CAAA,OAAA,EAAU,SAAS,CAAA,CAAA,EACrC,QAAA,kBAAAC,IAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,wBAAA,EACZ,QAAA,EAAA;AAAA,IAAA,OAAA;AAAA,IAGA,cAAA,IAAkB,UAAA,CAAW,MAAA,GAAS,CAAA,oBACrCA,KAAC,KAAA,EAAA,EAAI,YAAA,EAAW,sBAAA,EAAuB,SAAA,EAAU,4BAAA,EAC/C,QAAA,EAAA;AAAA,sBAAAD,GAAAA,CAAC,gBAAa,IAAA,EAAM,QAAA,EAAU,OAAM,KAAA,EAAM,MAAA,EAAQ,CAAC,cAAA,EAAgB,CAAA;AAAA,MAClE,UAAA,CAAW,GAAA,CAAI,CAAC,CAAA,qBACfA,GAAAA;AAAA,QAAC,YAAA;AAAA,QAAA;AAAA,UAEC,MAAM,CAAA,EAAG,QAAQ,CAAA,UAAA,EAAa,eAAA,CAAgB,CAAC,CAAC,CAAA,CAAA;AAAA,UAChD,KAAA,EAAO,CAAA;AAAA,UACP,QAAQ,cAAA,KAAmB;AAAA,SAAA;AAAA,QAHtB;AAAA,OAKR;AAAA,KAAA,EACH,CAAA;AAAA,IAGD,KAAA,CAAM,MAAA,GAAS,CAAA,mBACdA,GAAAA,CAAC,iBAAc,KAAA,EAAc,QAAA,EAAoB,CAAA,GAEjD,UAAA,oBACEC,IAAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,SAAA,EAAU,gEAAA;AAAA,QACV,KAAA,EAAO;AAAA,UACL,UAAA,EAAY,2CAAA;AAAA,UACZ,MAAA,EAAQ;AAAA,SACV;AAAA,QAEA,QAAA,EAAA;AAAA,0BAAAD,GAAAA;AAAA,YAAC,MAAA;AAAA,YAAA;AAAA,cACC,SAAA,EAAU,uBAAA;AAAA,cACV,KAAA,EAAO,EAAE,KAAA,EAAO,iCAAA,EAAkC;AAAA,cACnD,QAAA,EAAA;AAAA;AAAA,WAED;AAAA,0BACAA,GAAAA,CAAC,MAAA,EAAA,EAAK,SAAA,EAAU,WAAU,KAAA,EAAO,EAAE,KAAA,EAAO,kCAAA,EAAmC,EAC1E,QAAA,EAAA,cAAA,GACG,CAAA,qBAAA,EAAwB,cAAc,UACtC,6CAAA,EACN;AAAA;AAAA;AAAA,KACF;AAAA,IAKH,UAAA,GAAa,qBACZC,IAAAA;AAAA,MAAC,KAAA;AAAA,MAAA;AAAA,QACC,YAAA,EAAW,iBAAA;AAAA,QACX,SAAA,EAAU,8CAAA;AAAA,QAEV,QAAA,EAAA;AAAA,0BAAAD,GAAAA;AAAA,YAAC,QAAA;AAAA,YAAA;AAAA,cACC,IAAA,EAAM,QAAA,CAAS,WAAA,GAAc,CAAC,CAAA;AAAA,cAC9B,KAAA,EAAM,iBAAA;AAAA,cACN,UAAU,WAAA,IAAe;AAAA;AAAA,WAC3B;AAAA,0BACAC,KAAC,MAAA,EAAA,EAAK,SAAA,EAAU,WAAU,KAAA,EAAO,EAAE,KAAA,EAAO,kCAAA,EAAmC,EAAG,QAAA,EAAA;AAAA,YAAA,OAAA;AAAA,YACxE,WAAA;AAAA,YAAY,MAAA;AAAA,YAAK;AAAA,WAAA,EACzB,CAAA;AAAA,0BACAD,GAAAA;AAAA,YAAC,QAAA;AAAA,YAAA;AAAA,cACC,IAAA,EAAM,QAAA,CAAS,WAAA,GAAc,CAAC,CAAA;AAAA,cAC9B,KAAA,EAAM,aAAA;AAAA,cACN,UAAU,WAAA,IAAe;AAAA;AAAA;AAC3B;AAAA;AAAA;AACF,GAAA,EAEJ,CAAA,EACF,CAAA;AAEJ;AChMA,IAAM,SAAA,GAAY;AAAA;AAAA;AAAA;AAAA,CAAA;AAMlB,SAAS,cAAA,GAAiB;AACxB,EAAA,uBACEA,GAAAA,CAAC,OAAA,EAAA,EAAM,MAAK,cAAA,EAAe,UAAA,EAAW,WACnC,QAAA,EAAA,SAAA,EACH,CAAA;AAEJ;AAGO,SAAS,sBAAA,CAAuB,EAAE,KAAA,GAAQ,CAAA,EAAE,EAAuB;AACxE,EAAA,uBACEC,IAAAA,CAAC,SAAA,EAAA,EAAQ,SAAA,EAAU,cAAA,EACjB,QAAA,EAAA;AAAA,oBAAAD,IAAC,cAAA,EAAA,EAAe,CAAA;AAAA,oBAChBC,IAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,wBAAA,EACb,QAAA,EAAA;AAAA,sBAAAD,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,2BAAA,EAA4B,CAAA;AAAA,sBAC3CA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,4BAAA,EAA6B,CAAA;AAAA,sBAC5CA,IAAC,KAAA,EAAA,EAAI,SAAA,EAAU,wDACZ,QAAA,EAAA,KAAA,CAAM,IAAA,CAAK,EAAE,MAAA,EAAQ,KAAA,EAAO,CAAA,CAAE,GAAA,CAAI,CAAC,CAAA,EAAG,CAAA,qBACrCC,IAAAA,CAAC,KAAA,EAAA,EAAY,WAAU,qBAAA,EACrB,QAAA,EAAA;AAAA,wBAAAD,GAAAA,CAAC,SAAI,SAAA,EAAU,oBAAA,EAAqB,OAAO,EAAE,WAAA,EAAa,SAAQ,EAAG,CAAA;AAAA,wBACrEA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,uBAAA,EAAwB,CAAA;AAAA,wBACvCA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,uBAAA,EAAwB;AAAA,OAAA,EAAA,EAH/B,CAIV,CACD,CAAA,EACH;AAAA,KAAA,EACF;AAAA,GAAA,EACF,CAAA;AAEJ;AAGO,SAAS,qBAAA,GAAwB;AACtC,EAAA,uBACEC,IAAAA,CAAC,SAAA,EAAA,EAAQ,SAAA,EAAU,QAAA,EACjB,QAAA,EAAA;AAAA,oBAAAD,IAAC,cAAA,EAAA,EAAe,CAAA;AAAA,oBAChBA,IAAC,KAAA,EAAA,EAAI,SAAA,EAAU,gCACb,QAAA,kBAAAC,IAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,oDAAA,EACb,QAAA,EAAA;AAAA,sBAAAA,IAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,qBAAA,EACb,QAAA,EAAA;AAAA,wBAAAD,GAAAA,CAAC,SAAI,SAAA,EAAU,sBAAA,EAAuB,OAAO,EAAE,YAAA,EAAc,UAAS,EAAG,CAAA;AAAA,wBACzEA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,yBAAA,EAA0B,CAAA;AAAA,wBACzCA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,wBAAA,EAAyB,CAAA;AAAA,wBACxCA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,wBAAA,EAAyB,CAAA;AAAA,wBACxCA,GAAAA,CAAC,KAAA,EAAA,EAAI,SAAA,EAAU,uBAAA,EAAwB;AAAA,OAAA,EACzC,CAAA;AAAA,sBACAA,IAAC,KAAA,EAAA,EAAI,SAAA,EAAU,sBAAqB,KAAA,EAAO,EAAE,WAAA,EAAa,MAAA,EAAO,EAAG;AAAA,KAAA,EACtE,CAAA,EACF;AAAA,GAAA,EACF,CAAA;AAEJ;;;AC2PO,IAAM,uBAAA,GAA0B;AAAA,EACrC,eAAA;AAAA,EACA,qBAAA;AAAA,EACA,mBAAA;AAAA,EACA,kBAAA;AAAA,EACA,oBAAA;AAAA,EACA,mBAAA;AAAA,EACA,sBAAA;AAAA,EACA,kBAAA;AAAA,EACA,gBAAA;AAAA,EACA,eAAA;AAAA,EACA,2BAAA;AAAA,EACA,sBAAA;AAAA,EACA,0BAAA;AAAA,EACA,2BAAA;AAAA,EACA,sBAAA;AAAA,EACA,0BAAA;AAAA,EACA,2BAAA;AAAA,EACA,uBAAA;AAAA,EACA,kBAAA;AAAA,EACA,cAAA;AAAA,EACA;AACF","file":"chunk-7FFZX7C5.js","sourcesContent":["/**\n * PortfolioPage — Server component that renders a full portfolio case study.\n *\n * Maps each section in item.sections to the correct renderer component.\n * All sections below the hero are loaded lazily via next/dynamic.\n *\n * NOTE: This file is intentionally NOT a client component — it is designed\n * to be used as an RSC in a Next.js app directory page.\n *\n * Sections MUST be imported via `@sonordev/agency-site-kit/portfolio/sections/*`\n * (self-referencing subpaths, external in tsup's server build), never via\n * relative paths: a relative import would bundle the client sections into\n * this server chunk, the 'use client' boundary would be lost, and the whole\n * PortfolioItemFull would serialize into the RSC flight payload again.\n *\n * Whether a section is a server or client component is decided by the\n * section file itself (its 'use client' directive; tsup routes it to the\n * matching build). Server sections are imported statically — they ship zero\n * client JS, so there is nothing to lazy-load. Client sections load through\n * next/dynamic so each becomes its own browser chunk, fetched only when the\n * case study actually renders that section type.\n */\n\nimport React from 'react';\nimport dynamic from 'next/dynamic';\nimport type {\n PortfolioItemFull,\n PortfolioSection,\n PortfolioHeroData,\n PortfolioChallengesData,\n PortfolioStrategyData,\n PortfolioResultsData,\n PortfolioTechStackData,\n PortfolioServicesData,\n PortfolioTestimonialData,\n PortfolioGalleryData,\n PortfolioVideoData,\n PortfolioTeamData,\n PortfolioFeatureSpotlightData,\n PortfolioBeforeAfterData,\n PortfolioMetricsTimelineData,\n PortfolioConversionFunnelData,\n PortfolioDetailsData,\n PortfolioCTAData,\n} from '../../types';\n\n// Hero is loaded eagerly (above the fold, client — it animates)\nimport HeroSection from '@sonordev/agency-site-kit/portfolio/sections/HeroSection';\n\n// Server sections — no 'use client' in their files, so they render on the\n// server and ship ZERO component JS to the browser. Static imports: there is\n// no client chunk to defer.\nimport ChallengesSection from '@sonordev/agency-site-kit/portfolio/sections/ChallengesSection';\nimport ResultsSection from '@sonordev/agency-site-kit/portfolio/sections/ResultsSection';\nimport TechStackSection from '@sonordev/agency-site-kit/portfolio/sections/TechStackSection';\nimport ServicesSection from '@sonordev/agency-site-kit/portfolio/sections/ServicesSection';\nimport TestimonialSection from '@sonordev/agency-site-kit/portfolio/sections/TestimonialSection';\nimport SpeedComparisonSection from '@sonordev/agency-site-kit/portfolio/sections/SpeedComparisonSection';\nimport DesignSystemSection from '@sonordev/agency-site-kit/portfolio/sections/DesignSystemSection';\nimport DetailsSection from '@sonordev/agency-site-kit/portfolio/sections/DetailsSection';\n\n// Client sections — animated/interactive; lazily loaded so each becomes its\n// own browser chunk, fetched only when rendered.\nconst StrategySection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/StrategySection'), { ssr: true });\nconst GallerySection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/GallerySection'), { ssr: true });\nconst VideoSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/VideoSection'), { ssr: true });\nconst TeamSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/TeamSection'), { ssr: true });\nconst FeatureSpotlightSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/FeatureSpotlightSection'), { ssr: true });\nconst BeforeAfterSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/BeforeAfterSection'), { ssr: true });\nconst MetricsTimelineSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/MetricsTimelineSection'), { ssr: true });\nconst ConversionFunnelSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/ConversionFunnelSection'), { ssr: true });\nconst PerformanceSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/PerformanceSection'), { ssr: true });\nconst SiteArchitectureSection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/SiteArchitectureSection'), { ssr: true });\nconst CTASection = dynamic(() => import('@sonordev/agency-site-kit/portfolio/sections/CTASection'), { ssr: true });\n\ninterface PortfolioPageProps {\n item: PortfolioItemFull;\n}\n\n// Defense-in-depth only — the enforcing sanitize pass runs in\n// getPortfolioItem (portfolio/server.ts), the last genuinely-server-side hop.\n// See src/portfolio/sanitize.ts for the single source of truth and the story.\nimport { sanitizeSiteArchitectureData } from '../sanitize';\n\n// Display policy for proof points (Lighthouse dedupe, results curation) —\n// see src/portfolio/curate-proof.ts for the single source of truth.\nimport { curatePortfolioProof } from '../curate-proof';\n\n// Engagement-tracking island (client) — crosses the boundary via subpath.\nimport PortfolioAnalytics from '@sonordev/agency-site-kit/portfolio/components/PortfolioAnalytics';\n\nfunction renderSection(section: PortfolioSection, item: PortfolioItemFull, index: number) {\n const key = `${section.sectionType}-${index}`;\n\n switch (section.sectionType) {\n case 'portfolioHero':\n return (\n <HeroSection\n key={key}\n data={section.data as PortfolioHeroData}\n screenshots={item.hero_screenshots ?? undefined}\n liveUrl={item.live_url ?? undefined}\n />\n );\n case 'portfolioChallenges':\n return <ChallengesSection key={key} data={section.data as PortfolioChallengesData} />;\n case 'portfolioStrategy':\n return <StrategySection key={key} data={section.data as PortfolioStrategyData} />;\n case 'portfolioResults':\n return (\n <ResultsSection\n key={key}\n data={section.data as PortfolioResultsData}\n metricsDelta={item.metricsDelta}\n metricsRefreshedAt={item.metrics_last_refreshed_at}\n />\n );\n case 'portfolioTechStack':\n return <TechStackSection key={key} data={section.data as PortfolioTechStackData} />;\n case 'portfolioServices':\n return <ServicesSection key={key} data={section.data as PortfolioServicesData} />;\n case 'portfolioTestimonial':\n return <TestimonialSection key={key} data={section.data as PortfolioTestimonialData} />;\n case 'portfolioGallery':\n return <GallerySection key={key} data={section.data as PortfolioGalleryData} />;\n case 'portfolioVideo':\n return <VideoSection key={key} data={section.data as PortfolioVideoData} />;\n case 'portfolioTeam':\n return <TeamSection key={key} data={section.data as PortfolioTeamData} />;\n case 'portfolioFeatureSpotlight':\n return <FeatureSpotlightSection key={key} data={section.data as PortfolioFeatureSpotlightData} />;\n case 'portfolioBeforeAfter':\n return <BeforeAfterSection key={key} data={section.data as PortfolioBeforeAfterData} />;\n case 'portfolioMetricsTimeline':\n return <MetricsTimelineSection key={key} data={section.data as PortfolioMetricsTimelineData} />;\n case 'portfolioConversionFunnel':\n return <ConversionFunnelSection key={key} data={section.data as PortfolioConversionFunnelData} />;\n case 'portfolioPerformance':\n return <PerformanceSection key={key} data={section.data as any} />;\n case 'portfolioSpeedComparison':\n return <SpeedComparisonSection key={key} data={section.data as any} />;\n case 'portfolioSiteArchitecture':\n return <SiteArchitectureSection key={key} data={sanitizeSiteArchitectureData(section.data) as any} />;\n case 'portfolioDesignSystem':\n return <DesignSystemSection key={key} data={section.data as any} />;\n case 'portfolioDetails':\n return <DetailsSection key={key} data={section.data as PortfolioDetailsData} />;\n case 'portfolioCTA':\n return <CTASection key={key} data={section.data as PortfolioCTAData} />;\n case 'portfolioSeo':\n // SEO section is metadata-only; nothing to render visually.\n return null;\n default:\n return null;\n }\n}\n\n// Canonical display order — ensures consistent rendering regardless of API\n// order. Keys must match PortfolioSectionType exactly (note 'portfolioCTA' —\n// a lowercased 'portfolioCta' key silently fell through to the ?? 99 fallback\n// for years). Unknown/future types sort last via the fallback.\nconst SECTION_ORDER: Record<string, number> = {\n portfolioHero: 0,\n portfolioChallenges: 1,\n portfolioStrategy: 2,\n portfolioResults: 3,\n portfolioPerformance: 4,\n portfolioSpeedComparison: 5,\n portfolioSiteArchitecture: 6,\n portfolioTechStack: 7,\n portfolioDesignSystem: 8,\n portfolioServices: 9,\n portfolioBeforeAfter: 10,\n portfolioTestimonial: 11,\n portfolioMetricsTimeline: 12,\n portfolioConversionFunnel: 13,\n portfolioDetails: 14,\n portfolioSeo: 15,\n portfolioCTA: 16,\n};\n\nexport default function PortfolioPage({ item: rawItem }: PortfolioPageProps) {\n const item = curatePortfolioProof(rawItem);\n\n // Section order: once an agency has deliberately reordered in the\n // dashboard (item.sectionsOrdered), the array order IS the narrative —\n // respect it, with the hero pinned first (it's the LCP shell). Items\n // never reordered keep the canonical order below, so nothing shifts\n // out from under existing case studies.\n const sortedSections = item.sectionsOrdered\n ? [...item.sections].sort(\n (a, b) =>\n (a.sectionType === 'portfolioHero' ? 0 : 1) -\n (b.sectionType === 'portfolioHero' ? 0 : 1),\n )\n : [...item.sections].sort(\n (a, b) => (SECTION_ORDER[a.sectionType] ?? 99) - (SECTION_ORDER[b.sectionType] ?? 99)\n );\n\n return (\n <article className=\"flex w-full min-w-0 max-w-full flex-col overflow-x-clip\">\n {sortedSections.map((section, index) => {\n const rendered = renderSection(section, item, index);\n if (!rendered) return null;\n return (\n <div\n key={`wrapper-${section.sectionType}-${index}`}\n className=\"w-full\"\n data-portfolio-section={section.sectionType}\n >\n {rendered}\n </div>\n );\n })}\n {/* Engagement tracking — childless idle-deferred sibling, never a wrapper */}\n <PortfolioAnalytics slug={item.slug} />\n </article>\n );\n}\n","/**\n * PortfolioSchema — JSON-LD structured data for a case study page.\n *\n * Server component: renders a single <script type=\"application/ld+json\">\n * describing the case study as a schema.org Article (there is no dedicated\n * CaseStudy type; Article is what Google documents for editorial content).\n * Ships zero client JS and adds rich-result eligibility + entity clarity for\n * the agency's portfolio pages.\n *\n * Usage (in the case study page, next to <PortfolioPage />):\n *\n * ```tsx\n * <PortfolioSchema\n * item={item}\n * url={`https://youragency.com/work/${item.slug}`}\n * publisher={{ name: 'Your Agency', url: 'https://youragency.com' }}\n * />\n * ```\n *\n * Only reads fields from the already-sanitized PortfolioItemFull (see\n * getPortfolioItem / sanitize.ts) — never section internals.\n */\n\nimport React from 'react';\nimport type { PortfolioItemFull } from '../../types';\n\nexport interface PortfolioSchemaProps {\n item: PortfolioItemFull;\n /** Canonical URL of this case study page. */\n url: string;\n /** The agency, emitted as author + publisher Organization. */\n publisher?: {\n name: string;\n url?: string;\n /** Absolute URL to a logo image. */\n logo?: string;\n };\n}\n\n/** JSON-LD-safe serialization — escape `<` so `</script>` can never break out. */\nfunction jsonLd(value: unknown): string {\n return JSON.stringify(value).replace(/</g, '\\\\u003c');\n}\n\nexport default function PortfolioSchema({ item, url, publisher }: PortfolioSchemaProps) {\n const headline = item.seo?.metaTitle || item.title;\n const description = item.seo?.metaDescription || item.description;\n\n const keywords = [\n ...(item.seo?.keywords ?? []),\n item.category,\n ...item.services,\n ].filter(Boolean);\n\n const org = publisher\n ? {\n '@type': 'Organization',\n name: publisher.name,\n ...(publisher.url ? { url: publisher.url } : {}),\n ...(publisher.logo\n ? { logo: { '@type': 'ImageObject', url: publisher.logo } }\n : {}),\n }\n : undefined;\n\n const schema = {\n '@context': 'https://schema.org',\n '@type': 'Article',\n headline,\n ...(description ? { description } : {}),\n ...(item.hero_image ? { image: [item.hero_image] } : {}),\n url,\n mainEntityOfPage: { '@type': 'WebPage', '@id': url },\n ...(item.published_at ? { datePublished: item.published_at } : {}),\n ...(item.metrics_last_refreshed_at || item.published_at\n ? { dateModified: item.metrics_last_refreshed_at || item.published_at }\n : {}),\n ...(keywords.length ? { keywords: keywords.join(', ') } : {}),\n ...(org ? { author: org, publisher: org } : {}),\n ...(item.details?.industry ? { about: item.details.industry } : {}),\n };\n\n return (\n <script\n type=\"application/ld+json\"\n dangerouslySetInnerHTML={{ __html: jsonLd(schema) }}\n />\n );\n}\n","// Shared component — no directive on purpose: hook-free, so it renders\n// server-side (zero JS) from PortfolioIndex and stays usable from client\n// code via the /portfolio/client barrel.\nimport React from 'react';\nimport Image from 'next/image';\nimport Link from 'next/link';\nimport type { PortfolioItem } from '../../types';\nimport GlassCard from '@sonordev/agency-site-kit/portfolio/primitives/GlassCard';\nimport { curateHeroKpis } from '../curate-proof';\nimport { formatCategoryLabel, formatServiceTag } from '../format';\n\ninterface PortfolioCardProps {\n item: PortfolioItem;\n className?: string;\n /** Base path for portfolio detail links (default: '/work') */\n basePath?: string;\n}\n\nexport default function PortfolioCard({ item, className = '', basePath = '/work' }: PortfolioCardProps) {\n // Use hero_screenshots desktop → hero_image → trifolio screenshot convention\n const thumbnailSrc =\n item.hero_screenshots?.desktop ||\n item.hero_image ||\n (item.project_id ? `/portfolio-previews/${item.slug}.png` : undefined);\n const kpis = Array.isArray(item.kpis) ? item.kpis : [];\n const services = Array.isArray(item.services) ? item.services : [];\n // Collapse per-category Lighthouse KPIs so a card never reads\n // \"100/100 · 100/100 · 100/100\" — business outcomes lead the preview.\n const kpiPreview = curateHeroKpis(kpis).slice(0, 3);\n const servicePreview = services.slice(0, 3);\n\n return (\n <Link href={`${basePath}/${item.slug}`} className={`block group ${className}`}>\n <GlassCard padding=\"sm\" hover>\n <div className=\"flex flex-col gap-4\">\n {/* Thumbnail */}\n {thumbnailSrc && (\n <div\n className=\"relative w-full overflow-hidden rounded-xl\"\n style={{ aspectRatio: '16/10' }}\n >\n <Image\n src={thumbnailSrc}\n alt={item.hero_image_alt || item.title}\n fill\n sizes=\"(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw\"\n className=\"object-cover object-top transition-transform duration-500 group-hover:scale-105\"\n />\n {/* Category badge */}\n <span\n className=\"absolute top-3 left-3 px-3 py-1 rounded-full text-[11px] uppercase tracking-wider font-semibold\"\n style={{\n background: 'color-mix(in srgb, var(--sk-primary, #6366f1) 80%, transparent)',\n color: '#ffffff',\n backdropFilter: 'blur(8px)',\n }}\n >\n {formatCategoryLabel(item.category)}\n </span>\n </div>\n )}\n\n {/* Text content */}\n <div className=\"flex flex-col gap-2 px-2 pb-2\">\n <h3\n className=\"text-lg font-semibold line-clamp-1\"\n style={{ color: 'var(--sk-text-primary, #ffffff)' }}\n >\n {item.title}\n </h3>\n\n {item.subtitle && (\n <p\n className=\"text-sm line-clamp-2\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {item.subtitle}\n </p>\n )}\n\n {/* KPI preview */}\n {kpiPreview.length > 0 && (\n <div className=\"flex items-center gap-4 mt-1\">\n {kpiPreview.map((kpi, i) => (\n <div key={i} className=\"flex flex-col\">\n <span\n className=\"text-base font-bold\"\n style={{ color: 'var(--sk-primary, #6366f1)' }}\n >\n {kpi.prefix || ''}\n {kpi.value.toLocaleString()}\n {kpi.suffix}\n </span>\n <span\n className=\"text-[10px] uppercase tracking-wider font-medium\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {kpi.label}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Services tags */}\n {servicePreview.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5 mt-2\">\n {servicePreview.map((service) => (\n <span\n key={service}\n className=\"px-2.5 py-0.5 rounded-full text-[11px] font-medium\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-secondary, #a1a1aa)',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n {formatServiceTag(service)}\n </span>\n ))}\n {services.length > 3 && (\n <span\n className=\"px-2.5 py-0.5 rounded-full text-[11px] font-medium\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-tertiary, #71717a)',\n }}\n >\n +{item.services.length - 3}\n </span>\n )}\n </div>\n )}\n </div>\n </div>\n </GlassCard>\n </Link>\n );\n}\n","// Shared component — no directive on purpose: hook-free, so it renders\n// server-side (zero JS) from PortfolioIndex and stays usable from client\n// code via the /portfolio/client barrel.\nimport React from 'react';\nimport type { PortfolioItem } from '../../types';\nimport ScrollReveal from '@sonordev/agency-site-kit/portfolio/primitives/ScrollReveal';\nimport PortfolioCard from './PortfolioCard';\n\ninterface PortfolioGridProps {\n items: PortfolioItem[];\n className?: string;\n /** Base path for portfolio detail links (default: '/work') */\n basePath?: string;\n}\n\nexport default function PortfolioGrid({ items, className = '', basePath = '/work' }: PortfolioGridProps) {\n return (\n <ScrollReveal stagger={0.1}>\n <div\n className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 ${className}`}\n style={{\n gap: 'var(--sk-grid-gap, 24px)',\n }}\n >\n {items.map((item) => (\n <PortfolioCard key={item.id} item={item} basePath={basePath} />\n ))}\n </div>\n </ScrollReveal>\n );\n}\n","/**\n * PortfolioIndex — turnkey, server-rendered /work index.\n *\n * Fetches items + categories and renders: category nav (real links —\n * crawlable, no client state), the portfolio grid, pagination, and an\n * intentional empty state. Ships zero component JS.\n *\n * Category filtering is route-based for SEO:\n * /work → all items\n * /work/category/[category] → one category (slug via slugifyCategory)\n * Pagination via ?page= on either route.\n */\n\nimport React from 'react';\nimport {\n getPortfolioCategories,\n getPortfolioItems,\n slugifyCategory,\n} from '../server';\nimport PortfolioGrid from './PortfolioGrid';\n\nexport interface PortfolioIndexProps {\n /** Route prefix for case studies + category links. Defaults to \"/work\". */\n basePath?: string;\n /** Active category SLUG (from the [category] route param). */\n category?: string | null;\n /** 1-based page number (from ?page=). */\n page?: number;\n /** Items per page. Defaults to 12. */\n pageSize?: number;\n /** Optional heading block rendered above the category nav. */\n heading?: React.ReactNode;\n /** Override the empty state. */\n emptyState?: React.ReactNode;\n /** Hide the category nav (e.g. single-category portfolios). */\n showCategories?: boolean;\n className?: string;\n}\n\nfunction CategoryLink({\n href,\n label,\n active,\n}: {\n href: string;\n label: string;\n active: boolean;\n}) {\n return (\n <a\n href={href}\n aria-current={active ? 'page' : undefined}\n className=\"px-4 py-1.5 rounded-full text-sm font-medium transition-colors\"\n style={{\n background: active\n ? 'color-mix(in srgb, var(--sk-primary, #6366f1) 15%, transparent)'\n : 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: active ? 'var(--sk-primary, #6366f1)' : 'var(--sk-text-secondary, #a1a1aa)',\n border: `1px solid ${\n active\n ? 'color-mix(in srgb, var(--sk-primary, #6366f1) 25%, transparent)'\n : 'var(--sk-border, rgba(255,255,255,0.1))'\n }`,\n }}\n >\n {label}\n </a>\n );\n}\n\nfunction PageLink({ href, label, disabled }: { href: string; label: string; disabled: boolean }) {\n if (disabled) {\n return (\n <span\n aria-disabled=\"true\"\n className=\"px-4 py-2 rounded-xl text-sm font-medium opacity-40\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-tertiary, #71717a)',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n {label}\n </span>\n );\n }\n return (\n <a\n href={href}\n className=\"px-4 py-2 rounded-xl text-sm font-medium transition-colors\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-secondary, #a1a1aa)',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n {label}\n </a>\n );\n}\n\nexport default async function PortfolioIndex({\n basePath = '/work',\n category = null,\n page = 1,\n pageSize = 12,\n heading,\n emptyState,\n showCategories = true,\n className = '',\n}: PortfolioIndexProps) {\n const currentPage = Math.max(1, Math.floor(page));\n const categories = await getPortfolioCategories();\n\n // Route param is a slug; the API filter wants the original category value.\n const activeCategory = category\n ? categories.find((c) => slugifyCategory(c) === category) ?? null\n : null;\n\n const { items, total } = await getPortfolioItems({\n category: activeCategory ?? undefined,\n limit: pageSize,\n offset: (currentPage - 1) * pageSize,\n });\n\n const totalPages = Math.max(1, Math.ceil(total / pageSize));\n const pageHref = (p: number) => {\n const base = activeCategory\n ? `${basePath}/category/${slugifyCategory(activeCategory)}`\n : basePath;\n return p <= 1 ? base : `${base}?page=${p}`;\n };\n\n return (\n <section className={`w-full ${className}`}>\n <div className=\"max-w-7xl mx-auto px-6\">\n {heading}\n\n {/* Category nav — real links, crawlable */}\n {showCategories && categories.length > 1 && (\n <nav aria-label=\"Portfolio categories\" className=\"flex flex-wrap gap-2 mb-10\">\n <CategoryLink href={basePath} label=\"All\" active={!activeCategory} />\n {categories.map((c) => (\n <CategoryLink\n key={c}\n href={`${basePath}/category/${slugifyCategory(c)}`}\n label={c}\n active={activeCategory === c}\n />\n ))}\n </nav>\n )}\n\n {items.length > 0 ? (\n <PortfolioGrid items={items} basePath={basePath} />\n ) : (\n emptyState ?? (\n <div\n className=\"flex flex-col items-center gap-3 py-24 rounded-2xl text-center\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.03))',\n border: '1px dashed var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n <span\n className=\"text-lg font-semibold\"\n style={{ color: 'var(--sk-text-primary, #ffffff)' }}\n >\n Case studies coming soon\n </span>\n <span className=\"text-sm\" style={{ color: 'var(--sk-text-tertiary, #71717a)' }}>\n {activeCategory\n ? `Nothing published in ${activeCategory} yet.`\n : \"We're writing up our latest work right now.\"}\n </span>\n </div>\n )\n )}\n\n {/* Pagination */}\n {totalPages > 1 && (\n <nav\n aria-label=\"Portfolio pages\"\n className=\"flex items-center justify-center gap-3 mt-12\"\n >\n <PageLink\n href={pageHref(currentPage - 1)}\n label=\"← Previous\"\n disabled={currentPage <= 1}\n />\n <span className=\"text-sm\" style={{ color: 'var(--sk-text-tertiary, #71717a)' }}>\n Page {currentPage} of {totalPages}\n </span>\n <PageLink\n href={pageHref(currentPage + 1)}\n label=\"Next →\"\n disabled={currentPage >= totalPages}\n />\n </nav>\n )}\n </div>\n </section>\n );\n}\n","/**\n * Loading skeletons for portfolio routes — pure CSS pulse, server-safe,\n * zero JS. Drop into `loading.tsx`:\n *\n * ```tsx\n * export { PortfolioIndexSkeleton as default } from '@sonordev/agency-site-kit/portfolio';\n * ```\n */\n\nimport React from 'react';\n\nconst PULSE_CSS = `\n.sk-skeleton{animation:sk-skeleton-pulse 1.6s ease-in-out infinite;background:var(--sk-surface,rgba(255,255,255,0.06));border:1px solid var(--sk-border,rgba(255,255,255,0.08));border-radius:16px}\n@keyframes sk-skeleton-pulse{0%,100%{opacity:.55}50%{opacity:1}}\n@media (prefers-reduced-motion:reduce){.sk-skeleton{animation:none}}\n`;\n\nfunction SkeletonStyles() {\n return (\n <style href=\"sk-skeletons\" precedence=\"default\">\n {PULSE_CSS}\n </style>\n );\n}\n\n/** Grid of pulsing card placeholders for the /work index. */\nexport function PortfolioIndexSkeleton({ cards = 6 }: { cards?: number }) {\n return (\n <section className=\"w-full py-16\">\n <SkeletonStyles />\n <div className=\"max-w-7xl mx-auto px-6\">\n <div className=\"sk-skeleton h-9 w-56 mb-4\" />\n <div className=\"sk-skeleton h-5 w-80 mb-10\" />\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {Array.from({ length: cards }).map((_, i) => (\n <div key={i} className=\"flex flex-col gap-3\">\n <div className=\"sk-skeleton w-full\" style={{ aspectRatio: '16/10' }} />\n <div className=\"sk-skeleton h-5 w-3/4\" />\n <div className=\"sk-skeleton h-4 w-1/2\" />\n </div>\n ))}\n </div>\n </div>\n </section>\n );\n}\n\n/** Hero-shaped placeholder for /work/[slug] case study pages. */\nexport function PortfolioPageSkeleton() {\n return (\n <article className=\"w-full\">\n <SkeletonStyles />\n <div className=\"max-w-7xl mx-auto px-6 py-24\">\n <div className=\"grid grid-cols-1 lg:grid-cols-2 gap-12 items-start\">\n <div className=\"flex flex-col gap-5\">\n <div className=\"sk-skeleton h-7 w-32\" style={{ borderRadius: '9999px' }} />\n <div className=\"sk-skeleton h-12 w-full\" />\n <div className=\"sk-skeleton h-12 w-4/5\" />\n <div className=\"sk-skeleton h-5 w-full\" />\n <div className=\"sk-skeleton h-5 w-2/3\" />\n </div>\n <div className=\"sk-skeleton w-full\" style={{ aspectRatio: '11/5' }} />\n </div>\n </div>\n </article>\n );\n}\n","/**\n * @sonordev/agency-site-kit — Type definitions\n *\n * These types mirror the Portal API's cms/types.ts definitions for portfolio\n * content. Keep them in sync when modifying the API response shapes.\n */\n\n// ---------------------------------------------------------------------------\n// Brand Configuration\n// ---------------------------------------------------------------------------\n\nexport interface BrandConfig {\n /** Primary brand color (hex) */\n primary: string;\n /** Secondary/accent color (hex) */\n secondary: string;\n /** Background color (hex) */\n background: string;\n /** Elevated background — cards, modals (hex) */\n backgroundElevated: string;\n /** Surface color — interactive containers (hex) */\n surface: string;\n /** Surface hover state (hex) */\n surfaceHover: string;\n /** Surface border color (hex) */\n surfaceBorder: string;\n /** Primary text color (hex) */\n textPrimary: string;\n /** Secondary text color (hex) */\n textSecondary: string;\n /** Tertiary/muted text color (hex) */\n textTertiary: string;\n /** Heading font family */\n fontHeading: string;\n /** Body font family */\n fontBody: string;\n /** Logo URL (absolute or relative) */\n logoUrl?: string;\n /** Logo alt text */\n logoAlt?: string;\n /** Dark mode overrides — partial, merged with base values */\n darkMode?: Partial<Omit<BrandConfig, 'fontHeading' | 'fontBody' | 'logoUrl' | 'logoAlt' | 'darkMode'>>;\n /** Border radius tokens */\n radius?: {\n sm: string;\n md: string;\n lg: string;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Sanity Image Reference\n// ---------------------------------------------------------------------------\n\nexport interface SanityImageRef {\n _type: 'image';\n asset: { _type: 'reference'; _ref: string };\n hotspot?: { x: number; y: number; height: number; width: number };\n crop?: { top: number; bottom: number; left: number; right: number };\n alt?: string;\n}\n\n// ---------------------------------------------------------------------------\n// Metrics\n// ---------------------------------------------------------------------------\n\nexport type MetricSource = 'measured' | 'estimated';\n\nexport interface PortfolioKPI {\n label: string;\n value: number;\n suffix: string;\n prefix?: string;\n description: string;\n source: MetricSource;\n}\n\nexport interface MetricsDelta {\n metric: string;\n baseline: number;\n current: number;\n delta: number;\n deltaPercent: number;\n direction: 'up' | 'down' | 'flat';\n timespan: string;\n}\n\nexport interface MetricsSnapshot {\n seo: {\n clicks_28d?: number;\n impressions_28d?: number;\n avg_position?: number;\n ctr?: number;\n };\n cwv: {\n lcp_ms?: number;\n inp_ms?: number;\n cls?: number;\n performance_score?: number;\n };\n analytics: {\n sessions_28d?: number;\n page_views_28d?: number;\n conversion_rate?: number;\n bounce_rate?: number;\n };\n pages: {\n total_indexed?: number;\n total_crawled?: number;\n total_pages?: number;\n };\n}\n\n// ---------------------------------------------------------------------------\n// Portfolio Section Data Types\n// ---------------------------------------------------------------------------\n\nexport interface PortfolioHeroData {\n headline: string;\n subheadline: string;\n description: string;\n category: string;\n services: string[];\n liveUrl?: string;\n screenshots: {\n desktop?: string;\n tablet?: string;\n mobile?: string;\n };\n kpis: PortfolioKPI[];\n}\n\nexport interface PortfolioChallengeItem {\n title: string;\n description: string;\n solution: string;\n result?: string;\n icon?: string;\n}\n\nexport interface PortfolioChallengesData {\n items: PortfolioChallengeItem[];\n}\n\nexport interface PortfolioStrategyPhase {\n number: number;\n title: string;\n description: string;\n deliverables?: string[];\n icon?: string;\n timeline?: string;\n}\n\nexport interface PortfolioStrategyData {\n phases: PortfolioStrategyPhase[];\n}\n\nexport interface PortfolioResultItem {\n title: string;\n description: string;\n metric?: {\n value: number;\n suffix: string;\n prefix?: string;\n };\n source: MetricSource;\n icon?: string;\n}\n\nexport interface PortfolioResultsData {\n items: PortfolioResultItem[];\n}\n\nexport interface PortfolioTechItem {\n name: string;\n category: string;\n icon?: string;\n description?: string;\n}\n\nexport interface PortfolioTechStackData {\n technologies: PortfolioTechItem[];\n}\n\nexport interface PortfolioServiceItem {\n title: string;\n description: string;\n features: string[];\n icon?: string;\n}\n\nexport interface PortfolioServicesData {\n items: PortfolioServiceItem[];\n}\n\nexport interface PortfolioTestimonialData {\n quote: string;\n author: string;\n title: string;\n company: string;\n avatar?: SanityImageRef;\n rating?: number;\n}\n\nexport interface PortfolioGalleryImage {\n image: SanityImageRef;\n caption?: string;\n type?: 'screenshot' | 'before' | 'after' | 'design';\n}\n\nexport interface PortfolioGalleryData {\n images: PortfolioGalleryImage[];\n layout?: 'grid' | 'masonry';\n}\n\nexport interface PortfolioVideoData {\n url?: string;\n embedUrl?: string;\n thumbnail: SanityImageRef;\n title: string;\n duration?: string;\n platform?: 'youtube' | 'vimeo' | 'custom';\n}\n\nexport interface PortfolioTeamMember {\n name: string;\n role: string;\n avatar?: SanityImageRef;\n}\n\nexport interface PortfolioTeamData {\n members: PortfolioTeamMember[];\n}\n\nexport interface PortfolioAnnotation {\n x: number;\n y: number;\n label: string;\n description: string;\n icon?: string;\n}\n\nexport interface PortfolioFeatureSpotlightData {\n image: SanityImageRef;\n title: string;\n description: string;\n layout: 'left' | 'right' | 'full';\n annotations: PortfolioAnnotation[];\n}\n\nexport interface PortfolioBeforeAfterData {\n before: SanityImageRef;\n after: SanityImageRef;\n title?: string;\n description?: string;\n defaultPosition?: number;\n}\n\nexport interface PortfolioMetricsDataPoint {\n month: string;\n label: string;\n metrics: {\n traffic?: number;\n conversions?: number;\n revenue?: number;\n rankings?: number;\n };\n}\n\nexport interface PortfolioMetricsTimelineData {\n dataPoints: PortfolioMetricsDataPoint[];\n baselineDate: string;\n title?: string;\n description?: string;\n}\n\nexport interface PortfolioFunnelStage {\n label: string;\n value: number;\n icon?: string;\n color?: string;\n}\n\nexport interface PortfolioConversionFunnelData {\n stages: PortfolioFunnelStage[];\n title?: string;\n description?: string;\n}\n\nexport interface PortfolioDetailsData {\n industry: string;\n location?: string;\n website?: string;\n timeline?: string;\n launchDate?: string;\n budgetRange?: string;\n}\n\nexport interface PortfolioSeoData {\n metaTitle: string;\n metaDescription: string;\n keywords: string[];\n ogImage?: SanityImageRef;\n}\n\nexport interface PortfolioCTAData {\n headline: string;\n description: string;\n buttonText: string;\n buttonUrl: string;\n style?: 'primary' | 'glass';\n}\n\n// ---------------------------------------------------------------------------\n// Portfolio Sections\n// ---------------------------------------------------------------------------\n\nexport const PORTFOLIO_SECTION_TYPES = [\n 'portfolioHero',\n 'portfolioChallenges',\n 'portfolioStrategy',\n 'portfolioResults',\n 'portfolioTechStack',\n 'portfolioServices',\n 'portfolioTestimonial',\n 'portfolioGallery',\n 'portfolioVideo',\n 'portfolioTeam',\n 'portfolioFeatureSpotlight',\n 'portfolioBeforeAfter',\n 'portfolioMetricsTimeline',\n 'portfolioConversionFunnel',\n 'portfolioPerformance',\n 'portfolioSpeedComparison',\n 'portfolioSiteArchitecture',\n 'portfolioDesignSystem',\n 'portfolioDetails',\n 'portfolioSeo',\n 'portfolioCTA',\n] as const;\n\nexport type PortfolioSectionType = (typeof PORTFOLIO_SECTION_TYPES)[number];\n\nexport type PortfolioSectionData =\n | PortfolioHeroData\n | PortfolioChallengesData\n | PortfolioStrategyData\n | PortfolioResultsData\n | PortfolioTechStackData\n | PortfolioServicesData\n | PortfolioTestimonialData\n | PortfolioGalleryData\n | PortfolioVideoData\n | PortfolioTeamData\n | PortfolioFeatureSpotlightData\n | PortfolioBeforeAfterData\n | PortfolioMetricsTimelineData\n | PortfolioConversionFunnelData\n | PortfolioDetailsData\n | PortfolioSeoData\n | PortfolioCTAData;\n\nexport interface PortfolioSectionDataMap {\n portfolioHero: PortfolioHeroData;\n portfolioChallenges: PortfolioChallengesData;\n portfolioStrategy: PortfolioStrategyData;\n portfolioResults: PortfolioResultsData;\n portfolioTechStack: PortfolioTechStackData;\n portfolioServices: PortfolioServicesData;\n portfolioTestimonial: PortfolioTestimonialData;\n portfolioGallery: PortfolioGalleryData;\n portfolioVideo: PortfolioVideoData;\n portfolioTeam: PortfolioTeamData;\n portfolioFeatureSpotlight: PortfolioFeatureSpotlightData;\n portfolioBeforeAfter: PortfolioBeforeAfterData;\n portfolioMetricsTimeline: PortfolioMetricsTimelineData;\n portfolioConversionFunnel: PortfolioConversionFunnelData;\n portfolioDetails: PortfolioDetailsData;\n portfolioSeo: PortfolioSeoData;\n portfolioCTA: PortfolioCTAData;\n}\n\nexport interface PortfolioSection<T extends PortfolioSectionType = PortfolioSectionType> {\n sectionType: T;\n displayName: string;\n data: T extends keyof PortfolioSectionDataMap ? PortfolioSectionDataMap[T] : PortfolioSectionData;\n}\n\n// ---------------------------------------------------------------------------\n// Portfolio Item (mirrors public API response)\n// ---------------------------------------------------------------------------\n\nexport interface PortfolioItem {\n id: string;\n slug: string;\n title: string;\n subtitle: string;\n category: string;\n services: string[];\n description: string;\n hero_image: string;\n hero_image_alt: string;\n live_url: string | null;\n kpis: PortfolioKPI[];\n details: PortfolioDetailsData | null;\n seo: PortfolioSeoData | null;\n featured: boolean;\n order?: number;\n published_at: string | null;\n /** Sonor project ID (present for items generated from a managed project) */\n project_id: string | null;\n hero_screenshots: {\n desktop?: string;\n tablet?: string;\n mobile?: string;\n } | null;\n baseline_metrics: MetricsSnapshot | null;\n current_metrics: MetricsSnapshot | null;\n metrics_last_refreshed_at: string | null;\n}\n\nexport interface PortfolioItemFull extends PortfolioItem {\n sections: PortfolioSection[];\n metricsDelta: MetricsDelta[];\n /**\n * True once the agency deliberately reordered sections in the dashboard.\n * PortfolioPage respects the sections array order when set (hero pinned\n * first); otherwise it applies the canonical order.\n */\n sectionsOrdered?: boolean;\n}\n\n// ---------------------------------------------------------------------------\n// API Responses\n// ---------------------------------------------------------------------------\n\nexport interface PortfolioListResponse {\n items: PortfolioItem[];\n total: number;\n limit: number;\n offset: number;\n}\n\nexport interface PortfolioConfigResponse {\n brand: BrandConfig;\n orgName: string;\n orgSlug: string;\n}\n"]}
@@ -302,5 +302,5 @@ exports.getPortfolioItem = getPortfolioItem;
302
302
  exports.getPortfolioItems = getPortfolioItems;
303
303
  exports.sanitizeSiteArchitectureData = sanitizeSiteArchitectureData;
304
304
  exports.slugifyCategory = slugifyCategory;
305
- //# sourceMappingURL=chunk-7ZHRMWJU.cjs.map
306
- //# sourceMappingURL=chunk-7ZHRMWJU.cjs.map
305
+ //# sourceMappingURL=chunk-A5WCMHAE.cjs.map
306
+ //# sourceMappingURL=chunk-A5WCMHAE.cjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/portfolio/sanitize.ts","../src/portfolio/server.ts"],"names":["apiFetch","PORTFOLIO_CACHE_TAG","PORTFOLIO_PREVIEW_COOKIE"],"mappings":"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","file":"chunk-7ZHRMWJU.cjs","sourcesContent":["/**\n * Portfolio section sanitization — single source of truth.\n *\n * Site-architecture entries arrive from generation with raw CLIENT-site paths\n * (often title === path, e.g. \"/wheels/amp-tires\"). Those strings must never\n * cross a server→client props boundary: Googlebot extracts URL-like strings\n * from the RSC flight payload and crawls them against the AGENCY site's\n * origin, flooding Search Console with 404s (seen on upforge.io, June 2026 —\n * ~100 404'd client paths, all traced to /work/* case-study payloads).\n *\n * The enforcement point is `getPortfolioItem` (portfolio/server.ts) — the\n * last hop that is guaranteed server-side no matter how the build behaves.\n * PortfolioPage is a genuine RSC since 0.4.0 (and `pnpm verify` enforces\n * that), but rendered section data still crosses the client boundary as\n * props, and any future bundling regression would leak the whole item again.\n * Sanitize at the fetch, always. The PortfolioPage call is defense-in-depth.\n */\n\ninterface ArchPageEntry {\n path?: string;\n title?: string;\n}\n\n/** Convert a raw path/title entry into a display-only { title } label. */\nexport function humanizeArchPageEntry(page: ArchPageEntry): { title: string } {\n const title = (page?.title || '').trim();\n if (title && !title.startsWith('/')) return { title };\n const source = title || (page?.path || '').trim();\n const lastSegment = source.split(/[?#]/)[0].split('/').filter(Boolean).pop() || '';\n const label = lastSegment\n .replace(/\\.(html?|php|aspx?)$/i, '')\n .replace(/[-_]+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n return { title: label || 'Home' };\n}\n\n/** Strip raw page paths from a portfolioSiteArchitecture data payload. */\nexport function sanitizeSiteArchitectureData(data: any): any {\n if (!data || !Array.isArray(data.groups)) return data;\n return {\n ...data,\n groups: data.groups.map((group: any) => ({\n ...group,\n pages: Array.isArray(group.pages) ? group.pages.map(humanizeArchPageEntry) : [],\n })),\n };\n}\n\n/** Sanitize every section that can carry raw client-site paths. */\nexport function sanitizePortfolioSections<T extends { sectionType?: string; data?: any }>(\n sections: T[],\n): T[] {\n return sections.map((section) =>\n section?.sectionType === 'portfolioSiteArchitecture'\n ? { ...section, data: sanitizeSiteArchitectureData(section.data) }\n : section,\n );\n}\n","/**\n * @sonordev/agency-site-kit — Portfolio server-side data fetching\n *\n * RSC (React Server Component) data layer for portfolio pages.\n * All functions are async and designed for use in Next.js App Router\n * server components, generateMetadata(), and generateStaticParams().\n *\n * This file is server-only — no 'use client' directive.\n */\n\nimport { apiFetch } from '../shared/api';\nimport { PORTFOLIO_CACHE_TAG } from '../shared/cache';\nimport { PORTFOLIO_PREVIEW_COOKIE } from '../shared/preview';\nimport { sanitizePortfolioSections } from './sanitize';\nimport type {\n BrandConfig,\n MetricsDelta,\n PortfolioConfigResponse,\n PortfolioItemFull,\n PortfolioListResponse,\n PortfolioSeoData,\n PortfolioSection,\n} from '../types';\n\nconst LOG_PREFIX = '[@sonordev/agency-site-kit/portfolio]';\n\n// Re-export so consumers and route handlers can reference the same tag.\nexport { PORTFOLIO_CACHE_TAG } from '../shared/cache';\n\n/**\n * Error contract (single source of truth for portfolio content fetching):\n *\n * - \"Not found\" is a DATA answer: the API returns 200 with `found: false`,\n * and `getPortfolioItem` returns null. Consumers map null → notFound().\n * - Infrastructure failure (network error, 5xx, rate-limit exhaustion,\n * missing SONOR_API_KEY) THROWS PortfolioApiError. It must never be\n * conflated with \"not found\": treating an outage as a 404 turns indexed\n * /work URLs into real 404s during ISR regeneration (the GSC 404-flood\n * class of incident). A thrown error makes Next.js keep serving the last\n * good page during background revalidation, and fails builds loudly\n * instead of silently generating an empty portfolio.\n *\n * The one exception is getPortfolioBrandConfig, which degrades to default\n * branding on failure — cosmetic fallback is safe; content fallback is not.\n */\nexport class PortfolioApiError extends Error {\n readonly path: string;\n\n constructor(path: string, message?: string) {\n super(\n message ??\n `${LOG_PREFIX} Portfolio API request failed: ${path}. ` +\n 'This is an infrastructure/config failure (network, 5xx, or missing ' +\n 'SONOR_API_KEY), not a missing item — failing loudly so it cannot be ' +\n 'mistaken for a 404 or an empty portfolio.',\n );\n this.name = 'PortfolioApiError';\n this.path = path;\n }\n}\n\n/**\n * A 200 response whose body violates the expected shape. Thrown before any\n * mapping so drift between the API and this package surfaces as a loud,\n * named error instead of `undefined` text on live pages.\n */\nexport class PortfolioContractError extends PortfolioApiError {\n readonly field: string;\n\n constructor(path: string, field: string, detail: string) {\n super(\n path,\n `${LOG_PREFIX} Portfolio API contract violation at \"${field}\" (${path}): ${detail}. ` +\n 'Refusing to render partial or corrupt data.',\n );\n this.name = 'PortfolioContractError';\n this.field = field;\n }\n}\n\n/**\n * Minimal runtime shape check for the single-item response. Deliberately\n * lenient about unknown extra fields (the API may grow) and strict about\n * the fields this package renders from.\n */\nexport function assertPortfolioItemShape(\n item: unknown,\n path: string,\n): asserts item is PortfolioItemApiResponse['item'] {\n const fail = (field: string, detail: string): never => {\n throw new PortfolioContractError(path, field, detail);\n };\n\n if (typeof item !== 'object' || item === null) fail('item', 'not an object');\n const record = item as Record<string, unknown>;\n\n for (const field of ['id', 'slug', 'title'] as const) {\n const v = record[field];\n if (typeof v !== 'string' || v.length === 0) {\n fail(field, `expected a non-empty string, got ${JSON.stringify(v)}`);\n }\n }\n\n for (const field of ['sections', 'services', 'kpis', 'metricsDelta'] as const) {\n const v = record[field];\n if (v != null && !Array.isArray(v)) {\n fail(field, `expected an array when present, got ${typeof v}`);\n }\n }\n\n const sections = (record.sections ?? []) as unknown[];\n sections.forEach((section, i) => {\n if (typeof section !== 'object' || section === null) {\n fail(`sections[${i}]`, 'not an object');\n }\n const s = section as Record<string, unknown>;\n if (typeof s.sectionType !== 'string' || s.sectionType.length === 0) {\n fail(`sections[${i}].sectionType`, 'expected a non-empty string');\n }\n if (typeof s.data !== 'object' || s.data === null) {\n fail(`sections[${i}].data`, 'expected an object');\n }\n });\n}\n\n/**\n * GET against the public portfolio API with the shared portfolio cache tag,\n * so the Portal's revalidation webhook can invalidate every portfolio fetch\n * (list, detail, categories, config, sitemap data) via one revalidateTag.\n *\n * Retries transient failures (429/502/503 + network) up to 3 attempts, then\n * throws PortfolioApiError per the error contract above. All portfolio\n * endpoints answer \"unknown slug\" with 200 + `found: false`, so a null from\n * the fetcher can only mean infrastructure failure.\n */\nasync function portfolioGet<T>(\n path: string,\n opts: { revalidate?: number } = {},\n): Promise<T> {\n const data = await apiFetch<T>(path, {\n method: 'GET',\n tags: [PORTFOLIO_CACHE_TAG],\n retry: true,\n maxAttempts: 3,\n requireKey: true,\n ...(opts.revalidate !== undefined ? { revalidate: opts.revalidate } : {}),\n });\n\n if (data === null) throw new PortfolioApiError(path);\n return data;\n}\n\n/**\n * Draft preview: when Next draft mode is active AND the preview cookie is\n * set (both done by createPortfolioPreviewHandler), returns the raw token\n * so getPortfolioItem can fetch the unpublished item uncached. Fails soft —\n * outside a request scope (build, tests) this is simply not a draft.\n */\nasync function getDraftPreviewToken(): Promise<string | null> {\n try {\n const { draftMode, cookies } = await import('next/headers');\n const dm = await draftMode();\n if (!dm.isEnabled) return null;\n const jar = await cookies();\n return jar.get(PORTFOLIO_PREVIEW_COOKIE)?.value ?? null;\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Builds a query string from an object, omitting undefined/null values.\n */\nfunction buildQuery(params: Record<string, string | number | boolean | undefined | null>): string {\n const entries = Object.entries(params).filter(\n (entry): entry is [string, string | number | boolean] => entry[1] != null,\n );\n if (entries.length === 0) return '';\n const qs = new URLSearchParams(\n entries.map(([k, v]) => [k, String(v)]),\n ).toString();\n return `?${qs}`;\n}\n\n// ---------------------------------------------------------------------------\n// Public API — List items\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches published portfolio items from the public API.\n * Cached via Next.js ISR (revalidate: 3600).\n *\n * An empty portfolio is `{ items: [] }` from a healthy API. Infrastructure\n * failure throws PortfolioApiError (see the error contract above) — it never\n * silently renders an empty portfolio or an empty sitemap.\n */\nexport async function getPortfolioItems(options?: {\n category?: string;\n featured?: boolean;\n limit?: number;\n offset?: number;\n}): Promise<PortfolioListResponse> {\n const query = buildQuery({\n category: options?.category,\n featured: options?.featured,\n limit: options?.limit,\n offset: options?.offset,\n });\n\n return portfolioGet<PortfolioListResponse>(\n `/api/public/portfolio/items${query}`,\n );\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Single item\n// ---------------------------------------------------------------------------\n\n/** Shape returned by the single-item API endpoint. */\ninterface PortfolioItemApiResponse {\n found: boolean;\n item: {\n id: string;\n slug: string;\n title: string;\n subtitle: string;\n category: string;\n services: string[];\n description: string;\n hero_image: string;\n hero_image_alt: string;\n live_url: string | null;\n project_id: string | null;\n kpis: PortfolioItemFull['kpis'];\n details: PortfolioItemFull['details'];\n seo: PortfolioItemFull['seo'];\n featured: boolean;\n order?: number;\n published_at: string | null;\n hero_screenshots: PortfolioItemFull['hero_screenshots'];\n baseline_metrics: PortfolioItemFull['baseline_metrics'];\n current_metrics: PortfolioItemFull['current_metrics'];\n metrics_last_refreshed_at: string | null;\n sections: PortfolioSection[];\n metricsDelta: MetricsDelta[];\n sectionsOrdered?: boolean;\n };\n}\n\n/**\n * Fetches a single portfolio item by slug, including full Sanity sections\n * and live metrics delta. This is the main data function for portfolio detail pages.\n *\n * Returns null ONLY when the item genuinely does not exist (API answered\n * `found: false`) — safe to map to notFound(). Infrastructure failures throw\n * PortfolioApiError instead, so an API outage during ISR regeneration keeps\n * serving the last good page rather than minting a real 404.\n */\nexport async function getPortfolioItem(slug: string): Promise<PortfolioItemFull | null> {\n if (!slug) {\n console.warn(`${LOG_PREFIX} getPortfolioItem called with empty slug.`);\n return null;\n }\n\n // Draft preview (dashboard Preview button): fetch with the token, uncached.\n const previewToken = await getDraftPreviewToken();\n const path = previewToken\n ? `/api/public/portfolio/items/${encodeURIComponent(slug)}?preview=${encodeURIComponent(previewToken)}`\n : `/api/public/portfolio/items/${encodeURIComponent(slug)}`;\n\n const data = await portfolioGet<PortfolioItemApiResponse>(\n path,\n previewToken ? { revalidate: 0 } : {},\n );\n\n if (!data.found || !data.item) {\n console.warn(`${LOG_PREFIX} Portfolio item not found for slug: \"${slug}\".`);\n return null;\n }\n\n // Contract guard — a malformed 200 must not reach the renderer.\n assertPortfolioItemShape(data.item, `/api/public/portfolio/items/${slug}`);\n\n // Map the API response to the PortfolioItemFull type\n const { item } = data;\n return {\n id: item.id,\n slug: item.slug,\n title: item.title,\n subtitle: item.subtitle,\n category: item.category,\n services: item.services,\n description: item.description,\n hero_image: item.hero_image,\n hero_image_alt: item.hero_image_alt,\n live_url: item.live_url,\n project_id: item.project_id ?? null,\n kpis: item.kpis,\n details: item.details,\n seo: item.seo,\n featured: item.featured,\n order: item.order,\n published_at: item.published_at,\n hero_screenshots: item.hero_screenshots,\n baseline_metrics: item.baseline_metrics,\n current_metrics: item.current_metrics,\n metrics_last_refreshed_at: item.metrics_last_refreshed_at,\n // Sanitized HERE — the enforcing layer. Even with PortfolioPage as a\n // genuine RSC, each rendered section's data still crosses the client\n // boundary into the flight payload, and any future bundling regression\n // would leak the whole item again (the 0.3.1 GSC 404-flood vector).\n // Sanitize at the last server-side hop, always.\n sections: sanitizePortfolioSections(item.sections ?? []),\n metricsDelta: item.metricsDelta ?? [],\n sectionsOrdered: item.sectionsOrdered ?? false,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Categories\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches available portfolio categories.\n * Infrastructure failure throws PortfolioApiError (see error contract).\n */\nexport async function getPortfolioCategories(): Promise<string[]> {\n return portfolioGet<string[]>('/api/public/portfolio/categories');\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Brand config\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches brand configuration for the agency (colors, fonts, logo).\n * Returns sensible defaults when the API call fails — the ONE deliberate\n * exception to the throw-on-failure contract: brand config is cosmetic, and\n * an API blip must not take down the whole layout that wraps every page.\n */\nexport async function getPortfolioBrandConfig(): Promise<BrandConfig> {\n const fallback: BrandConfig = {\n primary: '#6366f1',\n secondary: '#8b5cf6',\n background: '#0a0a0f',\n backgroundElevated: '#12121a',\n surface: '#1a1a2e',\n surfaceHover: '#22223a',\n surfaceBorder: '#2a2a3e',\n textPrimary: '#f0f0f5',\n textSecondary: '#a0a0b5',\n textTertiary: '#6b6b80',\n fontHeading: 'Inter, system-ui, sans-serif',\n fontBody: 'Inter, system-ui, sans-serif',\n };\n\n try {\n const data = await portfolioGet<PortfolioConfigResponse>(\n '/api/public/portfolio/config',\n );\n if (!data.brand) {\n console.warn(`${LOG_PREFIX} Brand config response empty, using defaults.`);\n return fallback;\n }\n return data.brand;\n } catch {\n console.warn(`${LOG_PREFIX} Failed to fetch brand config, using defaults.`);\n return fallback;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Metadata helper\n// ---------------------------------------------------------------------------\n\n/**\n * Generates Next.js Metadata for a portfolio item page.\n * For use in generateMetadata() in the App Router.\n *\n * Returns an empty object when the item does not exist (safe to spread into\n * the metadata return value); infrastructure failures throw so a build never\n * silently ships pages with missing metadata.\n */\nexport async function generatePortfolioMetadata(slug: string): Promise<{\n title?: string;\n description?: string;\n keywords?: string[];\n openGraph?: {\n title?: string;\n description?: string;\n images?: string[];\n type?: string;\n };\n}> {\n const item = await getPortfolioItem(slug);\n\n if (!item) {\n return {};\n }\n\n // Prefer dedicated SEO data if available, fall back to item fields\n const seo: PortfolioSeoData | null = item.seo;\n const title = seo?.metaTitle || item.title;\n const description = seo?.metaDescription || item.description;\n const keywords = seo?.keywords ?? [];\n\n // Collect OG images: prefer SEO og image, then hero image\n const ogImages: string[] = [];\n if (item.hero_image) {\n ogImages.push(item.hero_image);\n }\n\n return {\n title,\n description,\n keywords: keywords.length > 0 ? keywords : undefined,\n openGraph: {\n title,\n description,\n images: ogImages.length > 0 ? ogImages : undefined,\n type: 'article',\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Static Params helper\n// ---------------------------------------------------------------------------\n\n/**\n * Generates static params for portfolio pages.\n * For use in generateStaticParams() in the App Router.\n *\n * Fetches all published portfolio items and returns their slugs.\n * Infrastructure failure throws, failing the build loudly instead of\n * silently building a site with zero portfolio pages.\n */\nexport async function generatePortfolioStaticParams(): Promise<Array<{ slug: string }>> {\n // Fetch a large batch to cover all published items\n const data = await getPortfolioItems({ limit: 500, offset: 0 });\n\n return data.items.map((item) => ({ slug: item.slug }));\n}\n\n// ---------------------------------------------------------------------------\n// Category helpers (route-based, crawlable filtering)\n// ---------------------------------------------------------------------------\n\n/**\n * URL slug for a category display value (\"Web Development\" → \"web-development\").\n * Single source of truth for category URLs — PortfolioIndex resolves slugs\n * back to the original values by matching against getPortfolioCategories().\n */\nexport function slugifyCategory(category: string): string {\n return category\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/**\n * Static params for the category filter routes.\n * For use in app/work/category/[category]/page.tsx:\n *\n * ```ts\n * export async function generateStaticParams() {\n * return generatePortfolioCategoryStaticParams();\n * }\n * ```\n */\nexport async function generatePortfolioCategoryStaticParams(): Promise<\n Array<{ category: string }>\n> {\n const categories = await getPortfolioCategories();\n return categories.map((c) => ({ category: slugifyCategory(c) }));\n}\n\n/**\n * Metadata for index + category + paginated portfolio routes: a stable\n * title/canonical so filtered and paginated variants don't compete in\n * search. Spread into generateMetadata()'s return value.\n */\nexport async function generatePortfolioIndexMetadata(options: {\n /** Site origin (e.g. \"https://youragency.com\"). */\n baseUrl: string;\n /** Route prefix. Defaults to \"/work\". */\n basePath?: string;\n /** Active category SLUG (from the route param), if any. */\n category?: string | null;\n /** 1-based page number. */\n page?: number;\n /** Title base, e.g. \"Our Work\". Category/page qualifiers are appended. */\n titleBase?: string;\n}): Promise<{ title: string; alternates: { canonical: string } }> {\n const {\n baseUrl,\n basePath = '/work',\n category = null,\n page = 1,\n titleBase = 'Our Work',\n } = options;\n\n const origin = baseUrl.replace(/\\/+$/, '');\n const prefix = basePath.startsWith('/') ? basePath : `/${basePath}`;\n\n let title = titleBase;\n let path = prefix;\n\n if (category) {\n const categories = await getPortfolioCategories();\n const match = categories.find((c) => slugifyCategory(c) === category);\n if (match) title = `${match} | ${titleBase}`;\n path = `${prefix}/category/${category}`;\n }\n if (page > 1) {\n title = `${title} — Page ${page}`;\n path = `${path}?page=${page}`;\n }\n\n return { title, alternates: { canonical: `${origin}${path}` } };\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Sitemap helper\n// ---------------------------------------------------------------------------\n\n/** One entry in the shape Next.js expects from app/sitemap.(js|ts). */\nexport interface PortfolioSitemapEntry {\n url: string;\n lastModified?: string;\n changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';\n priority?: number;\n}\n\n/**\n * Builds sitemap entries for every published portfolio item, in the shape\n * Next.js `app/sitemap.(js|ts)` expects — spread the result into the array\n * you return:\n *\n * ```ts\n * const portfolio = await generatePortfolioSitemapEntries({\n * baseUrl: 'https://youragency.com',\n * });\n * return [...staticEntries, ...portfolio];\n * ```\n *\n * `lastModified` prefers the metrics refresh timestamp (case studies change\n * when their live metrics update) and falls back to the publish date.\n * Infrastructure failure throws — a sitemap silently missing every case\n * study is how portfolio URLs fall out of the index.\n */\nexport async function generatePortfolioSitemapEntries(options: {\n /** Site origin, no trailing slash needed (e.g. \"https://youragency.com\"). */\n baseUrl: string;\n /** Route prefix for case studies. Defaults to \"/work\". */\n basePath?: string;\n changeFrequency?: PortfolioSitemapEntry['changeFrequency'];\n priority?: number;\n}): Promise<PortfolioSitemapEntry[]> {\n const {\n baseUrl,\n basePath = '/work',\n changeFrequency = 'monthly',\n priority = 0.7,\n } = options;\n\n const origin = baseUrl.replace(/\\/+$/, '');\n const prefix = basePath.startsWith('/') ? basePath : `/${basePath}`;\n\n const data = await getPortfolioItems({ limit: 500, offset: 0 });\n\n return data.items.map((item) => {\n const lastModified =\n item.metrics_last_refreshed_at || item.published_at || undefined;\n return {\n url: `${origin}${prefix}/${item.slug}`,\n ...(lastModified ? { lastModified } : {}),\n changeFrequency,\n priority,\n };\n });\n}\n"]}
1
+ {"version":3,"sources":["../src/portfolio/sanitize.ts","../src/portfolio/server.ts"],"names":["apiFetch","PORTFOLIO_CACHE_TAG","PORTFOLIO_PREVIEW_COOKIE"],"mappings":"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","file":"chunk-A5WCMHAE.cjs","sourcesContent":["/**\n * Portfolio section sanitization — single source of truth.\n *\n * Site-architecture entries arrive from generation with raw CLIENT-site paths\n * (often title === path, e.g. \"/wheels/amp-tires\"). Those strings must never\n * cross a server→client props boundary: Googlebot extracts URL-like strings\n * from the RSC flight payload and crawls them against the AGENCY site's\n * origin, flooding Search Console with 404s (seen on upforge.io, June 2026 —\n * ~100 404'd client paths, all traced to /work/* case-study payloads).\n *\n * The enforcement point is `getPortfolioItem` (portfolio/server.ts) — the\n * last hop that is guaranteed server-side no matter how the build behaves.\n * PortfolioPage is a genuine RSC since 0.4.0 (and `pnpm verify` enforces\n * that), but rendered section data still crosses the client boundary as\n * props, and any future bundling regression would leak the whole item again.\n * Sanitize at the fetch, always. The PortfolioPage call is defense-in-depth.\n */\n\ninterface ArchPageEntry {\n path?: string;\n title?: string;\n}\n\n/** Convert a raw path/title entry into a display-only { title } label. */\nexport function humanizeArchPageEntry(page: ArchPageEntry): { title: string } {\n const title = (page?.title || '').trim();\n if (title && !title.startsWith('/')) return { title };\n const source = title || (page?.path || '').trim();\n const lastSegment = source.split(/[?#]/)[0].split('/').filter(Boolean).pop() || '';\n const label = lastSegment\n .replace(/\\.(html?|php|aspx?)$/i, '')\n .replace(/[-_]+/g, ' ')\n .trim()\n .replace(/\\b\\w/g, (c) => c.toUpperCase());\n return { title: label || 'Home' };\n}\n\n/** Strip raw page paths from a portfolioSiteArchitecture data payload. */\nexport function sanitizeSiteArchitectureData(data: any): any {\n if (!data || !Array.isArray(data.groups)) return data;\n return {\n ...data,\n groups: data.groups.map((group: any) => ({\n ...group,\n pages: Array.isArray(group.pages) ? group.pages.map(humanizeArchPageEntry) : [],\n })),\n };\n}\n\n/** Sanitize every section that can carry raw client-site paths. */\nexport function sanitizePortfolioSections<T extends { sectionType?: string; data?: any }>(\n sections: T[],\n): T[] {\n return sections.map((section) =>\n section?.sectionType === 'portfolioSiteArchitecture'\n ? { ...section, data: sanitizeSiteArchitectureData(section.data) }\n : section,\n );\n}\n","/**\n * @sonordev/agency-site-kit — Portfolio server-side data fetching\n *\n * RSC (React Server Component) data layer for portfolio pages.\n * All functions are async and designed for use in Next.js App Router\n * server components, generateMetadata(), and generateStaticParams().\n *\n * This file is server-only — no 'use client' directive.\n */\n\nimport { apiFetch } from '../shared/api';\nimport { PORTFOLIO_CACHE_TAG } from '../shared/cache';\nimport { PORTFOLIO_PREVIEW_COOKIE } from '../shared/preview';\nimport { sanitizePortfolioSections } from './sanitize';\nimport type {\n BrandConfig,\n MetricsDelta,\n PortfolioConfigResponse,\n PortfolioItemFull,\n PortfolioListResponse,\n PortfolioSeoData,\n PortfolioSection,\n} from '../types';\n\nconst LOG_PREFIX = '[@sonordev/agency-site-kit/portfolio]';\n\n// Re-export so consumers and route handlers can reference the same tag.\nexport { PORTFOLIO_CACHE_TAG } from '../shared/cache';\n\n/**\n * Error contract (single source of truth for portfolio content fetching):\n *\n * - \"Not found\" is a DATA answer: the API returns 200 with `found: false`,\n * and `getPortfolioItem` returns null. Consumers map null → notFound().\n * - Infrastructure failure (network error, 5xx, rate-limit exhaustion,\n * missing SONOR_API_KEY) THROWS PortfolioApiError. It must never be\n * conflated with \"not found\": treating an outage as a 404 turns indexed\n * /work URLs into real 404s during ISR regeneration (the GSC 404-flood\n * class of incident). A thrown error makes Next.js keep serving the last\n * good page during background revalidation, and fails builds loudly\n * instead of silently generating an empty portfolio.\n *\n * The one exception is getPortfolioBrandConfig, which degrades to default\n * branding on failure — cosmetic fallback is safe; content fallback is not.\n */\nexport class PortfolioApiError extends Error {\n readonly path: string;\n\n constructor(path: string, message?: string) {\n super(\n message ??\n `${LOG_PREFIX} Portfolio API request failed: ${path}. ` +\n 'This is an infrastructure/config failure (network, 5xx, or missing ' +\n 'SONOR_API_KEY), not a missing item — failing loudly so it cannot be ' +\n 'mistaken for a 404 or an empty portfolio.',\n );\n this.name = 'PortfolioApiError';\n this.path = path;\n }\n}\n\n/**\n * A 200 response whose body violates the expected shape. Thrown before any\n * mapping so drift between the API and this package surfaces as a loud,\n * named error instead of `undefined` text on live pages.\n */\nexport class PortfolioContractError extends PortfolioApiError {\n readonly field: string;\n\n constructor(path: string, field: string, detail: string) {\n super(\n path,\n `${LOG_PREFIX} Portfolio API contract violation at \"${field}\" (${path}): ${detail}. ` +\n 'Refusing to render partial or corrupt data.',\n );\n this.name = 'PortfolioContractError';\n this.field = field;\n }\n}\n\n/**\n * Minimal runtime shape check for the single-item response. Deliberately\n * lenient about unknown extra fields (the API may grow) and strict about\n * the fields this package renders from.\n */\nexport function assertPortfolioItemShape(\n item: unknown,\n path: string,\n): asserts item is PortfolioItemApiResponse['item'] {\n const fail = (field: string, detail: string): never => {\n throw new PortfolioContractError(path, field, detail);\n };\n\n if (typeof item !== 'object' || item === null) fail('item', 'not an object');\n const record = item as Record<string, unknown>;\n\n for (const field of ['id', 'slug', 'title'] as const) {\n const v = record[field];\n if (typeof v !== 'string' || v.length === 0) {\n fail(field, `expected a non-empty string, got ${JSON.stringify(v)}`);\n }\n }\n\n for (const field of ['sections', 'services', 'kpis', 'metricsDelta'] as const) {\n const v = record[field];\n if (v != null && !Array.isArray(v)) {\n fail(field, `expected an array when present, got ${typeof v}`);\n }\n }\n\n const sections = (record.sections ?? []) as unknown[];\n sections.forEach((section, i) => {\n if (typeof section !== 'object' || section === null) {\n fail(`sections[${i}]`, 'not an object');\n }\n const s = section as Record<string, unknown>;\n if (typeof s.sectionType !== 'string' || s.sectionType.length === 0) {\n fail(`sections[${i}].sectionType`, 'expected a non-empty string');\n }\n if (typeof s.data !== 'object' || s.data === null) {\n fail(`sections[${i}].data`, 'expected an object');\n }\n });\n}\n\n/**\n * GET against the public portfolio API with the shared portfolio cache tag,\n * so the Portal's revalidation webhook can invalidate every portfolio fetch\n * (list, detail, categories, config, sitemap data) via one revalidateTag.\n *\n * Retries transient failures (429/502/503 + network) up to 3 attempts, then\n * throws PortfolioApiError per the error contract above. All portfolio\n * endpoints answer \"unknown slug\" with 200 + `found: false`, so a null from\n * the fetcher can only mean infrastructure failure.\n */\nasync function portfolioGet<T>(\n path: string,\n opts: { revalidate?: number } = {},\n): Promise<T> {\n const data = await apiFetch<T>(path, {\n method: 'GET',\n tags: [PORTFOLIO_CACHE_TAG],\n retry: true,\n maxAttempts: 3,\n requireKey: true,\n ...(opts.revalidate !== undefined ? { revalidate: opts.revalidate } : {}),\n });\n\n if (data === null) throw new PortfolioApiError(path);\n return data;\n}\n\n/**\n * Draft preview: when Next draft mode is active AND the preview cookie is\n * set (both done by createPortfolioPreviewHandler), returns the raw token\n * so getPortfolioItem can fetch the unpublished item uncached. Fails soft —\n * outside a request scope (build, tests) this is simply not a draft.\n */\nasync function getDraftPreviewToken(): Promise<string | null> {\n try {\n const { draftMode, cookies } = await import('next/headers');\n const dm = await draftMode();\n if (!dm.isEnabled) return null;\n const jar = await cookies();\n return jar.get(PORTFOLIO_PREVIEW_COOKIE)?.value ?? null;\n } catch {\n return null;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Internal helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Builds a query string from an object, omitting undefined/null values.\n */\nfunction buildQuery(params: Record<string, string | number | boolean | undefined | null>): string {\n const entries = Object.entries(params).filter(\n (entry): entry is [string, string | number | boolean] => entry[1] != null,\n );\n if (entries.length === 0) return '';\n const qs = new URLSearchParams(\n entries.map(([k, v]) => [k, String(v)]),\n ).toString();\n return `?${qs}`;\n}\n\n// ---------------------------------------------------------------------------\n// Public API — List items\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches published portfolio items from the public API.\n * Cached via Next.js ISR (revalidate: 3600).\n *\n * An empty portfolio is `{ items: [] }` from a healthy API. Infrastructure\n * failure throws PortfolioApiError (see the error contract above) — it never\n * silently renders an empty portfolio or an empty sitemap.\n */\nexport async function getPortfolioItems(options?: {\n category?: string;\n featured?: boolean;\n limit?: number;\n offset?: number;\n}): Promise<PortfolioListResponse> {\n const query = buildQuery({\n category: options?.category,\n featured: options?.featured,\n limit: options?.limit,\n offset: options?.offset,\n });\n\n return portfolioGet<PortfolioListResponse>(\n `/api/public/portfolio/items${query}`,\n );\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Single item\n// ---------------------------------------------------------------------------\n\n/** Shape returned by the single-item API endpoint. */\ninterface PortfolioItemApiResponse {\n found: boolean;\n item: {\n id: string;\n slug: string;\n title: string;\n subtitle: string;\n category: string;\n services: string[];\n description: string;\n hero_image: string;\n hero_image_alt: string;\n live_url: string | null;\n project_id: string | null;\n kpis: PortfolioItemFull['kpis'];\n details: PortfolioItemFull['details'];\n seo: PortfolioItemFull['seo'];\n featured: boolean;\n order?: number;\n published_at: string | null;\n hero_screenshots: PortfolioItemFull['hero_screenshots'];\n baseline_metrics: PortfolioItemFull['baseline_metrics'];\n current_metrics: PortfolioItemFull['current_metrics'];\n metrics_last_refreshed_at: string | null;\n sections: PortfolioSection[];\n metricsDelta: MetricsDelta[];\n sectionsOrdered?: boolean;\n };\n}\n\n/**\n * Fetches a single portfolio item by slug, including full Sanity sections\n * and live metrics delta. This is the main data function for portfolio detail pages.\n *\n * Returns null ONLY when the item genuinely does not exist (API answered\n * `found: false`) — safe to map to notFound(). Infrastructure failures throw\n * PortfolioApiError instead, so an API outage during ISR regeneration keeps\n * serving the last good page rather than minting a real 404.\n */\nexport async function getPortfolioItem(slug: string): Promise<PortfolioItemFull | null> {\n if (!slug) {\n console.warn(`${LOG_PREFIX} getPortfolioItem called with empty slug.`);\n return null;\n }\n\n // Draft preview (dashboard Preview button): fetch with the token, uncached.\n const previewToken = await getDraftPreviewToken();\n const path = previewToken\n ? `/api/public/portfolio/items/${encodeURIComponent(slug)}?preview=${encodeURIComponent(previewToken)}`\n : `/api/public/portfolio/items/${encodeURIComponent(slug)}`;\n\n const data = await portfolioGet<PortfolioItemApiResponse>(\n path,\n previewToken ? { revalidate: 0 } : {},\n );\n\n if (!data.found || !data.item) {\n console.warn(`${LOG_PREFIX} Portfolio item not found for slug: \"${slug}\".`);\n return null;\n }\n\n // Contract guard — a malformed 200 must not reach the renderer.\n assertPortfolioItemShape(data.item, `/api/public/portfolio/items/${slug}`);\n\n // Map the API response to the PortfolioItemFull type\n const { item } = data;\n return {\n id: item.id,\n slug: item.slug,\n title: item.title,\n subtitle: item.subtitle,\n category: item.category,\n services: item.services,\n description: item.description,\n hero_image: item.hero_image,\n hero_image_alt: item.hero_image_alt,\n live_url: item.live_url,\n project_id: item.project_id ?? null,\n kpis: item.kpis,\n details: item.details,\n seo: item.seo,\n featured: item.featured,\n order: item.order,\n published_at: item.published_at,\n hero_screenshots: item.hero_screenshots,\n baseline_metrics: item.baseline_metrics,\n current_metrics: item.current_metrics,\n metrics_last_refreshed_at: item.metrics_last_refreshed_at,\n // Sanitized HERE — the enforcing layer. Even with PortfolioPage as a\n // genuine RSC, each rendered section's data still crosses the client\n // boundary into the flight payload, and any future bundling regression\n // would leak the whole item again (the 0.3.1 GSC 404-flood vector).\n // Sanitize at the last server-side hop, always.\n sections: sanitizePortfolioSections(item.sections ?? []),\n metricsDelta: item.metricsDelta ?? [],\n sectionsOrdered: item.sectionsOrdered ?? false,\n };\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Categories\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches available portfolio categories.\n * Infrastructure failure throws PortfolioApiError (see error contract).\n */\nexport async function getPortfolioCategories(): Promise<string[]> {\n return portfolioGet<string[]>('/api/public/portfolio/categories');\n}\n\n// ---------------------------------------------------------------------------\n// Public API — Brand config\n// ---------------------------------------------------------------------------\n\n/**\n * Fetches brand configuration for the agency (colors, fonts, logo).\n * Returns sensible defaults when the API call fails — the ONE deliberate\n * exception to the throw-on-failure contract: brand config is cosmetic, and\n * an API blip must not take down the whole layout that wraps every page.\n */\nexport async function getPortfolioBrandConfig(): Promise<BrandConfig> {\n const fallback: BrandConfig = {\n primary: '#6366f1',\n secondary: '#8b5cf6',\n background: '#0a0a0f',\n backgroundElevated: '#12121a',\n surface: '#1a1a2e',\n surfaceHover: '#22223a',\n surfaceBorder: '#2a2a3e',\n textPrimary: '#f0f0f5',\n textSecondary: '#a0a0b5',\n textTertiary: '#6b6b80',\n fontHeading: 'Inter, system-ui, sans-serif',\n fontBody: 'Inter, system-ui, sans-serif',\n };\n\n try {\n const data = await portfolioGet<PortfolioConfigResponse>(\n '/api/public/portfolio/config',\n );\n if (!data.brand) {\n console.warn(`${LOG_PREFIX} Brand config response empty, using defaults.`);\n return fallback;\n }\n return data.brand;\n } catch {\n console.warn(`${LOG_PREFIX} Failed to fetch brand config, using defaults.`);\n return fallback;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Metadata helper\n// ---------------------------------------------------------------------------\n\n/**\n * Generates Next.js Metadata for a portfolio item page.\n * For use in generateMetadata() in the App Router.\n *\n * Returns an empty object when the item does not exist (safe to spread into\n * the metadata return value); infrastructure failures throw so a build never\n * silently ships pages with missing metadata.\n */\nexport async function generatePortfolioMetadata(slug: string): Promise<{\n title?: string;\n description?: string;\n keywords?: string[];\n openGraph?: {\n title?: string;\n description?: string;\n images?: string[];\n type?: string;\n };\n}> {\n const item = await getPortfolioItem(slug);\n\n if (!item) {\n return {};\n }\n\n // Prefer dedicated SEO data if available, fall back to item fields\n const seo: PortfolioSeoData | null = item.seo;\n const title = seo?.metaTitle || item.title;\n const description = seo?.metaDescription || item.description;\n const keywords = seo?.keywords ?? [];\n\n // Collect OG images: prefer SEO og image, then hero image\n const ogImages: string[] = [];\n if (item.hero_image) {\n ogImages.push(item.hero_image);\n }\n\n return {\n title,\n description,\n keywords: keywords.length > 0 ? keywords : undefined,\n openGraph: {\n title,\n description,\n images: ogImages.length > 0 ? ogImages : undefined,\n type: 'article',\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Static Params helper\n// ---------------------------------------------------------------------------\n\n/**\n * Generates static params for portfolio pages.\n * For use in generateStaticParams() in the App Router.\n *\n * Fetches all published portfolio items and returns their slugs.\n * Infrastructure failure throws, failing the build loudly instead of\n * silently building a site with zero portfolio pages.\n */\nexport async function generatePortfolioStaticParams(): Promise<Array<{ slug: string }>> {\n // Fetch a large batch to cover all published items\n const data = await getPortfolioItems({ limit: 500, offset: 0 });\n\n return data.items.map((item) => ({ slug: item.slug }));\n}\n\n// ---------------------------------------------------------------------------\n// Category helpers (route-based, crawlable filtering)\n// ---------------------------------------------------------------------------\n\n/**\n * URL slug for a category display value (\"Web Development\" → \"web-development\").\n * Single source of truth for category URLs — PortfolioIndex resolves slugs\n * back to the original values by matching against getPortfolioCategories().\n */\nexport function slugifyCategory(category: string): string {\n return category\n .toLowerCase()\n .trim()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '');\n}\n\n/**\n * Static params for the category filter routes.\n * For use in app/work/category/[category]/page.tsx:\n *\n * ```ts\n * export async function generateStaticParams() {\n * return generatePortfolioCategoryStaticParams();\n * }\n * ```\n */\nexport async function generatePortfolioCategoryStaticParams(): Promise<\n Array<{ category: string }>\n> {\n const categories = await getPortfolioCategories();\n return categories.map((c) => ({ category: slugifyCategory(c) }));\n}\n\n/**\n * Metadata for index + category + paginated portfolio routes: a stable\n * title/canonical so filtered and paginated variants don't compete in\n * search. Spread into generateMetadata()'s return value.\n */\nexport async function generatePortfolioIndexMetadata(options: {\n /** Site origin (e.g. \"https://youragency.com\"). */\n baseUrl: string;\n /** Route prefix. Defaults to \"/work\". */\n basePath?: string;\n /** Active category SLUG (from the route param), if any. */\n category?: string | null;\n /** 1-based page number. */\n page?: number;\n /** Title base, e.g. \"Our Work\". Category/page qualifiers are appended. */\n titleBase?: string;\n}): Promise<{ title: string; alternates: { canonical: string } }> {\n const {\n baseUrl,\n basePath = '/work',\n category = null,\n page = 1,\n titleBase = 'Our Work',\n } = options;\n\n const origin = baseUrl.replace(/\\/+$/, '');\n const prefix = basePath.startsWith('/') ? basePath : `/${basePath}`;\n\n let title = titleBase;\n let path = prefix;\n\n if (category) {\n const categories = await getPortfolioCategories();\n const match = categories.find((c) => slugifyCategory(c) === category);\n if (match) title = `${match} | ${titleBase}`;\n path = `${prefix}/category/${category}`;\n }\n if (page > 1) {\n title = `${title} — Page ${page}`;\n path = `${path}?page=${page}`;\n }\n\n return { title, alternates: { canonical: `${origin}${path}` } };\n}\n\n// ---------------------------------------------------------------------------\n// Next.js Sitemap helper\n// ---------------------------------------------------------------------------\n\n/** One entry in the shape Next.js expects from app/sitemap.(js|ts). */\nexport interface PortfolioSitemapEntry {\n url: string;\n lastModified?: string;\n changeFrequency?: 'always' | 'hourly' | 'daily' | 'weekly' | 'monthly' | 'yearly' | 'never';\n priority?: number;\n}\n\n/**\n * Builds sitemap entries for every published portfolio item, in the shape\n * Next.js `app/sitemap.(js|ts)` expects — spread the result into the array\n * you return:\n *\n * ```ts\n * const portfolio = await generatePortfolioSitemapEntries({\n * baseUrl: 'https://youragency.com',\n * });\n * return [...staticEntries, ...portfolio];\n * ```\n *\n * `lastModified` prefers the metrics refresh timestamp (case studies change\n * when their live metrics update) and falls back to the publish date.\n * Infrastructure failure throws — a sitemap silently missing every case\n * study is how portfolio URLs fall out of the index.\n */\nexport async function generatePortfolioSitemapEntries(options: {\n /** Site origin, no trailing slash needed (e.g. \"https://youragency.com\"). */\n baseUrl: string;\n /** Route prefix for case studies. Defaults to \"/work\". */\n basePath?: string;\n changeFrequency?: PortfolioSitemapEntry['changeFrequency'];\n priority?: number;\n}): Promise<PortfolioSitemapEntry[]> {\n const {\n baseUrl,\n basePath = '/work',\n changeFrequency = 'monthly',\n priority = 0.7,\n } = options;\n\n const origin = baseUrl.replace(/\\/+$/, '');\n const prefix = basePath.startsWith('/') ? basePath : `/${basePath}`;\n\n const data = await getPortfolioItems({ limit: 500, offset: 0 });\n\n return data.items.map((item) => {\n const lastModified =\n item.metrics_last_refreshed_at || item.published_at || undefined;\n return {\n url: `${origin}${prefix}/${item.slug}`,\n ...(lastModified ? { lastModified } : {}),\n changeFrequency,\n priority,\n };\n });\n}\n\n/* ──────────────────────────────────────────────────────────────────────────\n Display policy + label formatters (pure, server-safe).\n\n Also exported from the /portfolio barrel, but importing them from THERE in\n a server component registers every portfolio client component (the barrel\n re-exports /portfolio/client) into the importing page's chunk graph —\n ~40KB gz of sections/primitives on pages that only wanted a formatter\n (found on upforge's home page, 2026-07-14). Server components should\n import these from '@sonordev/agency-site-kit/portfolio/server'.\n ────────────────────────────────────────────────────────────────────────── */\nexport {\n curatePortfolioProof,\n curateHeroKpis,\n curateResultItems,\n gateMetricsDeltas,\n} from './curate-proof';\nexport { formatCategoryLabel, formatServiceTag } from './format';\n"]}
@@ -0,0 +1,50 @@
1
+ import { curateHeroKpis } from '@sonordev/site-kit/portfolio/contract';
2
+ export { curateHeroKpis } from '@sonordev/site-kit/portfolio/contract';
3
+
4
+ // src/portfolio/curate-proof.ts
5
+ var LIGHTHOUSE_RE = /lighthouse/i;
6
+ function isLighthouseRestatement(item) {
7
+ if (!/score/i.test(item.title)) return false;
8
+ if (LIGHTHOUSE_RE.test(`${item.title} ${item.description}`)) return true;
9
+ return item.metric?.suffix === "/100";
10
+ }
11
+ function curateResultItems(items, hasPerformanceSection) {
12
+ if (!hasPerformanceSection) return items;
13
+ const curated = items.filter((item) => !isLighthouseRestatement(item));
14
+ return curated.length > 0 ? curated : items.slice(0, 1);
15
+ }
16
+ function gateMetricsDeltas(deltas) {
17
+ if (!deltas?.length) return [];
18
+ return deltas.filter((d) => {
19
+ if (d.direction !== "up") return false;
20
+ if (Math.abs(d.deltaPercent) < 10) return false;
21
+ if (/bounce/i.test(d.metric) && d.current > 70) return false;
22
+ return true;
23
+ });
24
+ }
25
+ function curatePortfolioProof(item) {
26
+ const hasPerformanceSection = item.sections.some(
27
+ (s) => s.sectionType === "portfolioPerformance"
28
+ );
29
+ return {
30
+ ...item,
31
+ sections: item.sections.map((section) => {
32
+ if (section.sectionType === "portfolioHero") {
33
+ const data = section.data;
34
+ return { ...section, data: { ...data, kpis: curateHeroKpis(data.kpis ?? []) } };
35
+ }
36
+ if (section.sectionType === "portfolioResults") {
37
+ const data = section.data;
38
+ return {
39
+ ...section,
40
+ data: { ...data, items: curateResultItems(data.items ?? [], hasPerformanceSection) }
41
+ };
42
+ }
43
+ return section;
44
+ })
45
+ };
46
+ }
47
+
48
+ export { curatePortfolioProof, curateResultItems, gateMetricsDeltas };
49
+ //# sourceMappingURL=chunk-FM25LB67.js.map
50
+ //# sourceMappingURL=chunk-FM25LB67.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/portfolio/curate-proof.ts"],"names":[],"mappings":";;;;AAwCA,IAAM,aAAA,GAAgB,aAAA;AASf,SAAS,wBAAwB,IAAA,EAAoC;AAC1E,EAAA,IAAI,CAAC,QAAA,CAAS,IAAA,CAAK,IAAA,CAAK,KAAK,GAAG,OAAO,KAAA;AACvC,EAAA,IAAI,aAAA,CAAc,IAAA,CAAK,CAAA,EAAG,IAAA,CAAK,KAAK,IAAI,IAAA,CAAK,WAAW,CAAA,CAAE,CAAA,EAAG,OAAO,IAAA;AACpE,EAAA,OAAO,IAAA,CAAK,QAAQ,MAAA,KAAW,MAAA;AACjC;AAMO,SAAS,iBAAA,CACd,OACA,qBAAA,EACuB;AACvB,EAAA,IAAI,CAAC,uBAAuB,OAAO,KAAA;AACnC,EAAA,MAAM,OAAA,GAAU,MAAM,MAAA,CAAO,CAAC,SAAS,CAAC,uBAAA,CAAwB,IAAI,CAAC,CAAA;AAGrE,EAAA,OAAO,QAAQ,MAAA,GAAS,CAAA,GAAI,UAAU,KAAA,CAAM,KAAA,CAAM,GAAG,CAAC,CAAA;AACxD;AAYO,SAAS,kBAAkB,MAAA,EAA2D;AAC3F,EAAA,IAAI,CAAC,MAAA,EAAQ,MAAA,EAAQ,OAAO,EAAC;AAC7B,EAAA,OAAO,MAAA,CAAO,MAAA,CAAO,CAAC,CAAA,KAAM;AAC1B,IAAA,IAAI,CAAA,CAAE,SAAA,KAAc,IAAA,EAAM,OAAO,KAAA;AACjC,IAAA,IAAI,KAAK,GAAA,CAAI,CAAA,CAAE,YAAY,CAAA,GAAI,IAAI,OAAO,KAAA;AAC1C,IAAA,IAAI,SAAA,CAAU,KAAK,CAAA,CAAE,MAAM,KAAK,CAAA,CAAE,OAAA,GAAU,IAAI,OAAO,KAAA;AACvD,IAAA,OAAO,IAAA;AAAA,EACT,CAAC,CAAA;AACH;AAMO,SAAS,qBAAqB,IAAA,EAA4C;AAC/E,EAAA,MAAM,qBAAA,GAAwB,KAAK,QAAA,CAAS,IAAA;AAAA,IAC1C,CAAC,CAAA,KAAM,CAAA,CAAE,WAAA,KAAgB;AAAA,GAC3B;AAEA,EAAA,OAAO;AAAA,IACL,GAAG,IAAA;AAAA,IACH,QAAA,EAAU,IAAA,CAAK,QAAA,CAAS,GAAA,CAAI,CAAC,OAAA,KAAY;AACvC,MAAA,IAAI,OAAA,CAAQ,gBAAgB,eAAA,EAAiB;AAC3C,QAAA,MAAM,OAAO,OAAA,CAAQ,IAAA;AACrB,QAAA,OAAO,EAAE,GAAG,OAAA,EAAS,IAAA,EAAM,EAAE,GAAG,IAAA,EAAM,IAAA,EAAM,cAAA,CAAe,IAAA,CAAK,IAAA,IAAQ,EAAE,GAAE,EAAE;AAAA,MAChF;AACA,MAAA,IAAI,OAAA,CAAQ,gBAAgB,kBAAA,EAAoB;AAC9C,QAAA,MAAM,OAAO,OAAA,CAAQ,IAAA;AACrB,QAAA,OAAO;AAAA,UACL,GAAG,OAAA;AAAA,UACH,IAAA,EAAM,EAAE,GAAG,IAAA,EAAM,KAAA,EAAO,iBAAA,CAAkB,IAAA,CAAK,KAAA,IAAS,EAAC,EAAG,qBAAqB,CAAA;AAAE,SACrF;AAAA,MACF;AACA,MAAA,OAAO,OAAA;AAAA,IACT,CAAC;AAAA,GACH;AACF","file":"chunk-FM25LB67.js","sourcesContent":["/**\n * curate-proof — single source of truth for which proof points a public case\n * study is allowed to display, and in what shape.\n *\n * Two classes of problem this guards against (both shipped to production on\n * upforge.io before this existed):\n *\n * 1. Repetition: generated items arrive with four \"100/100 Lighthouse X\"\n * KPIs in the hero, the same four restated as Results cards, AND a full\n * Performance section with score gauges. One differentiator repeated\n * twelve times per page stops differentiating. The Performance section is\n * the one home for the Lighthouse breakdown; hero/results/cards collapse\n * to at most one summary tile.\n *\n * 2. Self-harming live metrics: MetricsDelta rows are computed from raw\n * analytics baselines with no editorial judgment — a bounce rate of\n * \"100 → 99.69\" rendered as a green win. Marketing surfaces only show a\n * delta when it's a genuinely flattering, meaningful story.\n *\n * Used by PortfolioPage (hero KPIs + results items), PortfolioCard (KPI\n * preview), and MetricsProvenance (delta gate). Change display policy here,\n * nowhere else.\n */\n\nimport type {\n MetricsDelta,\n PortfolioHeroData,\n PortfolioItemFull,\n PortfolioResultItem,\n PortfolioResultsData,\n} from '../types';\n// The Lighthouse-KPI collapse policy is the shared cross-repo contract\n// (@sonordev/site-kit/portfolio/contract) — the same source signal-api mirrors\n// for content-time collapse. Imported (not re-implemented) so the display side\n// and the content side can never drift; re-exported so this module stays the\n// portfolio's single display-policy entry point.\nimport { curateHeroKpis, isLighthouseKpi } from '@sonordev/site-kit/portfolio/contract';\n\nexport { curateHeroKpis, isLighthouseKpi };\n\nconst LIGHTHOUSE_RE = /lighthouse/i;\n\n/**\n * True when a Results card merely restates a Lighthouse category score\n * (e.g. \"Perfect performance score … measured Lighthouse score of 100/100\").\n * Business outcomes (\"Complete 14-page rebuild\", \"682% session growth\")\n * never match: the title must be about a score AND the card must reference\n * Lighthouse or carry a x/100 metric.\n */\nexport function isLighthouseRestatement(item: PortfolioResultItem): boolean {\n if (!/score/i.test(item.title)) return false;\n if (LIGHTHOUSE_RE.test(`${item.title} ${item.description}`)) return true;\n return item.metric?.suffix === '/100';\n}\n\n/**\n * Drop Results cards that restate Lighthouse scores when the case study also\n * renders a Performance section (the score breakdown's single home).\n */\nexport function curateResultItems(\n items: PortfolioResultItem[],\n hasPerformanceSection: boolean,\n): PortfolioResultItem[] {\n if (!hasPerformanceSection) return items;\n const curated = items.filter((item) => !isLighthouseRestatement(item));\n // Never curate a Results section into emptiness — if every card was a\n // Lighthouse restatement, keep the strongest single card instead.\n return curated.length > 0 ? curated : items.slice(0, 1);\n}\n\n/**\n * Editorial gate for live metric deltas (\"Verified from live analytics\").\n * `direction` comes from the platform with 'up' meaning IMPROVED (lower-is-\n * better metrics are inverted server-side). A public case study only shows a\n * delta when it's a win worth telling:\n * - improved ('up'), never a regression or flat noise\n * - moved by a double-digit relative margin (small drift isn't a story)\n * - bounce-rate-style metrics must also be respectable in absolute terms —\n * \"100 → 99.69\" is technically an improvement and still self-harm\n */\nexport function gateMetricsDeltas(deltas: MetricsDelta[] | undefined | null): MetricsDelta[] {\n if (!deltas?.length) return [];\n return deltas.filter((d) => {\n if (d.direction !== 'up') return false;\n if (Math.abs(d.deltaPercent) < 10) return false;\n if (/bounce/i.test(d.metric) && d.current > 70) return false;\n return true;\n });\n}\n\n/**\n * Apply the full display policy to a portfolio item before rendering.\n * Pure — returns a new item; the fetched payload is never mutated.\n */\nexport function curatePortfolioProof(item: PortfolioItemFull): PortfolioItemFull {\n const hasPerformanceSection = item.sections.some(\n (s) => s.sectionType === 'portfolioPerformance',\n );\n\n return {\n ...item,\n sections: item.sections.map((section) => {\n if (section.sectionType === 'portfolioHero') {\n const data = section.data as PortfolioHeroData;\n return { ...section, data: { ...data, kpis: curateHeroKpis(data.kpis ?? []) } };\n }\n if (section.sectionType === 'portfolioResults') {\n const data = section.data as PortfolioResultsData;\n return {\n ...section,\n data: { ...data, items: curateResultItems(data.items ?? [], hasPerformanceSection) },\n };\n }\n return section;\n }),\n };\n}\n"]}
@@ -287,5 +287,5 @@ async function generatePortfolioSitemapEntries(options) {
287
287
  }
288
288
 
289
289
  export { PortfolioApiError, PortfolioContractError, assertPortfolioItemShape, generatePortfolioCategoryStaticParams, generatePortfolioIndexMetadata, generatePortfolioMetadata, generatePortfolioSitemapEntries, generatePortfolioStaticParams, getPortfolioBrandConfig, getPortfolioCategories, getPortfolioItem, getPortfolioItems, sanitizeSiteArchitectureData, slugifyCategory };
290
- //# sourceMappingURL=chunk-KXLROAO5.js.map
291
- //# sourceMappingURL=chunk-KXLROAO5.js.map
290
+ //# sourceMappingURL=chunk-FT2SNEYP.js.map
291
+ //# sourceMappingURL=chunk-FT2SNEYP.js.map