@sonordev/agency-site-kit 0.4.1 → 0.4.2

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 (61) hide show
  1. package/dist/{chunk-QGSW43TM.cjs → chunk-2Y2RJB3A.cjs} +12 -12
  2. package/dist/{chunk-QGSW43TM.cjs.map → chunk-2Y2RJB3A.cjs.map} +1 -1
  3. package/dist/{chunk-UXUU7FVT.cjs → chunk-6OWFFV6N.cjs} +4 -65
  4. package/dist/chunk-6OWFFV6N.cjs.map +1 -0
  5. package/dist/{chunk-NLGUPJZA.js → chunk-7FFZX7C5.js} +4 -4
  6. package/dist/{chunk-NLGUPJZA.js.map → chunk-7FFZX7C5.js.map} +1 -1
  7. package/dist/{chunk-7ZHRMWJU.cjs → chunk-A5WCMHAE.cjs} +2 -2
  8. package/dist/{chunk-7ZHRMWJU.cjs.map → chunk-A5WCMHAE.cjs.map} +1 -1
  9. package/dist/chunk-FM25LB67.js +50 -0
  10. package/dist/chunk-FM25LB67.js.map +1 -0
  11. package/dist/{chunk-KXLROAO5.js → chunk-FT2SNEYP.js} +2 -2
  12. package/dist/{chunk-KXLROAO5.js.map → chunk-FT2SNEYP.js.map} +1 -1
  13. package/dist/chunk-X76U7VLF.cjs +57 -0
  14. package/dist/chunk-X76U7VLF.cjs.map +1 -0
  15. package/dist/{chunk-RWQ675N6.js → chunk-X7UCJ45A.js} +4 -61
  16. package/dist/chunk-X7UCJ45A.js.map +1 -0
  17. package/dist/format-C-axf13G.d.cts +68 -0
  18. package/dist/format-poUpLtQ2.d.ts +68 -0
  19. package/dist/index.cjs +18 -17
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +5 -3
  22. package/dist/index.d.ts +5 -3
  23. package/dist/index.js +4 -3
  24. package/dist/index.js.map +1 -1
  25. package/dist/layout/index.d.cts +1 -1
  26. package/dist/layout/index.d.ts +1 -1
  27. package/dist/portfolio/client.cjs +2 -17
  28. package/dist/portfolio/client.cjs.map +1 -1
  29. package/dist/portfolio/client.js +1 -16
  30. package/dist/portfolio/client.js.map +1 -1
  31. package/dist/portfolio/index.cjs +18 -17
  32. package/dist/portfolio/index.d.cts +5 -74
  33. package/dist/portfolio/index.d.ts +5 -74
  34. package/dist/portfolio/index.js +4 -3
  35. package/dist/portfolio/sections/ChallengesSection.d.cts +1 -1
  36. package/dist/portfolio/sections/ChallengesSection.d.ts +1 -1
  37. package/dist/portfolio/sections/DetailsSection.d.cts +1 -1
  38. package/dist/portfolio/sections/DetailsSection.d.ts +1 -1
  39. package/dist/portfolio/sections/HeroSection.d.cts +1 -1
  40. package/dist/portfolio/sections/HeroSection.d.ts +1 -1
  41. package/dist/portfolio/sections/ResultsSection.cjs +4 -3
  42. package/dist/portfolio/sections/ResultsSection.cjs.map +1 -1
  43. package/dist/portfolio/sections/ResultsSection.d.cts +1 -1
  44. package/dist/portfolio/sections/ResultsSection.d.ts +1 -1
  45. package/dist/portfolio/sections/ResultsSection.js +2 -1
  46. package/dist/portfolio/sections/ResultsSection.js.map +1 -1
  47. package/dist/portfolio/sections/ServicesSection.d.cts +1 -1
  48. package/dist/portfolio/sections/ServicesSection.d.ts +1 -1
  49. package/dist/portfolio/sections/TechStackSection.d.cts +1 -1
  50. package/dist/portfolio/sections/TechStackSection.d.ts +1 -1
  51. package/dist/portfolio/sections/TestimonialSection.d.cts +1 -1
  52. package/dist/portfolio/sections/TestimonialSection.d.ts +1 -1
  53. package/dist/portfolio/server.cjs +40 -14
  54. package/dist/portfolio/server.d.cts +3 -1
  55. package/dist/portfolio/server.d.ts +3 -1
  56. package/dist/portfolio/server.js +3 -1
  57. package/dist/{types-0tVPy4pU.d.ts → types-He5A9okM.d.cts} +1 -1
  58. package/dist/{types-0tVPy4pU.d.cts → types-He5A9okM.d.ts} +1 -1
  59. package/package.json +3 -3
  60. package/dist/chunk-RWQ675N6.js.map +0 -1
  61. package/dist/chunk-UXUU7FVT.cjs.map +0 -1
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/portfolio/sanitize.ts","../src/portfolio/server.ts"],"names":[],"mappings":"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","file":"chunk-KXLROAO5.js","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":[],"mappings":";;;;;AAwBO,SAAS,sBAAsB,IAAA,EAAwC;AAC5E,EAAA,MAAM,KAAA,GAAA,CAAS,IAAA,EAAM,KAAA,IAAS,EAAA,EAAI,IAAA,EAAK;AACvC,EAAA,IAAI,KAAA,IAAS,CAAC,KAAA,CAAM,UAAA,CAAW,GAAG,CAAA,EAAG,OAAO,EAAE,KAAA,EAAM;AACpD,EAAA,MAAM,MAAA,GAAS,KAAA,IAAA,CAAU,IAAA,EAAM,IAAA,IAAQ,IAAI,IAAA,EAAK;AAChD,EAAA,MAAM,WAAA,GAAc,MAAA,CAAO,KAAA,CAAM,MAAM,EAAE,CAAC,CAAA,CAAE,KAAA,CAAM,GAAG,CAAA,CAAE,MAAA,CAAO,OAAO,CAAA,CAAE,KAAI,IAAK,EAAA;AAChF,EAAA,MAAM,QAAQ,WAAA,CACX,OAAA,CAAQ,yBAAyB,EAAE,CAAA,CACnC,QAAQ,QAAA,EAAU,GAAG,CAAA,CACrB,IAAA,GACA,OAAA,CAAQ,OAAA,EAAS,CAAC,CAAA,KAAM,CAAA,CAAE,aAAa,CAAA;AAC1C,EAAA,OAAO,EAAE,KAAA,EAAO,KAAA,IAAS,MAAA,EAAO;AAClC;AAGO,SAAS,6BAA6B,IAAA,EAAgB;AAC3D,EAAA,IAAI,CAAC,QAAQ,CAAC,KAAA,CAAM,QAAQ,IAAA,CAAK,MAAM,GAAG,OAAO,IAAA;AACjD,EAAA,OAAO;AAAA,IACL,GAAG,IAAA;AAAA,IACH,MAAA,EAAQ,IAAA,CAAK,MAAA,CAAO,GAAA,CAAI,CAAC,KAAA,MAAgB;AAAA,MACvC,GAAG,KAAA;AAAA,MACH,KAAA,EAAO,KAAA,CAAM,OAAA,CAAQ,KAAA,CAAM,KAAK,CAAA,GAAI,KAAA,CAAM,KAAA,CAAM,GAAA,CAAI,qBAAqB,CAAA,GAAI;AAAC,KAChF,CAAE;AAAA,GACJ;AACF;AAGO,SAAS,0BACd,QAAA,EACK;AACL,EAAA,OAAO,QAAA,CAAS,GAAA;AAAA,IAAI,CAAC,OAAA,KACnB,OAAA,EAAS,WAAA,KAAgB,2BAAA,GACrB,EAAE,GAAG,OAAA,EAAS,IAAA,EAAM,4BAAA,CAA6B,OAAA,CAAQ,IAAI,GAAE,GAC/D;AAAA,GACN;AACF;;;AClCA,IAAM,UAAA,GAAa,uCAAA;AAqBZ,IAAM,iBAAA,GAAN,cAAgC,KAAA,CAAM;AAAA,EAG3C,WAAA,CAAY,MAAc,OAAA,EAAkB;AAC1C,IAAA,KAAA;AAAA,MACE,OAAA,IACE,CAAA,EAAG,UAAU,CAAA,+BAAA,EAAkC,IAAI,CAAA,uLAAA;AAAA,KAIvD;AACA,IAAA,IAAA,CAAK,IAAA,GAAO,mBAAA;AACZ,IAAA,IAAA,CAAK,IAAA,GAAO,IAAA;AAAA,EACd;AACF;AAOO,IAAM,sBAAA,GAAN,cAAqC,iBAAA,CAAkB;AAAA,EAG5D,WAAA,CAAY,IAAA,EAAc,KAAA,EAAe,MAAA,EAAgB;AACvD,IAAA,KAAA;AAAA,MACE,IAAA;AAAA,MACA,GAAG,UAAU,CAAA,sCAAA,EAAyC,KAAK,CAAA,GAAA,EAAM,IAAI,MAAM,MAAM,CAAA,6CAAA;AAAA,KAEnF;AACA,IAAA,IAAA,CAAK,IAAA,GAAO,wBAAA;AACZ,IAAA,IAAA,CAAK,KAAA,GAAQ,KAAA;AAAA,EACf;AACF;AAOO,SAAS,wBAAA,CACd,MACA,IAAA,EACkD;AAClD,EAAA,MAAM,IAAA,GAAO,CAAC,KAAA,EAAe,MAAA,KAA0B;AACrD,IAAA,MAAM,IAAI,sBAAA,CAAuB,IAAA,EAAM,KAAA,EAAO,MAAM,CAAA;AAAA,EACtD,CAAA;AAEA,EAAA,IAAI,OAAO,IAAA,KAAS,QAAA,IAAY,SAAS,IAAA,EAAM,IAAA,CAAK,QAAQ,eAAe,CAAA;AAC3E,EAAA,MAAM,MAAA,GAAS,IAAA;AAEf,EAAA,KAAA,MAAW,KAAA,IAAS,CAAC,IAAA,EAAM,MAAA,EAAQ,OAAO,CAAA,EAAY;AACpD,IAAA,MAAM,CAAA,GAAI,OAAO,KAAK,CAAA;AACtB,IAAA,IAAI,OAAO,CAAA,KAAM,QAAA,IAAY,CAAA,CAAE,WAAW,CAAA,EAAG;AAC3C,MAAA,IAAA,CAAK,OAAO,CAAA,iCAAA,EAAoC,IAAA,CAAK,SAAA,CAAU,CAAC,CAAC,CAAA,CAAE,CAAA;AAAA,IACrE;AAAA,EACF;AAEA,EAAA,KAAA,MAAW,SAAS,CAAC,UAAA,EAAY,UAAA,EAAY,MAAA,EAAQ,cAAc,CAAA,EAAY;AAC7E,IAAA,MAAM,CAAA,GAAI,OAAO,KAAK,CAAA;AACtB,IAAA,IAAI,KAAK,IAAA,IAAQ,CAAC,KAAA,CAAM,OAAA,CAAQ,CAAC,CAAA,EAAG;AAClC,MAAA,IAAA,CAAK,KAAA,EAAO,CAAA,oCAAA,EAAuC,OAAO,CAAC,CAAA,CAAE,CAAA;AAAA,IAC/D;AAAA,EACF;AAEA,EAAA,MAAM,QAAA,GAAY,MAAA,CAAO,QAAA,IAAY,EAAC;AACtC,EAAA,QAAA,CAAS,OAAA,CAAQ,CAAC,OAAA,EAAS,CAAA,KAAM;AAC/B,IAAA,IAAI,OAAO,OAAA,KAAY,QAAA,IAAY,OAAA,KAAY,IAAA,EAAM;AACnD,MAAA,IAAA,CAAK,CAAA,SAAA,EAAY,CAAC,CAAA,CAAA,CAAA,EAAK,eAAe,CAAA;AAAA,IACxC;AACA,IAAA,MAAM,CAAA,GAAI,OAAA;AACV,IAAA,IAAI,OAAO,CAAA,CAAE,WAAA,KAAgB,YAAY,CAAA,CAAE,WAAA,CAAY,WAAW,CAAA,EAAG;AACnE,MAAA,IAAA,CAAK,CAAA,SAAA,EAAY,CAAC,CAAA,aAAA,CAAA,EAAiB,6BAA6B,CAAA;AAAA,IAClE;AACA,IAAA,IAAI,OAAO,CAAA,CAAE,IAAA,KAAS,QAAA,IAAY,CAAA,CAAE,SAAS,IAAA,EAAM;AACjD,MAAA,IAAA,CAAK,CAAA,SAAA,EAAY,CAAC,CAAA,MAAA,CAAA,EAAU,oBAAoB,CAAA;AAAA,IAClD;AAAA,EACF,CAAC,CAAA;AACH;AAYA,eAAe,YAAA,CACb,IAAA,EACA,IAAA,GAAgC,EAAC,EACrB;AACZ,EAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAY,IAAA,EAAM;AAAA,IACnC,MAAA,EAAQ,KAAA;AAAA,IACR,IAAA,EAAM,CAAC,mBAAmB,CAAA;AAAA,IAC1B,KAAA,EAAO,IAAA;AAAA,IACP,WAAA,EAAa,CAAA;AAAA,IACb,UAAA,EAAY,IAAA;AAAA,IACZ,GAAI,KAAK,UAAA,KAAe,MAAA,GAAY,EAAE,UAAA,EAAY,IAAA,CAAK,UAAA,EAAW,GAAI;AAAC,GACxE,CAAA;AAED,EAAA,IAAI,IAAA,KAAS,IAAA,EAAM,MAAM,IAAI,kBAAkB,IAAI,CAAA;AACnD,EAAA,OAAO,IAAA;AACT;AAQA,eAAe,oBAAA,GAA+C;AAC5D,EAAA,IAAI;AACF,IAAA,MAAM,EAAE,SAAA,EAAW,OAAA,EAAQ,GAAI,MAAM,OAAO,cAAc,CAAA;AAC1D,IAAA,MAAM,EAAA,GAAK,MAAM,SAAA,EAAU;AAC3B,IAAA,IAAI,CAAC,EAAA,CAAG,SAAA,EAAW,OAAO,IAAA;AAC1B,IAAA,MAAM,GAAA,GAAM,MAAM,OAAA,EAAQ;AAC1B,IAAA,OAAO,GAAA,CAAI,GAAA,CAAI,wBAAwB,CAAA,EAAG,KAAA,IAAS,IAAA;AAAA,EACrD,CAAA,CAAA,MAAQ;AACN,IAAA,OAAO,IAAA;AAAA,EACT;AACF;AASA,SAAS,WAAW,MAAA,EAA8E;AAChG,EAAA,MAAM,OAAA,GAAU,MAAA,CAAO,OAAA,CAAQ,MAAM,CAAA,CAAE,MAAA;AAAA,IACrC,CAAC,KAAA,KAAwD,KAAA,CAAM,CAAC,CAAA,IAAK;AAAA,GACvE;AACA,EAAA,IAAI,OAAA,CAAQ,MAAA,KAAW,CAAA,EAAG,OAAO,EAAA;AACjC,EAAA,MAAM,KAAK,IAAI,eAAA;AAAA,IACb,OAAA,CAAQ,GAAA,CAAI,CAAC,CAAC,CAAA,EAAG,CAAC,CAAA,KAAM,CAAC,CAAA,EAAG,MAAA,CAAO,CAAC,CAAC,CAAC;AAAA,IACtC,QAAA,EAAS;AACX,EAAA,OAAO,IAAI,EAAE,CAAA,CAAA;AACf;AAcA,eAAsB,kBAAkB,OAAA,EAKL;AACjC,EAAA,MAAM,QAAQ,UAAA,CAAW;AAAA,IACvB,UAAU,OAAA,EAAS,QAAA;AAAA,IACnB,UAAU,OAAA,EAAS,QAAA;AAAA,IACnB,OAAO,OAAA,EAAS,KAAA;AAAA,IAChB,QAAQ,OAAA,EAAS;AAAA,GAClB,CAAA;AAED,EAAA,OAAO,YAAA;AAAA,IACL,8BAA8B,KAAK,CAAA;AAAA,GACrC;AACF;AA8CA,eAAsB,iBAAiB,IAAA,EAAiD;AACtF,EAAA,IAAI,CAAC,IAAA,EAAM;AACT,IAAA,OAAA,CAAQ,IAAA,CAAK,CAAA,EAAG,UAAU,CAAA,yCAAA,CAA2C,CAAA;AACrE,IAAA,OAAO,IAAA;AAAA,EACT;AAGA,EAAA,MAAM,YAAA,GAAe,MAAM,oBAAA,EAAqB;AAChD,EAAA,MAAM,IAAA,GAAO,YAAA,GACT,CAAA,4BAAA,EAA+B,kBAAA,CAAmB,IAAI,CAAC,CAAA,SAAA,EAAY,kBAAA,CAAmB,YAAY,CAAC,CAAA,CAAA,GACnG,CAAA,4BAAA,EAA+B,kBAAA,CAAmB,IAAI,CAAC,CAAA,CAAA;AAE3D,EAAA,MAAM,OAAO,MAAM,YAAA;AAAA,IACjB,IAAA;AAAA,IACA,YAAA,GAAe,EAAE,UAAA,EAAY,CAAA,KAAM;AAAC,GACtC;AAEA,EAAA,IAAI,CAAC,IAAA,CAAK,KAAA,IAAS,CAAC,KAAK,IAAA,EAAM;AAC7B,IAAA,OAAA,CAAQ,IAAA,CAAK,CAAA,EAAG,UAAU,CAAA,qCAAA,EAAwC,IAAI,CAAA,EAAA,CAAI,CAAA;AAC1E,IAAA,OAAO,IAAA;AAAA,EACT;AAGA,EAAA,wBAAA,CAAyB,IAAA,CAAK,IAAA,EAAM,CAAA,4BAAA,EAA+B,IAAI,CAAA,CAAE,CAAA;AAGzE,EAAA,MAAM,EAAE,MAAK,GAAI,IAAA;AACjB,EAAA,OAAO;AAAA,IACL,IAAI,IAAA,CAAK,EAAA;AAAA,IACT,MAAM,IAAA,CAAK,IAAA;AAAA,IACX,OAAO,IAAA,CAAK,KAAA;AAAA,IACZ,UAAU,IAAA,CAAK,QAAA;AAAA,IACf,UAAU,IAAA,CAAK,QAAA;AAAA,IACf,UAAU,IAAA,CAAK,QAAA;AAAA,IACf,aAAa,IAAA,CAAK,WAAA;AAAA,IAClB,YAAY,IAAA,CAAK,UAAA;AAAA,IACjB,gBAAgB,IAAA,CAAK,cAAA;AAAA,IACrB,UAAU,IAAA,CAAK,QAAA;AAAA,IACf,UAAA,EAAY,KAAK,UAAA,IAAc,IAAA;AAAA,IAC/B,MAAM,IAAA,CAAK,IAAA;AAAA,IACX,SAAS,IAAA,CAAK,OAAA;AAAA,IACd,KAAK,IAAA,CAAK,GAAA;AAAA,IACV,UAAU,IAAA,CAAK,QAAA;AAAA,IACf,OAAO,IAAA,CAAK,KAAA;AAAA,IACZ,cAAc,IAAA,CAAK,YAAA;AAAA,IACnB,kBAAkB,IAAA,CAAK,gBAAA;AAAA,IACvB,kBAAkB,IAAA,CAAK,gBAAA;AAAA,IACvB,iBAAiB,IAAA,CAAK,eAAA;AAAA,IACtB,2BAA2B,IAAA,CAAK,yBAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,IAMhC,QAAA,EAAU,yBAAA,CAA0B,IAAA,CAAK,QAAA,IAAY,EAAE,CAAA;AAAA,IACvD,YAAA,EAAc,IAAA,CAAK,YAAA,IAAgB,EAAC;AAAA,IACpC,eAAA,EAAiB,KAAK,eAAA,IAAmB;AAAA,GAC3C;AACF;AAUA,eAAsB,sBAAA,GAA4C;AAChE,EAAA,OAAO,aAAuB,kCAAkC,CAAA;AAClE;AAYA,eAAsB,uBAAA,GAAgD;AACpE,EAAA,MAAM,QAAA,GAAwB;AAAA,IAC5B,OAAA,EAAS,SAAA;AAAA,IACT,SAAA,EAAW,SAAA;AAAA,IACX,UAAA,EAAY,SAAA;AAAA,IACZ,kBAAA,EAAoB,SAAA;AAAA,IACpB,OAAA,EAAS,SAAA;AAAA,IACT,YAAA,EAAc,SAAA;AAAA,IACd,aAAA,EAAe,SAAA;AAAA,IACf,WAAA,EAAa,SAAA;AAAA,IACb,aAAA,EAAe,SAAA;AAAA,IACf,YAAA,EAAc,SAAA;AAAA,IACd,WAAA,EAAa,8BAAA;AAAA,IACb,QAAA,EAAU;AAAA,GACZ;AAEA,EAAA,IAAI;AACF,IAAA,MAAM,OAAO,MAAM,YAAA;AAAA,MACjB;AAAA,KACF;AACA,IAAA,IAAI,CAAC,KAAK,KAAA,EAAO;AACf,MAAA,OAAA,CAAQ,IAAA,CAAK,CAAA,EAAG,UAAU,CAAA,6CAAA,CAA+C,CAAA;AACzE,MAAA,OAAO,QAAA;AAAA,IACT;AACA,IAAA,OAAO,IAAA,CAAK,KAAA;AAAA,EACd,CAAA,CAAA,MAAQ;AACN,IAAA,OAAA,CAAQ,IAAA,CAAK,CAAA,EAAG,UAAU,CAAA,8CAAA,CAAgD,CAAA;AAC1E,IAAA,OAAO,QAAA;AAAA,EACT;AACF;AAcA,eAAsB,0BAA0B,IAAA,EAU7C;AACD,EAAA,MAAM,IAAA,GAAO,MAAM,gBAAA,CAAiB,IAAI,CAAA;AAExC,EAAA,IAAI,CAAC,IAAA,EAAM;AACT,IAAA,OAAO,EAAC;AAAA,EACV;AAGA,EAAA,MAAM,MAA+B,IAAA,CAAK,GAAA;AAC1C,EAAA,MAAM,KAAA,GAAQ,GAAA,EAAK,SAAA,IAAa,IAAA,CAAK,KAAA;AACrC,EAAA,MAAM,WAAA,GAAc,GAAA,EAAK,eAAA,IAAmB,IAAA,CAAK,WAAA;AACjD,EAAA,MAAM,QAAA,GAAW,GAAA,EAAK,QAAA,IAAY,EAAC;AAGnC,EAAA,MAAM,WAAqB,EAAC;AAC5B,EAAA,IAAI,KAAK,UAAA,EAAY;AACnB,IAAA,QAAA,CAAS,IAAA,CAAK,KAAK,UAAU,CAAA;AAAA,EAC/B;AAEA,EAAA,OAAO;AAAA,IACL,KAAA;AAAA,IACA,WAAA;AAAA,IACA,QAAA,EAAU,QAAA,CAAS,MAAA,GAAS,CAAA,GAAI,QAAA,GAAW,MAAA;AAAA,IAC3C,SAAA,EAAW;AAAA,MACT,KAAA;AAAA,MACA,WAAA;AAAA,MACA,MAAA,EAAQ,QAAA,CAAS,MAAA,GAAS,CAAA,GAAI,QAAA,GAAW,MAAA;AAAA,MACzC,IAAA,EAAM;AAAA;AACR,GACF;AACF;AAcA,eAAsB,6BAAA,GAAkE;AAEtF,EAAA,MAAM,IAAA,GAAO,MAAM,iBAAA,CAAkB,EAAE,OAAO,GAAA,EAAK,MAAA,EAAQ,GAAG,CAAA;AAE9D,EAAA,OAAO,IAAA,CAAK,MAAM,GAAA,CAAI,CAAC,UAAU,EAAE,IAAA,EAAM,IAAA,CAAK,IAAA,EAAK,CAAE,CAAA;AACvD;AAWO,SAAS,gBAAgB,QAAA,EAA0B;AACxD,EAAA,OAAO,QAAA,CACJ,WAAA,EAAY,CACZ,IAAA,EAAK,CACL,OAAA,CAAQ,aAAA,EAAe,GAAG,CAAA,CAC1B,OAAA,CAAQ,UAAA,EAAY,EAAE,CAAA;AAC3B;AAYA,eAAsB,qCAAA,GAEpB;AACA,EAAA,MAAM,UAAA,GAAa,MAAM,sBAAA,EAAuB;AAChD,EAAA,OAAO,UAAA,CAAW,IAAI,CAAC,CAAA,MAAO,EAAE,QAAA,EAAU,eAAA,CAAgB,CAAC,CAAA,EAAE,CAAE,CAAA;AACjE;AAOA,eAAsB,+BAA+B,OAAA,EAWa;AAChE,EAAA,MAAM;AAAA,IACJ,OAAA;AAAA,IACA,QAAA,GAAW,OAAA;AAAA,IACX,QAAA,GAAW,IAAA;AAAA,IACX,IAAA,GAAO,CAAA;AAAA,IACP,SAAA,GAAY;AAAA,GACd,GAAI,OAAA;AAEJ,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AACzC,EAAA,MAAM,SAAS,QAAA,CAAS,UAAA,CAAW,GAAG,CAAA,GAAI,QAAA,GAAW,IAAI,QAAQ,CAAA,CAAA;AAEjE,EAAA,IAAI,KAAA,GAAQ,SAAA;AACZ,EAAA,IAAI,IAAA,GAAO,MAAA;AAEX,EAAA,IAAI,QAAA,EAAU;AACZ,IAAA,MAAM,UAAA,GAAa,MAAM,sBAAA,EAAuB;AAChD,IAAA,MAAM,KAAA,GAAQ,WAAW,IAAA,CAAK,CAAC,MAAM,eAAA,CAAgB,CAAC,MAAM,QAAQ,CAAA;AACpE,IAAA,IAAI,KAAA,EAAO,KAAA,GAAQ,CAAA,EAAG,KAAK,MAAM,SAAS,CAAA,CAAA;AAC1C,IAAA,IAAA,GAAO,CAAA,EAAG,MAAM,CAAA,UAAA,EAAa,QAAQ,CAAA,CAAA;AAAA,EACvC;AACA,EAAA,IAAI,OAAO,CAAA,EAAG;AACZ,IAAA,KAAA,GAAQ,CAAA,EAAG,KAAK,CAAA,aAAA,EAAW,IAAI,CAAA,CAAA;AAC/B,IAAA,IAAA,GAAO,CAAA,EAAG,IAAI,CAAA,MAAA,EAAS,IAAI,CAAA,CAAA;AAAA,EAC7B;AAEA,EAAA,OAAO,EAAE,KAAA,EAAO,UAAA,EAAY,EAAE,SAAA,EAAW,GAAG,MAAM,CAAA,EAAG,IAAI,CAAA,CAAA,EAAG,EAAE;AAChE;AA+BA,eAAsB,gCAAgC,OAAA,EAOjB;AACnC,EAAA,MAAM;AAAA,IACJ,OAAA;AAAA,IACA,QAAA,GAAW,OAAA;AAAA,IACX,eAAA,GAAkB,SAAA;AAAA,IAClB,QAAA,GAAW;AAAA,GACb,GAAI,OAAA;AAEJ,EAAA,MAAM,MAAA,GAAS,OAAA,CAAQ,OAAA,CAAQ,MAAA,EAAQ,EAAE,CAAA;AACzC,EAAA,MAAM,SAAS,QAAA,CAAS,UAAA,CAAW,GAAG,CAAA,GAAI,QAAA,GAAW,IAAI,QAAQ,CAAA,CAAA;AAEjE,EAAA,MAAM,IAAA,GAAO,MAAM,iBAAA,CAAkB,EAAE,OAAO,GAAA,EAAK,MAAA,EAAQ,GAAG,CAAA;AAE9D,EAAA,OAAO,IAAA,CAAK,KAAA,CAAM,GAAA,CAAI,CAAC,IAAA,KAAS;AAC9B,IAAA,MAAM,YAAA,GACJ,IAAA,CAAK,yBAAA,IAA6B,IAAA,CAAK,YAAA,IAAgB,MAAA;AACzD,IAAA,OAAO;AAAA,MACL,KAAK,CAAA,EAAG,MAAM,GAAG,MAAM,CAAA,CAAA,EAAI,KAAK,IAAI,CAAA,CAAA;AAAA,MACpC,GAAI,YAAA,GAAe,EAAE,YAAA,KAAiB,EAAC;AAAA,MACvC,eAAA;AAAA,MACA;AAAA,KACF;AAAA,EACF,CAAC,CAAA;AACH","file":"chunk-FT2SNEYP.js","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,57 @@
1
+ 'use strict';
2
+
3
+ var contract = require('@sonordev/site-kit/portfolio/contract');
4
+
5
+ // src/portfolio/curate-proof.ts
6
+ var LIGHTHOUSE_RE = /lighthouse/i;
7
+ function isLighthouseRestatement(item) {
8
+ if (!/score/i.test(item.title)) return false;
9
+ if (LIGHTHOUSE_RE.test(`${item.title} ${item.description}`)) return true;
10
+ return item.metric?.suffix === "/100";
11
+ }
12
+ function curateResultItems(items, hasPerformanceSection) {
13
+ if (!hasPerformanceSection) return items;
14
+ const curated = items.filter((item) => !isLighthouseRestatement(item));
15
+ return curated.length > 0 ? curated : items.slice(0, 1);
16
+ }
17
+ function gateMetricsDeltas(deltas) {
18
+ if (!deltas?.length) return [];
19
+ return deltas.filter((d) => {
20
+ if (d.direction !== "up") return false;
21
+ if (Math.abs(d.deltaPercent) < 10) return false;
22
+ if (/bounce/i.test(d.metric) && d.current > 70) return false;
23
+ return true;
24
+ });
25
+ }
26
+ function curatePortfolioProof(item) {
27
+ const hasPerformanceSection = item.sections.some(
28
+ (s) => s.sectionType === "portfolioPerformance"
29
+ );
30
+ return {
31
+ ...item,
32
+ sections: item.sections.map((section) => {
33
+ if (section.sectionType === "portfolioHero") {
34
+ const data = section.data;
35
+ return { ...section, data: { ...data, kpis: contract.curateHeroKpis(data.kpis ?? []) } };
36
+ }
37
+ if (section.sectionType === "portfolioResults") {
38
+ const data = section.data;
39
+ return {
40
+ ...section,
41
+ data: { ...data, items: curateResultItems(data.items ?? [], hasPerformanceSection) }
42
+ };
43
+ }
44
+ return section;
45
+ })
46
+ };
47
+ }
48
+
49
+ Object.defineProperty(exports, "curateHeroKpis", {
50
+ enumerable: true,
51
+ get: function () { return contract.curateHeroKpis; }
52
+ });
53
+ exports.curatePortfolioProof = curatePortfolioProof;
54
+ exports.curateResultItems = curateResultItems;
55
+ exports.gateMetricsDeltas = gateMetricsDeltas;
56
+ //# sourceMappingURL=chunk-X76U7VLF.cjs.map
57
+ //# sourceMappingURL=chunk-X76U7VLF.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/portfolio/curate-proof.ts"],"names":["curateHeroKpis"],"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,EAAMA,uBAAA,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-X76U7VLF.cjs","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"]}
@@ -1,63 +1,6 @@
1
+ import { gateMetricsDeltas } from './chunk-FM25LB67.js';
1
2
  import { jsxs, jsx } from 'react/jsx-runtime';
