Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
294 changes: 137 additions & 157 deletions apps/web/components/comparator-7.tsx
Original file line number Diff line number Diff line change
@@ -1,189 +1,169 @@
import { cn } from '@workspace/ui/lib/utils'
import { TooltipProvider, Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip'
import { Check, X } from "lucide-react"

const plans = ['rollYourOwn', 'hostedSaas', 'helpbase'] as const
import { cn } from "@workspace/ui/lib/utils"

type Plan = (typeof plans)[number]

type Cell = boolean | string

type Feature = {
type Option = {
key: "roll-your-own" | "hosted-saas" | "helpbase"
name: string
description?: string
plans: Record<Plan, Cell>
}

const planLabels: Record<Plan, string> = {
rollYourOwn: 'Roll your own',
hostedSaas: 'Hosted SaaS',
helpbase: 'Helpbase',
metric: string
metricSubtitle: string
body: string
signals: Array<{ positive: boolean; text: string }>
cost: string
costNote?: string
emphasized?: boolean
}

const features: Feature[] = [
{
name: 'Time to first site',
description: 'From zero to a live docs site on your domain.',
plans: { rollYourOwn: '2-3 days', hostedSaas: '1 day', helpbase: '3 min' },
},
const options: Option[] = [
{
name: 'You own every file',
description: 'Code lives in your repo, commits land in your git history.',
plans: { rollYourOwn: true, hostedSaas: false, helpbase: true },
key: "roll-your-own",
name: "Roll your own",
metric: "2–3 days",
metricSubtitle: "to first site",
body: "Build a docs site in Next.js from scratch. Burn the weekend writing the framework, then another maintaining it.",
signals: [
{ positive: true, text: "You own every file" },
{ positive: false, text: "No MCP, no llms.txt" },
{ positive: false, text: "No hosted option" },
],
cost: "Your weekend",
},
{
name: 'MCP server',
description: 'Model Context Protocol endpoint Claude / Cursor / ChatGPT can query.',
plans: { rollYourOwn: 'DIY', hostedSaas: 'Limited', helpbase: 'Built in' },
key: "hosted-saas",
name: "Hosted docs SaaS",
metric: "1 day",
metricSubtitle: "to first site",
body: "Pay a hosted vendor to run your docs. Fast to start. Your content lives in their database. Migrating off is a project.",
signals: [
{ positive: false, text: "Your docs, their database" },
{ positive: false, text: "MCP is a Pro-tier add-on" },
{ positive: false, text: "Vendor-locked hosting" },
],
cost: "$200–1k",
costNote: "/ month",
},
{
name: 'llms.txt + typed output',
description: 'Discoverability manifest + machine-readable content, by default.',
plans: { rollYourOwn: false, hostedSaas: false, helpbase: true },
},
{
name: 'Deploy anywhere',
description: 'No platform lock-in. Ship your docs on whatever infra you already use \u2014 Vercel, Fly, self-host, your call.',
plans: { rollYourOwn: true, hostedSaas: false, helpbase: true },
},
{
name: 'Cost',
description: 'Sticker price for a working help center at seed stage.',
plans: { rollYourOwn: 'Your weekend', hostedSaas: '$200-1k/mo', helpbase: 'Free or hosted' },
},
{
name: 'Maintained without you',
description: 'Hosted tier handles updates, caching, MCP scaling.',
plans: { rollYourOwn: false, hostedSaas: true, helpbase: 'Hosted tier' },
},
{
name: 'Agents can read it',
description: 'Your editor autocompletes from your real docs instead of guessing.',
plans: { rollYourOwn: 'DIY', hostedSaas: 'Limited', helpbase: true },
key: "helpbase",
name: "Helpbase",
metric: "3 min",
metricSubtitle: "to first site",
body: "Open source. One command drops a Next.js help center into your repo. MCP server and llms.txt built in. Host it yourself, or with us.",
signals: [
{ positive: true, text: "Own every file" },
{ positive: true, text: "MCP + llms.txt built in" },
{ positive: true, text: "Host anywhere, or with us" },
],
cost: "Free or hosted",
emphasized: true,
},
]

const renderPlanColumn = (plan: Plan) => {
const isPrimary = plan === 'helpbase'
const header = (
<div className={cn('sticky top-0 flex h-14 flex-col items-center justify-center gap-1.5 px-4 text-center lg:px-6', isPrimary && 'rounded-t-xl')}>
<span className={cn('text-foreground text-sm font-semibold', isPrimary && 'text-primary')}>{planLabels[plan]}</span>
</div>
)

return (
<div
data-plan={plan}
className={cn(isPrimary && 'ring-border bg-card/50 shadow-black/6.5 relative z-10 rounded-xl shadow-xl ring-1')}>
{header}

<div>
{features.map((feature, index) => {
const value = feature.plans[plan]
return (
<div
key={index}
className="odd:bg-card flex h-14 items-center justify-center px-4 text-sm last:h-[calc(3.5rem+1px)] last:border-b group-last:odd:rounded-r-lg">
<div className="text-center">
{value === true ? (
<Indicator checked />
) : value === false ? (
<Indicator />
) : (
<span className={cn('text-muted-foreground text-xs font-medium', isPrimary && 'text-foreground')}>{value}</span>
)}
</div>
</div>
)
})}
<div className="h-6"></div>
</div>
</div>
)
}

export default function Comparator() {
return (
<section
aria-labelledby="comparator-heading"
className="bg-background py-16 md:py-24">
<div className="mx-auto max-w-5xl px-6">
<div className="grid gap-12 lg:grid-cols-[1fr_2fr]">
<div className="max-w-lg max-md:px-6">
<div className="text-balance lg:max-w-xs">
<h2
id="comparator-heading"
className="text-foreground text-3xl font-semibold md:text-4xl">
Two options today. Both are compromises.
</h2>
<p className="text-muted-foreground mt-4 text-balance lg:mt-6">
Build your own in Next.js and burn a weekend. Pay a hosted docs SaaS and get locked in. Helpbase is the third option: free as open source, paid only when you want us to host it.
</p>
</div>
</div>

<div className="grid grid-cols-[1fr_1fr_1fr_1.1fr]">
<div>
<div className="z-1 sticky top-0 flex h-14 items-end gap-1.5 px-4 py-2 lg:px-6">
<div className="text-muted-foreground text-sm font-medium">Feature</div>
</div>

{features.map((feature, index) => (
<div
key={index}
className="text-muted-foreground md:nth-2:rounded-tl-xl even:bg-card flex h-14 items-center gap-1 rounded-l-lg px-4 last:h-[calc(3.5rem+1px)] md:last:rounded-bl-xl lg:px-6">
<div className="text-sm">{feature.name}</div>
{feature.description && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger className="flex size-7">
<span className="bg-foreground/10 text-foreground/65 m-auto flex size-4 items-center justify-center rounded-full text-sm">?</span>
</TooltipTrigger>
<TooltipContent className="max-w-56 text-sm">{feature.description}</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
))}
</div>
<div className="mx-auto mb-12 max-w-2xl text-center">
<h2
id="comparator-heading"
className="text-foreground text-3xl font-semibold md:text-4xl">
Two options today. Both are compromises.
</h2>
<p className="text-muted-foreground mx-auto mt-4 max-w-xl text-balance text-lg">
Helpbase is the third option.
</p>
</div>

{plans.map((plan) => (
<div
key={plan}
className="group">
{renderPlanColumn(plan)}
</div>
))}
</div>
<div className="grid gap-4 md:grid-cols-3">
{options.map((option) => (
<OptionCard
key={option.key}
option={option}
/>
))}
</div>
</div>
</section>
)
}

const Indicator = ({ checked = false }: { checked?: boolean }) => {
function OptionCard({ option }: { option: Option }) {
return (
<span
<div
className={cn(
'mx-auto flex size-4 items-center justify-center rounded-full bg-rose-500 font-sans text-xs font-semibold text-white',
checked && 'bg-emerald-600 text-white',
"ring-border relative flex flex-col rounded-2xl border border-transparent p-6 ring-1",
option.emphasized
? "bg-card shadow-black/6.5 z-10 shadow-xl"
: "bg-card/40",
Comment on lines +95 to +98
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

# Find and read tailwind config and package.json to understand Tailwind version and opacity settings
find . -type f \( -name "tailwind.config.*" -o -name "package.json" \) -not -path "*/node_modules/*" | head -20

Repository: Codehagen/Helpbase

Length of output: 384


🏁 Script executed:

# Check the specific file mentioned in the review
cat -n apps/web/components/comparator-7.tsx | sed -n '90,105p'

Repository: Codehagen/Helpbase

Length of output: 808


🏁 Script executed:

# Search for other instances of shadow opacity syntax in the codebase
rg "shadow-.*/([\d\.]+)" --no-heading -A 1 -B 1 | head -40

Repository: Codehagen/Helpbase

Length of output: 15361


🏁 Script executed:

# Check Tailwind configuration for custom opacity tokens
find . -type f -name "tailwind.config.*" -not -path "*/node_modules/*" | head -5

Repository: Codehagen/Helpbase

Length of output: 44


🏁 Script executed:

# Read the main tailwind config
cat tailwind.config.ts 2>/dev/null || cat tailwind.config.js 2>/dev/null || cat tailwind.config.mjs 2>/dev/null || echo "No tailwind config found at root"

Repository: Codehagen/Helpbase

Length of output: 94


🏁 Script executed:

# Check package.json for Tailwind version
grep -A 2 "tailwindcss" ./package.json

Repository: Codehagen/Helpbase

Length of output: 157


🏁 Script executed:

# Check if opacity 6.5 is used elsewhere in the codebase
rg "/(6\.5|0\.065)" --no-heading -B 2 -A 2

Repository: Codehagen/Helpbase

Length of output: 14166


🏁 Script executed:

# Search more carefully for Tailwind config files (might be in nested packages)
find . -type f \( -name "tailwind.config.*" -o -name "tailwind.css" \) -not -path "*/node_modules/*" -not -path "*/.next/*" 2>/dev/null

Repository: Codehagen/Helpbase

Length of output: 44


🏁 Script executed:

# Check package.json files for tailwind config references or versions
grep -r "tailwindcss" */package.json --include="package.json" | grep -v node_modules | head -20

Repository: Codehagen/Helpbase

Length of output: 109


🏁 Script executed:

# Look for CSS files that might define custom opacity or theme extensions
find . -type f -name "*.css" -not -path "*/node_modules/*" | xargs grep -l "opacity\|theme\|6\.5" 2>/dev/null | head -10

Repository: Codehagen/Helpbase

Length of output: 202


🏁 Script executed:

# Search more broadly for tailwind configuration in the entire repository
find . -type f \( -name "*.ts" -o -name "*.js" -o -name "*.mjs" \) -not -path "*/node_modules/*" -not -path "*/.next/*" | xargs grep -l "tailwind.*config\|theme.*opacity" 2>/dev/null | head -10

Repository: Codehagen/Helpbase

Length of output: 44


🏁 Script executed:

# Check one of the CSS files to understand the theme setup
cat ./packages/ui/src/styles/globals.css

Repository: Codehagen/Helpbase

Length of output: 12037


🏁 Script executed:

# Search for any custom theme or opacity configuration in TS/JS files
rg "opacity.*[6\.5|0\.065]|@layer|theme\s*:" -A 5 -B 2 --type ts --type js | head -60

Repository: Codehagen/Helpbase

Length of output: 4902


🏁 Script executed:

# Check if there's any custom opacity configuration in the CSS variables or extended theme
rg "opacity|--opacity" packages/ui/src/styles/globals.css apps/web/styles/ 2>/dev/null || echo "No opacity in theme"

Repository: Codehagen/Helpbase

Length of output: 795


🏁 Script executed:

# Check Tailwind documentation to understand the version being used
grep -r "tailwindcss\|@tailwindcss" package.json apps/web/package.json 2>/dev/null | head -5

Repository: Codehagen/Helpbase

Length of output: 296


🏁 Script executed:

# Check for postcss config
find . -type f \( -name "postcss.config.*" -o -name ".postcssrc*" \) -not -path "*/node_modules/*"

Repository: Codehagen/Helpbase

Length of output: 180


🏁 Script executed:

# Check the apps/web package.json for any theme or tailwind-specific config
cat apps/web/package.json | grep -A 50 "tailwind\|theme" || head -100 apps/web/package.json

Repository: Codehagen/Helpbase

Length of output: 1133


🏁 Script executed:

# Search for any extend theme or opacity configuration in the app
rg "extend.*theme|opacity.*scale" apps/web --type ts --type js | head -20

Repository: Codehagen/Helpbase

Length of output: 316


🏁 Script executed:

# Check the postcss config
cat apps/web/postcss.config.mjs

Repository: Codehagen/Helpbase

Length of output: 117


🏁 Script executed:

# Search for any CSS custom properties or theme extensions related to opacity
rg "6\.5|0\.065" apps/web/ packages/ui/src/styles/ -B 3 -A 3 --type css --type ts | head -100

Repository: Codehagen/Helpbase

Length of output: 8015


🏁 Script executed:

# Check if Tailwind v4 docs mention how /6.5 is handled
# Let's test by looking at what the actual build might generate
find apps/web -type f -name "*.tsx" -o -name "*.css" | xargs grep -l "shadow-black/6.5" | wc -l

Repository: Codehagen/Helpbase

Length of output: 64


🏁 Script executed:

# Check the workspace UI postcss config to see if there's any custom opacity definition
cat packages/ui/postcss.config.mjs

Repository: Codehagen/Helpbase

Length of output: 202


🏁 Script executed:

# Verify the actual Tailwind v4 syntax by checking their documentation patterns
# Let's also count how many build-time warnings might be generated
rg "shadow-" apps/web/components/*.tsx | grep -c "shadow-black"

Repository: Codehagen/Helpbase

Length of output: 63


🏁 Script executed:

# Check the `@theme` block in globals.css for any opacity configuration
cat packages/ui/src/styles/globals.css | grep -A 100 "@theme inline" | grep -i opacity

Repository: Codehagen/Helpbase

Length of output: 44


🏁 Script executed:

# Check if there's any Tailwind config that might extend opacity values in Tailwind v4
# Also look for any extend or custom theme configuration
find . -type f \( -name "*.ts" -o -name "*.js" -o -name "*.css" \) -not -path "*/node_modules/*" | xargs grep -l "opacity.*6\.5\|extend.*opacity" 2>/dev/null | head -10

Repository: Codehagen/Helpbase

Length of output: 44


🏁 Script executed:

# Let's verify what the standard Tailwind v4 opacity scale is by checking if 6.5 converts to decimal correctly
# 6.5 would be 6.5% opacity = 0.065 in decimal (or as a ratio 6.5/100)
python3 << 'EOF'
# Standard Tailwind opacity scale: 5, 10, 15, ... 95
# In percentages and decimal equivalents
standard_scale = [5, 10, 15, 20, 25, 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95]
print("Standard Tailwind opacity scale:")
for val in standard_scale:
    print(f"  /{val} = {val/100}")

print("\nFor 6.5:")
print(f"  /6.5 would be {6.5/100} (not in standard scale)")
print(f"  /[0.065] would be 0.065 (6.5%)")
EOF

Repository: Codehagen/Helpbase

Length of output: 420


Use valid Tailwind opacity syntax for the emphasized shadow.

shadow-black/6.5 is not a standard Tailwind opacity value. Tailwind's opacity scale includes 5, 10, 15... 95 (increments of 5). Arbitrary opacity values must use bracket notation.

🎨 Proposed fix
-                    ? "bg-card shadow-black/6.5 z-10 shadow-xl"
+                    ? "bg-card z-10 shadow-xl shadow-black/[0.065]"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
"ring-border relative flex flex-col rounded-2xl border border-transparent p-6 ring-1",
option.emphasized
? "bg-card shadow-black/6.5 z-10 shadow-xl"
: "bg-card/40",
"ring-border relative flex flex-col rounded-2xl border border-transparent p-6 ring-1",
option.emphasized
? "bg-card z-10 shadow-xl shadow-black/[0.065]"
: "bg-card/40",
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/comparator-7.tsx` around lines 95 - 98, Replace the
invalid Tailwind opacity token "shadow-black/6.5" inside the emphasized class
string (the ternary using option.emphasized) with a valid Tailwind
opacity—either a nearest standard step like "shadow-black/5" or an arbitrary
value using bracket notation such as "shadow-black/[0.065]"; update the class
array element in the comparator-7 component where option.emphasized controls the
string so the resulting class list uses a valid Tailwind opacity token.

)}>
{checked ? <CheckIcon /> : '✗'}
</span>
)
}
{option.emphasized && (
<span className="bg-foreground text-background absolute -top-3 right-6 rounded-full px-2 py-0.5 text-[0.65rem] font-medium uppercase tracking-wider">
Recommended
</span>
)}

const CheckIcon = () => {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 512 512"
className="size-2.5">
<path
fill="currentColor"
d="M17.47 250.9C88.82 328.1 158 397.6 224.5 485.5c72.3-143.8 146.3-288.1 268.4-444.37L460 26.06C356.9 135.4 276.8 238.9 207.2 361.9c-48.4-43.6-126.62-105.3-174.38-137z"
/>
</svg>
<div className="text-muted-foreground text-sm font-medium">
{option.name}
</div>

<div className="mt-2">
<div className="text-foreground text-3xl font-semibold tabular-nums">
{option.metric}
</div>
<div className="text-muted-foreground text-xs">
{option.metricSubtitle}
</div>
</div>

<p className="text-muted-foreground mt-4 text-sm leading-relaxed">
{option.body}
</p>

<ul
role="list"
className="mt-5 space-y-2.5 text-sm">
{option.signals.map((signal, i) => (
<li
key={i}
className="flex items-start gap-2">
{signal.positive ? (
<span className="bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 mt-px flex size-4 shrink-0 items-center justify-center rounded-full">
<Check
className="size-2.5"
strokeWidth={3}
/>
</span>
) : (
<span className="bg-foreground/10 text-muted-foreground mt-px flex size-4 shrink-0 items-center justify-center rounded-full">
<X
className="size-2.5"
strokeWidth={3}
/>
</span>
)}
<span
className={cn(
signal.positive
? "text-foreground"
: "text-muted-foreground",
)}>
{signal.text}
Comment on lines +126 to +151
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Expose the check/X meaning to assistive tech.

The positive/tradeoff state is currently visual-only. Add hidden text for the state and mark decorative icons as hidden.

♿ Proposed fix
                         {signal.positive ? (
                             <span className="bg-emerald-500/15 text-emerald-600 dark:text-emerald-400 mt-px flex size-4 shrink-0 items-center justify-center rounded-full">
                                 <Check
+                                    aria-hidden="true"
                                     className="size-2.5"
                                     strokeWidth={3}
                                 />
                             </span>
                         ) : (
                             <span className="bg-foreground/10 text-muted-foreground mt-px flex size-4 shrink-0 items-center justify-center rounded-full">
                                 <X
+                                    aria-hidden="true"
                                     className="size-2.5"
                                     strokeWidth={3}
                                 />
                             </span>
                         )}
                         <span
                             className={cn(
                                 signal.positive
                                     ? "text-foreground"
                                     : "text-muted-foreground",
                             )}>
+                            <span className="sr-only">
+                                {signal.positive ? "Advantage: " : "Tradeoff: "}
+                            </span>
                             {signal.text}
                         </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/comparator-7.tsx` around lines 126 - 151, The signal
icons are currently decorative only; inside the option.signals.map rendering add
accessible state text and mark the SVG icons as decorative: set the Check and X
components to aria-hidden="true" and add a visually-hidden span (e.g., className
"sr-only") next to each icon that outputs the semantic state based on
signal.positive (for example "Positive" vs "Tradeoff"/"Negative") so screen
readers receive the meaning while visual users keep the same UI; update the
spans wrapping the icons and the icon components accordingly.

</span>
</li>
))}
</ul>

<div className="border-border/60 mt-6 flex items-baseline gap-1 border-t pt-4">
<span className="text-foreground text-sm font-medium">
{option.cost}
</span>
{option.costNote && (
<span className="text-muted-foreground text-xs">
{option.costNote}
</span>
)}
</div>
</div>
)
}
7 changes: 4 additions & 3 deletions apps/web/components/illustrations/flow.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
'use client'

import { LogoIcon } from "@/components/logo"
import { Vercel } from '@/components/ui/svgs/vercel'
import { Supabase } from '@/components/ui/svgs/supabase'
import { Firebase } from '@/components/ui/svgs/firebase'
Expand Down Expand Up @@ -184,8 +183,10 @@ export const FlowIllustration = () => {
</div>

<div className="relative z-10 mb-20 mt-24">
<div className="dark:bg-illustration/75 dark:ring-border-illustration relative flex size-14 items-center justify-center rounded-full bg-black/75 shadow-xl shadow-black/20 ring-1 ring-black backdrop-blur">
<LogoIcon className="size-6" />
<div className="dark:bg-illustration/75 dark:ring-border-illustration relative flex size-14 items-center justify-center rounded-full bg-black/85 shadow-xl shadow-black/20 ring-1 ring-black backdrop-blur">
<span className="text-background text-lg font-semibold leading-none tracking-tight">
h
</span>
</div>
Comment on lines +186 to 190
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Verify dark-mode contrast of the "h" glyph.

The badge background switches to dark:bg-illustration/75 in dark mode while the glyph keeps text-background. Since --background is typically dark in dark themes and --illustration also tends to be a dark/neutral surface, the "h" may render dark-on-dark and become barely visible. In light mode it's fine (white h on bg-black/85). Consider forcing a light foreground in dark mode (e.g. dark:text-foreground or a fixed dark:text-white) so the wordmark stays legible on both themes.

The whole illustration is aria-hidden, so this is purely visual, not an a11y blocker.

🎨 Suggested tweak
-                    <span className="text-background text-lg font-semibold leading-none tracking-tight">
+                    <span className="text-background dark:text-foreground text-lg font-semibold leading-none tracking-tight">
                         h
                     </span>
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/web/components/illustrations/flow.tsx` around lines 186 - 190, The "h"
glyph span uses class text-background which can render too dark against the
dark-mode badge (dark:bg-illustration/75); update the span's dark-mode
foreground to ensure legibility by adding a dark-specific text class (for
example dark:text-foreground or dark:text-white) to the span containing the "h"
so it forces a light color in dark mode while leaving light-mode styling
unchanged.

</div>

Expand Down
Loading
Loading