mirror of
https://github.com/hanzo-apps/hanzo.ai.git
synced 2026-10-29 07:20:43 +00:00
The lint caught a token I invented (--borderColor is a Tamagui theme key, not a CSS variable) and four style props that had a component already written for them. The measured/reported switch is now the same ToggleGroup the pricing plans use, and the table is Text render="tr"/"td" the way marketing/Comparison.tsx already does it. Co-Authored-By: Claude Code <[email protected]>
159 lines
5.9 KiB
TypeScript
159 lines
5.9 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
import { Anchor, Box, Text, ToggleGroup, ToggleGroupItem, XStack, YStack } from '@hanzo/ui'
|
|
import type { Comparison, CompareRow } from '@/lib/compare'
|
|
|
|
/* A rung and its own leading, so one row cannot come out a pixel and a half
|
|
taller than the one beside it. */
|
|
const SMALL = { fontSize: '$2', lineHeight: '1.1607rem' } as const
|
|
|
|
/**
|
|
* The switch between what we ran and what was published.
|
|
*
|
|
* Measured is the default and reported is one click away, because the two are
|
|
* not the same kind of claim and a reader should never have to scan for which is
|
|
* which. Showing everything at once is what every comparison table does, and it
|
|
* is how a relayed number ends up looking as solid as a reproduced one.
|
|
*/
|
|
function Toggle({
|
|
showReported,
|
|
onChange,
|
|
measuredCount,
|
|
reportedCount,
|
|
}: {
|
|
showReported: boolean
|
|
onChange: (v: boolean) => void
|
|
measuredCount: number
|
|
reportedCount: number
|
|
}) {
|
|
return (
|
|
<XStack mb="$4" p={4} rounded={9999} bg="var(--surface-2)" self="flex-start">
|
|
<ToggleGroup
|
|
type="single"
|
|
value={showReported ? 'reported' : 'measured'}
|
|
onValueChange={(v) => onChange(v === 'reported')}
|
|
disableDeactivation
|
|
aria-label="Which rows to show"
|
|
>
|
|
<ToggleGroupItem value="measured" variant="outline" rounded="$10" borderColor="transparent" px="$4">
|
|
Measured ({measuredCount})
|
|
</ToggleGroupItem>
|
|
<ToggleGroupItem value="reported" variant="outline" rounded="$10" borderColor="transparent" px="$4">
|
|
+ Reported ({reportedCount})
|
|
</ToggleGroupItem>
|
|
</ToggleGroup>
|
|
</XStack>
|
|
)
|
|
}
|
|
|
|
/** A dated link to where a figure came from. */
|
|
function Ref({ href, date }: { href: string; date: string }) {
|
|
return (
|
|
<>
|
|
{' '}
|
|
<Anchor href={href} target="_blank" rel="noreferrer noopener" color="var(--muted-foreground)">
|
|
[{date}]
|
|
</Anchor>
|
|
</>
|
|
)
|
|
}
|
|
|
|
function Row({ row, comparison }: { row: CompareRow; comparison: Comparison }) {
|
|
return (
|
|
<Text render="tr" borderBottomWidth={1} borderColor="var(--border)" $platform-web={{ display: 'table-row' }}>
|
|
<Text render="th" py={12} pl={1} pr={16} text="left" fontWeight="500" {...SMALL} $platform-web={{ display: 'table-cell' }}>
|
|
{row.attribute}
|
|
</Text>
|
|
<Text render="td" py={12} pl={1} pr={16} {...SMALL} $platform-web={{ display: 'table-cell' }}>
|
|
{row.ours}
|
|
{row.source === 'measured' && row.oursRef ? <Ref {...row.oursRef} /> : null}
|
|
</Text>
|
|
{comparison.rivals.map((r) => (
|
|
<Text
|
|
key={r.id}
|
|
render="td"
|
|
py={12}
|
|
pl={1}
|
|
pr={16}
|
|
color="var(--muted-foreground)"
|
|
{...SMALL}
|
|
$platform-web={{ display: 'table-cell' }}
|
|
>
|
|
{row.theirs[r.id] ?? 'not published'}
|
|
{row.theirRefs?.[r.id] ? <Ref {...row.theirRefs[r.id]} /> : null}
|
|
</Text>
|
|
))}
|
|
<Text render="td" py={12} pl={1} pr={1} color="var(--muted-foreground)" {...SMALL} $platform-web={{ display: 'table-cell' }}>
|
|
{row.source === 'measured' ? 'we ran this' : 'they published this'}
|
|
</Text>
|
|
</Text>
|
|
)
|
|
}
|
|
|
|
export function CompareTable({ comparison }: { comparison: Comparison }) {
|
|
const measured = comparison.rows.filter((r) => r.source === 'measured')
|
|
const reported = comparison.rows.filter((r) => r.source === 'reported')
|
|
// A category with nothing measured yet opens on the reported rows rather than
|
|
// showing an empty table, and says so.
|
|
const [showReported, setShowReported] = useState(measured.length === 0)
|
|
const rows = showReported ? comparison.rows : measured
|
|
const notes = comparison.rows.filter((r) => r.note && (showReported || r.source === 'measured'))
|
|
|
|
return (
|
|
<YStack>
|
|
<Toggle
|
|
showReported={showReported}
|
|
onChange={setShowReported}
|
|
measuredCount={measured.length}
|
|
reportedCount={reported.length}
|
|
/>
|
|
{/* Four vendors plus the attribute and provenance columns stop being
|
|
readable under ~720px; the wrapper scrolls rather than crushing them. */}
|
|
<Box overflowX="auto">
|
|
<Text
|
|
render="table"
|
|
width="100%"
|
|
minW={720}
|
|
text="left"
|
|
{...SMALL}
|
|
$platform-web={{ display: 'table', borderCollapse: 'collapse' }}
|
|
>
|
|
<thead>
|
|
<Text render="tr" borderColor="var(--border)" $platform-web={{ display: 'table-row' }}>
|
|
<Text render="th" py={12} pl={1} pr={16} text="left" fontWeight="500" color="var(--muted-foreground)" {...SMALL} $platform-web={{ display: 'table-cell' }}>
|
|
{comparison.subject.name} vs {comparison.rivals.map((r) => r.name).join(', ')}
|
|
</Text>
|
|
<Text render="th" py={12} pl={1} pr={16} text="left" fontWeight="500" {...SMALL} $platform-web={{ display: 'table-cell' }}>
|
|
Hanzo {comparison.subject.name}
|
|
</Text>
|
|
{comparison.rivals.map((r) => (
|
|
<Text key={r.id} render="th" py={12} pl={1} pr={16} text="left" fontWeight="500" {...SMALL} $platform-web={{ display: 'table-cell' }}>
|
|
{r.name}
|
|
</Text>
|
|
))}
|
|
<Text render="th" py={12} pl={1} pr={1} text="left" fontWeight="500" color="var(--muted-foreground)" {...SMALL} $platform-web={{ display: 'table-cell' }}>
|
|
Provenance
|
|
</Text>
|
|
</Text>
|
|
</thead>
|
|
<tbody>
|
|
{rows.map((row) => (
|
|
<Row key={row.attribute} row={row} comparison={comparison} />
|
|
))}
|
|
</tbody>
|
|
</Text>
|
|
</Box>
|
|
{notes.length ? (
|
|
<YStack mt="$3" gap="$2">
|
|
{notes.map((r) => (
|
|
<Text render="p" display="block" key={r.attribute} fontSize="$2" lineHeight="1.25rem" color="var(--muted-foreground)">
|
|
<strong>{r.attribute}:</strong> {r.note}
|
|
</Text>
|
|
))}
|
|
</YStack>
|
|
) : null}
|
|
</YStack>
|
|
)
|
|
}
|