Files
zeekayandHanzo Dev e8936429d9 billing wears a rail, quotes annual first, and builds again
THE BUILD WAS BROKEN, and not by any of this: `@hanzogui/shell` is imported by
lib/components/billing-shell.tsx and was declared in package.json but never
installed, so every build died — and Next's own code-frame highlighter panicked
while formatting the error, which is why it read as an internal crash rather
than a missing module. Installed and pinned exactly at 8.1.44.

THE CHROME IS A RAIL. This is an account surface — you come to do one thing to
your own billing and leave — and a bar across the top spends the dimension a
page has least of on four links that never change, while pushing the thing you
came for down. A column spends the dimension there is plenty of, and it is what
hanzo.app and hanzo.chat already do, so moving between them costs no
relearning. Hidden below `md`, where 240px of navigation is most of a phone and
the footer already carries every link.

`display` lives in the CLASS, not the inline style. It is the one property the
media query has to change, and stating it inline then fighting that with
`!important` would leave two homes for one property.

ANNUAL IS THE DEFAULT. It is the cheaper of the two and the one most buyers
take, so it is the price the page should answer "what does this cost?" with —
Team reads $20 per user rather than $25, and monthly is one click away. The same
choice hanzo.ai's team strip already made, so two surfaces stop quoting one plan
at two different numbers.

The wordmark says "Hanzo AI".

Verified on the built export at its real base path (/admin/billing — serving it
at root 404s every stylesheet and reads as a CSS bug): rail 240px and flex at
1440 and 1024, display:none at 767 and 390, no sideways scroll at any width,
and Annual selected on load.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-16 17:39:24 -07:00

86 lines
3.0 KiB
TypeScript

'use client'
import { useState } from 'react'
import { PlanCard, type PricingPlan } from './plan-card'
interface BillingToggleProps {
plans: PricingPlan[]
}
export function BillingToggle({ plans }: BillingToggleProps) {
// ANNUAL first. It is the cheaper of the two and the one most buyers take, so
// it is the price the page should be answering "what does this cost?" with —
// Team reads $20 per user rather than $25, and the monthly rate is one click
// away for anyone who wants it.
//
// The same choice hanzo.ai's team strip already made, for the same reason
// stated there: annual is the headline, month-to-month is the note under it.
// Two surfaces quoting the same plan at two different numbers is the thing
// worth avoiding.
const [interval, setInterval] = useState<'monthly' | 'annual'>('annual')
return (
<>
{/* Toggle */}
<div style={{ display: 'flex', justifyContent: 'center', marginBottom: '40px' }}>
<div style={{
display: 'inline-flex',
background: 'color-mix(in oklab, var(--color-text) 4%, transparent)',
border: '1px solid color-mix(in oklab, var(--color-text) 10%, transparent)',
borderRadius: '10px',
padding: '4px',
gap: '4px',
}}>
{(['monthly', 'annual'] as const).map((iv) => (
<button
key={iv}
type="button"
onClick={() => setInterval(iv)}
style={{
background: interval === iv ? 'color-mix(in oklab, var(--color-text) 10%, transparent)' : 'transparent',
border: interval === iv ? '1px solid color-mix(in oklab, var(--color-text) 15%, transparent)' : '1px solid transparent',
borderRadius: '7px',
padding: '6px 16px',
color: interval === iv ? 'var(--color-text)' : 'color-mix(in oklab, var(--color-text) 40%, transparent)',
fontSize: '13px',
fontWeight: 500,
cursor: 'pointer',
transition: 'all 0.15s',
display: 'flex',
alignItems: 'center',
gap: '6px',
}}
>
{iv === 'monthly' ? 'Monthly' : 'Annual'}
{iv === 'annual' && (
<span style={{
background: 'rgba(74,222,128,0.15)',
color: '#4ade80',
fontSize: '10px',
fontWeight: 700,
padding: '1px 5px',
borderRadius: '4px',
letterSpacing: '0.03em',
}}>
Save 20%
</span>
)}
</button>
))}
</div>
</div>
{/* Plans grid */}
<div style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))',
gap: '20px',
}}>
{plans.map((plan) => (
<PlanCard key={plan.id} plan={plan} billingInterval={interval} />
))}
</div>
</>
)
}