2
3
 
3
- // src/portfolio/curate-proof.ts
4
- var LIGHTHOUSE_RE = /lighthouse/i;
5
- function isLighthouseKpi(kpi) {
6
- return LIGHTHOUSE_RE.test(kpi.label);
7
- }
8
- function isLighthouseRestatement(item) {
9
- if (!/score/i.test(item.title)) return false;
10
- if (LIGHTHOUSE_RE.test(`${item.title} ${item.description}`)) return true;
11
- return item.metric?.suffix === "/100";
12
- }
13
- function curateHeroKpis(kpis) {
14
- const business = kpis.filter((k) => !isLighthouseKpi(k));
15
- const lighthouse = kpis.filter(isLighthouseKpi);
16
- if (lighthouse.length <= 1) return kpis;
17
- const allPerfect = lighthouse.length >= 4 && lighthouse.every((k) => k.value === 100);
18
- const summary = allPerfect ? {
19
- ...lighthouse[0],
20
- label: "Lighthouse, all 4 categories",
21
- description: "Perfect scores across performance, SEO, accessibility, and best practices."
22
- } : lighthouse.find((k) => /performance/i.test(k.label)) ?? lighthouse[0];
23
- return [...business, summary];
24
- }
25
- function curateResultItems(items, hasPerformanceSection) {
26
- if (!hasPerformanceSection) return items;
27
- const curated = items.filter((item) => !isLighthouseRestatement(item));
28
- return curated.length > 0 ? curated : items.slice(0, 1);
29
- }
30
- function gateMetricsDeltas(deltas) {
31
- if (!deltas?.length) return [];
32
- return deltas.filter((d) => {
33
- if (d.direction !== "up") return false;
34
- if (Math.abs(d.deltaPercent) < 10) return false;
35
- if (/bounce/i.test(d.metric) && d.current > 70) return false;
36
- return true;
37
- });
38
- }
39
- function curatePortfolioProof(item) {
40
- const hasPerformanceSection = item.sections.some(
41
- (s) => s.sectionType === "portfolioPerformance"
42
- );
43
- return {
44
- ...item,
45
- sections: item.sections.map((section) => {
46
- if (section.sectionType === "portfolioHero") {
47
- const data = section.data;
48
- return { ...section, data: { ...data, kpis: curateHeroKpis(data.kpis ?? []) } };
49
- }
50
- if (section.sectionType === "portfolioResults") {
51
- const data = section.data;
52
- return {
53
- ...section,
54
- data: { ...data, items: curateResultItems(data.items ?? [], hasPerformanceSection) }
55
- };
56
- }
57
- return section;
58
- })
59
- };
60
- }
61
4
  var DIRECTION_COLOR = {
62
5
  up: "#10b981",
63
6
  down: "#ef4444",
@@ -160,6 +103,6 @@ function MetricsProvenance({ deltas, refreshedAt }) {
160
103
  );
161
104
  }
