diff --git a/apps/web/components/comparator-7.tsx b/apps/web/components/comparator-7.tsx index 7c2cea6..ebc87c7 100644 --- a/apps/web/components/comparator-7.tsx +++ b/apps/web/components/comparator-7.tsx @@ -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 -} - -const planLabels: Record = { - 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 = ( -
- {planLabels[plan]} -
- ) - - return ( -
- {header} - -
- {features.map((feature, index) => { - const value = feature.plans[plan] - return ( -
-
- {value === true ? ( - - ) : value === false ? ( - - ) : ( - {value} - )} -
-
- ) - })} -
-
-
- ) -} - export default function Comparator() { return (
-
-
-
-

- Two options today. Both are compromises. -

-

- 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. -

-
-
- -
-
-
-
Feature
-
- - {features.map((feature, index) => ( -
-
{feature.name}
- {feature.description && ( - - - - ? - - {feature.description} - - - )} -
- ))} -
+
+

+ Two options today. Both are compromises. +

+

+ Helpbase is the third option. +

+
- {plans.map((plan) => ( -
- {renderPlanColumn(plan)} -
- ))} -
+
+ {options.map((option) => ( + + ))}
) } -const Indicator = ({ checked = false }: { checked?: boolean }) => { +function OptionCard({ option }: { option: Option }) { return ( - - {checked ? : '✗'} - - ) -} + {option.emphasized && ( + + Recommended + + )} -const CheckIcon = () => { - return ( - - - +
+ {option.name} +
+ +
+
+ {option.metric} +
+
+ {option.metricSubtitle} +
+
+ +

+ {option.body} +

+ + + +
+ + {option.cost} + + {option.costNote && ( + + {option.costNote} + + )} +
+ ) } diff --git a/apps/web/components/illustrations/flow.tsx b/apps/web/components/illustrations/flow.tsx index 1008e7e..707ff68 100644 --- a/apps/web/components/illustrations/flow.tsx +++ b/apps/web/components/illustrations/flow.tsx @@ -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' @@ -184,8 +183,10 @@ export const FlowIllustration = () => {
-
- +
+ + h +
diff --git a/apps/web/components/illustrations/mdx-source-preview.tsx b/apps/web/components/illustrations/mdx-source-preview.tsx index 31dd286..f1d35b7 100644 --- a/apps/web/components/illustrations/mdx-source-preview.tsx +++ b/apps/web/components/illustrations/mdx-source-preview.tsx @@ -1,36 +1,73 @@ +import { FileText, Lightbulb } from "lucide-react" + export const MdxSourcePreview = () => (
-
- content/getting-started.mdx + className="ring-border bg-card relative z-10 mx-auto w-full max-w-sm overflow-hidden rounded-xl border border-transparent ring-1"> +
+
+ + + getting-started.mdx + +
+ + Source +
-
-
---
+ +
+
---
- title:{" "} - Getting started + title:{" "} + Install helpbase
- order:{" "} - 1 + order:{" "} + 1
-
---
-
-
# Install helpbase
-
-
- Run one command in your repo to +
---
+
+
+ # Install helpbase
- create a full Next.js help center. + Run one command to get started. +
+
+
+ {"{" "} + type + = + "tip" + {">"} +
+
+ Edit and ship. +
+
{""}
+
+ +
+
+ Rendered +
+
+ Install helpbase +
+
+ Run one command to get started.
-
-
{``}
-
- Edit this file to add your own content. +
+ + + Edit and ship. +
-
{``}
) diff --git a/apps/web/components/marketing/hero.tsx b/apps/web/components/marketing/hero.tsx index 1a7a639..e6458d4 100644 --- a/apps/web/components/marketing/hero.tsx +++ b/apps/web/components/marketing/hero.tsx @@ -41,7 +41,7 @@ export function Hero() {
-
+

\n
\n

\n Built with{\" \"}\n \n helpbase\n \n

\n
\n \n GitHub\n \n |\n \n Twitter\n \n
\n
\n \n )\n}\n", + "content": "import { Button } from '@/components/ui/button'\nimport Link from 'next/link'\n\nconst links = [\n {\n group: 'Product',\n items: [\n { title: 'Pricing', href: '/#pricing' },\n { title: 'Demo', href: 'https://demo.helpbase.dev' },\n { title: 'FAQ', href: '/#faq' },\n ],\n },\n {\n group: 'Resources',\n items: [\n { title: 'Docs', href: '/docs' },\n { title: 'GitHub', href: 'https://github.com/Codehagen/helpbase' },\n { title: 'MCP', href: '/docs/mcp' },\n ],\n },\n {\n group: 'Company',\n items: [\n { title: 'Changelog', href: 'https://github.com/Codehagen/helpbase/releases' },\n { title: 'License', href: 'https://github.com/Codehagen/helpbase/blob/main/LICENSE' },\n { title: 'Privacy', href: '/docs/privacy' },\n ],\n },\n]\n\nexport default function FooterSection() {\n return (\n \n
\n
\n
\n \n helpbase\n \n\n

\n Open-source help centers with an MCP server and llms.txt built in. Self-host free, or host it with us.\n

\n
\n\n
\n \n \n \n \n \n \n \n \n \n \n
\n
\n \n
\n
\n {links.map((group) => (\n \n {group.group}\n\n
\n {group.items.map((item) => (\n \n {item.title}\n \n ))}\n
\n
\n ))}\n
\n\n
\n
\n
Updates, every release
\n
\n \n \n Follow on GitHub Releases\n \n ↗\n \n \n \n
\n

\n Release notes and shipped features, straight from the repo. No newsletter inbox to opt out of.\n

\n
\n
\n
\n\n
\n \n © {new Date().getFullYear()} helpbase. Built with shadcn/ui, Next.js, Supabase, Vercel.\n \n
\n
\n \n \n
\n Open source, live\n
\n
\n

\n \n )\n}\n\nexport { FooterSection as Footer }\n", "type": "registry:component", "target": "components/footer.tsx" }, diff --git a/apps/web/public/r/registry.json b/apps/web/public/r/registry.json index 31f11ff..1d1c936 100644 --- a/apps/web/public/r/registry.json +++ b/apps/web/public/r/registry.json @@ -17,9 +17,10 @@ "rehype-pretty-code", "shiki", "zod", - "lucide-react" + "lucide-react", + "@radix-ui/react-slot" ], - "registryDependencies": ["badge", "accordion", "tabs"], + "registryDependencies": ["badge", "accordion", "button", "tabs"], "files": [ { "path": "registry/helpbase/app/(docs)/layout.tsx", diff --git a/packages/create-helpbase/templates/components/ui/button.tsx b/packages/create-helpbase/templates/components/ui/button.tsx new file mode 100644 index 0000000..59e8eb9 --- /dev/null +++ b/packages/create-helpbase/templates/components/ui/button.tsx @@ -0,0 +1,65 @@ +import * as React from "react" +import { cva, type VariantProps } from "class-variance-authority" +import { Slot } from "radix-ui" + +import { cn } from "@/lib/utils" + +const buttonVariants = cva( + "group/button inline-flex shrink-0 items-center justify-center rounded-4xl border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/30 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4", + { + variants: { + variant: { + default: "bg-primary text-primary-foreground hover:bg-primary/80", + outline: + "border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:bg-transparent dark:hover:bg-input/30", + secondary: + "bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground", + ghost: + "hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50", + destructive: + "bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40", + link: "text-primary underline-offset-4 hover:underline", + }, + size: { + default: + "h-9 gap-1.5 px-3 has-data-[icon=inline-end]:pr-2.5 has-data-[icon=inline-start]:pl-2.5", + xs: "h-6 gap-1 px-2.5 text-xs has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-3", + sm: "h-8 gap-1 px-3 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2", + lg: "h-10 gap-1.5 px-4 has-data-[icon=inline-end]:pr-3 has-data-[icon=inline-start]:pl-3", + icon: "size-9", + "icon-xs": "size-6 [&_svg:not([class*='size-'])]:size-3", + "icon-sm": "size-8", + "icon-lg": "size-10", + }, + }, + defaultVariants: { + variant: "default", + size: "default", + }, + } +) + +function Button({ + className, + variant = "default", + size = "default", + asChild = false, + ...props +}: React.ComponentProps<"button"> & + VariantProps & { + asChild?: boolean + }) { + const Comp = asChild ? Slot.Root : "button" + + return ( + + ) +} + +export { Button, buttonVariants } diff --git a/packages/create-helpbase/templates/components/ui/card.tsx b/packages/create-helpbase/templates/components/ui/card.tsx deleted file mode 100644 index 96681a2..0000000 --- a/packages/create-helpbase/templates/components/ui/card.tsx +++ /dev/null @@ -1,70 +0,0 @@ -import * as React from 'react' -import { cn } from '@/lib/utils' -import { Slot } from '@radix-ui/react-slot' - -export interface CardProps extends React.HTMLAttributes { - asChild?: boolean -} - -function Card({ className, asChild = false, ...props }: CardProps) { - const Comp = asChild ? Slot : 'div' - return ( - - ) -} - -function CardHeader({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ) -} - -function CardTitle({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ) -} - -function CardDescription({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ) -} - -function CardContent({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ) -} - -function CardFooter({ className, ...props }: React.ComponentProps<'div'>) { - return ( -
- ) -} - -export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } \ No newline at end of file diff --git a/packages/create-helpbase/templates/components/ui/chart.tsx b/packages/create-helpbase/templates/components/ui/chart.tsx deleted file mode 100644 index 17a5927..0000000 --- a/packages/create-helpbase/templates/components/ui/chart.tsx +++ /dev/null @@ -1,373 +0,0 @@ -"use client" - -import * as React from "react" -import * as RechartsPrimitive from "recharts" -import type { TooltipValueType } from "recharts" - -import { cn } from "@/lib/utils" - -// Format: { THEME_NAME: CSS_SELECTOR } -const THEMES = { light: "", dark: ".dark" } as const - -const INITIAL_DIMENSION = { width: 320, height: 200 } as const -type TooltipNameType = number | string - -export type ChartConfig = Record< - string, - { - label?: React.ReactNode - icon?: React.ComponentType - } & ( - | { color?: string; theme?: never } - | { color?: never; theme: Record } - ) -> - -type ChartContextProps = { - config: ChartConfig -} - -const ChartContext = React.createContext(null) - -function useChart() { - const context = React.useContext(ChartContext) - - if (!context) { - throw new Error("useChart must be used within a ") - } - - return context -} - -function ChartContainer({ - id, - className, - children, - config, - initialDimension = INITIAL_DIMENSION, - ...props -}: React.ComponentProps<"div"> & { - config: ChartConfig - children: React.ComponentProps< - typeof RechartsPrimitive.ResponsiveContainer - >["children"] - initialDimension?: { - width: number - height: number - } -}) { - const uniqueId = React.useId() - const chartId = `chart-${id ?? uniqueId.replace(/:/g, "")}` - - return ( - -
- - - {children} - -
-
- ) -} - -const ChartStyle = ({ id, config }: { id: string; config: ChartConfig }) => { - const colorConfig = Object.entries(config).filter( - ([, config]) => config.theme ?? config.color - ) - - if (!colorConfig.length) { - return null - } - - return ( -