@highjumpdigitalsoftware/blog-kit 0.6.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/INTEGRATION.md +76 -0
- package/LICENSE +74 -0
- package/README.md +102 -0
- package/astro/AdPreview.astro +64 -0
- package/astro/AdPreviewPair.astro +10 -0
- package/astro/AuditFindings.astro +29 -0
- package/astro/AuditScores.astro +60 -0
- package/astro/AuthorCard.astro +32 -0
- package/astro/BeforeAfter.astro +26 -0
- package/astro/BlogBehaviors.astro +15 -0
- package/astro/CTABanner.astro +28 -0
- package/astro/CalloutBox.astro +28 -0
- package/astro/CaseStudyHero.astro +45 -0
- package/astro/ChannelMixBars.astro +33 -0
- package/astro/Checklist.astro +24 -0
- package/astro/ChecklistItem.astro +15 -0
- package/astro/CodeSnippet.astro +20 -0
- package/astro/ComparisonTable.astro +103 -0
- package/astro/Definition.astro +30 -0
- package/astro/DeliveryComparison.astro +40 -0
- package/astro/FAQList.astro +43 -0
- package/astro/FurtherReading.astro +34 -0
- package/astro/ImageFeature.astro +22 -0
- package/astro/Infographic.astro +12 -0
- package/astro/KeyMetric.astro +40 -0
- package/astro/KeywordTable.astro +69 -0
- package/astro/List.astro +46 -0
- package/astro/MetricHighlight.astro +77 -0
- package/astro/NewsletterCTA.astro +40 -0
- package/astro/NumberedCard.astro +6 -0
- package/astro/ProConBlock.astro +38 -0
- package/astro/ProseList.astro +46 -0
- package/astro/QuoteBlock.astro +72 -0
- package/astro/RegionCallout.astro +24 -0
- package/astro/RelatedPosts.astro +47 -0
- package/astro/ResultsStrip.astro +59 -0
- package/astro/ScoreBar.astro +19 -0
- package/astro/SerpPreview.astro +35 -0
- package/astro/ServicePromoCard.astro +21 -0
- package/astro/StatCard.astro +48 -0
- package/astro/StepBlock.astro +5 -0
- package/astro/TableOfContents.astro +12 -0
- package/astro/TimelineBlock.astro +30 -0
- package/astro/TipBox.astro +14 -0
- package/astro/TrafficChart.astro +61 -0
- package/astro/VerdictCard.astro +48 -0
- package/astro/blogkit/Article.astro +63 -0
- package/astro/blogkit/BlogIndex.astro +144 -0
- package/core/behaviors/code.js +78 -0
- package/core/behaviors/comparison.js +52 -0
- package/core/behaviors/delivery-comparison.js +52 -0
- package/core/behaviors/faq.js +61 -0
- package/core/behaviors/index.d.ts +3 -0
- package/core/behaviors/index.js +35 -0
- package/core/behaviors/keyword-table.js +52 -0
- package/core/behaviors/toc.js +130 -0
- package/core/css/base.css +146 -0
- package/core/css/components.css +2632 -0
- package/core/css/index-listing.css +207 -0
- package/core/css/index.css +13 -0
- package/core/css/tokens.css +127 -0
- package/core/icons.ts +20 -0
- package/core/lib.ts +70 -0
- package/core/manifest/components.json +573 -0
- package/core/manifest/frontmatter.json +19 -0
- package/core/manifest/templates.json +77 -0
- package/dist/core/behaviors/code.js +78 -0
- package/dist/core/behaviors/comparison.js +52 -0
- package/dist/core/behaviors/delivery-comparison.js +52 -0
- package/dist/core/behaviors/faq.js +61 -0
- package/dist/core/behaviors/index.d.ts +3 -0
- package/dist/core/behaviors/index.js +35 -0
- package/dist/core/behaviors/keyword-table.js +52 -0
- package/dist/core/behaviors/toc.js +130 -0
- package/dist/core/css/base.css +146 -0
- package/dist/core/css/components.css +2632 -0
- package/dist/core/css/index-listing.css +207 -0
- package/dist/core/css/index.css +13 -0
- package/dist/core/css/tokens.css +127 -0
- package/dist/core/icons.d.ts +2 -0
- package/dist/core/icons.d.ts.map +1 -0
- package/dist/core/icons.js +20 -0
- package/dist/core/icons.js.map +1 -0
- package/dist/core/lib.d.ts +21 -0
- package/dist/core/lib.d.ts.map +1 -0
- package/dist/core/lib.js +57 -0
- package/dist/core/lib.js.map +1 -0
- package/dist/core/manifest/components.json +573 -0
- package/dist/core/manifest/frontmatter.json +19 -0
- package/dist/core/manifest/templates.json +77 -0
- package/dist/package/adapters/hjd-api.d.ts +14 -0
- package/dist/package/adapters/hjd-api.d.ts.map +1 -0
- package/dist/package/adapters/hjd-api.js +57 -0
- package/dist/package/adapters/hjd-api.js.map +1 -0
- package/dist/package/adapters/index.d.ts +13 -0
- package/dist/package/adapters/index.d.ts.map +1 -0
- package/dist/package/adapters/index.js +16 -0
- package/dist/package/adapters/index.js.map +1 -0
- package/dist/package/adapters/local.d.ts +13 -0
- package/dist/package/adapters/local.d.ts.map +1 -0
- package/dist/package/adapters/local.js +72 -0
- package/dist/package/adapters/local.js.map +1 -0
- package/dist/package/adapters/source.d.ts +39 -0
- package/dist/package/adapters/source.d.ts.map +1 -0
- package/dist/package/adapters/source.js +19 -0
- package/dist/package/adapters/source.js.map +1 -0
- package/dist/package/article.d.ts +17 -0
- package/dist/package/article.d.ts.map +1 -0
- package/dist/package/article.js +37 -0
- package/dist/package/article.js.map +1 -0
- package/dist/package/astro/data.d.ts +45 -0
- package/dist/package/astro/data.d.ts.map +1 -0
- package/dist/package/astro/data.js +81 -0
- package/dist/package/astro/data.js.map +1 -0
- package/dist/package/astro/freshness.d.ts +11 -0
- package/dist/package/astro/freshness.d.ts.map +1 -0
- package/dist/package/astro/freshness.js +48 -0
- package/dist/package/astro/freshness.js.map +1 -0
- package/dist/package/astro/index.d.ts +12 -0
- package/dist/package/astro/index.d.ts.map +1 -0
- package/dist/package/astro/index.js +31 -0
- package/dist/package/astro/index.js.map +1 -0
- package/dist/package/blog-index.d.ts +10 -0
- package/dist/package/blog-index.d.ts.map +1 -0
- package/dist/package/blog-index.js +27 -0
- package/dist/package/blog-index.js.map +1 -0
- package/dist/package/cli/exchange.d.ts +27 -0
- package/dist/package/cli/exchange.d.ts.map +1 -0
- package/dist/package/cli/exchange.js +94 -0
- package/dist/package/cli/exchange.js.map +1 -0
- package/dist/package/cli/index.d.ts +3 -0
- package/dist/package/cli/index.d.ts.map +1 -0
- package/dist/package/cli/index.js +301 -0
- package/dist/package/cli/index.js.map +1 -0
- package/dist/package/config/define.d.ts +13 -0
- package/dist/package/config/define.d.ts.map +1 -0
- package/dist/package/config/define.js +14 -0
- package/dist/package/config/define.js.map +1 -0
- package/dist/package/config/resolve.d.ts +11 -0
- package/dist/package/config/resolve.d.ts.map +1 -0
- package/dist/package/config/resolve.js +43 -0
- package/dist/package/config/resolve.js.map +1 -0
- package/dist/package/config/types.d.ts +74 -0
- package/dist/package/config/types.d.ts.map +1 -0
- package/dist/package/config/types.js +13 -0
- package/dist/package/config/types.js.map +1 -0
- package/dist/package/index-core.d.ts +28 -0
- package/dist/package/index-core.d.ts.map +1 -0
- package/dist/package/index-core.js +102 -0
- package/dist/package/index-core.js.map +1 -0
- package/dist/package/index.d.ts +13 -0
- package/dist/package/index.d.ts.map +1 -0
- package/dist/package/index.js +25 -0
- package/dist/package/index.js.map +1 -0
- package/dist/package/mdx/render-astro.d.ts +18 -0
- package/dist/package/mdx/render-astro.d.ts.map +1 -0
- package/dist/package/mdx/render-astro.js +75 -0
- package/dist/package/mdx/render-astro.js.map +1 -0
- package/dist/package/mdx/render.d.ts +13 -0
- package/dist/package/mdx/render.d.ts.map +1 -0
- package/dist/package/mdx/render.js +37 -0
- package/dist/package/mdx/render.js.map +1 -0
- package/dist/react/AdPreview.d.ts +26 -0
- package/dist/react/AdPreview.d.ts.map +1 -0
- package/dist/react/AdPreview.js +8 -0
- package/dist/react/AdPreview.js.map +1 -0
- package/dist/react/AdPreviewPair.d.ts +7 -0
- package/dist/react/AdPreviewPair.d.ts.map +1 -0
- package/dist/react/AdPreviewPair.js +5 -0
- package/dist/react/AdPreviewPair.js.map +1 -0
- package/dist/react/AuditFindings.d.ts +14 -0
- package/dist/react/AuditFindings.d.ts.map +1 -0
- package/dist/react/AuditFindings.js +5 -0
- package/dist/react/AuditFindings.js.map +1 -0
- package/dist/react/AuditScores.d.ts +12 -0
- package/dist/react/AuditScores.d.ts.map +1 -0
- package/dist/react/AuditScores.js +25 -0
- package/dist/react/AuditScores.js.map +1 -0
- package/dist/react/AuthorCard.d.ts +10 -0
- package/dist/react/AuthorCard.d.ts.map +1 -0
- package/dist/react/AuthorCard.js +6 -0
- package/dist/react/AuthorCard.js.map +1 -0
- package/dist/react/BeforeAfter.d.ts +12 -0
- package/dist/react/BeforeAfter.d.ts.map +1 -0
- package/dist/react/BeforeAfter.js +7 -0
- package/dist/react/BeforeAfter.js.map +1 -0
- package/dist/react/BlogBehaviors.d.ts +10 -0
- package/dist/react/BlogBehaviors.d.ts.map +1 -0
- package/dist/react/BlogBehaviors.js +20 -0
- package/dist/react/BlogBehaviors.js.map +1 -0
- package/dist/react/CTABanner.d.ts +8 -0
- package/dist/react/CTABanner.d.ts.map +1 -0
- package/dist/react/CTABanner.js +9 -0
- package/dist/react/CTABanner.js.map +1 -0
- package/dist/react/CalloutBox.d.ts +13 -0
- package/dist/react/CalloutBox.d.ts.map +1 -0
- package/dist/react/CalloutBox.js +9 -0
- package/dist/react/CalloutBox.js.map +1 -0
- package/dist/react/CaseStudyHero.d.ts +20 -0
- package/dist/react/CaseStudyHero.d.ts.map +1 -0
- package/dist/react/CaseStudyHero.js +7 -0
- package/dist/react/CaseStudyHero.js.map +1 -0
- package/dist/react/ChannelMixBars.d.ts +18 -0
- package/dist/react/ChannelMixBars.d.ts.map +1 -0
- package/dist/react/ChannelMixBars.js +6 -0
- package/dist/react/ChannelMixBars.js.map +1 -0
- package/dist/react/Checklist.d.ts +10 -0
- package/dist/react/Checklist.d.ts.map +1 -0
- package/dist/react/Checklist.js +7 -0
- package/dist/react/Checklist.js.map +1 -0
- package/dist/react/ChecklistItem.d.ts +7 -0
- package/dist/react/ChecklistItem.d.ts.map +1 -0
- package/dist/react/ChecklistItem.js +5 -0
- package/dist/react/ChecklistItem.js.map +1 -0
- package/dist/react/CodeSnippet.d.ts +17 -0
- package/dist/react/CodeSnippet.d.ts.map +1 -0
- package/dist/react/CodeSnippet.js +14 -0
- package/dist/react/CodeSnippet.js.map +1 -0
- package/dist/react/ComparisonTable.d.ts +22 -0
- package/dist/react/ComparisonTable.d.ts.map +1 -0
- package/dist/react/ComparisonTable.js +35 -0
- package/dist/react/ComparisonTable.js.map +1 -0
- package/dist/react/Definition.d.ts +9 -0
- package/dist/react/Definition.d.ts.map +1 -0
- package/dist/react/Definition.js +19 -0
- package/dist/react/Definition.js.map +1 -0
- package/dist/react/DeliveryComparison.d.ts +16 -0
- package/dist/react/DeliveryComparison.d.ts.map +1 -0
- package/dist/react/DeliveryComparison.js +7 -0
- package/dist/react/DeliveryComparison.js.map +1 -0
- package/dist/react/FAQList.d.ts +20 -0
- package/dist/react/FAQList.d.ts.map +1 -0
- package/dist/react/FAQList.js +19 -0
- package/dist/react/FAQList.js.map +1 -0
- package/dist/react/FurtherReading.d.ts +21 -0
- package/dist/react/FurtherReading.d.ts.map +1 -0
- package/dist/react/FurtherReading.js +13 -0
- package/dist/react/FurtherReading.js.map +1 -0
- package/dist/react/ImageFeature.d.ts +9 -0
- package/dist/react/ImageFeature.d.ts.map +1 -0
- package/dist/react/ImageFeature.js +6 -0
- package/dist/react/ImageFeature.js.map +1 -0
- package/dist/react/Infographic.d.ts +6 -0
- package/dist/react/Infographic.d.ts.map +1 -0
- package/dist/react/Infographic.js +7 -0
- package/dist/react/Infographic.js.map +1 -0
- package/dist/react/KeyMetric.d.ts +16 -0
- package/dist/react/KeyMetric.d.ts.map +1 -0
- package/dist/react/KeyMetric.js +15 -0
- package/dist/react/KeyMetric.js.map +1 -0
- package/dist/react/KeywordTable.d.ts +18 -0
- package/dist/react/KeywordTable.d.ts.map +1 -0
- package/dist/react/KeywordTable.js +23 -0
- package/dist/react/KeywordTable.js.map +1 -0
- package/dist/react/List.d.ts +11 -0
- package/dist/react/List.d.ts.map +1 -0
- package/dist/react/List.js +21 -0
- package/dist/react/List.js.map +1 -0
- package/dist/react/MetricHighlight.d.ts +15 -0
- package/dist/react/MetricHighlight.d.ts.map +1 -0
- package/dist/react/MetricHighlight.js +26 -0
- package/dist/react/MetricHighlight.js.map +1 -0
- package/dist/react/NewsletterCTA.d.ts +9 -0
- package/dist/react/NewsletterCTA.d.ts.map +1 -0
- package/dist/react/NewsletterCTA.js +5 -0
- package/dist/react/NewsletterCTA.js.map +1 -0
- package/dist/react/NumberedCard.d.ts +9 -0
- package/dist/react/NumberedCard.d.ts.map +1 -0
- package/dist/react/NumberedCard.js +7 -0
- package/dist/react/NumberedCard.js.map +1 -0
- package/dist/react/ProConBlock.d.ts +6 -0
- package/dist/react/ProConBlock.d.ts.map +1 -0
- package/dist/react/ProConBlock.js +7 -0
- package/dist/react/ProConBlock.js.map +1 -0
- package/dist/react/ProseList.d.ts +17 -0
- package/dist/react/ProseList.d.ts.map +1 -0
- package/dist/react/ProseList.js +26 -0
- package/dist/react/ProseList.js.map +1 -0
- package/dist/react/QuoteBlock.d.ts +17 -0
- package/dist/react/QuoteBlock.d.ts.map +1 -0
- package/dist/react/QuoteBlock.js +26 -0
- package/dist/react/QuoteBlock.js.map +1 -0
- package/dist/react/RegionCallout.d.ts +13 -0
- package/dist/react/RegionCallout.d.ts.map +1 -0
- package/dist/react/RegionCallout.js +5 -0
- package/dist/react/RegionCallout.js.map +1 -0
- package/dist/react/RelatedPosts.d.ts +20 -0
- package/dist/react/RelatedPosts.d.ts.map +1 -0
- package/dist/react/RelatedPosts.js +7 -0
- package/dist/react/RelatedPosts.js.map +1 -0
- package/dist/react/ResultsStrip.d.ts +18 -0
- package/dist/react/ResultsStrip.d.ts.map +1 -0
- package/dist/react/ResultsStrip.js +22 -0
- package/dist/react/ResultsStrip.js.map +1 -0
- package/dist/react/ScoreBar.d.ts +8 -0
- package/dist/react/ScoreBar.d.ts.map +1 -0
- package/dist/react/ScoreBar.js +6 -0
- package/dist/react/ScoreBar.js.map +1 -0
- package/dist/react/SerpPreview.d.ts +18 -0
- package/dist/react/SerpPreview.d.ts.map +1 -0
- package/dist/react/SerpPreview.js +13 -0
- package/dist/react/SerpPreview.js.map +1 -0
- package/dist/react/ServicePromoCard.d.ts +14 -0
- package/dist/react/ServicePromoCard.d.ts.map +1 -0
- package/dist/react/ServicePromoCard.js +12 -0
- package/dist/react/ServicePromoCard.js.map +1 -0
- package/dist/react/StatCard.d.ts +13 -0
- package/dist/react/StatCard.d.ts.map +1 -0
- package/dist/react/StatCard.js +20 -0
- package/dist/react/StatCard.js.map +1 -0
- package/dist/react/StepBlock.d.ts +8 -0
- package/dist/react/StepBlock.d.ts.map +1 -0
- package/dist/react/StepBlock.js +5 -0
- package/dist/react/StepBlock.js.map +1 -0
- package/dist/react/TableOfContents.d.ts +14 -0
- package/dist/react/TableOfContents.d.ts.map +1 -0
- package/dist/react/TableOfContents.js +12 -0
- package/dist/react/TableOfContents.js.map +1 -0
- package/dist/react/TimelineBlock.d.ts +14 -0
- package/dist/react/TimelineBlock.d.ts.map +1 -0
- package/dist/react/TimelineBlock.js +8 -0
- package/dist/react/TimelineBlock.js.map +1 -0
- package/dist/react/TipBox.d.ts +6 -0
- package/dist/react/TipBox.d.ts.map +1 -0
- package/dist/react/TipBox.js +5 -0
- package/dist/react/TipBox.js.map +1 -0
- package/dist/react/TrafficChart.d.ts +16 -0
- package/dist/react/TrafficChart.d.ts.map +1 -0
- package/dist/react/TrafficChart.js +14 -0
- package/dist/react/TrafficChart.js.map +1 -0
- package/dist/react/VerdictCard.d.ts +15 -0
- package/dist/react/VerdictCard.d.ts.map +1 -0
- package/dist/react/VerdictCard.js +5 -0
- package/dist/react/VerdictCard.js.map +1 -0
- package/dist/react/components-map.d.ts +133 -0
- package/dist/react/components-map.d.ts.map +1 -0
- package/dist/react/components-map.js +120 -0
- package/dist/react/components-map.js.map +1 -0
- package/dist/react/index.d.ts +5 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +13 -0
- package/dist/react/index.js.map +1 -0
- package/package.json +116 -0
- package/react/AdPreview.tsx +94 -0
- package/react/AdPreviewPair.tsx +16 -0
- package/react/AuditFindings.tsx +43 -0
- package/react/AuditScores.tsx +73 -0
- package/react/AuthorCard.tsx +35 -0
- package/react/BeforeAfter.tsx +27 -0
- package/react/BlogBehaviors.tsx +21 -0
- package/react/CTABanner.tsx +32 -0
- package/react/CalloutBox.tsx +31 -0
- package/react/CaseStudyHero.tsx +71 -0
- package/react/ChannelMixBars.tsx +50 -0
- package/react/Checklist.tsx +31 -0
- package/react/ChecklistItem.tsx +19 -0
- package/react/CodeSnippet.tsx +36 -0
- package/react/ComparisonTable.tsx +114 -0
- package/react/Definition.tsx +36 -0
- package/react/DeliveryComparison.tsx +62 -0
- package/react/FAQList.tsx +61 -0
- package/react/FurtherReading.tsx +46 -0
- package/react/ImageFeature.tsx +26 -0
- package/react/Infographic.tsx +18 -0
- package/react/KeyMetric.tsx +61 -0
- package/react/KeywordTable.tsx +92 -0
- package/react/List.tsx +58 -0
- package/react/MetricHighlight.tsx +86 -0
- package/react/NewsletterCTA.tsx +48 -0
- package/react/NumberedCard.tsx +7 -0
- package/react/ProConBlock.tsx +42 -0
- package/react/ProseList.tsx +72 -0
- package/react/QuoteBlock.tsx +89 -0
- package/react/RegionCallout.tsx +38 -0
- package/react/RelatedPosts.tsx +58 -0
- package/react/ResultsStrip.tsx +77 -0
- package/react/ScoreBar.tsx +27 -0
- package/react/SerpPreview.tsx +59 -0
- package/react/ServicePromoCard.tsx +43 -0
- package/react/StatCard.tsx +62 -0
- package/react/StepBlock.tsx +5 -0
- package/react/TableOfContents.tsx +27 -0
- package/react/TimelineBlock.tsx +35 -0
- package/react/TipBox.tsx +16 -0
- package/react/TrafficChart.tsx +79 -0
- package/react/VerdictCard.tsx +60 -0
- package/react/components-map.ts +122 -0
- package/react/index.ts +13 -0
- package/templates/blogkit/app/api/blogkit/revalidate/route.ts.tmpl +32 -0
- package/templates/blogkit/app/blog/[slug]/page.tsx.tmpl +41 -0
- package/templates/blogkit/app/blog/page.tsx.tmpl +18 -0
- package/templates/blogkit/blogkit.config.ts.tmpl +23 -0
- package/templates/blogkit-astro/BLOGKIT_ASTRO_SETUP.md.tmpl +49 -0
- package/templates/blogkit-astro/src/blogkit.config.ts.tmpl +29 -0
- package/templates/blogkit-astro/src/pages/api/blogkit/revalidate.ts.tmpl +46 -0
- package/templates/blogkit-astro/src/pages/blog/[slug].astro.tmpl +39 -0
- package/templates/blogkit-astro/src/pages/blog/index.astro.tmpl +29 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/* blog-kit React adapter — CaseStudyHero
|
|
2
|
+
Thin wrapper: all styling lives in core/css/components.css (.bk-case-study-hero).
|
|
3
|
+
A dark case-study banner: a white logo plate (client logo image, an
|
|
4
|
+
HTML/text wordmark, or the auto-broken client name), then a body with
|
|
5
|
+
uppercase chips, a headline, a sub-line, and a row of brand-coloured
|
|
6
|
+
result tiles (big number + uppercase label). */
|
|
7
|
+
|
|
8
|
+
export interface CaseStudyResult {
|
|
9
|
+
num: string;
|
|
10
|
+
label: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface CaseStudyHeroProps {
|
|
14
|
+
/** Client logo URL — preferred plate content. */
|
|
15
|
+
logoImage?: string;
|
|
16
|
+
/** HTML/text wordmark (supports <br/>) — used when no logoImage. */
|
|
17
|
+
logo?: string;
|
|
18
|
+
/** Client name — auto-broken at spaces as a last-resort plate. */
|
|
19
|
+
client?: string;
|
|
20
|
+
/** Uppercase tag chips shown above the headline. */
|
|
21
|
+
chips?: string[];
|
|
22
|
+
title?: string;
|
|
23
|
+
sub?: string;
|
|
24
|
+
/** Result tiles — each a big number + uppercase label. */
|
|
25
|
+
results?: CaseStudyResult[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function CaseStudyHero({
|
|
29
|
+
logoImage,
|
|
30
|
+
logo,
|
|
31
|
+
client,
|
|
32
|
+
chips = [],
|
|
33
|
+
title,
|
|
34
|
+
sub,
|
|
35
|
+
results = [],
|
|
36
|
+
}: CaseStudyHeroProps) {
|
|
37
|
+
return (
|
|
38
|
+
<div className="bk-case-study-hero">
|
|
39
|
+
<div className="bk-case-study-hero__logo">
|
|
40
|
+
{logoImage ? (
|
|
41
|
+
<img src={logoImage} alt={client ? `${client} logo` : "Client logo"} />
|
|
42
|
+
) : (
|
|
43
|
+
<strong
|
|
44
|
+
dangerouslySetInnerHTML={{
|
|
45
|
+
__html: logo || client?.replace(/ /g, "<br/>") || "",
|
|
46
|
+
}}
|
|
47
|
+
/>
|
|
48
|
+
)}
|
|
49
|
+
</div>
|
|
50
|
+
<div className="bk-case-study-hero__body">
|
|
51
|
+
<div className="bk-case-study-hero__chips">
|
|
52
|
+
{chips.map((c) => (
|
|
53
|
+
<span key={c} className="bk-case-study-hero__chip">
|
|
54
|
+
{c}
|
|
55
|
+
</span>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
<h2 className="bk-case-study-hero__heading">{title}</h2>
|
|
59
|
+
<p className="bk-case-study-hero__sub">{sub}</p>
|
|
60
|
+
<div className="bk-case-study-hero__results">
|
|
61
|
+
{results.map((r, i) => (
|
|
62
|
+
<div key={i} className="bk-case-study-hero__result">
|
|
63
|
+
<div className="bk-case-study-hero__result-num">{r.num}</div>
|
|
64
|
+
<div className="bk-case-study-hero__result-label">{r.label}</div>
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
</div>
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* blog-kit React adapter — ChannelMixBars
|
|
2
|
+
Thin wrapper: all styling lives in core/css/components.css (.bk-channel-mix).
|
|
3
|
+
A horizontal bar breakdown of a metric across channels — an uppercase
|
|
4
|
+
heading + optional total, then one labelled row per channel with a
|
|
5
|
+
proportional fill (width = value / max) and a right-aligned value.
|
|
6
|
+
The first row uses the brand fill; subsequent rows step through a
|
|
7
|
+
neutral colour ramp. Ported from hjd-website's blog ChannelMixBars. */
|
|
8
|
+
|
|
9
|
+
export interface ChannelMixRow {
|
|
10
|
+
/** Channel name (rendered uppercase as the row label). */
|
|
11
|
+
name: string;
|
|
12
|
+
/** Numeric magnitude used to compute the bar width. */
|
|
13
|
+
value: number;
|
|
14
|
+
/** Optional pre-formatted value shown on the right (falls back to value). */
|
|
15
|
+
display?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ChannelMixBarsProps {
|
|
19
|
+
/** One entry per channel. Renders nothing meaningful if empty. */
|
|
20
|
+
rows?: ChannelMixRow[];
|
|
21
|
+
/** Optional total, shown in the header after "Total · ". */
|
|
22
|
+
total?: string;
|
|
23
|
+
/** Optional header label (defaults to "Channel split"). */
|
|
24
|
+
period?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ChannelMixBars({ rows = [], total, period }: ChannelMixBarsProps) {
|
|
28
|
+
const max = Math.max(...rows.map((r) => Number(r.value) || 0), 1);
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<div className="bk-channel-mix">
|
|
32
|
+
<div className="bk-channel-mix__head">
|
|
33
|
+
<div className="bk-channel-mix__title">{period || "Channel split"}</div>
|
|
34
|
+
{total && <div className="bk-channel-mix__total">Total · {total}</div>}
|
|
35
|
+
</div>
|
|
36
|
+
{rows.map((r, i) => (
|
|
37
|
+
<div key={i} className="bk-channel-mix__row">
|
|
38
|
+
<div className="bk-channel-mix__name">{r.name}</div>
|
|
39
|
+
<div className="bk-channel-mix__bar">
|
|
40
|
+
<div
|
|
41
|
+
className={`bk-channel-mix__fill${i > 0 ? ` bk-channel-mix__fill--${Math.min(i + 1, 5)}` : ""}`}
|
|
42
|
+
style={{ width: `${(Number(r.value) / max) * 100}%` }}
|
|
43
|
+
/>
|
|
44
|
+
</div>
|
|
45
|
+
<div className="bk-channel-mix__val">{r.display || r.value}</div>
|
|
46
|
+
</div>
|
|
47
|
+
))}
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
export interface ChecklistEntry {
|
|
2
|
+
title: string;
|
|
3
|
+
sub?: string;
|
|
4
|
+
done?: boolean;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export interface ChecklistProps {
|
|
8
|
+
items: ChecklistEntry[];
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Checklist({ items }: ChecklistProps) {
|
|
12
|
+
if (!items || items.length === 0) return null;
|
|
13
|
+
return (
|
|
14
|
+
<ul className="bk-checklist">
|
|
15
|
+
{items.map((it, i) => (
|
|
16
|
+
<li
|
|
17
|
+
key={i}
|
|
18
|
+
className={`bk-checklist__item${it.done ? " bk-checklist__item--done" : ""}`}
|
|
19
|
+
>
|
|
20
|
+
<span className="bk-checklist__box" aria-hidden="true">
|
|
21
|
+
{it.done ? "✓" : ""}
|
|
22
|
+
</span>
|
|
23
|
+
<span className="bk-checklist__text">
|
|
24
|
+
<span className="bk-checklist__title">{it.title}</span>
|
|
25
|
+
{it.sub && <span className="bk-checklist__sub">{it.sub}</span>}
|
|
26
|
+
</span>
|
|
27
|
+
</li>
|
|
28
|
+
))}
|
|
29
|
+
</ul>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface ChecklistItemProps {
|
|
2
|
+
checked: boolean;
|
|
3
|
+
label: string;
|
|
4
|
+
description?: string;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function ChecklistItem({ checked, label, description }: ChecklistItemProps) {
|
|
8
|
+
return (
|
|
9
|
+
<div className={`bk-checklist-item${checked ? " bk-checklist-item--checked" : ""}`}>
|
|
10
|
+
<span className="bk-checklist-item__box" aria-hidden={!checked}>
|
|
11
|
+
{checked ? "✓" : ""}
|
|
12
|
+
</span>
|
|
13
|
+
<span className="bk-checklist-item__text">
|
|
14
|
+
<span className="bk-checklist-item__label">{label}</span>
|
|
15
|
+
{description && <span className="bk-checklist-item__desc">{description}</span>}
|
|
16
|
+
</span>
|
|
17
|
+
</div>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ICONS } from "../core/icons";
|
|
2
|
+
|
|
3
|
+
export interface CodeSnippetProps {
|
|
4
|
+
/** The raw code to display and copy. */
|
|
5
|
+
code: string;
|
|
6
|
+
/** Language hint, used for the label and the `language-*` class. */
|
|
7
|
+
language?: string;
|
|
8
|
+
/** Optional filename shown in the header instead of the language. */
|
|
9
|
+
filename?: string;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* A dark, framework-agnostic code block with a header bar and a
|
|
14
|
+
* copy-to-clipboard button. The copy interaction is handled by the
|
|
15
|
+
* shared vanilla-JS behaviour, which attaches via the [data-bk-code]
|
|
16
|
+
* hook on load — no framework state. The raw source is carried in a
|
|
17
|
+
* hidden <template> so the script can copy the exact text.
|
|
18
|
+
*/
|
|
19
|
+
export function CodeSnippet({ code, language = "text", filename }: CodeSnippetProps) {
|
|
20
|
+
const label = filename || (language !== "text" ? language : "code");
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="bk-code" data-bk-code>
|
|
24
|
+
<div className="bk-code__bar">
|
|
25
|
+
<span className="bk-code__label">{label}</span>
|
|
26
|
+
<button type="button" className="bk-code__copy" data-bk-code-copy>
|
|
27
|
+
<span className="bk-code__copy-icon" dangerouslySetInnerHTML={{ __html: ICONS.copy }} />
|
|
28
|
+
<span className="bk-code__copy-label">Copy</span>
|
|
29
|
+
</button>
|
|
30
|
+
</div>
|
|
31
|
+
<pre className="bk-code__pre">
|
|
32
|
+
<code className={`bk-code__code language-${language}`}>{code}</code>
|
|
33
|
+
</pre>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
export interface ComparisonItem {
|
|
2
|
+
feature: string;
|
|
3
|
+
optionA: string;
|
|
4
|
+
optionB: string;
|
|
5
|
+
/** Which column wins this row, if any. */
|
|
6
|
+
winner?: "a" | "b" | "tie";
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface ComparisonTableProps {
|
|
10
|
+
/** Optional heading rendered above the table. */
|
|
11
|
+
title?: string;
|
|
12
|
+
optionALabel: string;
|
|
13
|
+
optionBLabel: string;
|
|
14
|
+
items?: ComparisonItem[];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const TICK = "✓";
|
|
18
|
+
const CROSS = "✕";
|
|
19
|
+
|
|
20
|
+
type Cell = { kind: "yes" | "no" | "text"; text: string };
|
|
21
|
+
|
|
22
|
+
/** Normalise a cell value: tick/"yes" and cross/"no" become glyph marks. */
|
|
23
|
+
function resolveCell(text: string): Cell {
|
|
24
|
+
const v = String(text ?? "").trim();
|
|
25
|
+
const lower = v.toLowerCase();
|
|
26
|
+
if (v === TICK || lower === "yes") return { kind: "yes", text: TICK };
|
|
27
|
+
if (v === CROSS || lower === "no") return { kind: "no", text: CROSS };
|
|
28
|
+
return { kind: "text", text: v };
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Side-by-side comparison of two options across a set of features.
|
|
33
|
+
* Renders twice: stacked cards on mobile (avoids the hidden-horizontal-
|
|
34
|
+
* scroll trap) and a table on tablet/desktop. The behaviour script adds a
|
|
35
|
+
* scroll-edge hint to the table when it overflows its container.
|
|
36
|
+
*/
|
|
37
|
+
export function ComparisonTable({
|
|
38
|
+
title,
|
|
39
|
+
optionALabel,
|
|
40
|
+
optionBLabel,
|
|
41
|
+
items = [],
|
|
42
|
+
}: ComparisonTableProps) {
|
|
43
|
+
if (!items || items.length === 0) return null;
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
<div className="bk-comparison">
|
|
47
|
+
{title && <h3 className="bk-comparison__title">{title}</h3>}
|
|
48
|
+
|
|
49
|
+
<div className="bk-comparison__cards">
|
|
50
|
+
{items.map((item, i) => (
|
|
51
|
+
<div className="bk-comparison__card" key={i}>
|
|
52
|
+
<div className="bk-comparison__card-feature">{item.feature}</div>
|
|
53
|
+
<div
|
|
54
|
+
className={`bk-comparison__card-row${item.winner === "a" ? " bk-comparison__card-row--best" : ""}`}
|
|
55
|
+
>
|
|
56
|
+
<span className="bk-comparison__card-label">{optionALabel}</span>
|
|
57
|
+
<ComparisonValue text={item.optionA} best={item.winner === "a"} />
|
|
58
|
+
</div>
|
|
59
|
+
<div
|
|
60
|
+
className={`bk-comparison__card-row${item.winner === "b" ? " bk-comparison__card-row--best" : ""}`}
|
|
61
|
+
>
|
|
62
|
+
<span className="bk-comparison__card-label">{optionBLabel}</span>
|
|
63
|
+
<ComparisonValue text={item.optionB} best={item.winner === "b"} />
|
|
64
|
+
</div>
|
|
65
|
+
</div>
|
|
66
|
+
))}
|
|
67
|
+
</div>
|
|
68
|
+
|
|
69
|
+
<div className="bk-comparison__table-wrap" data-bk-comparison>
|
|
70
|
+
<div className="bk-comparison__scroll">
|
|
71
|
+
<table className="bk-comparison__table">
|
|
72
|
+
<thead>
|
|
73
|
+
<tr>
|
|
74
|
+
<th className="bk-comparison__th">Feature</th>
|
|
75
|
+
<th className="bk-comparison__th">{optionALabel}</th>
|
|
76
|
+
<th className="bk-comparison__th">{optionBLabel}</th>
|
|
77
|
+
</tr>
|
|
78
|
+
</thead>
|
|
79
|
+
<tbody>
|
|
80
|
+
{items.map((item, i) => (
|
|
81
|
+
<tr className="bk-comparison__tr" key={i}>
|
|
82
|
+
<td className="bk-comparison__td bk-comparison__td--feature">
|
|
83
|
+
{item.feature}
|
|
84
|
+
</td>
|
|
85
|
+
<td
|
|
86
|
+
className={`bk-comparison__td${item.winner === "a" ? " bk-comparison__td--best" : ""}`}
|
|
87
|
+
>
|
|
88
|
+
<ComparisonValue text={item.optionA} best={item.winner === "a"} />
|
|
89
|
+
</td>
|
|
90
|
+
<td
|
|
91
|
+
className={`bk-comparison__td${item.winner === "b" ? " bk-comparison__td--best" : ""}`}
|
|
92
|
+
>
|
|
93
|
+
<ComparisonValue text={item.optionB} best={item.winner === "b"} />
|
|
94
|
+
</td>
|
|
95
|
+
</tr>
|
|
96
|
+
))}
|
|
97
|
+
</tbody>
|
|
98
|
+
</table>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</div>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function ComparisonValue({ text, best }: { text: string; best?: boolean }) {
|
|
106
|
+
const cell = resolveCell(text);
|
|
107
|
+
if (cell.kind === "yes") {
|
|
108
|
+
return <span className="bk-comparison__mark bk-comparison__mark--yes">{cell.text}</span>;
|
|
109
|
+
}
|
|
110
|
+
if (cell.kind === "no") {
|
|
111
|
+
return <span className="bk-comparison__mark bk-comparison__mark--no">{cell.text}</span>;
|
|
112
|
+
}
|
|
113
|
+
return <span className={best ? "bk-comparison__value--best" : undefined}>{cell.text}</span>;
|
|
114
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface DefinitionProps {
|
|
4
|
+
term: string;
|
|
5
|
+
name?: string;
|
|
6
|
+
pronunciation?: string;
|
|
7
|
+
children?: ReactNode;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function Definition({ term, name, pronunciation, children }: DefinitionProps) {
|
|
11
|
+
// `term` is meant to be a 2-3 char monogram. If an author passes a full
|
|
12
|
+
// phrase, derive initials so the tile doesn't overflow.
|
|
13
|
+
const rawTerm = String(term ?? "").trim();
|
|
14
|
+
const looksLikePhrase = rawTerm.length > 4 || /\s|-/.test(rawTerm);
|
|
15
|
+
const monogram = looksLikePhrase
|
|
16
|
+
? rawTerm
|
|
17
|
+
.split(/[\s-]+/)
|
|
18
|
+
.map((w) => w[0])
|
|
19
|
+
.filter(Boolean)
|
|
20
|
+
.join("")
|
|
21
|
+
.slice(0, 3)
|
|
22
|
+
.toUpperCase()
|
|
23
|
+
: rawTerm.toUpperCase();
|
|
24
|
+
const resolvedName = name ?? (looksLikePhrase ? rawTerm : undefined);
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="bk-definition">
|
|
28
|
+
<div className="bk-definition__monogram" aria-hidden="true">{monogram}</div>
|
|
29
|
+
<div className="bk-definition__content">
|
|
30
|
+
{resolvedName && <p className="bk-definition__name">{resolvedName}</p>}
|
|
31
|
+
{pronunciation && <p className="bk-definition__pronunciation">{pronunciation}</p>}
|
|
32
|
+
<p className="bk-definition__body">{children}</p>
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/* blog-kit React adapter — DeliveryComparison
|
|
2
|
+
Thin wrapper: all styling lives in core/css/components.css (.bk-delivery-comparison).
|
|
3
|
+
A flexible N-column comparison matrix — a header row of column labels (the
|
|
4
|
+
first header cell is intentionally blank, sitting above the row-label column),
|
|
5
|
+
then one row per criterion: a bold row label with optional sub-text, followed
|
|
6
|
+
by one cell per column. Overflows horizontally on narrow viewports; the
|
|
7
|
+
[data-bk-delivery-comparison] behaviour adds scroll-edge hints. Ported from
|
|
8
|
+
hjd-website's blog DeliveryComparison. */
|
|
9
|
+
|
|
10
|
+
export interface DeliveryComparisonRow {
|
|
11
|
+
/** Bold row label (the left-hand criterion name). */
|
|
12
|
+
label: string;
|
|
13
|
+
/** Optional secondary line shown under the label. */
|
|
14
|
+
sub?: string;
|
|
15
|
+
/** One value per column, in the same order as `columns`. */
|
|
16
|
+
values: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface DeliveryComparisonProps {
|
|
20
|
+
/** Column headers. The matrix renders a blank header above the label column. */
|
|
21
|
+
columns?: string[];
|
|
22
|
+
/** One entry per criterion row. Renders nothing if empty. */
|
|
23
|
+
rows?: DeliveryComparisonRow[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function DeliveryComparison({ columns = [], rows = [] }: DeliveryComparisonProps) {
|
|
27
|
+
if (!rows || rows.length === 0) return null;
|
|
28
|
+
|
|
29
|
+
return (
|
|
30
|
+
<div className="bk-delivery-comparison" data-bk-delivery-comparison>
|
|
31
|
+
<div className="bk-delivery-comparison__scroll">
|
|
32
|
+
<table className="bk-delivery-comparison__table">
|
|
33
|
+
<thead>
|
|
34
|
+
<tr>
|
|
35
|
+
<th className="bk-delivery-comparison__th" />
|
|
36
|
+
{columns.map((c, i) => (
|
|
37
|
+
<th className="bk-delivery-comparison__th" key={i}>
|
|
38
|
+
{c}
|
|
39
|
+
</th>
|
|
40
|
+
))}
|
|
41
|
+
</tr>
|
|
42
|
+
</thead>
|
|
43
|
+
<tbody>
|
|
44
|
+
{rows.map((r, i) => (
|
|
45
|
+
<tr className="bk-delivery-comparison__tr" key={i}>
|
|
46
|
+
<td className="bk-delivery-comparison__td bk-delivery-comparison__td--label">
|
|
47
|
+
<strong className="bk-delivery-comparison__label">{r.label}</strong>
|
|
48
|
+
{r.sub}
|
|
49
|
+
</td>
|
|
50
|
+
{r.values.map((v, j) => (
|
|
51
|
+
<td className="bk-delivery-comparison__td" key={j}>
|
|
52
|
+
{v}
|
|
53
|
+
</td>
|
|
54
|
+
))}
|
|
55
|
+
</tr>
|
|
56
|
+
))}
|
|
57
|
+
</tbody>
|
|
58
|
+
</table>
|
|
59
|
+
</div>
|
|
60
|
+
</div>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
|
|
3
|
+
export interface FAQItem {
|
|
4
|
+
q: string;
|
|
5
|
+
/** Body text. Split into paragraphs on blank lines. Inline HTML is rendered. */
|
|
6
|
+
a: string;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface FAQListProps {
|
|
10
|
+
/** Q&A pairs. Optional — children may carry the body instead (MDX). */
|
|
11
|
+
items?: FAQItem[];
|
|
12
|
+
/** Optional heading rendered above the list. */
|
|
13
|
+
heading?: string;
|
|
14
|
+
children?: ReactNode;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Split an answer body into paragraphs on blank lines. */
|
|
18
|
+
function toParagraphs(a: string): string[] {
|
|
19
|
+
return String(a ?? "")
|
|
20
|
+
.split(/\n{2,}/)
|
|
21
|
+
.filter(Boolean);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Expandable Q&A list built on native <details> for accessibility and SEO.
|
|
26
|
+
* The first item is open by default to signal interaction. The behaviour
|
|
27
|
+
* script lifts every rendered Q&A pair into FAQPage JSON-LD on load.
|
|
28
|
+
*/
|
|
29
|
+
export function FAQList({ items, heading, children }: FAQListProps) {
|
|
30
|
+
const list = items ?? [];
|
|
31
|
+
|
|
32
|
+
return (
|
|
33
|
+
<div className="bk-faq" data-bk-faq>
|
|
34
|
+
{heading && <p className="bk-faq__heading">{heading}</p>}
|
|
35
|
+
<div className="bk-faq__list">
|
|
36
|
+
{list.length > 0
|
|
37
|
+
? list.map((it, i) => (
|
|
38
|
+
<details className="bk-faq__item" key={i} open={i === 0}>
|
|
39
|
+
<summary className="bk-faq__summary">
|
|
40
|
+
<span className="bk-faq__question">{it.q}</span>
|
|
41
|
+
<span className="bk-faq__toggle" aria-hidden="true">
|
|
42
|
+
<span className="bk-faq__bar bk-faq__bar--h" />
|
|
43
|
+
<span className="bk-faq__bar bk-faq__bar--v" />
|
|
44
|
+
</span>
|
|
45
|
+
</summary>
|
|
46
|
+
<div className="bk-faq__answer">
|
|
47
|
+
{toParagraphs(it.a).map((p, j) => (
|
|
48
|
+
<p
|
|
49
|
+
className="bk-faq__para"
|
|
50
|
+
key={j}
|
|
51
|
+
dangerouslySetInnerHTML={{ __html: p }}
|
|
52
|
+
/>
|
|
53
|
+
))}
|
|
54
|
+
</div>
|
|
55
|
+
</details>
|
|
56
|
+
))
|
|
57
|
+
: children}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
export interface FurtherReadingSource {
|
|
2
|
+
/** Display title of the linked resource. */
|
|
3
|
+
title: string;
|
|
4
|
+
/** Short source/publisher label shown on the right. */
|
|
5
|
+
source: string;
|
|
6
|
+
/** Destination URL (opens in a new tab, rel=nofollow). */
|
|
7
|
+
url: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface FurtherReadingProps {
|
|
11
|
+
/** List of external resources. Renders nothing when empty. */
|
|
12
|
+
sources?: FurtherReadingSource[];
|
|
13
|
+
/** Optional heading override (default "Further reading"). */
|
|
14
|
+
heading?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Card listing external resources for further reading. Each row is a
|
|
19
|
+
* title linking out (new tab, nofollow) with a source label on the right.
|
|
20
|
+
* Rows are dashed-separated; the last row has no separator.
|
|
21
|
+
*/
|
|
22
|
+
export function FurtherReading({ sources, heading }: FurtherReadingProps) {
|
|
23
|
+
const list = sources ?? [];
|
|
24
|
+
if (list.length === 0) return null;
|
|
25
|
+
|
|
26
|
+
return (
|
|
27
|
+
<div className="bk-further-reading">
|
|
28
|
+
<p className="bk-further-reading__title">{heading ?? "Further reading"}</p>
|
|
29
|
+
<ul className="bk-further-reading__list">
|
|
30
|
+
{list.map((s, i) => (
|
|
31
|
+
<li className="bk-further-reading__item" key={i}>
|
|
32
|
+
<a
|
|
33
|
+
className="bk-further-reading__link"
|
|
34
|
+
href={s.url}
|
|
35
|
+
target="_blank"
|
|
36
|
+
rel="nofollow noopener noreferrer"
|
|
37
|
+
>
|
|
38
|
+
{s.title}
|
|
39
|
+
</a>
|
|
40
|
+
<span className="bk-further-reading__source">{s.source}</span>
|
|
41
|
+
</li>
|
|
42
|
+
))}
|
|
43
|
+
</ul>
|
|
44
|
+
</div>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/* blog-kit React adapter — ImageFeature
|
|
2
|
+
Thin wrapper: all styling lives in core/css/components.css (.bk-image-feature).
|
|
3
|
+
Framework-neutral <img> (no next/image) so React + Astro emit identical markup. */
|
|
4
|
+
export interface ImageFeatureProps {
|
|
5
|
+
src: string;
|
|
6
|
+
alt: string;
|
|
7
|
+
caption?: string;
|
|
8
|
+
credit?: string;
|
|
9
|
+
position?: "left" | "right" | "full";
|
|
10
|
+
}
|
|
11
|
+
export function ImageFeature({ src, alt, caption, credit, position = "full" }: ImageFeatureProps) {
|
|
12
|
+
const pos = position === "left" || position === "right" ? position : "full";
|
|
13
|
+
return (
|
|
14
|
+
<figure className={`bk-image-feature bk-image-feature--${pos}`}>
|
|
15
|
+
<div className="bk-image-feature__frame">
|
|
16
|
+
<img className="bk-image-feature__img" src={src} alt={alt} loading="lazy" decoding="async" />
|
|
17
|
+
</div>
|
|
18
|
+
{(caption || credit) && (
|
|
19
|
+
<figcaption className="bk-image-feature__caption">
|
|
20
|
+
{caption && <span className="bk-image-feature__text">{caption}</span>}
|
|
21
|
+
{credit && <span className="bk-image-feature__credit">{credit}</span>}
|
|
22
|
+
</figcaption>
|
|
23
|
+
)}
|
|
24
|
+
</figure>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/* blog-kit React adapter — Infographic
|
|
2
|
+
AI-generated, brand-locked infographic image. Frameless and borderless on
|
|
3
|
+
purpose: the image is the whole point and every pixel carries information.
|
|
4
|
+
Renders at the image's natural aspect ratio (full width, auto height) so
|
|
5
|
+
nothing gets cropped. Framework-neutral <img> (no next/image) so React +
|
|
6
|
+
Astro emit identical markup. All styling lives in .bk-infographic. */
|
|
7
|
+
export interface InfographicProps {
|
|
8
|
+
src: string;
|
|
9
|
+
alt: string;
|
|
10
|
+
}
|
|
11
|
+
export function Infographic({ src, alt }: InfographicProps) {
|
|
12
|
+
if (!src) return null;
|
|
13
|
+
return (
|
|
14
|
+
<figure className="bk-infographic">
|
|
15
|
+
<img className="bk-infographic__img" src={src} alt={alt} loading="lazy" decoding="async" />
|
|
16
|
+
</figure>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/* blog-kit React adapter — KeyMetric
|
|
2
|
+
Thin wrapper: all styling lives in core/css/components.css (.bk-key-metric).
|
|
3
|
+
A dark, full-width hero metric panel: one oversized brand-coloured number on
|
|
4
|
+
the left, and a copy column on the right (optional uppercase label, an
|
|
5
|
+
optional rich-text title that accepts inline HTML, an optional sub-line, and
|
|
6
|
+
an optional dashed-divider "SOURCE" attribution row that links out when a URL
|
|
7
|
+
is given). Ported from hjd's blog KeyMetric (+ inlined _StatSourceRow);
|
|
8
|
+
cyan/dark mapped to neutral tokens. */
|
|
9
|
+
import { ICONS } from "../core/icons";
|
|
10
|
+
|
|
11
|
+
export interface KeyMetricProps {
|
|
12
|
+
/** The oversized headline figure, e.g. "3.2×" or "87%". */
|
|
13
|
+
number: string;
|
|
14
|
+
/** Short uppercase caption above the title. */
|
|
15
|
+
label?: string;
|
|
16
|
+
/** Rich-text headline; accepts inline HTML (rendered as markup). */
|
|
17
|
+
title?: string;
|
|
18
|
+
/** Supporting line under the title. */
|
|
19
|
+
sub?: string;
|
|
20
|
+
/** Optional source attribution, shown below in a dashed-divider row. */
|
|
21
|
+
source?: string;
|
|
22
|
+
/** Optional URL — when set alongside `source`, the row becomes a link. */
|
|
23
|
+
sourceUrl?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function KeyMetric({ number, label, title, sub, source, sourceUrl }: KeyMetricProps) {
|
|
27
|
+
return (
|
|
28
|
+
<div className="bk-key-metric">
|
|
29
|
+
<div className="bk-key-metric__num">{number}</div>
|
|
30
|
+
<div className="bk-key-metric__copy">
|
|
31
|
+
{label && <div className="bk-key-metric__label">{label}</div>}
|
|
32
|
+
{title && (
|
|
33
|
+
<p className="bk-key-metric__title" dangerouslySetInnerHTML={{ __html: title }} />
|
|
34
|
+
)}
|
|
35
|
+
{sub && <p className="bk-key-metric__sub">{sub}</p>}
|
|
36
|
+
{source &&
|
|
37
|
+
(sourceUrl ? (
|
|
38
|
+
<a
|
|
39
|
+
className="bk-key-metric__source bk-key-metric__source--link"
|
|
40
|
+
href={sourceUrl}
|
|
41
|
+
target="_blank"
|
|
42
|
+
rel="nofollow noopener noreferrer"
|
|
43
|
+
>
|
|
44
|
+
<span className="bk-key-metric__source-kicker">Source</span>
|
|
45
|
+
{source}
|
|
46
|
+
<span
|
|
47
|
+
className="bk-key-metric__source-icon"
|
|
48
|
+
aria-hidden="true"
|
|
49
|
+
dangerouslySetInnerHTML={{ __html: ICONS["external"] }}
|
|
50
|
+
/>
|
|
51
|
+
</a>
|
|
52
|
+
) : (
|
|
53
|
+
<div className="bk-key-metric__source">
|
|
54
|
+
<span className="bk-key-metric__source-kicker">Source</span>
|
|
55
|
+
{source}
|
|
56
|
+
</div>
|
|
57
|
+
))}
|
|
58
|
+
</div>
|
|
59
|
+
</div>
|
|
60
|
+
);
|
|
61
|
+
}
|