diff --git a/modules/blox/blox/comparison-table/client.jsx b/modules/blox/blox/comparison-table/client.jsx new file mode 100644 index 00000000..0da79da5 --- /dev/null +++ b/modules/blox/blox/comparison-table/client.jsx @@ -0,0 +1,15 @@ +import {render} from "preact"; +import {ComparisonTableBlock} from "./component.jsx"; + +function renderComparisonTableBlocks() { + const blocks = document.querySelectorAll('[data-block-type="comparison-table"]'); + blocks.forEach((block) => { + const propsData = block.dataset.props; + if (propsData) { + const props = JSON.parse(propsData); + render(, block); + } + }); +} + +renderComparisonTableBlocks(); diff --git a/modules/blox/blox/comparison-table/component.jsx b/modules/blox/blox/comparison-table/component.jsx new file mode 100644 index 00000000..a7baf9cf --- /dev/null +++ b/modules/blox/blox/comparison-table/component.jsx @@ -0,0 +1,319 @@ +import {Icon} from "../../shared/components/Icon.jsx"; + +const CHECK_SVG = ``; +const X_SVG = ``; +const MINUS_SVG = ``; + +function renderText(text) { + if (!text) return ""; + return String(text) + .replace(/\*\*(.*?)\*\*/g, "$1") + .replace(/\*(.*?)\*/g, "$1") + .replace(/`(.*?)`/g, (_, code) => `${code}`); +} + +// Resolve a cell value into one of: {kind: "check"} | {kind: "cross"} | {kind: "partial", note} | {kind: "text", value, emphasis} +function resolveCell(raw) { + if (raw === true) return {kind: "check"}; + if (raw === false || raw == null || raw === "") return {kind: "cross"}; + if (typeof raw === "object") { + if (raw.value === true) return {kind: "check", note: raw.note}; + if (raw.value === false) return {kind: "cross", note: raw.note}; + if (raw.value === "partial" || raw.partial) return {kind: "partial", note: raw.note ?? raw.value}; + return {kind: "text", value: raw.value ?? "", emphasis: raw.emphasis ?? false, note: raw.note}; + } + const s = String(raw).trim(); + if (s === "true" || s === "yes" || s === "✓") return {kind: "check"}; + if (s === "false" || s === "no" || s === "✗" || s === "—") return {kind: "cross"}; + if (s === "partial" || s === "limited") return {kind: "partial", note: s}; + return {kind: "text", value: s}; +} + +function CellContent({cell, isHighlighted}) { + const positiveColor = isHighlighted + ? "text-primary-600 dark:text-primary-400" + : "text-green-500 dark:text-green-400"; + const partialColor = "text-amber-500 dark:text-amber-400"; + + if (cell.kind === "check") { + return ( +
+ + {cell.note && {cell.note}} +
+ ); + } + if (cell.kind === "cross") { + return ( +
+ + {cell.note && {cell.note}} +
+ ); + } + if (cell.kind === "partial") { + return ( +
+ + {cell.note && {cell.note}} +
+ ); + } + // text — highlighted column gets semibold dark to visually "win" alongside the brand-coloured checks + const isStrong = cell.emphasis || isHighlighted; + const weight = isStrong ? "font-semibold text-gray-900 dark:text-white" : "text-gray-700 dark:text-gray-300"; + return ( +
+ {cell.value} + {cell.note &&
{cell.note}
} +
+ ); +} + +function DesktopTable({competitors, rows, cta, icon_svgs, rowStriping = false}) { + const highlightIdx = competitors.findIndex((c) => c.highlight); + const ctaIconSvg = cta?.icon ? (icon_svgs?.[cta.icon] ?? null) : null; + const isExtCta = cta?.url && (cta.url.startsWith("http://") || cta.url.startsWith("https://")); + const highlighted = highlightIdx >= 0 ? competitors[highlightIdx] : null; + // Column widths: feature label column gets 1.5× a competitor column for readable row labels + const colWidthPct = 100 / (competitors.length + 1.5); + const labelColPct = colWidthPct * 1.5; + const colLeftPct = labelColPct + colWidthPct * (highlightIdx >= 0 ? highlightIdx : 0); + + return ( + // pt-5 reserves vertical space above the table for the ring-tab badge to sit on the column's top edge +