162
105
 
163
- export { MetricsProvenance, curateHeroKpis, curatePortfolioProof, curateResultItems, gateMetricsDeltas };
164
- //# sourceMappingURL=chunk-RWQ675N6.js.map
165
- //# sourceMappingURL=chunk-RWQ675N6.js.map
106
+ export { MetricsProvenance };
107
+ //# sourceMappingURL=chunk-X7UCJ45A.js.map
108
+ //# sourceMappingURL=chunk-X7UCJ45A.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/portfolio/components/MetricsProvenance.tsx"],"names":[],"mappings":"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","file":"chunk-X7UCJ45A.js","sourcesContent":["/**\n * MetricsProvenance — \"these numbers are real\" strip for case studies.\n *\n * Server component, zero JS. Renders the live metric deltas the platform\n * computes from GSC/analytics baselines (MetricsDelta[]) plus the refresh\n * timestamp — the trust signal a static agency portfolio can't fake.\n *\n * `direction` semantics come from the API: 'up' ALWAYS means \"improved\"\n * (lower-is-better metrics like Avg Position/LCP are inverted server-side\n * in metrics-baseline.service), so 'up' renders green. The ARROW glyph\n * follows the raw numbers instead — an improved bounce rate shows a green ▼,\n * never a green ▲ next to a value that visibly went down.\n *\n * Deltas pass through gateMetricsDeltas (curate-proof.ts) before display:\n * raw platform deltas carry no editorial judgment, and this strip only\n * exists to flatter — see that module for the policy.\n */\n\nimport React from 'react';\nimport type { MetricsDelta } from '../../types';\nimport { gateMetricsDeltas } from '../curate-proof';\n\ninterface MetricsProvenanceProps {\n deltas: MetricsDelta[];\n /** ISO timestamp of the last live-metrics refresh. */\n refreshedAt?: string | null;\n}\n\nconst DIRECTION_COLOR: Record<MetricsDelta['direction'], string> = {\n up: '#10b981',\n down: '#ef4444',\n flat: 'var(--sk-text-tertiary, #71717a)',\n};\n\nfunction formatValue(v: number): string {\n return Number.isInteger(v)\n ? v.toLocaleString('en-US')\n : v.toLocaleString('en-US', { maximumFractionDigits: 2 });\n}\n\nfunction formatDate(iso: string): string {\n try {\n return new Intl.DateTimeFormat('en-US', {\n year: 'numeric',\n month: 'long',\n day: 'numeric',\n }).format(new Date(iso));\n } catch {\n return iso;\n }\n}\n\nexport default function MetricsProvenance({ deltas, refreshedAt }: MetricsProvenanceProps) {\n const shown = gateMetricsDeltas(deltas);\n if (!shown.length) return null;\n\n return (\n <div\n className=\"mt-10 rounded-2xl overflow-hidden\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n <div className=\"grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3\">\n {shown.map((d, i) => {\n const color = DIRECTION_COLOR[d.direction] ?? DIRECTION_COLOR.flat;\n // Arrow tracks the raw numbers; color tracks improved/worsened.\n const arrow = d.current === d.baseline ? '—' : d.current > d.baseline ? '▲' : '▼';\n return (\n <div\n key={`${d.metric}-${i}`}\n className=\"flex flex-col gap-1 p-5\"\n style={{ borderBottom: '1px solid var(--sk-border, rgba(255,255,255,0.06))' }}\n >\n <span\n className=\"text-xs font-semibold uppercase tracking-wider\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {d.metric}\n </span>\n <span className=\"flex items-baseline gap-2\">\n <span\n className=\"text-lg font-bold\"\n style={{ color: 'var(--sk-text-primary, #ffffff)' }}\n >\n {formatValue(d.baseline)} → {formatValue(d.current)}\n </span>\n <span className=\"text-sm font-semibold\" style={{ color }}>\n {arrow} {Math.abs(d.deltaPercent)}%\n </span>\n </span>\n <span className=\"text-xs\" style={{ color: 'var(--sk-text-tertiary, #71717a)' }}>\n {d.timespan}\n </span>\n </div>\n );\n })}\n </div>\n\n <div\n className=\"flex items-center gap-2 px-5 py-3\"\n style={{\n background: 'color-mix(in srgb, #10b981 6%, transparent)',\n color: 'var(--sk-text-secondary, #a1a1aa)',\n }}\n >\n <span\n className=\"inline-block w-2 h-2 rounded-full shrink-0\"\n style={{ background: '#10b981' }}\n />\n <span className=\"text-xs font-medium\">\n Verified from live analytics\n {refreshedAt ? ` · Refreshed ${formatDate(refreshedAt)}` : ''}\n </span>\n </div>\n </div>\n );\n}\n"]}
@@ -0,0 +1,68 @@
1
+ import { P as PortfolioItemFull, j as PortfolioResultItem, M as MetricsDelta } from './types-He5A9okM.cjs';
2
+
3
+ /**
4
+ * curate-proof — single source of truth for which proof points a public case
5
+ * study is allowed to display, and in what shape.
6
+ *
7
+ * Two classes of problem this guards against (both shipped to production on
8
+ * upforge.io before this existed):
9
+ *
10
+ * 1. Repetition: generated items arrive with four "100/100 Lighthouse X"
11
+ * KPIs in the hero, the same four restated as Results cards, AND a full
12
+ * Performance section with score gauges. One differentiator repeated
13
+ * twelve times per page stops differentiating. The Performance section is
14
+ * the one home for the Lighthouse breakdown; hero/results/cards collapse
15
+ * to at most one summary tile.
16
+ *
17
+ * 2. Self-harming live metrics: MetricsDelta rows are computed from raw
18
+ * analytics baselines with no editorial judgment — a bounce rate of
19
+ * "100 → 99.69" rendered as a green win. Marketing surfaces only show a
20
+ * delta when it's a genuinely flattering, meaningful story.
21
+ *
22
+ * Used by PortfolioPage (hero KPIs + results items), PortfolioCard (KPI
23
+ * preview), and MetricsProvenance (delta gate). Change display policy here,
24
+ * nowhere else.
25
+ */
26
+
27
+ /**
28
+ * Drop Results cards that restate Lighthouse scores when the case study also
29
+ * renders a Performance section (the score breakdown's single home).
30
+ */
31
+ declare function curateResultItems(items: PortfolioResultItem[], hasPerformanceSection: boolean): PortfolioResultItem[];
32
+ /**
33
+ * Editorial gate for live metric deltas ("Verified from live analytics").
34
+ * `direction` comes from the platform with 'up' meaning IMPROVED (lower-is-
35
+ * better metrics are inverted server-side). A public case study only shows a
36
+ * delta when it's a win worth telling:
37
+ * - improved ('up'), never a regression or flat noise
38
+ * - moved by a double-digit relative margin (small drift isn't a story)
39
+ * - bounce-rate-style metrics must also be respectable in absolute terms —
40
+ * "100 → 99.69" is technically an improvement and still self-harm
41
+ */
42
+ declare function gateMetricsDeltas(deltas: MetricsDelta[] | undefined | null): MetricsDelta[];
43
+ /**
44
+ * Apply the full display policy to a portfolio item before rendering.
45
+ * Pure — returns a new item; the fetched payload is never mutated.
46
+ */
47
+ declare function curatePortfolioProof(item: PortfolioItemFull): PortfolioItemFull;
48
+
49
+ /**
50
+ * Presentation formatters shared by portfolio components and consuming apps.
51
+ */
52
+ /**
53
+ * Human label for a category slug. Single source of truth — HeroSection,
54
+ * PortfolioCard, and app-side filter chips all render categories through
55
+ * this. Underscores become spaces; hyphens are PRESERVED so compound slugs
56
+ * keep their canonical form ("e-commerce" → "E-Commerce", never "E Commerce"),
57
+ * and every word start (including after a hyphen) is capitalized.
58
+ */
59
+ declare function formatCategoryLabel(slug: string | undefined | null): string;
60
+ /**
61
+ * Normalize a service tag's casing for display: the first character of each
62
+ * word is uppercased, the rest left alone (so "Next.js development" becomes
63
+ * "Next.js Development" and "Technical SEO implementation" keeps "SEO").
64
+ * Fixes per-item casing drift in stored tags without touching the data.
65
+ */
66
+ declare function formatServiceTag(tag: string | undefined | null): string;
67
+
68
+ export { curateResultItems as a, formatServiceTag as b, curatePortfolioProof as c, formatCategoryLabel as f, gateMetricsDeltas as g };
@@ -0,0 +1,68 @@
1
+ import { P as PortfolioItemFull, j as PortfolioResultItem, M as MetricsDelta } from './types-He5A9okM.js';
2
+
3
+ /**
4
+ * curate-proof — single source of truth for which proof points a public case
5
+ * study is allowed to display, and in what shape.
6
+ *
7
+ * Two classes of problem this guards against (both shipped to production on
8
+ * upforge.io before this existed):
9
+ *
10
+ * 1. Repetition: generated items arrive with four "100/100 Lighthouse X"
11
+ * KPIs in the hero, the same four restated as Results cards, AND a full
12
+ * Performance section with score gauges. One differentiator repeated
13
+ * twelve times per page stops differentiating. The Performance section is
14
+ * the one home for the Lighthouse breakdown; hero/results/cards collapse
15
+ * to at most one summary tile.
16
+ *
17
+ * 2. Self-harming live metrics: MetricsDelta rows are computed from raw
18
+ * analytics baselines with no editorial judgment — a bounce rate of
19
+ * "100 → 99.69" rendered as a green win. Marketing surfaces only show a
20
+ * delta when it's a genuinely flattering, meaningful story.
21
+ *
22
+ * Used by PortfolioPage (hero KPIs + results items), PortfolioCard (KPI
23
+ * preview), and MetricsProvenance (delta gate). Change display policy here,
24
+ * nowhere else.
25
+ */
26
+
27
+ /**
28
+ * Drop Results cards that restate Lighthouse scores when the case study also
29
+ * renders a Performance section (the score breakdown's single home).
30
+ */
31
+ declare function curateResultItems(items: PortfolioResultItem[], hasPerformanceSection: boolean): PortfolioResultItem[];
32
+ /**
33
+ * Editorial gate for live metric deltas ("Verified from live analytics").
34
+ * `direction` comes from the platform with 'up' meaning IMPROVED (lower-is-
35
+ * better metrics are inverted server-side). A public case study only shows a
36
+ * delta when it's a win worth telling:
37
+ * - improved ('up'), never a regression or flat noise
38
+ * - moved by a double-digit relative margin (small drift isn't a story)
39
+ * - bounce-rate-style metrics must also be respectable in absolute terms —
40
+ * "100 → 99.69" is technically an improvement and still self-harm
41
+ */
42
+ declare function gateMetricsDeltas(deltas: MetricsDelta[] | undefined | null): MetricsDelta[];
43
+ /**
44
+ * Apply the full display policy to a portfolio item before rendering.
45
+ * Pure — returns a new item; the fetched payload is never mutated.
46
+ */
47
+ declare function curatePortfolioProof(item: PortfolioItemFull): PortfolioItemFull;
48
+
49
+ /**
50
+ * Presentation formatters shared by portfolio components and consuming apps.
51
+ */
52
+ /**
53
+ * Human label for a category slug. Single source of truth — HeroSection,
54
+ * PortfolioCard, and app-side filter chips all render categories through
55
+ * this. Underscores become spaces; hyphens are PRESERVED so compound slugs
56
+ * keep their canonical form ("e-commerce" → "E-Commerce", never "E Commerce"),
57
+ * and every word start (including after a hyphen) is capitalized.
58
+ */
59
+ declare function formatCategoryLabel(slug: string | undefined | null): string;
60
+ /**
61
+ * Normalize a service tag's casing for display: the first character of each
62
+ * word is uppercased, the rest left alone (so "Next.js development" becomes
63
+ * "Next.js Development" and "Technical SEO implementation" keeps "SEO").
64
+ * Fixes per-item casing drift in stored tags without touching the data.
65
+ */
66
+ declare function formatServiceTag(tag: string | undefined | null): string;
67
+
68
+ export { curateResultItems as a, formatServiceTag as b, curatePortfolioProof as c, formatCategoryLabel as f, gateMetricsDeltas as g };
package/dist/index.cjs CHANGED
@@ -1,9 +1,10 @@
1
1
  'use strict';
