@sonordev/agency-site-kit 0.4.1 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-4IES23GV.js → chunk-2RW4C42Y.js} +16 -4
- package/dist/chunk-2RW4C42Y.js.map +1 -0
- package/dist/{chunk-QGSW43TM.cjs → chunk-2Y2RJB3A.cjs} +12 -12
- package/dist/{chunk-QGSW43TM.cjs.map → chunk-2Y2RJB3A.cjs.map} +1 -1
- package/dist/{chunk-UXUU7FVT.cjs → chunk-6OWFFV6N.cjs} +4 -65
- package/dist/chunk-6OWFFV6N.cjs.map +1 -0
- package/dist/{chunk-NLGUPJZA.js → chunk-7FFZX7C5.js} +4 -4
- package/dist/{chunk-NLGUPJZA.js.map → chunk-7FFZX7C5.js.map} +1 -1
- package/dist/{chunk-7ZHRMWJU.cjs → chunk-A5WCMHAE.cjs} +2 -2
- package/dist/{chunk-7ZHRMWJU.cjs.map → chunk-A5WCMHAE.cjs.map} +1 -1
- package/dist/chunk-FM25LB67.js +50 -0
- package/dist/chunk-FM25LB67.js.map +1 -0
- package/dist/{chunk-KXLROAO5.js → chunk-FT2SNEYP.js} +2 -2
- package/dist/{chunk-KXLROAO5.js.map → chunk-FT2SNEYP.js.map} +1 -1
- package/dist/{chunk-5MEZU3OG.cjs → chunk-OG5RBKQJ.cjs} +15 -3
- package/dist/chunk-OG5RBKQJ.cjs.map +1 -0
- package/dist/chunk-X76U7VLF.cjs +57 -0
- package/dist/chunk-X76U7VLF.cjs.map +1 -0
- package/dist/{chunk-RWQ675N6.js → chunk-X7UCJ45A.js} +4 -61
- package/dist/chunk-X7UCJ45A.js.map +1 -0
- package/dist/format-C-axf13G.d.cts +68 -0
- package/dist/format-poUpLtQ2.d.ts +68 -0
- package/dist/index.cjs +25 -24
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -3
- package/dist/index.d.ts +5 -3
- package/dist/index.js +5 -4
- package/dist/index.js.map +1 -1
- package/dist/layout/index.cjs +2 -2
- package/dist/layout/index.d.cts +12 -2
- package/dist/layout/index.d.ts +12 -2
- package/dist/layout/index.js +1 -1
- package/dist/portfolio/client.cjs +2 -17
- package/dist/portfolio/client.cjs.map +1 -1
- package/dist/portfolio/client.js +1 -16
- package/dist/portfolio/client.js.map +1 -1
- package/dist/portfolio/index.cjs +18 -17
- package/dist/portfolio/index.d.cts +5 -74
- package/dist/portfolio/index.d.ts +5 -74
- package/dist/portfolio/index.js +4 -3
- package/dist/portfolio/sections/ChallengesSection.d.cts +1 -1
- package/dist/portfolio/sections/ChallengesSection.d.ts +1 -1
- package/dist/portfolio/sections/DetailsSection.d.cts +1 -1
- package/dist/portfolio/sections/DetailsSection.d.ts +1 -1
- package/dist/portfolio/sections/HeroSection.d.cts +1 -1
- package/dist/portfolio/sections/HeroSection.d.ts +1 -1
- package/dist/portfolio/sections/ResultsSection.cjs +4 -3
- package/dist/portfolio/sections/ResultsSection.cjs.map +1 -1
- package/dist/portfolio/sections/ResultsSection.d.cts +1 -1
- package/dist/portfolio/sections/ResultsSection.d.ts +1 -1
- package/dist/portfolio/sections/ResultsSection.js +2 -1
- package/dist/portfolio/sections/ResultsSection.js.map +1 -1
- package/dist/portfolio/sections/ServicesSection.d.cts +1 -1
- package/dist/portfolio/sections/ServicesSection.d.ts +1 -1
- package/dist/portfolio/sections/TechStackSection.d.cts +1 -1
- package/dist/portfolio/sections/TechStackSection.d.ts +1 -1
- package/dist/portfolio/sections/TestimonialSection.d.cts +1 -1
- package/dist/portfolio/sections/TestimonialSection.d.ts +1 -1
- package/dist/portfolio/server.cjs +40 -14
- package/dist/portfolio/server.d.cts +3 -1
- package/dist/portfolio/server.d.ts +3 -1
- package/dist/portfolio/server.js +3 -1
- package/dist/{types-0tVPy4pU.d.ts → types-He5A9okM.d.cts} +1 -1
- package/dist/{types-0tVPy4pU.d.cts → types-He5A9okM.d.ts} +1 -1
- package/package.json +3 -3
- package/dist/chunk-4IES23GV.js.map +0 -1
- package/dist/chunk-5MEZU3OG.cjs.map +0 -1
- package/dist/chunk-RWQ675N6.js.map +0 -1
- package/dist/chunk-UXUU7FVT.cjs.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/portfolio/components/PortfolioGrid.tsx","../../src/portfolio/components/PortfolioCard.tsx","../../src/portfolio/curate-proof.ts","../../src/portfolio/format.ts"],"sourcesContent":["// Shared component — no directive on purpose: hook-free, so it renders\n// server-side (zero JS) from PortfolioIndex and stays usable from client\n// code via the /portfolio/client barrel.\nimport React from 'react';\nimport type { PortfolioItem } from '../../types';\nimport ScrollReveal from '@sonordev/agency-site-kit/portfolio/primitives/ScrollReveal';\nimport PortfolioCard from './PortfolioCard';\n\ninterface PortfolioGridProps {\n items: PortfolioItem[];\n className?: string;\n /** Base path for portfolio detail links (default: '/work') */\n basePath?: string;\n}\n\nexport default function PortfolioGrid({ items, className = '', basePath = '/work' }: PortfolioGridProps) {\n return (\n <ScrollReveal stagger={0.1}>\n <div\n className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 ${className}`}\n style={{\n gap: 'var(--sk-grid-gap, 24px)',\n }}\n >\n {items.map((item) => (\n <PortfolioCard key={item.id} item={item} basePath={basePath} />\n ))}\n </div>\n </ScrollReveal>\n );\n}\n","// Shared component — no directive on purpose: hook-free, so it renders\n// server-side (zero JS) from PortfolioIndex and stays usable from client\n// code via the /portfolio/client barrel.\nimport React from 'react';\nimport Image from 'next/image';\nimport Link from 'next/link';\nimport type { PortfolioItem } from '../../types';\nimport GlassCard from '@sonordev/agency-site-kit/portfolio/primitives/GlassCard';\nimport { curateHeroKpis } from '../curate-proof';\nimport { formatCategoryLabel, formatServiceTag } from '../format';\n\ninterface PortfolioCardProps {\n item: PortfolioItem;\n className?: string;\n /** Base path for portfolio detail links (default: '/work') */\n basePath?: string;\n}\n\nexport default function PortfolioCard({ item, className = '', basePath = '/work' }: PortfolioCardProps) {\n // Use hero_screenshots desktop → hero_image → trifolio screenshot convention\n const thumbnailSrc =\n item.hero_screenshots?.desktop ||\n item.hero_image ||\n (item.project_id ? `/portfolio-previews/${item.slug}.png` : undefined);\n const kpis = Array.isArray(item.kpis) ? item.kpis : [];\n const services = Array.isArray(item.services) ? item.services : [];\n // Collapse per-category Lighthouse KPIs so a card never reads\n // \"100/100 · 100/100 · 100/100\" — business outcomes lead the preview.\n const kpiPreview = curateHeroKpis(kpis).slice(0, 3);\n const servicePreview = services.slice(0, 3);\n\n return (\n <Link href={`${basePath}/${item.slug}`} className={`block group ${className}`}>\n <GlassCard padding=\"sm\" hover>\n <div className=\"flex flex-col gap-4\">\n {/* Thumbnail */}\n {thumbnailSrc && (\n <div\n className=\"relative w-full overflow-hidden rounded-xl\"\n style={{ aspectRatio: '16/10' }}\n >\n <Image\n src={thumbnailSrc}\n alt={item.hero_image_alt || item.title}\n fill\n sizes=\"(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw\"\n className=\"object-cover object-top transition-transform duration-500 group-hover:scale-105\"\n />\n {/* Category badge */}\n <span\n className=\"absolute top-3 left-3 px-3 py-1 rounded-full text-[11px] uppercase tracking-wider font-semibold\"\n style={{\n background: 'color-mix(in srgb, var(--sk-primary, #6366f1) 80%, transparent)',\n color: '#ffffff',\n backdropFilter: 'blur(8px)',\n }}\n >\n {formatCategoryLabel(item.category)}\n </span>\n </div>\n )}\n\n {/* Text content */}\n <div className=\"flex flex-col gap-2 px-2 pb-2\">\n <h3\n className=\"text-lg font-semibold line-clamp-1\"\n style={{ color: 'var(--sk-text-primary, #ffffff)' }}\n >\n {item.title}\n </h3>\n\n {item.subtitle && (\n <p\n className=\"text-sm line-clamp-2\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {item.subtitle}\n </p>\n )}\n\n {/* KPI preview */}\n {kpiPreview.length > 0 && (\n <div className=\"flex items-center gap-4 mt-1\">\n {kpiPreview.map((kpi, i) => (\n <div key={i} className=\"flex flex-col\">\n <span\n className=\"text-base font-bold\"\n style={{ color: 'var(--sk-primary, #6366f1)' }}\n >\n {kpi.prefix || ''}\n {kpi.value.toLocaleString()}\n {kpi.suffix}\n </span>\n <span\n className=\"text-[10px] uppercase tracking-wider font-medium\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {kpi.label}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Services tags */}\n {servicePreview.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5 mt-2\">\n {servicePreview.map((service) => (\n <span\n key={service}\n className=\"px-2.5 py-0.5 rounded-full text-[11px] font-medium\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-secondary, #a1a1aa)',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n {formatServiceTag(service)}\n </span>\n ))}\n {services.length > 3 && (\n <span\n className=\"px-2.5 py-0.5 rounded-full text-[11px] font-medium\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-tertiary, #71717a)',\n }}\n >\n +{item.services.length - 3}\n </span>\n )}\n </div>\n )}\n </div>\n </div>\n </GlassCard>\n </Link>\n );\n}\n","/**\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 PortfolioKPI,\n PortfolioResultItem,\n PortfolioResultsData,\n} from '../types';\n\nconst LIGHTHOUSE_RE = /lighthouse/i;\n\n/** True when a KPI tile is a per-category Lighthouse score. */\nexport function isLighthouseKpi(kpi: PortfolioKPI): boolean {\n return LIGHTHOUSE_RE.test(kpi.label);\n}\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 * Collapse per-category Lighthouse KPIs to at most one summary tile, business\n * outcomes first. When every category is present at 100 the summary says so;\n * otherwise the Performance category (the one buyers know) represents the set.\n */\nexport function curateHeroKpis(kpis: PortfolioKPI[]): PortfolioKPI[] {\n const business = kpis.filter((k) => !isLighthouseKpi(k));\n const lighthouse = kpis.filter(isLighthouseKpi);\n if (lighthouse.length <= 1) return kpis;\n\n const allPerfect = lighthouse.length >= 4 && lighthouse.every((k) => k.value === 100);\n const summary: PortfolioKPI = allPerfect\n ? {\n ...lighthouse[0],\n label: 'Lighthouse, all 4 categories',\n description: 'Perfect scores across performance, SEO, accessibility, and best practices.',\n }\n : (lighthouse.find((k) => /performance/i.test(k.label)) ?? lighthouse[0]);\n\n return [...business, summary];\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","/**\n * Presentation formatters shared by portfolio components and consuming apps.\n */\n\n/**\n * Human label for a category slug. Single source of truth — HeroSection,\n * PortfolioCard, and app-side filter chips all render categories through\n * this. Underscores become spaces; hyphens are PRESERVED so compound slugs\n * keep their canonical form (\"e-commerce\" → \"E-Commerce\", never \"E Commerce\"),\n * and every word start (including after a hyphen) is capitalized.\n */\nexport function formatCategoryLabel(slug: string | undefined | null): string {\n if (!slug) return '';\n return slug.replace(/_/g, ' ').replace(/\\b\\w/g, (c: string) => c.toUpperCase());\n}\n\n/**\n * Normalize a service tag's casing for display: the first character of each\n * word is uppercased, the rest left alone (so \"Next.js development\" becomes\n * \"Next.js Development\" and \"Technical SEO implementation\" keeps \"SEO\").\n * Fixes per-item casing drift in stored tags without touching the data.\n */\nexport function formatServiceTag(tag: string | undefined | null): string {\n if (!tag) return '';\n return tag.replace(/(^|[\\s/])([a-z])/g, (_m, sep: string, ch: string) => sep + ch.toUpperCase());\n}\n"],"mappings":"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","names":["jsx"]}
|
|
1
|
+
{"version":3,"sources":["../../src/portfolio/components/PortfolioGrid.tsx","../../src/portfolio/components/PortfolioCard.tsx","../../src/portfolio/curate-proof.ts","../../src/portfolio/format.ts"],"sourcesContent":["// Shared component — no directive on purpose: hook-free, so it renders\n// server-side (zero JS) from PortfolioIndex and stays usable from client\n// code via the /portfolio/client barrel.\nimport React from 'react';\nimport type { PortfolioItem } from '../../types';\nimport ScrollReveal from '@sonordev/agency-site-kit/portfolio/primitives/ScrollReveal';\nimport PortfolioCard from './PortfolioCard';\n\ninterface PortfolioGridProps {\n items: PortfolioItem[];\n className?: string;\n /** Base path for portfolio detail links (default: '/work') */\n basePath?: string;\n}\n\nexport default function PortfolioGrid({ items, className = '', basePath = '/work' }: PortfolioGridProps) {\n return (\n <ScrollReveal stagger={0.1}>\n <div\n className={`grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 ${className}`}\n style={{\n gap: 'var(--sk-grid-gap, 24px)',\n }}\n >\n {items.map((item) => (\n <PortfolioCard key={item.id} item={item} basePath={basePath} />\n ))}\n </div>\n </ScrollReveal>\n );\n}\n","// Shared component — no directive on purpose: hook-free, so it renders\n// server-side (zero JS) from PortfolioIndex and stays usable from client\n// code via the /portfolio/client barrel.\nimport React from 'react';\nimport Image from 'next/image';\nimport Link from 'next/link';\nimport type { PortfolioItem } from '../../types';\nimport GlassCard from '@sonordev/agency-site-kit/portfolio/primitives/GlassCard';\nimport { curateHeroKpis } from '../curate-proof';\nimport { formatCategoryLabel, formatServiceTag } from '../format';\n\ninterface PortfolioCardProps {\n item: PortfolioItem;\n className?: string;\n /** Base path for portfolio detail links (default: '/work') */\n basePath?: string;\n}\n\nexport default function PortfolioCard({ item, className = '', basePath = '/work' }: PortfolioCardProps) {\n // Use hero_screenshots desktop → hero_image → trifolio screenshot convention\n const thumbnailSrc =\n item.hero_screenshots?.desktop ||\n item.hero_image ||\n (item.project_id ? `/portfolio-previews/${item.slug}.png` : undefined);\n const kpis = Array.isArray(item.kpis) ? item.kpis : [];\n const services = Array.isArray(item.services) ? item.services : [];\n // Collapse per-category Lighthouse KPIs so a card never reads\n // \"100/100 · 100/100 · 100/100\" — business outcomes lead the preview.\n const kpiPreview = curateHeroKpis(kpis).slice(0, 3);\n const servicePreview = services.slice(0, 3);\n\n return (\n <Link href={`${basePath}/${item.slug}`} className={`block group ${className}`}>\n <GlassCard padding=\"sm\" hover>\n <div className=\"flex flex-col gap-4\">\n {/* Thumbnail */}\n {thumbnailSrc && (\n <div\n className=\"relative w-full overflow-hidden rounded-xl\"\n style={{ aspectRatio: '16/10' }}\n >\n <Image\n src={thumbnailSrc}\n alt={item.hero_image_alt || item.title}\n fill\n sizes=\"(max-width: 768px) 100vw, (max-width: 1024px) 50vw, 33vw\"\n className=\"object-cover object-top transition-transform duration-500 group-hover:scale-105\"\n />\n {/* Category badge */}\n <span\n className=\"absolute top-3 left-3 px-3 py-1 rounded-full text-[11px] uppercase tracking-wider font-semibold\"\n style={{\n background: 'color-mix(in srgb, var(--sk-primary, #6366f1) 80%, transparent)',\n color: '#ffffff',\n backdropFilter: 'blur(8px)',\n }}\n >\n {formatCategoryLabel(item.category)}\n </span>\n </div>\n )}\n\n {/* Text content */}\n <div className=\"flex flex-col gap-2 px-2 pb-2\">\n <h3\n className=\"text-lg font-semibold line-clamp-1\"\n style={{ color: 'var(--sk-text-primary, #ffffff)' }}\n >\n {item.title}\n </h3>\n\n {item.subtitle && (\n <p\n className=\"text-sm line-clamp-2\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {item.subtitle}\n </p>\n )}\n\n {/* KPI preview */}\n {kpiPreview.length > 0 && (\n <div className=\"flex items-center gap-4 mt-1\">\n {kpiPreview.map((kpi, i) => (\n <div key={i} className=\"flex flex-col\">\n <span\n className=\"text-base font-bold\"\n style={{ color: 'var(--sk-primary, #6366f1)' }}\n >\n {kpi.prefix || ''}\n {kpi.value.toLocaleString()}\n {kpi.suffix}\n </span>\n <span\n className=\"text-[10px] uppercase tracking-wider font-medium\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {kpi.label}\n </span>\n </div>\n ))}\n </div>\n )}\n\n {/* Services tags */}\n {servicePreview.length > 0 && (\n <div className=\"flex flex-wrap gap-1.5 mt-2\">\n {servicePreview.map((service) => (\n <span\n key={service}\n className=\"px-2.5 py-0.5 rounded-full text-[11px] font-medium\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-secondary, #a1a1aa)',\n border: '1px solid var(--sk-border, rgba(255,255,255,0.1))',\n }}\n >\n {formatServiceTag(service)}\n </span>\n ))}\n {services.length > 3 && (\n <span\n className=\"px-2.5 py-0.5 rounded-full text-[11px] font-medium\"\n style={{\n background: 'var(--sk-surface, rgba(255,255,255,0.05))',\n color: 'var(--sk-text-tertiary, #71717a)',\n }}\n >\n +{item.services.length - 3}\n </span>\n )}\n </div>\n )}\n </div>\n </div>\n </GlassCard>\n </Link>\n );\n}\n","/**\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","/**\n * Presentation formatters shared by portfolio components and consuming apps.\n */\n\n/**\n * Human label for a category slug. Single source of truth — HeroSection,\n * PortfolioCard, and app-side filter chips all render categories through\n * this. Underscores become spaces; hyphens are PRESERVED so compound slugs\n * keep their canonical form (\"e-commerce\" → \"E-Commerce\", never \"E Commerce\"),\n * and every word start (including after a hyphen) is capitalized.\n */\nexport function formatCategoryLabel(slug: string | undefined | null): string {\n if (!slug) return '';\n return slug.replace(/_/g, ' ').replace(/\\b\\w/g, (c: string) => c.toUpperCase());\n}\n\n/**\n * Normalize a service tag's casing for display: the first character of each\n * word is uppercased, the rest left alone (so \"Next.js development\" becomes\n * \"Next.js Development\" and \"Technical SEO implementation\" keeps \"SEO\").\n * Fixes per-item casing drift in stored tags without touching the data.\n */\nexport function formatServiceTag(tag: string | undefined | null): string {\n if (!tag) return '';\n return tag.replace(/(^|[\\s/])([a-z])/g, (_m, sep: string, ch: string) => sep + ch.toUpperCase());\n}\n"],"mappings":"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","names":["jsx"]}
|
package/dist/portfolio/index.cjs
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
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
|
|
6
|
-
require('../chunk-7ZHRMWJU.cjs');
|
|
7
|
+
var chunkX76U7VLF_cjs = require('../chunk-X76U7VLF.cjs');
|
|
7
8
|
require('../chunk-FCKDJ4MJ.cjs');
|
|
8
9
|
require('../chunk-HQGAX2LI.cjs');
|
|
9
10
|
require('../chunk-XOWSXZYW.cjs');
|
|
@@ -12,27 +13,31 @@ require('../chunk-XOWSXZYW.cjs');
|
|
|
12
13
|
|
|
13
14
|
Object.defineProperty(exports, "PORTFOLIO_SECTION_TYPES", {
|
|
14
15
|
enumerable: true,
|
|
15
|
-
get: function () { return
|
|
16
|
+
get: function () { return chunk2Y2RJB3A_cjs.PORTFOLIO_SECTION_TYPES; }
|
|
16
17
|
});
|
|
17
18
|
Object.defineProperty(exports, "PortfolioIndex", {
|
|
18
19
|
enumerable: true,
|
|
19
|
-
get: function () { return
|
|
20
|
+
get: function () { return chunk2Y2RJB3A_cjs.PortfolioIndex; }
|
|
20
21
|
});
|
|
21
22
|
Object.defineProperty(exports, "PortfolioIndexSkeleton", {
|
|
22
23
|
enumerable: true,
|
|
23
|
-
get: function () { return
|
|
24
|
+
get: function () { return chunk2Y2RJB3A_cjs.PortfolioIndexSkeleton; }
|
|
24
25
|
});
|
|
25
26
|
Object.defineProperty(exports, "PortfolioPage", {
|
|
26
27
|
enumerable: true,
|
|
27
|
-
get: function () { return
|
|
28
|
+
get: function () { return chunk2Y2RJB3A_cjs.PortfolioPage; }
|
|
28
29
|
});
|
|
29
30
|
Object.defineProperty(exports, "PortfolioPageSkeleton", {
|
|
30
31
|
enumerable: true,
|
|
31
|
-
get: function () { return
|
|
32
|
+
get: function () { return chunk2Y2RJB3A_cjs.PortfolioPageSkeleton; }
|
|
32
33
|
});
|
|
33
34
|
Object.defineProperty(exports, "PortfolioSchema", {
|
|
34
35
|
enumerable: true,
|
|
35
|
-
get: function () { return
|
|
36
|
+
get: function () { return chunk2Y2RJB3A_cjs.PortfolioSchema; }
|
|
37
|
+
});
|
|
38
|
+
Object.defineProperty(exports, "MetricsProvenance", {
|
|
39
|
+
enumerable: true,
|
|
40
|
+
get: function () { return chunk6OWFFV6N_cjs.MetricsProvenance; }
|
|
36
41
|
});
|
|
37
42
|
Object.defineProperty(exports, "formatCategoryLabel", {
|
|
38
43
|
enumerable: true,
|
|
@@ -42,25 +47,21 @@ Object.defineProperty(exports, "formatServiceTag", {
|
|
|
42
47
|
enumerable: true,
|
|
43
48
|
get: function () { return chunkZBPF7TLZ_cjs.formatServiceTag; }
|
|
44
49
|
});
|
|
45
|
-
Object.defineProperty(exports, "MetricsProvenance", {
|
|
46
|
-
enumerable: true,
|
|
47
|
-
get: function () { return chunkUXUU7FVT_cjs.MetricsProvenance; }
|
|
48
|
-
});
|
|
49
50
|
Object.defineProperty(exports, "curateHeroKpis", {
|
|
50
51
|
enumerable: true,
|
|
51
|
-
get: function () { return
|
|
52
|
+
get: function () { return chunkX76U7VLF_cjs.curateHeroKpis; }
|
|
52
53
|
});
|
|
53
54
|
Object.defineProperty(exports, "curatePortfolioProof", {
|
|
54
55
|
enumerable: true,
|
|
55
|
-
get: function () { return
|
|
56
|
+
get: function () { return chunkX76U7VLF_cjs.curatePortfolioProof; }
|
|
56
57
|
});
|
|
57
58
|
Object.defineProperty(exports, "curateResultItems", {
|
|
58
59
|
enumerable: true,
|
|
59
|
-
get: function () { return
|
|
60
|
+
get: function () { return chunkX76U7VLF_cjs.curateResultItems; }
|
|
60
61
|
});
|
|
61
62
|
Object.defineProperty(exports, "gateMetricsDeltas", {
|
|
62
63
|
enumerable: true,
|
|
63
|
-
get: function () { return
|
|
64
|
+
get: function () { return chunkX76U7VLF_cjs.gateMetricsDeltas; }
|
|
64
65
|
});
|
|
65
66
|
//# sourceMappingURL=index.cjs.map
|
|
66
67
|
//# sourceMappingURL=index.cjs.map
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { P as PortfolioItemFull, M as MetricsDelta
|
|
3
|
-
export {
|
|
2
|
+
import { P as PortfolioItemFull, M as MetricsDelta } from '../types-He5A9okM.cjs';
|
|
3
|
+
export { k as MetricSource, l as MetricsSnapshot, m as PORTFOLIO_SECTION_TYPES, o as PortfolioBeforeAfterData, p as PortfolioCTAData, c as PortfolioChallengesData, s as PortfolioConversionFunnelData, d as PortfolioDetailsData, t as PortfolioFeatureSpotlightData, v as PortfolioGalleryData, e as PortfolioHeroData, x as PortfolioItem, y as PortfolioKPI, b as PortfolioListResponse, A as PortfolioMetricsTimelineData, f as PortfolioResultsData, a as PortfolioSection, C as PortfolioSectionData, D as PortfolioSectionDataMap, E as PortfolioSectionType, F as PortfolioSeoData, g as PortfolioServicesData, H as PortfolioStrategyData, J as PortfolioTeamData, h as PortfolioTechStackData, i as PortfolioTestimonialData, N as PortfolioVideoData } from '../types-He5A9okM.cjs';
|
|
4
|
+
export { c as curatePortfolioProof, a as curateResultItems, f as formatCategoryLabel, b as formatServiceTag, g as gateMetricsDeltas } from '../format-C-axf13G.cjs';
|
|
4
5
|
export * from '@sonordev/agency-site-kit/portfolio/client';
|
|
6
|
+
export { curateHeroKpis } from '@sonordev/site-kit/portfolio/contract';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* PortfolioPage — Server component that renders a full portfolio case study.
|
|
@@ -125,77 +127,6 @@ interface MetricsProvenanceProps {
|
|
|
125
127
|
}
|
|
126
128
|
declare function MetricsProvenance({ deltas, refreshedAt }: MetricsProvenanceProps): React.JSX.Element | null;
|
|
127
129
|
|
|
128
|
-
/**
|
|
129
|
-
* curate-proof — single source of truth for which proof points a public case
|
|
130
|
-
* study is allowed to display, and in what shape.
|
|
131
|
-
*
|
|
132
|
-
* Two classes of problem this guards against (both shipped to production on
|
|
133
|
-
* upforge.io before this existed):
|
|
134
|
-
*
|
|
135
|
-
* 1. Repetition: generated items arrive with four "100/100 Lighthouse X"
|
|
136
|
-
* KPIs in the hero, the same four restated as Results cards, AND a full
|
|
137
|
-
* Performance section with score gauges. One differentiator repeated
|
|
138
|
-
* twelve times per page stops differentiating. The Performance section is
|
|
139
|
-
* the one home for the Lighthouse breakdown; hero/results/cards collapse
|
|
140
|
-
* to at most one summary tile.
|
|
141
|
-
*
|
|
142
|
-
* 2. Self-harming live metrics: MetricsDelta rows are computed from raw
|
|
143
|
-
* analytics baselines with no editorial judgment — a bounce rate of
|
|
144
|
-
* "100 → 99.69" rendered as a green win. Marketing surfaces only show a
|
|
145
|
-
* delta when it's a genuinely flattering, meaningful story.
|
|
146
|
-
*
|
|
147
|
-
* Used by PortfolioPage (hero KPIs + results items), PortfolioCard (KPI
|
|
148
|
-
* preview), and MetricsProvenance (delta gate). Change display policy here,
|
|
149
|
-
* nowhere else.
|
|
150
|
-
*/
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Collapse per-category Lighthouse KPIs to at most one summary tile, business
|
|
154
|
-
* outcomes first. When every category is present at 100 the summary says so;
|
|
155
|
-
* otherwise the Performance category (the one buyers know) represents the set.
|
|
156
|
-
*/
|
|
157
|
-
declare function curateHeroKpis(kpis: PortfolioKPI[]): PortfolioKPI[];
|
|
158
|
-
/**
|
|
159
|
-
* Drop Results cards that restate Lighthouse scores when the case study also
|
|
160
|
-
* renders a Performance section (the score breakdown's single home).
|
|
161
|
-
*/
|
|
162
|
-
declare function curateResultItems(items: PortfolioResultItem[], hasPerformanceSection: boolean): PortfolioResultItem[];
|
|
163
|
-
/**
|
|
164
|
-
* Editorial gate for live metric deltas ("Verified from live analytics").
|
|
165
|
-
* `direction` comes from the platform with 'up' meaning IMPROVED (lower-is-
|
|
166
|
-
* better metrics are inverted server-side). A public case study only shows a
|
|
167
|
-
* delta when it's a win worth telling:
|
|
168
|
-
* - improved ('up'), never a regression or flat noise
|
|
169
|
-
* - moved by a double-digit relative margin (small drift isn't a story)
|
|
170
|
-
* - bounce-rate-style metrics must also be respectable in absolute terms —
|
|
171
|
-
* "100 → 99.69" is technically an improvement and still self-harm
|
|
172
|
-
*/
|
|
173
|
-
declare function gateMetricsDeltas(deltas: MetricsDelta[] | undefined | null): MetricsDelta[];
|
|
174
|
-
/**
|
|
175
|
-
* Apply the full display policy to a portfolio item before rendering.
|
|
176
|
-
* Pure — returns a new item; the fetched payload is never mutated.
|
|
177
|
-
*/
|
|
178
|
-
declare function curatePortfolioProof(item: PortfolioItemFull): PortfolioItemFull;
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Presentation formatters shared by portfolio components and consuming apps.
|
|
182
|
-
*/
|
|
183
|
-
/**
|
|
184
|
-
* Human label for a category slug. Single source of truth — HeroSection,
|
|
185
|
-
* PortfolioCard, and app-side filter chips all render categories through
|
|
186
|
-
* this. Underscores become spaces; hyphens are PRESERVED so compound slugs
|
|
187
|
-
* keep their canonical form ("e-commerce" → "E-Commerce", never "E Commerce"),
|
|
188
|
-
* and every word start (including after a hyphen) is capitalized.
|
|
189
|
-
*/
|
|
190
|
-
declare function formatCategoryLabel(slug: string | undefined | null): string;
|
|
191
|
-
/**
|
|
192
|
-
* Normalize a service tag's casing for display: the first character of each
|
|
193
|
-
* word is uppercased, the rest left alone (so "Next.js development" becomes
|
|
194
|
-
* "Next.js Development" and "Technical SEO implementation" keeps "SEO").
|
|
195
|
-
* Fixes per-item casing drift in stored tags without touching the data.
|
|
196
|
-
*/
|
|
197
|
-
declare function formatServiceTag(tag: string | undefined | null): string;
|
|
198
|
-
|
|
199
130
|
/**
|
|
200
131
|
* Loading skeletons for portfolio routes — pure CSS pulse, server-safe,
|
|
201
132
|
* zero JS. Drop into `loading.tsx`:
|
|
@@ -212,4 +143,4 @@ declare function PortfolioIndexSkeleton({ cards }: {
|
|
|
212
143
|
/** Hero-shaped placeholder for /work/[slug] case study pages. */
|
|
213
144
|
declare function PortfolioPageSkeleton(): React.JSX.Element;
|
|
214
145
|
|
|
215
|
-
export { MetricsDelta, MetricsProvenance, PortfolioIndex, type PortfolioIndexProps, PortfolioIndexSkeleton, PortfolioItemFull,
|
|
146
|
+
export { MetricsDelta, MetricsProvenance, PortfolioIndex, type PortfolioIndexProps, PortfolioIndexSkeleton, PortfolioItemFull, PortfolioPage, PortfolioPageSkeleton, PortfolioSchema };
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { P as PortfolioItemFull, M as MetricsDelta
|
|
3
|
-
export {
|
|
2
|
+
import { P as PortfolioItemFull, M as MetricsDelta } from '../types-He5A9okM.js';
|
|
3
|
+
export { k as MetricSource, l as MetricsSnapshot, m as PORTFOLIO_SECTION_TYPES, o as PortfolioBeforeAfterData, p as PortfolioCTAData, c as PortfolioChallengesData, s as PortfolioConversionFunnelData, d as PortfolioDetailsData, t as PortfolioFeatureSpotlightData, v as PortfolioGalleryData, e as PortfolioHeroData, x as PortfolioItem, y as PortfolioKPI, b as PortfolioListResponse, A as PortfolioMetricsTimelineData, f as PortfolioResultsData, a as PortfolioSection, C as PortfolioSectionData, D as PortfolioSectionDataMap, E as PortfolioSectionType, F as PortfolioSeoData, g as PortfolioServicesData, H as PortfolioStrategyData, J as PortfolioTeamData, h as PortfolioTechStackData, i as PortfolioTestimonialData, N as PortfolioVideoData } from '../types-He5A9okM.js';
|
|
4
|
+
export { c as curatePortfolioProof, a as curateResultItems, f as formatCategoryLabel, b as formatServiceTag, g as gateMetricsDeltas } from '../format-poUpLtQ2.js';
|
|
4
5
|
export * from '@sonordev/agency-site-kit/portfolio/client';
|
|
6
|
+
export { curateHeroKpis } from '@sonordev/site-kit/portfolio/contract';
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* PortfolioPage — Server component that renders a full portfolio case study.
|
|
@@ -125,77 +127,6 @@ interface MetricsProvenanceProps {
|
|
|
125
127
|
}
|
|
126
128
|
declare function MetricsProvenance({ deltas, refreshedAt }: MetricsProvenanceProps): React.JSX.Element | null;
|
|
127
129
|
|
|
128
|
-
/**
|
|
129
|
-
* curate-proof — single source of truth for which proof points a public case
|
|
130
|
-
* study is allowed to display, and in what shape.
|
|
131
|
-
*
|
|
132
|
-
* Two classes of problem this guards against (both shipped to production on
|
|
133
|
-
* upforge.io before this existed):
|
|
134
|
-
*
|
|
135
|
-
* 1. Repetition: generated items arrive with four "100/100 Lighthouse X"
|
|
136
|
-
* KPIs in the hero, the same four restated as Results cards, AND a full
|
|
137
|
-
* Performance section with score gauges. One differentiator repeated
|
|
138
|
-
* twelve times per page stops differentiating. The Performance section is
|
|
139
|
-
* the one home for the Lighthouse breakdown; hero/results/cards collapse
|
|
140
|
-
* to at most one summary tile.
|
|
141
|
-
*
|
|
142
|
-
* 2. Self-harming live metrics: MetricsDelta rows are computed from raw
|
|
143
|
-
* analytics baselines with no editorial judgment — a bounce rate of
|
|
144
|
-
* "100 → 99.69" rendered as a green win. Marketing surfaces only show a
|
|
145
|
-
* delta when it's a genuinely flattering, meaningful story.
|
|
146
|
-
*
|
|
147
|
-
* Used by PortfolioPage (hero KPIs + results items), PortfolioCard (KPI
|
|
148
|
-
* preview), and MetricsProvenance (delta gate). Change display policy here,
|
|
149
|
-
* nowhere else.
|
|
150
|
-
*/
|
|
151
|
-
|
|
152
|
-
/**
|
|
153
|
-
* Collapse per-category Lighthouse KPIs to at most one summary tile, business
|
|
154
|
-
* outcomes first. When every category is present at 100 the summary says so;
|
|
155
|
-
* otherwise the Performance category (the one buyers know) represents the set.
|
|
156
|
-
*/
|
|
157
|
-
declare function curateHeroKpis(kpis: PortfolioKPI[]): PortfolioKPI[];
|
|
158
|
-
/**
|
|
159
|
-
* Drop Results cards that restate Lighthouse scores when the case study also
|
|
160
|
-
* renders a Performance section (the score breakdown's single home).
|
|
161
|
-
*/
|
|
162
|
-
declare function curateResultItems(items: PortfolioResultItem[], hasPerformanceSection: boolean): PortfolioResultItem[];
|
|
163
|
-
/**
|
|
164
|
-
* Editorial gate for live metric deltas ("Verified from live analytics").
|
|
165
|
-
* `direction` comes from the platform with 'up' meaning IMPROVED (lower-is-
|
|
166
|
-
* better metrics are inverted server-side). A public case study only shows a
|
|
167
|
-
* delta when it's a win worth telling:
|
|
168
|
-
* - improved ('up'), never a regression or flat noise
|
|
169
|
-
* - moved by a double-digit relative margin (small drift isn't a story)
|
|
170
|
-
* - bounce-rate-style metrics must also be respectable in absolute terms —
|
|
171
|
-
* "100 → 99.69" is technically an improvement and still self-harm
|
|
172
|
-
*/
|
|
173
|
-
declare function gateMetricsDeltas(deltas: MetricsDelta[] | undefined | null): MetricsDelta[];
|
|
174
|
-
/**
|
|
175
|
-
* Apply the full display policy to a portfolio item before rendering.
|
|
176
|
-
* Pure — returns a new item; the fetched payload is never mutated.
|
|
177
|
-
*/
|
|
178
|
-
declare function curatePortfolioProof(item: PortfolioItemFull): PortfolioItemFull;
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Presentation formatters shared by portfolio components and consuming apps.
|
|
182
|
-
*/
|
|
183
|
-
/**
|
|
184
|
-
* Human label for a category slug. Single source of truth — HeroSection,
|
|
185
|
-
* PortfolioCard, and app-side filter chips all render categories through
|
|
186
|
-
* this. Underscores become spaces; hyphens are PRESERVED so compound slugs
|
|
187
|
-
* keep their canonical form ("e-commerce" → "E-Commerce", never "E Commerce"),
|
|
188
|
-
* and every word start (including after a hyphen) is capitalized.
|
|
189
|
-
*/
|
|
190
|
-
declare function formatCategoryLabel(slug: string | undefined | null): string;
|
|
191
|
-
/**
|
|
192
|
-
* Normalize a service tag's casing for display: the first character of each
|
|
193
|
-
* word is uppercased, the rest left alone (so "Next.js development" becomes
|
|
194
|
-
* "Next.js Development" and "Technical SEO implementation" keeps "SEO").
|
|
195
|
-
* Fixes per-item casing drift in stored tags without touching the data.
|
|
196
|
-
*/
|
|
197
|
-
declare function formatServiceTag(tag: string | undefined | null): string;
|
|
198
|
-
|
|
199
130
|
/**
|
|
200
131
|
* Loading skeletons for portfolio routes — pure CSS pulse, server-safe,
|
|
201
132
|
* zero JS. Drop into `loading.tsx`:
|
|
@@ -212,4 +143,4 @@ declare function PortfolioIndexSkeleton({ cards }: {
|
|
|
212
143
|
/** Hero-shaped placeholder for /work/[slug] case study pages. */
|
|
213
144
|
declare function PortfolioPageSkeleton(): React.JSX.Element;
|
|
214
145
|
|
|
215
|
-
export { MetricsDelta, MetricsProvenance, PortfolioIndex, type PortfolioIndexProps, PortfolioIndexSkeleton, PortfolioItemFull,
|
|
146
|
+
export { MetricsDelta, MetricsProvenance, PortfolioIndex, type PortfolioIndexProps, PortfolioIndexSkeleton, PortfolioItemFull, PortfolioPage, PortfolioPageSkeleton, PortfolioSchema };
|
package/dist/portfolio/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export { PORTFOLIO_SECTION_TYPES, PortfolioIndex, PortfolioIndexSkeleton, PortfolioPage, PortfolioPageSkeleton, PortfolioSchema } from '../chunk-
|
|
1
|
+
export { PORTFOLIO_SECTION_TYPES, PortfolioIndex, PortfolioIndexSkeleton, PortfolioPage, PortfolioPageSkeleton, PortfolioSchema } from '../chunk-7FFZX7C5.js';
|
|
2
|
+
export { MetricsProvenance } from '../chunk-X7UCJ45A.js';
|
|
3
|
+
import '../chunk-FT2SNEYP.js';
|
|
2
4
|
export { formatCategoryLabel, formatServiceTag } from '../chunk-7D4H36UF.js';
|
|
3
|
-
export {
|
|
4
|
-
import '../chunk-KXLROAO5.js';
|
|
5
|
+
export { curateHeroKpis, curatePortfolioProof, curateResultItems, gateMetricsDeltas } from '../chunk-FM25LB67.js';
|
|
5
6
|
import '../chunk-N7TVKUYQ.js';
|
|
6
7
|
import '../chunk-L4EOQVRJ.js';
|
|
7
8
|
import '../chunk-QL6KVIFR.js';
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
var
|
|
3
|
+
var chunk6OWFFV6N_cjs = require('../../chunk-6OWFFV6N.cjs');
|
|
4
|
+
var chunkX76U7VLF_cjs = require('../../chunk-X76U7VLF.cjs');
|
|
4
5
|
var ScrollReveal = require('@sonordev/agency-site-kit/portfolio/primitives/ScrollReveal');
|
|
5
6
|
var AnimatedCounter = require('@sonordev/agency-site-kit/portfolio/primitives/AnimatedCounter');
|
|
6
7
|
var GlassCard = require('@sonordev/agency-site-kit/portfolio/primitives/GlassCard');
|
|
@@ -17,7 +18,7 @@ function ResultsSection({
|
|
|
17
18
|
metricsDelta,
|
|
18
19
|
metricsRefreshedAt
|
|
19
20
|
}) {
|
|
20
|
-
if (!data.items.length && !
|
|
21
|
+
if (!data.items.length && !chunkX76U7VLF_cjs.gateMetricsDeltas(metricsDelta).length) return null;
|
|
21
22
|
return /* @__PURE__ */ jsxRuntime.jsx(
|
|
22
23
|
"section",
|
|
23
24
|
{
|
|
@@ -97,7 +98,7 @@ function ResultsSection({
|
|
|
97
98
|
}
|
|
98
99
|
)
|
|
99
100
|
] }) }, index)) }) }),
|
|
100
|
-
metricsDelta && metricsDelta.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ScrollReveal__default.default, { y: 30, delay: 0.1, children: /* @__PURE__ */ jsxRuntime.jsx(
|
|
101
|
+
metricsDelta && metricsDelta.length > 0 && /* @__PURE__ */ jsxRuntime.jsx(ScrollReveal__default.default, { y: 30, delay: 0.1, children: /* @__PURE__ */ jsxRuntime.jsx(chunk6OWFFV6N_cjs.MetricsProvenance, { deltas: metricsDelta, refreshedAt: metricsRefreshedAt }) })
|
|
101
102
|
] })
|
|
102
103
|
}
|
|
103
104
|
);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/portfolio/components/sections/ResultsSection.tsx"],"names":["gateMetricsDeltas","jsx","jsxs","ScrollReveal","GlassCard","AnimatedCounter","MetricsProvenance"],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/portfolio/components/sections/ResultsSection.tsx"],"names":["gateMetricsDeltas","jsx","jsxs","ScrollReveal","GlassCard","AnimatedCounter","MetricsProvenance"],"mappings":"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","file":"ResultsSection.cjs","sourcesContent":["// Server component — renders on the server; the client primitives below cross the\n// RSC boundary via package subpaths (never import them relatively here).\nimport React from 'react';\nimport type { MetricsDelta, PortfolioResultsData } from '../../../types';\nimport ScrollReveal from '@sonordev/agency-site-kit/portfolio/primitives/ScrollReveal';\nimport AnimatedCounter from '@sonordev/agency-site-kit/portfolio/primitives/AnimatedCounter';\nimport GlassCard from '@sonordev/agency-site-kit/portfolio/primitives/GlassCard';\nimport MetricsProvenance from '../MetricsProvenance';\nimport { gateMetricsDeltas } from '../../curate-proof';\n\ninterface ResultsSectionProps {\n data: PortfolioResultsData;\n /** Live metric deltas computed by the platform (item.metricsDelta). */\n metricsDelta?: MetricsDelta[];\n /** ISO timestamp of the last live-metrics refresh. */\n metricsRefreshedAt?: string | null;\n}\n\nexport default function ResultsSection({\n data,\n metricsDelta,\n metricsRefreshedAt,\n}: ResultsSectionProps) {\n // Nothing survives display curation → no section, not an empty header.\n if (!data.items.length && !gateMetricsDeltas(metricsDelta).length) return null;\n\n return (\n <section\n className=\"w-full py-20 md:py-28\"\n style={{ background: 'var(--sk-bg, #0a0a0a)' }}\n >\n <div className=\"max-w-7xl mx-auto px-6\">\n <ScrollReveal y={30}>\n <h2\n className=\"text-3xl md:text-4xl font-bold mb-4\"\n style={{\n color: 'var(--sk-text-primary, #ffffff)',\n fontFamily: 'var(--sk-font-heading, inherit)',\n }}\n >\n Results\n </h2>\n <p\n className=\"text-lg mb-12 max-w-2xl\"\n style={{ color: 'var(--sk-text-secondary, #a1a1aa)' }}\n >\n Measurable outcomes that speak for themselves.\n </p>\n </ScrollReveal>\n\n <ScrollReveal stagger={0.1}>\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {data.items.map((item, index) => (\n <GlassCard key={index} padding=\"lg\" hover>\n <div className=\"flex flex-col gap-3\">\n {/* Metric counter */}\n {item.metric && (\n <div\n className=\"text-3xl md:text-4xl font-bold\"\n style={{ color: 'var(--sk-primary, #6366f1)' }}\n >\n <AnimatedCounter\n value={item.metric.value}\n suffix={item.metric.suffix}\n prefix={item.metric.prefix}\n duration={2.5}\n />\n </div>\n )}\n\n <h3\n className=\"text-lg font-semibold\"\n style={{ color: 'var(--sk-text-primary, #ffffff)' }}\n >\n {item.title}\n </h3>\n\n <p\n className=\"text-sm leading-relaxed\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {item.description}\n </p>\n\n {/* Source badge */}\n <span\n className=\"inline-flex self-start items-center gap-1 px-2.5 py-1 rounded-full text-[11px] uppercase tracking-wider font-medium mt-1\"\n style={{\n background:\n item.source === 'measured'\n ? 'color-mix(in srgb, #10b981 12%, transparent)'\n : 'color-mix(in srgb, #f59e0b 12%, transparent)',\n color: item.source === 'measured' ? '#10b981' : '#f59e0b',\n border: `1px solid ${\n item.source === 'measured'\n ? 'color-mix(in srgb, #10b981 20%, transparent)'\n : 'color-mix(in srgb, #f59e0b 20%, transparent)'\n }`,\n }}\n >\n {item.source === 'measured' ? (\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\">\n <path d=\"M10 3L4.5 8.5 2 6\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n ) : (\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\">\n <circle cx=\"6\" cy=\"6\" r=\"4\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n <path d=\"M6 4v2.5l1.5 1\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n </svg>\n )}\n {item.source}\n </span>\n </div>\n </GlassCard>\n ))}\n </div>\n </ScrollReveal>\n\n {/* Live verified metrics — the numbers a static portfolio can't fake */}\n {metricsDelta && metricsDelta.length > 0 && (\n <ScrollReveal y={30} delay={0.1}>\n <MetricsProvenance deltas={metricsDelta} refreshedAt={metricsRefreshedAt} />\n </ScrollReveal>\n )}\n </div>\n </section>\n );\n}\n"]}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { f as PortfolioResultsData, M as MetricsDelta } from '../../types-
|
|
2
|
+
import { f as PortfolioResultsData, M as MetricsDelta } from '../../types-He5A9okM.cjs';
|
|
3
3
|
|
|
4
4
|
interface ResultsSectionProps {
|
|
5
5
|
data: PortfolioResultsData;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React from 'react';
|
|
2
|
-
import { f as PortfolioResultsData, M as MetricsDelta } from '../../types-
|
|
2
|
+
import { f as PortfolioResultsData, M as MetricsDelta } from '../../types-He5A9okM.js';
|
|
3
3
|
|
|
4
4
|
interface ResultsSectionProps {
|
|
5
5
|
data: PortfolioResultsData;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { MetricsProvenance } from '../../chunk-X7UCJ45A.js';
|
|
2
|
+
import { gateMetricsDeltas } from '../../chunk-FM25LB67.js';
|
|
2
3
|
import ScrollReveal from '@sonordev/agency-site-kit/portfolio/primitives/ScrollReveal';
|
|
3
4
|
import AnimatedCounter from '@sonordev/agency-site-kit/portfolio/primitives/AnimatedCounter';
|
|
4
5
|
import GlassCard from '@sonordev/agency-site-kit/portfolio/primitives/GlassCard';
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../../src/portfolio/components/sections/ResultsSection.tsx"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"sources":["../../../src/portfolio/components/sections/ResultsSection.tsx"],"names":[],"mappings":"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","file":"ResultsSection.js","sourcesContent":["// Server component — renders on the server; the client primitives below cross the\n// RSC boundary via package subpaths (never import them relatively here).\nimport React from 'react';\nimport type { MetricsDelta, PortfolioResultsData } from '../../../types';\nimport ScrollReveal from '@sonordev/agency-site-kit/portfolio/primitives/ScrollReveal';\nimport AnimatedCounter from '@sonordev/agency-site-kit/portfolio/primitives/AnimatedCounter';\nimport GlassCard from '@sonordev/agency-site-kit/portfolio/primitives/GlassCard';\nimport MetricsProvenance from '../MetricsProvenance';\nimport { gateMetricsDeltas } from '../../curate-proof';\n\ninterface ResultsSectionProps {\n data: PortfolioResultsData;\n /** Live metric deltas computed by the platform (item.metricsDelta). */\n metricsDelta?: MetricsDelta[];\n /** ISO timestamp of the last live-metrics refresh. */\n metricsRefreshedAt?: string | null;\n}\n\nexport default function ResultsSection({\n data,\n metricsDelta,\n metricsRefreshedAt,\n}: ResultsSectionProps) {\n // Nothing survives display curation → no section, not an empty header.\n if (!data.items.length && !gateMetricsDeltas(metricsDelta).length) return null;\n\n return (\n <section\n className=\"w-full py-20 md:py-28\"\n style={{ background: 'var(--sk-bg, #0a0a0a)' }}\n >\n <div className=\"max-w-7xl mx-auto px-6\">\n <ScrollReveal y={30}>\n <h2\n className=\"text-3xl md:text-4xl font-bold mb-4\"\n style={{\n color: 'var(--sk-text-primary, #ffffff)',\n fontFamily: 'var(--sk-font-heading, inherit)',\n }}\n >\n Results\n </h2>\n <p\n className=\"text-lg mb-12 max-w-2xl\"\n style={{ color: 'var(--sk-text-secondary, #a1a1aa)' }}\n >\n Measurable outcomes that speak for themselves.\n </p>\n </ScrollReveal>\n\n <ScrollReveal stagger={0.1}>\n <div className=\"grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6\">\n {data.items.map((item, index) => (\n <GlassCard key={index} padding=\"lg\" hover>\n <div className=\"flex flex-col gap-3\">\n {/* Metric counter */}\n {item.metric && (\n <div\n className=\"text-3xl md:text-4xl font-bold\"\n style={{ color: 'var(--sk-primary, #6366f1)' }}\n >\n <AnimatedCounter\n value={item.metric.value}\n suffix={item.metric.suffix}\n prefix={item.metric.prefix}\n duration={2.5}\n />\n </div>\n )}\n\n <h3\n className=\"text-lg font-semibold\"\n style={{ color: 'var(--sk-text-primary, #ffffff)' }}\n >\n {item.title}\n </h3>\n\n <p\n className=\"text-sm leading-relaxed\"\n style={{ color: 'var(--sk-text-tertiary, #71717a)' }}\n >\n {item.description}\n </p>\n\n {/* Source badge */}\n <span\n className=\"inline-flex self-start items-center gap-1 px-2.5 py-1 rounded-full text-[11px] uppercase tracking-wider font-medium mt-1\"\n style={{\n background:\n item.source === 'measured'\n ? 'color-mix(in srgb, #10b981 12%, transparent)'\n : 'color-mix(in srgb, #f59e0b 12%, transparent)',\n color: item.source === 'measured' ? '#10b981' : '#f59e0b',\n border: `1px solid ${\n item.source === 'measured'\n ? 'color-mix(in srgb, #10b981 20%, transparent)'\n : 'color-mix(in srgb, #f59e0b 20%, transparent)'\n }`,\n }}\n >\n {item.source === 'measured' ? (\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\">\n <path d=\"M10 3L4.5 8.5 2 6\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" strokeLinejoin=\"round\" />\n </svg>\n ) : (\n <svg width=\"12\" height=\"12\" viewBox=\"0 0 12 12\" fill=\"none\">\n <circle cx=\"6\" cy=\"6\" r=\"4\" stroke=\"currentColor\" strokeWidth=\"1.5\" />\n <path d=\"M6 4v2.5l1.5 1\" stroke=\"currentColor\" strokeWidth=\"1.5\" strokeLinecap=\"round\" />\n </svg>\n )}\n {item.source}\n </span>\n </div>\n </GlassCard>\n ))}\n </div>\n </ScrollReveal>\n\n {/* Live verified metrics — the numbers a static portfolio can't fake */}\n {metricsDelta && metricsDelta.length > 0 && (\n <ScrollReveal y={30} delay={0.1}>\n <MetricsProvenance deltas={metricsDelta} refreshedAt={metricsRefreshedAt} />\n </ScrollReveal>\n )}\n </div>\n </section>\n );\n}\n"]}
|