@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,92 @@
|
|
|
1
|
+
/* blog-kit React adapter — KeywordTable
|
|
2
|
+
Thin wrapper: all styling lives in core/css/components.css (.bk-keyword-table).
|
|
3
|
+
An SEO keyword-ranking table — keyword, search volume, a colour-coded
|
|
4
|
+
difficulty pill (low/med/high), before/after rank positions, and a
|
|
5
|
+
coloured up/down rank-change delta. Overflows horizontally on narrow
|
|
6
|
+
viewports; the [data-bk-keyword-table] behaviour adds scroll-edge hints. */
|
|
7
|
+
|
|
8
|
+
export interface KeywordRow {
|
|
9
|
+
keyword: string;
|
|
10
|
+
/** Monthly search volume, pre-formatted (e.g. "1.2k"). */
|
|
11
|
+
volume: string;
|
|
12
|
+
/** Keyword difficulty 0–100; bucketed low (<40) / med (<60) / high. */
|
|
13
|
+
difficulty: number;
|
|
14
|
+
/** Previous rank position; falls back to an em dash when absent. */
|
|
15
|
+
was?: number | string;
|
|
16
|
+
/** Current rank position. */
|
|
17
|
+
now: number | string;
|
|
18
|
+
/** Signed rank change; negative renders as a red down-arrow. */
|
|
19
|
+
delta: number | string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface KeywordTableProps {
|
|
23
|
+
rows?: KeywordRow[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Bucket a 0–100 difficulty score into low / med / high. */
|
|
27
|
+
function difficultyBucket(difficulty: number): "low" | "med" | "high" {
|
|
28
|
+
return difficulty < 40 ? "low" : difficulty < 60 ? "med" : "high";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A negative delta means the rank dropped (down); anything else is up. */
|
|
32
|
+
function deltaDirection(delta: number | string): "up" | "down" {
|
|
33
|
+
return delta && String(delta).startsWith("-") ? "down" : "up";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Show the magnitude when numeric, else the raw value. */
|
|
37
|
+
function deltaDisplay(delta: number | string): number | string {
|
|
38
|
+
return Math.abs(Number(delta)) || delta;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function KeywordTable({ rows = [] }: KeywordTableProps) {
|
|
42
|
+
if (!rows || rows.length === 0) return null;
|
|
43
|
+
|
|
44
|
+
return (
|
|
45
|
+
<div className="bk-keyword-table" data-bk-keyword-table>
|
|
46
|
+
<div className="bk-keyword-table__scroll">
|
|
47
|
+
<table className="bk-keyword-table__table">
|
|
48
|
+
<thead>
|
|
49
|
+
<tr>
|
|
50
|
+
<th className="bk-keyword-table__th">Keyword</th>
|
|
51
|
+
<th className="bk-keyword-table__th bk-keyword-table__th--r">Volume</th>
|
|
52
|
+
<th className="bk-keyword-table__th">Difficulty</th>
|
|
53
|
+
<th className="bk-keyword-table__th bk-keyword-table__th--r">Was</th>
|
|
54
|
+
<th className="bk-keyword-table__th bk-keyword-table__th--r">Now</th>
|
|
55
|
+
<th className="bk-keyword-table__th bk-keyword-table__th--r">Δ</th>
|
|
56
|
+
</tr>
|
|
57
|
+
</thead>
|
|
58
|
+
<tbody>
|
|
59
|
+
{rows.map((r, i) => {
|
|
60
|
+
const diff = difficultyBucket(r.difficulty);
|
|
61
|
+
const direction = deltaDirection(r.delta);
|
|
62
|
+
return (
|
|
63
|
+
<tr className="bk-keyword-table__tr" key={i}>
|
|
64
|
+
<td className="bk-keyword-table__td bk-keyword-table__td--kw">{r.keyword}</td>
|
|
65
|
+
<td className="bk-keyword-table__td bk-keyword-table__td--r">{r.volume}</td>
|
|
66
|
+
<td className="bk-keyword-table__td">
|
|
67
|
+
<span className={`bk-keyword-table__diff bk-keyword-table__diff--${diff}`}>
|
|
68
|
+
{r.difficulty}
|
|
69
|
+
</span>
|
|
70
|
+
</td>
|
|
71
|
+
<td className="bk-keyword-table__td bk-keyword-table__td--r bk-keyword-table__rank">
|
|
72
|
+
{r.was ?? "—"}
|
|
73
|
+
</td>
|
|
74
|
+
<td className="bk-keyword-table__td bk-keyword-table__td--r bk-keyword-table__rank">
|
|
75
|
+
{r.now}
|
|
76
|
+
</td>
|
|
77
|
+
<td className="bk-keyword-table__td bk-keyword-table__td--r">
|
|
78
|
+
<span
|
|
79
|
+
className={`bk-keyword-table__delta bk-keyword-table__delta--${direction}`}
|
|
80
|
+
>
|
|
81
|
+
{deltaDisplay(r.delta)}
|
|
82
|
+
</span>
|
|
83
|
+
</td>
|
|
84
|
+
</tr>
|
|
85
|
+
);
|
|
86
|
+
})}
|
|
87
|
+
</tbody>
|
|
88
|
+
</table>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
);
|
|
92
|
+
}
|
package/react/List.tsx
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ICONS } from "../core/icons";
|
|
2
|
+
|
|
3
|
+
export type ListVariant = "ordered" | "bullet" | "check";
|
|
4
|
+
|
|
5
|
+
export interface ListProps {
|
|
6
|
+
/** "ordered" — zero-padded counters (01, 02, …); "bullet" — dot markers; "check" — tick marks. */
|
|
7
|
+
variant?: ListVariant;
|
|
8
|
+
/** Each item is an HTML string — use <strong>…</strong> for a bold lead-in. */
|
|
9
|
+
items: string[];
|
|
10
|
+
/** Optional uppercase heading rendered above the list. */
|
|
11
|
+
heading?: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const VARIANTS: Record<string, ListVariant> = {
|
|
15
|
+
ordered: "ordered",
|
|
16
|
+
numbered: "ordered",
|
|
17
|
+
number: "ordered",
|
|
18
|
+
bullet: "bullet",
|
|
19
|
+
bulleted: "bullet",
|
|
20
|
+
dot: "bullet",
|
|
21
|
+
check: "check",
|
|
22
|
+
checked: "check",
|
|
23
|
+
checklist: "check",
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export function List({ variant, items, heading }: ListProps) {
|
|
27
|
+
if (!Array.isArray(items) || items.length === 0) return null;
|
|
28
|
+
const v = VARIANTS[(variant ?? "bullet").toLowerCase().trim()] ?? "bullet";
|
|
29
|
+
const Tag = v === "ordered" ? "ol" : "ul";
|
|
30
|
+
return (
|
|
31
|
+
<div className={`bk-list bk-list--${v}`}>
|
|
32
|
+
{heading && <p className="bk-list__heading">{heading}</p>}
|
|
33
|
+
<Tag className="bk-list__items">
|
|
34
|
+
{items.map((item, i) => (
|
|
35
|
+
<li className="bk-list__item" key={i}>
|
|
36
|
+
{v === "ordered" ? (
|
|
37
|
+
<span className="bk-list__marker" aria-hidden="true">
|
|
38
|
+
{String(i + 1).padStart(2, "0")}
|
|
39
|
+
</span>
|
|
40
|
+
) : v === "check" ? (
|
|
41
|
+
<span
|
|
42
|
+
className="bk-list__marker"
|
|
43
|
+
aria-hidden="true"
|
|
44
|
+
dangerouslySetInnerHTML={{ __html: ICONS["check-tick"] }}
|
|
45
|
+
/>
|
|
46
|
+
) : (
|
|
47
|
+
<span className="bk-list__marker" aria-hidden="true" />
|
|
48
|
+
)}
|
|
49
|
+
<span
|
|
50
|
+
className="bk-list__text"
|
|
51
|
+
dangerouslySetInnerHTML={{ __html: item }}
|
|
52
|
+
/>
|
|
53
|
+
</li>
|
|
54
|
+
))}
|
|
55
|
+
</Tag>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/* blog-kit React adapter — MetricHighlight
|
|
2
|
+
Thin wrapper: all styling lives in core/css/components.css (.bk-metric-highlight).
|
|
3
|
+
One or more headline metrics. A single metric renders as a big brand-filled
|
|
4
|
+
hero (giant value + label + optional change + optional SOURCE row); two or
|
|
5
|
+
more render as a responsive grid of bordered cards. The grid column count is
|
|
6
|
+
driven by a --metric-highlight--<n> modifier (2 | 3 | 4+). */
|
|
7
|
+
import { ICONS } from "../core/icons";
|
|
8
|
+
|
|
9
|
+
interface Metric {
|
|
10
|
+
value: string;
|
|
11
|
+
label: string;
|
|
12
|
+
change?: string;
|
|
13
|
+
/** Optional source attribution, e.g. "Zuko form benchmarks, 2026". */
|
|
14
|
+
source?: string;
|
|
15
|
+
/** Optional URL — when set alongside `source`, the row becomes a link. */
|
|
16
|
+
sourceUrl?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MetricHighlightProps {
|
|
20
|
+
metrics?: Metric[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function SourceRow({ source, sourceUrl, dark }: { source: string; sourceUrl?: string; dark?: boolean }) {
|
|
24
|
+
const cls = `bk-metric-highlight__source${dark ? " bk-metric-highlight__source--dark" : ""}`;
|
|
25
|
+
if (sourceUrl) {
|
|
26
|
+
return (
|
|
27
|
+
<a
|
|
28
|
+
className={`${cls} bk-metric-highlight__source--link`}
|
|
29
|
+
href={sourceUrl}
|
|
30
|
+
target="_blank"
|
|
31
|
+
rel="nofollow noopener noreferrer"
|
|
32
|
+
>
|
|
33
|
+
<span className="bk-metric-highlight__source-kicker">Source</span>
|
|
34
|
+
{source}
|
|
35
|
+
<span
|
|
36
|
+
className="bk-metric-highlight__source-icon"
|
|
37
|
+
aria-hidden="true"
|
|
38
|
+
dangerouslySetInnerHTML={{ __html: ICONS["external"] }}
|
|
39
|
+
/>
|
|
40
|
+
</a>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return (
|
|
44
|
+
<div className={cls}>
|
|
45
|
+
<span className="bk-metric-highlight__source-kicker">Source</span>
|
|
46
|
+
{source}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function MetricHighlight({ metrics = [] }: MetricHighlightProps) {
|
|
52
|
+
if (!metrics || metrics.length === 0) return null;
|
|
53
|
+
|
|
54
|
+
if (metrics.length === 1) {
|
|
55
|
+
const m = metrics[0];
|
|
56
|
+
return (
|
|
57
|
+
<div className="bk-metric-highlight bk-metric-highlight--hero">
|
|
58
|
+
<div className="bk-metric-highlight__hero-value">{m.value}</div>
|
|
59
|
+
<div className="bk-metric-highlight__hero-body">
|
|
60
|
+
{m.change && <div className="bk-metric-highlight__hero-change">{m.change}</div>}
|
|
61
|
+
<div className="bk-metric-highlight__hero-label">{m.label}</div>
|
|
62
|
+
{m.source && <SourceRow source={m.source} sourceUrl={m.sourceUrl} dark />}
|
|
63
|
+
</div>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const cols = metrics.length === 2 ? 2 : metrics.length === 3 ? 3 : 4;
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<div className={`bk-metric-highlight bk-metric-highlight--grid bk-metric-highlight--${cols}`}>
|
|
72
|
+
{metrics.map((metric, i) => (
|
|
73
|
+
<div className="bk-metric-highlight__card" key={i}>
|
|
74
|
+
<div className="bk-metric-highlight__value">{metric.value}</div>
|
|
75
|
+
<div className="bk-metric-highlight__label">{metric.label}</div>
|
|
76
|
+
{metric.change && <div className="bk-metric-highlight__change">{metric.change}</div>}
|
|
77
|
+
{metric.source && (
|
|
78
|
+
<div className="bk-metric-highlight__source-wrap">
|
|
79
|
+
<SourceRow source={metric.source} sourceUrl={metric.sourceUrl} />
|
|
80
|
+
</div>
|
|
81
|
+
)}
|
|
82
|
+
</div>
|
|
83
|
+
))}
|
|
84
|
+
</div>
|
|
85
|
+
);
|
|
86
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/* blog-kit React adapter — NewsletterCTA
|
|
2
|
+
Thin wrapper: all styling lives in core/css/components.css (.bk-newsletter).
|
|
3
|
+
Dark email-capture block with a brand radial glow, heading/description on
|
|
4
|
+
one side and an inline subscribe form (with honeypot) on the other.
|
|
5
|
+
The form posts natively to `action` — no client JS in the kit. */
|
|
6
|
+
export interface NewsletterCTAProps {
|
|
7
|
+
heading?: string;
|
|
8
|
+
description?: string;
|
|
9
|
+
action?: string;
|
|
10
|
+
placeholder?: string;
|
|
11
|
+
buttonText?: string;
|
|
12
|
+
}
|
|
13
|
+
export function NewsletterCTA({
|
|
14
|
+
heading = "One email, once a week.",
|
|
15
|
+
description = "Field-tested ideas for turning chat traffic into revenue. No filler.",
|
|
16
|
+
action = "/api/newsletter",
|
|
17
|
+
placeholder = "you@studio.com",
|
|
18
|
+
buttonText = "Subscribe",
|
|
19
|
+
}: NewsletterCTAProps) {
|
|
20
|
+
return (
|
|
21
|
+
<div className="bk-newsletter">
|
|
22
|
+
<span className="bk-newsletter__glow" aria-hidden="true" />
|
|
23
|
+
<div className="bk-newsletter__copy">
|
|
24
|
+
<h3 className="bk-newsletter__heading">{heading}</h3>
|
|
25
|
+
<p className="bk-newsletter__desc">{description}</p>
|
|
26
|
+
</div>
|
|
27
|
+
<div className="bk-newsletter__action">
|
|
28
|
+
<form className="bk-newsletter__form" action={action} method="post">
|
|
29
|
+
<label className="bk-newsletter__honeypot" aria-hidden="true">
|
|
30
|
+
Leave this field empty
|
|
31
|
+
<input type="text" name="website" tabIndex={-1} autoComplete="off" />
|
|
32
|
+
</label>
|
|
33
|
+
<input
|
|
34
|
+
className="bk-newsletter__input"
|
|
35
|
+
type="email"
|
|
36
|
+
name="email"
|
|
37
|
+
required
|
|
38
|
+
placeholder={placeholder}
|
|
39
|
+
autoComplete="email"
|
|
40
|
+
/>
|
|
41
|
+
<button className="bk-newsletter__button" type="submit">
|
|
42
|
+
{buttonText}
|
|
43
|
+
</button>
|
|
44
|
+
</form>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
export interface NumberedCardProps { number?: number | string; title?: string; description?: ReactNode; children?: ReactNode; }
|
|
3
|
+
export function NumberedCard({ number, title, description, children }: NumberedCardProps) {
|
|
4
|
+
const n = String(number ?? "").padStart(2, "0");
|
|
5
|
+
const body = description ?? children;
|
|
6
|
+
return (<div className="bk-numbered-card"><span className="bk-numbered-card__badge">{n}</span>{title ? <h4 className="bk-numbered-card__title">{title}</h4> : null}{body ? <p className="bk-numbered-card__body">{body}</p> : null}</div>);
|
|
7
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
export interface ProConBlockProps {
|
|
2
|
+
pros?: string[];
|
|
3
|
+
cons?: string[];
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export function ProConBlock({ pros = [], cons = [] }: ProConBlockProps) {
|
|
7
|
+
if (!pros?.length && !cons?.length) return null;
|
|
8
|
+
return (
|
|
9
|
+
<div className="bk-procon">
|
|
10
|
+
{pros.length > 0 && (
|
|
11
|
+
<div className="bk-procon__card bk-procon__card--pro" data-kind="pro">
|
|
12
|
+
<h4 className="bk-procon__heading">Pros</h4>
|
|
13
|
+
<ul className="bk-procon__list">
|
|
14
|
+
{pros.map((item, i) => (
|
|
15
|
+
<li key={i} className="bk-procon__item">
|
|
16
|
+
<span className="bk-procon__mark" aria-hidden="true">
|
|
17
|
+
✓
|
|
18
|
+
</span>
|
|
19
|
+
<span className="bk-procon__text">{item}</span>
|
|
20
|
+
</li>
|
|
21
|
+
))}
|
|
22
|
+
</ul>
|
|
23
|
+
</div>
|
|
24
|
+
)}
|
|
25
|
+
{cons.length > 0 && (
|
|
26
|
+
<div className="bk-procon__card bk-procon__card--con" data-kind="con">
|
|
27
|
+
<h4 className="bk-procon__heading">Cons</h4>
|
|
28
|
+
<ul className="bk-procon__list">
|
|
29
|
+
{cons.map((item, i) => (
|
|
30
|
+
<li key={i} className="bk-procon__item">
|
|
31
|
+
<span className="bk-procon__mark" aria-hidden="true">
|
|
32
|
+
✕
|
|
33
|
+
</span>
|
|
34
|
+
<span className="bk-procon__text">{item}</span>
|
|
35
|
+
</li>
|
|
36
|
+
))}
|
|
37
|
+
</ul>
|
|
38
|
+
</div>
|
|
39
|
+
)}
|
|
40
|
+
</div>
|
|
41
|
+
);
|
|
42
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/* blog-kit React adapter — ProseList
|
|
2
|
+
Thin wrapper: all styling lives in core/css/components.css (.bk-prose-list).
|
|
3
|
+
Body-copy bullet (ul) or zero-padded ordered (ol) list. Items may be plain
|
|
4
|
+
HTML strings, or {title, description} objects — any title/description object
|
|
5
|
+
flips the whole list into a two-column grid so titles and descriptions align
|
|
6
|
+
across rows. Add `divided` for dashed-divider breathing room between items. */
|
|
7
|
+
import { cx } from "../core/lib";
|
|
8
|
+
|
|
9
|
+
export type ProseListVariant = "ul" | "ol";
|
|
10
|
+
|
|
11
|
+
export interface ProseListItem {
|
|
12
|
+
/** Bold lead-in (HTML allowed). */
|
|
13
|
+
title?: string;
|
|
14
|
+
/** Supporting copy (HTML allowed). */
|
|
15
|
+
description?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface ProseListProps {
|
|
19
|
+
/** "ul" — dash markers; "ol" — zero-padded counters (01, 02, …). */
|
|
20
|
+
variant?: ProseListVariant;
|
|
21
|
+
/** Plain HTML strings, or {title, description} objects (triggers grid mode). */
|
|
22
|
+
items?: Array<string | ProseListItem>;
|
|
23
|
+
/** Dashed divider between items. */
|
|
24
|
+
divided?: boolean;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function ProseList({ variant = "ul", items = [], divided = false }: ProseListProps) {
|
|
28
|
+
if (!Array.isArray(items) || items.length === 0) return null;
|
|
29
|
+
const v: ProseListVariant = variant === "ol" ? "ol" : "ul";
|
|
30
|
+
const Tag = v === "ol" ? "ol" : "ul";
|
|
31
|
+
// Grid layout kicks in when any item is an object with title+description, so
|
|
32
|
+
// titles and descriptions align into consistent columns across every row.
|
|
33
|
+
const isGrid = items.some(
|
|
34
|
+
(it) => it && typeof it === "object" && (it.title || it.description)
|
|
35
|
+
);
|
|
36
|
+
const cls = cx(
|
|
37
|
+
"bk-prose-list",
|
|
38
|
+
`bk-prose-list--${v}`,
|
|
39
|
+
divided && "bk-prose-list--divided",
|
|
40
|
+
isGrid && "bk-prose-list--grid"
|
|
41
|
+
);
|
|
42
|
+
return (
|
|
43
|
+
<Tag className={cls}>
|
|
44
|
+
{items.map((item, i) => {
|
|
45
|
+
if (isGrid) {
|
|
46
|
+
const obj: ProseListItem =
|
|
47
|
+
item && typeof item === "object" ? item : { description: String(item ?? "") };
|
|
48
|
+
return (
|
|
49
|
+
<li key={i} className="bk-prose-list__item">
|
|
50
|
+
<span className="bk-prose-list__marker" aria-hidden="true" />
|
|
51
|
+
<span
|
|
52
|
+
className="bk-prose-list__title"
|
|
53
|
+
dangerouslySetInnerHTML={{ __html: obj.title || "" }}
|
|
54
|
+
/>
|
|
55
|
+
<span
|
|
56
|
+
className="bk-prose-list__desc"
|
|
57
|
+
dangerouslySetInnerHTML={{ __html: obj.description || "" }}
|
|
58
|
+
/>
|
|
59
|
+
</li>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
return (
|
|
63
|
+
<li
|
|
64
|
+
key={i}
|
|
65
|
+
className="bk-prose-list__item"
|
|
66
|
+
dangerouslySetInnerHTML={{ __html: typeof item === "string" ? item : "" }}
|
|
67
|
+
/>
|
|
68
|
+
);
|
|
69
|
+
})}
|
|
70
|
+
</Tag>
|
|
71
|
+
);
|
|
72
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { ICONS } from "../core/icons";
|
|
3
|
+
|
|
4
|
+
export interface QuoteBlockProps {
|
|
5
|
+
/** The quote text. Optional — children may carry the body instead (MDX). */
|
|
6
|
+
quote?: string;
|
|
7
|
+
author?: string;
|
|
8
|
+
role?: string;
|
|
9
|
+
company?: string;
|
|
10
|
+
/** Source attribution text, e.g. "Forrester AI Study, 2024". */
|
|
11
|
+
source?: string;
|
|
12
|
+
/** When set alongside `source`, the attribution row links to the original. */
|
|
13
|
+
sourceUrl?: string;
|
|
14
|
+
/** Explicit avatar URL — overrides the favicon-from-sourceUrl default. */
|
|
15
|
+
avatarUrl?: string;
|
|
16
|
+
children?: ReactNode;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Google's public favicon-resolver URL for the source URL's host, or null when
|
|
21
|
+
* the URL is missing/unparseable so the avatar slot can be dropped entirely.
|
|
22
|
+
*/
|
|
23
|
+
function deriveFaviconUrl(sourceUrl?: string): string | null {
|
|
24
|
+
if (!sourceUrl) return null;
|
|
25
|
+
try {
|
|
26
|
+
const host = new URL(sourceUrl).hostname;
|
|
27
|
+
if (!host) return null;
|
|
28
|
+
return `https://www.google.com/s2/favicons?domain=${encodeURIComponent(host)}&sz=64`;
|
|
29
|
+
} catch {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function QuoteBlock({
|
|
35
|
+
quote,
|
|
36
|
+
author,
|
|
37
|
+
role,
|
|
38
|
+
company,
|
|
39
|
+
source,
|
|
40
|
+
sourceUrl,
|
|
41
|
+
avatarUrl,
|
|
42
|
+
children,
|
|
43
|
+
}: QuoteBlockProps) {
|
|
44
|
+
const subtitle = [role, company].filter(Boolean).join(" · ");
|
|
45
|
+
const avatarSrc = avatarUrl ?? deriveFaviconUrl(sourceUrl);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<figure className="bk-quote">
|
|
49
|
+
<span className="bk-quote__mark" aria-hidden>“</span>
|
|
50
|
+
<blockquote className="bk-quote__body">{quote ?? children}</blockquote>
|
|
51
|
+
{(author || subtitle) && (
|
|
52
|
+
<figcaption className="bk-quote__cite">
|
|
53
|
+
{avatarSrc && (
|
|
54
|
+
<img
|
|
55
|
+
className="bk-quote__avatar"
|
|
56
|
+
src={avatarSrc}
|
|
57
|
+
alt=""
|
|
58
|
+
aria-hidden
|
|
59
|
+
width={40}
|
|
60
|
+
height={40}
|
|
61
|
+
/>
|
|
62
|
+
)}
|
|
63
|
+
<span className="bk-quote__who">
|
|
64
|
+
{author && <cite className="bk-quote__author">{author}</cite>}
|
|
65
|
+
{subtitle && <span className="bk-quote__role">{subtitle}</span>}
|
|
66
|
+
</span>
|
|
67
|
+
</figcaption>
|
|
68
|
+
)}
|
|
69
|
+
{source &&
|
|
70
|
+
(sourceUrl ? (
|
|
71
|
+
<a
|
|
72
|
+
className="bk-quote__source bk-quote__source--link"
|
|
73
|
+
href={sourceUrl}
|
|
74
|
+
target="_blank"
|
|
75
|
+
rel="nofollow noopener noreferrer"
|
|
76
|
+
>
|
|
77
|
+
<span className="bk-quote__source-kicker">Source</span>
|
|
78
|
+
{source}
|
|
79
|
+
<span className="bk-quote__source-icon" dangerouslySetInnerHTML={{ __html: ICONS["external"] }} />
|
|
80
|
+
</a>
|
|
81
|
+
) : (
|
|
82
|
+
<div className="bk-quote__source">
|
|
83
|
+
<span className="bk-quote__source-kicker">Source</span>
|
|
84
|
+
{source}
|
|
85
|
+
</div>
|
|
86
|
+
))}
|
|
87
|
+
</figure>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/* blog-kit React adapter — RegionCallout
|
|
2
|
+
Thin wrapper: all styling lives in core/css/components.css (.bk-region-callout).
|
|
3
|
+
A bordered grid of office cells — each a flag/colour swatch beside an
|
|
4
|
+
uppercase office name and a brand-coloured monospace phone number.
|
|
5
|
+
Ported from hjd RegionCallout (.b-region). Cyan phone → --blog-brand,
|
|
6
|
+
#fff cells → --blog-paper, greys → border/bg tokens. */
|
|
7
|
+
|
|
8
|
+
export interface RegionCalloutOffice {
|
|
9
|
+
/** Office / region name, rendered uppercase. */
|
|
10
|
+
name: string;
|
|
11
|
+
/** Phone number, shown in brand-coloured monospace. */
|
|
12
|
+
phone?: string;
|
|
13
|
+
/** Optional CSS background for the swatch (colour, gradient, or url()). */
|
|
14
|
+
flag?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface RegionCalloutProps {
|
|
18
|
+
offices?: RegionCalloutOffice[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function RegionCallout({ offices = [] }: RegionCalloutProps) {
|
|
22
|
+
return (
|
|
23
|
+
<div className="bk-region-callout">
|
|
24
|
+
{offices.map((o, i) => (
|
|
25
|
+
<div key={i} className="bk-region-callout__cell">
|
|
26
|
+
<div
|
|
27
|
+
className="bk-region-callout__flag"
|
|
28
|
+
{...(o.flag ? { style: { background: o.flag } } : {})}
|
|
29
|
+
/>
|
|
30
|
+
<div className="bk-region-callout__info">
|
|
31
|
+
<div className="bk-region-callout__name">{o.name}</div>
|
|
32
|
+
{o.phone && <div className="bk-region-callout__phone">{o.phone}</div>}
|
|
33
|
+
</div>
|
|
34
|
+
</div>
|
|
35
|
+
))}
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/* blog-kit React adapter — RelatedPosts
|
|
2
|
+
Thin wrapper: all styling lives in core/css/components.css (.bk-related).
|
|
3
|
+
Framework-neutral <img> (no next/image) so React + Astro emit identical markup.
|
|
4
|
+
"Keep reading" grid of related-post cards. Featured image when present,
|
|
5
|
+
otherwise a tokenised gradient placeholder cycled per index. */
|
|
6
|
+
export interface RelatedPost {
|
|
7
|
+
slug: string;
|
|
8
|
+
title: string;
|
|
9
|
+
excerpt?: string;
|
|
10
|
+
date?: string;
|
|
11
|
+
/** Optional category pill shown above the title. */
|
|
12
|
+
category?: string;
|
|
13
|
+
/** Featured image URL; renders as the thumbnail when present. */
|
|
14
|
+
featuredImage?: string;
|
|
15
|
+
featuredImageAlt?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface RelatedPostsProps {
|
|
19
|
+
posts: RelatedPost[];
|
|
20
|
+
/** Section heading. */
|
|
21
|
+
heading?: string;
|
|
22
|
+
/** Optional override for the per-card link href; receives the slug. */
|
|
23
|
+
hrefBase?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function RelatedPosts({ posts, heading = "Keep reading", hrefBase = "/blog/" }: RelatedPostsProps) {
|
|
27
|
+
if (!posts || posts.length === 0) return null;
|
|
28
|
+
return (
|
|
29
|
+
<div className="bk-related">
|
|
30
|
+
<p className="bk-related__eyebrow">{heading}</p>
|
|
31
|
+
<div className="bk-related__grid">
|
|
32
|
+
{posts.map((post, i) => (
|
|
33
|
+
<a key={post.slug} className="bk-related__card" href={`${hrefBase}${post.slug}`}>
|
|
34
|
+
<div
|
|
35
|
+
className={`bk-related__thumb${post.featuredImage ? "" : ` bk-related__thumb--g${(i % 3) + 1}`}`}
|
|
36
|
+
aria-hidden={post.featuredImage ? undefined : true}
|
|
37
|
+
>
|
|
38
|
+
{post.featuredImage && (
|
|
39
|
+
<img
|
|
40
|
+
className="bk-related__img"
|
|
41
|
+
src={post.featuredImage}
|
|
42
|
+
alt={post.featuredImageAlt || ""}
|
|
43
|
+
loading="lazy"
|
|
44
|
+
decoding="async"
|
|
45
|
+
/>
|
|
46
|
+
)}
|
|
47
|
+
</div>
|
|
48
|
+
<div className="bk-related__body">
|
|
49
|
+
{post.category && <span className="bk-related__cat">{post.category}</span>}
|
|
50
|
+
<h5 className="bk-related__title">{post.title}</h5>
|
|
51
|
+
{post.excerpt && <p className="bk-related__excerpt">{post.excerpt}</p>}
|
|
52
|
+
</div>
|
|
53
|
+
</a>
|
|
54
|
+
))}
|
|
55
|
+
</div>
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/* blog-kit React adapter — ResultsStrip
|
|
2
|
+
Thin wrapper: all styling lives in core/css/components.css (.bk-results-strip).
|
|
3
|
+
A horizontal strip of bordered KPI cells — each an uppercase label, a big
|
|
4
|
+
brand-coloured number, an optional mono delta (green up / red down), and an
|
|
5
|
+
optional dashed-divider "SOURCE" attribution row (links out when sourceUrl
|
|
6
|
+
is set). Collapses to a 2-up grid on narrow screens. */
|
|
7
|
+
import { ICONS } from "../core/icons";
|
|
8
|
+
|
|
9
|
+
interface Kpi {
|
|
10
|
+
label: string;
|
|
11
|
+
num: string;
|
|
12
|
+
/** Optional change indicator, e.g. "+182%". Rendered in mono. */
|
|
13
|
+
delta?: string;
|
|
14
|
+
/** "up" (green, default) or "down" (red). */
|
|
15
|
+
deltaDirection?: "up" | "down";
|
|
16
|
+
/** Optional source attribution, shown below in a dashed-divider row. */
|
|
17
|
+
source?: string;
|
|
18
|
+
/** Optional URL — when set alongside `source`, the row becomes a link. */
|
|
19
|
+
sourceUrl?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ResultsStripProps {
|
|
23
|
+
kpis?: Kpi[];
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function SourceRow({ source, sourceUrl }: { source?: string; sourceUrl?: string }) {
|
|
27
|
+
if (!source) return null;
|
|
28
|
+
if (sourceUrl) {
|
|
29
|
+
return (
|
|
30
|
+
<a
|
|
31
|
+
className="bk-results-strip__source bk-results-strip__source--link"
|
|
32
|
+
href={sourceUrl}
|
|
33
|
+
target="_blank"
|
|
34
|
+
rel="nofollow noopener noreferrer"
|
|
35
|
+
>
|
|
36
|
+
<span className="bk-results-strip__source-kicker">Source</span>
|
|
37
|
+
{source}
|
|
38
|
+
<span
|
|
39
|
+
className="bk-results-strip__source-icon"
|
|
40
|
+
aria-hidden="true"
|
|
41
|
+
dangerouslySetInnerHTML={{ __html: ICONS["external"] }}
|
|
42
|
+
/>
|
|
43
|
+
</a>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return (
|
|
47
|
+
<div className="bk-results-strip__source">
|
|
48
|
+
<span className="bk-results-strip__source-kicker">Source</span>
|
|
49
|
+
{source}
|
|
50
|
+
</div>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function ResultsStrip({ kpis = [] }: ResultsStripProps) {
|
|
55
|
+
if (!kpis || kpis.length === 0) return null;
|
|
56
|
+
|
|
57
|
+
return (
|
|
58
|
+
<div className="bk-results-strip">
|
|
59
|
+
{kpis.map((k, i) => (
|
|
60
|
+
<div className="bk-results-strip__cell" key={i}>
|
|
61
|
+
<div className="bk-results-strip__label">{k.label}</div>
|
|
62
|
+
<div className="bk-results-strip__num">{k.num}</div>
|
|
63
|
+
{k.delta && (
|
|
64
|
+
<div
|
|
65
|
+
className={`bk-results-strip__delta${
|
|
66
|
+
k.deltaDirection === "down" ? " bk-results-strip__delta--down" : ""
|
|
67
|
+
}`}
|
|
68
|
+
>
|
|
69
|
+
{k.delta}
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
<SourceRow source={k.source} sourceUrl={k.sourceUrl} />
|
|
73
|
+
</div>
|
|
74
|
+
))}
|
|
75
|
+
</div>
|
|
76
|
+
);
|
|
77
|
+
}
|