2
2
 
3
- var chunkQGSW43TM_cjs = require('./chunk-QGSW43TM.cjs');
3
+ var chunk2Y2RJB3A_cjs = require('./chunk-2Y2RJB3A.cjs');
4
+ var chunk6OWFFV6N_cjs = require('./chunk-6OWFFV6N.cjs');
5
+ require('./chunk-A5WCMHAE.cjs');
4
6
  var chunkZBPF7TLZ_cjs = require('./chunk-ZBPF7TLZ.cjs');
5
- var chunkUXUU7FVT_cjs = require('./chunk-UXUU7FVT.cjs');
6
- require('./chunk-7ZHRMWJU.cjs');
7
+ var chunkX76U7VLF_cjs = require('./chunk-X76U7VLF.cjs');
7
8
  var chunk5MEZU3OG_cjs = require('./chunk-5MEZU3OG.cjs');
8
9
  require('./chunk-FCKDJ4MJ.cjs');
9
10
  require('./chunk-HQGAX2LI.cjs');
@@ -200,27 +201,31 @@ function Icon({
200
201
 
201
202
  Object.defineProperty(exports, "PORTFOLIO_SECTION_TYPES", {
202
203
  enumerable: true,
203
- get: function () { return chunkQGSW43TM_cjs.PORTFOLIO_SECTION_TYPES; }
204
+ get: function () { return chunk2Y2RJB3A_cjs.PORTFOLIO_SECTION_TYPES; }
204
205
  });
205
206
  Object.defineProperty(exports, "PortfolioIndex", {
206
207
  enumerable: true,
207
- get: function () { return chunkQGSW43TM_cjs.PortfolioIndex; }
208
+ get: function () { return chunk2Y2RJB3A_cjs.PortfolioIndex; }
208
209
  });
209
210
  Object.defineProperty(exports, "PortfolioIndexSkeleton", {
210
211
  enumerable: true,
211
- get: function () { return chunkQGSW43TM_cjs.PortfolioIndexSkeleton; }
212
+ get: function () { return chunk2Y2RJB3A_cjs.PortfolioIndexSkeleton; }
212
213
  });
213
214
  Object.defineProperty(exports, "PortfolioPage", {
214
215
  enumerable: true,
215
- get: function () { return chunkQGSW43TM_cjs.PortfolioPage; }
216
+ get: function () { return chunk2Y2RJB3A_cjs.PortfolioPage; }
216
217
  });
217
218
  Object.defineProperty(exports, "PortfolioPageSkeleton", {
218
219
  enumerable: true,
219
- get: function () { return chunkQGSW43TM_cjs.PortfolioPageSkeleton; }
220
+ get: function () { return chunk2Y2RJB3A_cjs.PortfolioPageSkeleton; }
220
221
  });
221
222
  Object.defineProperty(exports, "PortfolioSchema", {
222
223
  enumerable: true,
223
- get: function () { return chunkQGSW43TM_cjs.PortfolioSchema; }
224
+ get: function () { return chunk2Y2RJB3A_cjs.PortfolioSchema; }
225
+ });
226
+ Object.defineProperty(exports, "MetricsProvenance", {
227
+ enumerable: true,
228
+ get: function () { return chunk6OWFFV6N_cjs.MetricsProvenance; }
224
229
  });
225
230
  Object.defineProperty(exports, "formatCategoryLabel", {
226
231
  enumerable: true,
@@ -230,25 +235,21 @@ Object.defineProperty(exports, "formatServiceTag", {
230
235
  enumerable: true,
231
236
  get: function () { return chunkZBPF7TLZ_cjs.formatServiceTag; }
232
237
  });
233
- Object.defineProperty(exports, "MetricsProvenance", {
234
- enumerable: true,
235
- get: function () { return chunkUXUU7FVT_cjs.MetricsProvenance; }
236
- });
237
238
  Object.defineProperty(exports, "curateHeroKpis", {
238
239
  enumerable: true,
239
- get: function () { return chunkUXUU7FVT_cjs.curateHeroKpis; }
240
+ get: function () { return chunkX76U7VLF_cjs.curateHeroKpis; }
240
241
  });
241
242
  Object.defineProperty(exports, "curatePortfolioProof", {
242
243
  enumerable: true,
243
- get: function () { return chunkUXUU7FVT_cjs.curatePortfolioProof; }
244
+ get: function () { return chunkX76U7VLF_cjs.curatePortfolioProof; }
244
245
  });
245
246
  Object.defineProperty(exports, "curateResultItems", {
246
247
  enumerable: true,
247
- get: function () { return chunkUXUU7FVT_cjs.curateResultItems; }
248
+ get: function () { return chunkX76U7VLF_cjs.curateResultItems; }
248
249
  });
249
250
  Object.defineProperty(exports, "gateMetricsDeltas", {
250
251
  enumerable: true,
251
- get: function () { return chunkUXUU7FVT_cjs.gateMetricsDeltas; }
252
+ get: function () { return chunkX76U7VLF_cjs.gateMetricsDeltas; }
252
253
  });
253
254
  Object.defineProperty(exports, "AgencySiteKitLayout", {
254
255
  enumerable: true,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/shared/icons.ts"],"names":["React"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @sonordev/agency-site-kit — Icon registry\n *\n * Lightweight icon system that maps string names to SVG path data.\n * No external icon library dependency — keeps the bundle small.\n * Icons are rendered as inline SVGs.\n */\n\nimport React from 'react';\n\ninterface IconDefinition {\n viewBox: string;\n paths: string[];\n /** Optional fill-rule for complex paths */\n fillRule?: 'evenodd' | 'nonzero';\n}\n\n/**\n * Icon registry — 24x24 viewBox icons (matching Lucide/Heroicons conventions).\n * All paths use stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\".\n */\nconst ICON_REGISTRY: Record<string, IconDefinition> = {\n 'arrow-up': {\n viewBox: '0 0 24 24',\n paths: ['M12 19V5', 'M5 12l7-7 7 7'],\n },\n 'arrow-down': {\n viewBox: '0 0 24 24',\n paths: ['M12 5v14', 'M19 12l-7 7-7-7'],\n },\n 'chart-bar': {\n viewBox: '0 0 24 24',\n paths: ['M12 20V10', 'M18 20V4', 'M6 20v-4'],\n },\n check: {\n viewBox: '0 0 24 24',\n paths: ['M20 6L9 17l-5-5'],\n },\n clock: {\n viewBox: '0 0 24 24',\n paths: ['M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z', 'M12 6v6l4 2'],\n },\n code: {\n viewBox: '0 0 24 24',\n paths: ['M16 18l6-6-6-6', 'M8 6l-6 6 6 6'],\n },\n globe: {\n viewBox: '0 0 24 24',\n paths: [\n 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z',\n 'M2 12h20',\n 'M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z',\n ],\n },\n layout: {\n viewBox: '0 0 24 24',\n paths: [\n 'M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z',\n 'M3 9h18',\n 'M9 21V9',\n ],\n },\n lightning: {\n viewBox: '0 0 24 24',\n paths: ['M13 2L3 14h9l-1 8 10-12h-9l1-8'],\n },\n link: {\n viewBox: '0 0 24 24',\n paths: [\n 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71',\n 'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71',\n ],\n },\n lock: {\n viewBox: '0 0 24 24',\n paths: [\n 'M19 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2z',\n 'M7 11V7a5 5 0 0 1 10 0v4',\n ],\n },\n mail: {\n viewBox: '0 0 24 24',\n paths: [\n 'M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z',\n 'M22 6l-10 7L2 6',\n ],\n },\n monitor: {\n viewBox: '0 0 24 24',\n paths: [\n 'M20 3H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z',\n 'M8 21h8',\n 'M12 17v4',\n ],\n },\n palette: {\n viewBox: '0 0 24 24',\n paths: [\n 'M12 2C6.49 2 2 6.49 2 12s4.49 10 10 10a2 2 0 0 0 2-2v-.09a1.65 1.65 0 0 1 .92-1.48A1.65 1.65 0 0 1 16.5 19H19a2 2 0 0 0 2-2 10 10 0 0 0-9-15z',\n 'M6.5 11.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z',\n 'M10 7.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z',\n 'M14 7.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z',\n 'M17.5 11.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z',\n ],\n },\n rocket: {\n viewBox: '0 0 24 24',\n paths: [\n 'M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z',\n 'M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z',\n 'M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0',\n 'M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5',\n ],\n },\n search: {\n viewBox: '0 0 24 24',\n paths: [\n 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z',\n 'M21 21l-4.35-4.35',\n ],\n },\n settings: {\n viewBox: '0 0 24 24',\n paths: [\n 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',\n 'M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1.08-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c.26.604.852.997 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1.08z',\n ],\n },\n shield: {\n viewBox: '0 0 24 24',\n paths: ['M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z'],\n },\n star: {\n viewBox: '0 0 24 24',\n paths: [\n 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z',\n ],\n },\n target: {\n viewBox: '0 0 24 24',\n paths: [\n 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z',\n 'M12 18a6 6 0 1 0 0-12 6 6 0 0 0 0 12z',\n 'M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4z',\n ],\n },\n 'trending-up': {\n viewBox: '0 0 24 24',\n paths: ['M23 6l-9.5 9.5-5-5L1 18', 'M17 6h6v6'],\n },\n users: {\n viewBox: '0 0 24 24',\n paths: [\n 'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2',\n 'M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',\n 'M23 21v-2a4 4 0 0 0-3-3.87',\n 'M16 3.13a4 4 0 0 1 0 7.75',\n ],\n },\n zap: {\n viewBox: '0 0 24 24',\n paths: ['M13 2L3 14h9l-1 8 10-12h-9l1-8'],\n },\n};\n\n/**\n * Returns the icon definition (viewBox + paths) for a given icon name.\n * Returns null if the icon is not found.\n */\nexport function getIconDefinition(name: string): IconDefinition | null {\n return ICON_REGISTRY[name] ?? null;\n}\n\n/**\n * Returns just the SVG path strings for a given icon name.\n * Useful when building custom SVG elements.\n */\nexport function getIconPaths(name: string): string[] | null {\n const def = ICON_REGISTRY[name];\n return def ? def.paths : null;\n}\n\n/**\n * Returns a list of all available icon names.\n */\nexport function getAvailableIcons(): string[] {\n return Object.keys(ICON_REGISTRY);\n}\n\n/**\n * Renders an icon as a React SVG element.\n *\n * @param name Icon name from the registry\n * @param size Width/height in pixels (default 24)\n * @param className Optional CSS class name\n * @returns React SVG element, or null if icon not found\n */\nexport function renderIcon(\n name: string,\n size: number = 24,\n className?: string,\n): React.ReactElement | null {\n const def = ICON_REGISTRY[name];\n if (!def) return null;\n\n return React.createElement(\n 'svg',\n {\n xmlns: 'http://www.w3.org/2000/svg',\n width: size,\n height: size,\n viewBox: def.viewBox,\n fill: 'none',\n stroke: 'currentColor',\n strokeWidth: 2,\n strokeLinecap: 'round' as const,\n strokeLinejoin: 'round' as const,\n className,\n 'aria-hidden': 'true',\n },\n ...def.paths.map((d, i) =>\n React.createElement('path', { key: i, d }),\n ),\n );\n}\n\n/**\n * Icon component for use in JSX.\n *\n * Usage:\n * ```tsx\n * <Icon name=\"trending-up\" size={20} className=\"text-green-500\" />\n * ```\n */\nexport function Icon({\n name,\n size = 24,\n className,\n}: {\n name: string;\n size?: number;\n className?: string;\n}): React.ReactElement | null {\n return renderIcon(name, size, className);\n}\n"]}
1
+ {"version":3,"sources":["../src/shared/icons.ts"],"names":["React"],"mappings":"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","file":"index.cjs","sourcesContent":["/**\n * @sonordev/agency-site-kit — Icon registry\n *\n * Lightweight icon system that maps string names to SVG path data.\n * No external icon library dependency — keeps the bundle small.\n * Icons are rendered as inline SVGs.\n */\n\nimport React from 'react';\n\ninterface IconDefinition {\n viewBox: string;\n paths: string[];\n /** Optional fill-rule for complex paths */\n fillRule?: 'evenodd' | 'nonzero';\n}\n\n/**\n * Icon registry — 24x24 viewBox icons (matching Lucide/Heroicons conventions).\n * All paths use stroke=\"currentColor\" strokeWidth=\"2\" strokeLinecap=\"round\" strokeLinejoin=\"round\".\n */\nconst ICON_REGISTRY: Record<string, IconDefinition> = {\n 'arrow-up': {\n viewBox: '0 0 24 24',\n paths: ['M12 19V5', 'M5 12l7-7 7 7'],\n },\n 'arrow-down': {\n viewBox: '0 0 24 24',\n paths: ['M12 5v14', 'M19 12l-7 7-7-7'],\n },\n 'chart-bar': {\n viewBox: '0 0 24 24',\n paths: ['M12 20V10', 'M18 20V4', 'M6 20v-4'],\n },\n check: {\n viewBox: '0 0 24 24',\n paths: ['M20 6L9 17l-5-5'],\n },\n clock: {\n viewBox: '0 0 24 24',\n paths: ['M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z', 'M12 6v6l4 2'],\n },\n code: {\n viewBox: '0 0 24 24',\n paths: ['M16 18l6-6-6-6', 'M8 6l-6 6 6 6'],\n },\n globe: {\n viewBox: '0 0 24 24',\n paths: [\n 'M12 2a10 10 0 1 0 0 20 10 10 0 0 0 0-20z',\n 'M2 12h20',\n 'M12 2a15.3 15.3 0 0 1 4 10 15.3 15.3 0 0 1-4 10 15.3 15.3 0 0 1-4-10A15.3 15.3 0 0 1 12 2z',\n ],\n },\n layout: {\n viewBox: '0 0 24 24',\n paths: [\n 'M19 3H5a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z',\n 'M3 9h18',\n 'M9 21V9',\n ],\n },\n lightning: {\n viewBox: '0 0 24 24',\n paths: ['M13 2L3 14h9l-1 8 10-12h-9l1-8'],\n },\n link: {\n viewBox: '0 0 24 24',\n paths: [\n 'M10 13a5 5 0 0 0 7.54.54l3-3a5 5 0 0 0-7.07-7.07l-1.72 1.71',\n 'M14 11a5 5 0 0 0-7.54-.54l-3 3a5 5 0 0 0 7.07 7.07l1.71-1.71',\n ],\n },\n lock: {\n viewBox: '0 0 24 24',\n paths: [\n 'M19 11H5a2 2 0 0 0-2 2v7a2 2 0 0 0 2 2h14a2 2 0 0 0 2-2v-7a2 2 0 0 0-2-2z',\n 'M7 11V7a5 5 0 0 1 10 0v4',\n ],\n },\n mail: {\n viewBox: '0 0 24 24',\n paths: [\n 'M4 4h16c1.1 0 2 .9 2 2v12c0 1.1-.9 2-2 2H4c-1.1 0-2-.9-2-2V6c0-1.1.9-2 2-2z',\n 'M22 6l-10 7L2 6',\n ],\n },\n monitor: {\n viewBox: '0 0 24 24',\n paths: [\n 'M20 3H4a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V5a2 2 0 0 0-2-2z',\n 'M8 21h8',\n 'M12 17v4',\n ],\n },\n palette: {\n viewBox: '0 0 24 24',\n paths: [\n 'M12 2C6.49 2 2 6.49 2 12s4.49 10 10 10a2 2 0 0 0 2-2v-.09a1.65 1.65 0 0 1 .92-1.48A1.65 1.65 0 0 1 16.5 19H19a2 2 0 0 0 2-2 10 10 0 0 0-9-15z',\n 'M6.5 11.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z',\n 'M10 7.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z',\n 'M14 7.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z',\n 'M17.5 11.5a1.5 1.5 0 1 0 0-3 1.5 1.5 0 0 0 0 3z',\n ],\n },\n rocket: {\n viewBox: '0 0 24 24',\n paths: [\n 'M4.5 16.5c-1.5 1.26-2 5-2 5s3.74-.5 5-2c.71-.84.7-2.13-.09-2.91a2.18 2.18 0 0 0-2.91-.09z',\n 'M12 15l-3-3a22 22 0 0 1 2-3.95A12.88 12.88 0 0 1 22 2c0 2.72-.78 7.5-6 11a22.35 22.35 0 0 1-4 2z',\n 'M9 12H4s.55-3.03 2-4c1.62-1.08 5 0 5 0',\n 'M12 15v5s3.03-.55 4-2c1.08-1.62 0-5 0-5',\n ],\n },\n search: {\n viewBox: '0 0 24 24',\n paths: [\n 'M11 19a8 8 0 1 0 0-16 8 8 0 0 0 0 16z',\n 'M21 21l-4.35-4.35',\n ],\n },\n settings: {\n viewBox: '0 0 24 24',\n paths: [\n 'M12 15a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',\n 'M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09a1.65 1.65 0 0 0-1.08-1.51 1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09a1.65 1.65 0 0 0 1.51-1.08 1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9c.26.604.852.997 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1.08z',\n ],\n },\n shield: {\n viewBox: '0 0 24 24',\n paths: ['M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z'],\n },\n star: {\n viewBox: '0 0 24 24',\n paths: [\n 'M12 2l3.09 6.26L22 9.27l-5 4.87 1.18 6.88L12 17.77l-6.18 3.25L7 14.14 2 9.27l6.91-1.01L12 2z',\n ],\n },\n target: {\n viewBox: '0 0 24 24',\n paths: [\n 'M12 22a10 10 0 1 0 0-20 10 10 0 0 0 0 20z',\n 'M12 18a6 6 0 1 0 0-12 6 6 0 0 0 0 12z',\n 'M12 14a2 2 0 1 0 0-4 2 2 0 0 0 0 4z',\n ],\n },\n 'trending-up': {\n viewBox: '0 0 24 24',\n paths: ['M23 6l-9.5 9.5-5-5L1 18', 'M17 6h6v6'],\n },\n users: {\n viewBox: '0 0 24 24',\n paths: [\n 'M17 21v-2a4 4 0 0 0-4-4H5a4 4 0 0 0-4 4v2',\n 'M9 11a4 4 0 1 0 0-8 4 4 0 0 0 0 8z',\n 'M23 21v-2a4 4 0 0 0-3-3.87',\n 'M16 3.13a4 4 0 0 1 0 7.75',\n ],\n },\n zap: {\n viewBox: '0 0 24 24',\n paths: ['M13 2L3 14h9l-1 8 10-12h-9l1-8'],\n },\n};\n\n/**\n * Returns the icon definition (viewBox + paths) for a given icon name.\n * Returns null if the icon is not found.\n */\nexport function getIconDefinition(name: string): IconDefinition | null {\n return ICON_REGISTRY[name] ?? null;\n}\n\n/**\n * Returns just the SVG path strings for a given icon name.\n * Useful when building custom SVG elements.\n */\nexport function getIconPaths(name: string): string[] | null {\n const def = ICON_REGISTRY[name];\n return def ? def.paths : null;\n}\n\n/**\n * Returns a list of all available icon names.\n */\nexport function getAvailableIcons(): string[] {\n return Object.keys(ICON_REGISTRY);\n}\n\n/**\n * Renders an icon as a React SVG element.\n *\n * @param name Icon name from the registry\n * @param size Width/height in pixels (default 24)\n * @param className Optional CSS class name\n * @returns React SVG element, or null if icon not found\n */\nexport function renderIcon(\n name: string,\n size: number = 24,\n className?: string,\n): React.ReactElement | null {\n const def = ICON_REGISTRY[name];\n if (!def) return null;\n\n return React.createElement(\n 'svg',\n {\n xmlns: 'http://www.w3.org/2000/svg',\n width: size,\n height: size,\n viewBox: def.viewBox,\n fill: 'none',\n stroke: 'currentColor',\n strokeWidth: 2,\n strokeLinecap: 'round' as const,\n strokeLinejoin: 'round' as const,\n className,\n 'aria-hidden': 'true',\n },\n ...def.paths.map((d, i) =>\n React.createElement('path', { key: i, d }),\n ),\n );\n}\n\n/**\n * Icon component for use in JSX.\n *\n * Usage:\n * ```tsx\n * <Icon name=\"trending-up\" size={20} className=\"text-green-500\" />\n * ```\n */\nexport function Icon({\n name,\n size = 24,\n className,\n}: {\n name: string;\n size?: number;\n className?: string;\n}): React.ReactElement | null {\n return renderIcon(name, size, className);\n}\n"]}