Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
2d3efb1
Remove REVIEWS.md to root
haydenbleasel Dec 10, 2025
3eea240
Install Geistdocs in docs folder
haydenbleasel Dec 10, 2025
5cfaf20
Migrate content from flags-sdk-dev
haydenbleasel Dec 10, 2025
b2fe4cd
Move docs into apps
haydenbleasel Dec 10, 2025
601e8d4
Update package.json
haydenbleasel Dec 10, 2025
e94c44c
Update pnpm-lock.yaml
haydenbleasel Dec 10, 2025
78fdc41
Update .gitignore
haydenbleasel Dec 10, 2025
5ab0731
Update .gitignore
haydenbleasel Dec 10, 2025
8975343
Restore custom components
haydenbleasel Dec 10, 2025
51960fa
Export multiple sources
haydenbleasel Dec 10, 2025
2a39677
Update config
haydenbleasel Dec 10, 2025
b6b59c1
Update source.ts
haydenbleasel Dec 10, 2025
44f1858
Add root-level redirects, misc fixes
haydenbleasel Dec 10, 2025
8d8fe29
Update page.tsx
haydenbleasel Dec 10, 2025
d77dc0e
Update page.tsx
haydenbleasel Dec 10, 2025
770dbd2
Misc fixes
haydenbleasel Dec 10, 2025
caa3407
Update page.tsx
haydenbleasel Dec 10, 2025
9f9f999
Misc fixes
haydenbleasel Dec 10, 2025
657def1
Update proxy.ts
haydenbleasel Dec 10, 2025
5886e8a
Update source.ts
haydenbleasel Dec 10, 2025
b8709c5
Update page.tsx
haydenbleasel Dec 10, 2025
58b838e
Update package.json
haydenbleasel Dec 10, 2025
a17dcb9
Update route.ts
haydenbleasel Dec 10, 2025
44ba129
Start working on homepage
haydenbleasel Dec 11, 2025
437f3c3
Migrate config panel
haydenbleasel Dec 11, 2025
f3cc3dd
Update illustrations.tsx
haydenbleasel Dec 11, 2025
366378f
Update mobile-menu.tsx
haydenbleasel Dec 15, 2025
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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,10 @@ npm-debug.log
**/playwright-report/
**/blob-report/
**/playwright/.cache/

# Fumadocs
.source

# Next.js
dist
.next
File renamed without changes.
26 changes: 26 additions & 0 deletions apps/docs/app/[lang]/(home)/components/centered-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type { ReactNode } from "react";

type CenteredSectionProps = {
title: string;
description: string;
children: ReactNode;
};

export const CenteredSection = ({
title,
description,
children,
}: CenteredSectionProps) => (
<div className="grid items-center gap-10 overflow-hidden px-4 py-8 sm:px-12 sm:py-12">
<div className="mx-auto grid max-w-lg gap-4 text-center">
<h2 className="font-semibold text-xl tracking-tight sm:text-2xl md:text-3xl lg:text-[40px]">
{title}
</h2>
<p className="text-balance text-lg text-muted-foreground">
{description}
</p>
</div>

{children}
</div>
);
35 changes: 35 additions & 0 deletions apps/docs/app/[lang]/(home)/components/copy-button.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
"use client";

import { CheckIcon, CopyIcon } from "lucide-react";
import { useState } from "react";
import { toast } from "sonner";
import { Button } from "@/components/ui/button";

export const CopyButton = ({ code }: { code: string }) => {
const [isCopied, setIsCopied] = useState(false);

const handleCopy = async () => {
try {
await navigator.clipboard.writeText(code);
setIsCopied(true);
setTimeout(() => setIsCopied(false), 2000);
} catch (error) {
toast.error("Failed to copy code");
const message = error instanceof Error ? error.message : "Unknown error";
toast.error(message);
}
};

const Icon = isCopied ? CheckIcon : CopyIcon;

return (
<Button
className="-m-2 shrink-0"
onClick={handleCopy}
size="icon"
variant="ghost"
>
<Icon size={16} />
</Button>
);
};
19 changes: 19 additions & 0 deletions apps/docs/app/[lang]/(home)/components/cta.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import DynamicLink from "fumadocs-core/dynamic-link";
import { Button } from "@/components/ui/button";

type CTAProps = {
title: string;
href: string;
cta: string;
};

export const CTA = ({ title, href, cta }: CTAProps) => (
<section className="flex flex-col gap-4 px-8 py-10 sm:px-12 md:flex-row md:items-center md:justify-between">
<h2 className="font-semibold text-xl tracking-tight sm:text-2xl md:text-3xl lg:text-[40px]">
{title}
</h2>
<Button asChild size="lg">
<DynamicLink href={`/[lang]${href}`}>{cta}</DynamicLink>
</Button>
</section>
);
100 changes: 100 additions & 0 deletions apps/docs/app/[lang]/(home)/components/demo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import { SiTypescript } from "@icons-pack/react-simple-icons";
import { codeToHtml } from "shiki";
import { CopyButton } from "./copy-button";
import { cn } from "@/lib/utils";

const preClassNames = cn("[&_.shiki]:bg-transparent!");

const darkModeClassNames = cn(
"dark:[&_.shiki]:text-[var(--shiki-dark)]!",
"dark:[&_.shiki]:[font-style:var(--shiki-dark-font-style)]!",
"dark:[&_.shiki]:[font-weight:var(--shiki-dark-font-weight)]!",
"dark:[&_.shiki]:[text-decoration:var(--shiki-dark-text-decoration)]!",
"dark:[&_.shiki_span]:text-[var(--shiki-dark)]!",
"dark:[&_.shiki_span]:[font-style:var(--shiki-dark-font-style)]!",
"dark:[&_.shiki_span]:[font-weight:var(--shiki-dark-font-weight)]!",
"dark:[&_.shiki_span]:[text-decoration:var(--shiki-dark-text-decoration)]!"
);

const defineCode = `import { flag } from 'flags/next';

export const exampleFlag = flag({
key: 'example-flag',
decide() {
return Math.random() > 0.5;
},
});`;

const consumeCode = `import { exampleFlag } from "../flags";

export default async function Page() {
const example = await exampleFlag();

return <div>Flag {example ? "on" : "off"}</div>;
}`;

const CodeBlock = ({
filename,
children,
source,
}: {
filename: string;
children: string;
source: string;
}) => (
<div className="size-full divide-y overflow-hidden rounded-md border bg-background">
<div className="flex items-center bg-sidebar p-4 text-muted-foreground text-sm">
<SiTypescript className="mr-2 size-4 shrink-0" />
<span className="flex-1">{filename}</span>
<CopyButton code={source} />
</div>
<div
className={cn(
"size-full overflow-auto py-4 text-sm",
preClassNames,
darkModeClassNames
)}
// biome-ignore lint/security/noDangerouslySetInnerHtml: "this is needed."
dangerouslySetInnerHTML={{ __html: children }}
/>
</div>
);

export const Demo = async () => {
const defineCodeHtml = await codeToHtml(defineCode, {
lang: "javascript",
themes: { light: "github-light", dark: "github-dark" },
});

const consumeCodeHtml = await codeToHtml(consumeCode, {
lang: "javascript",
themes: { light: "github-light", dark: "github-dark" },
});

return (
<div className="grid gap-4 md:grid-cols-2">
<div className="flex h-full flex-col gap-2">
<CodeBlock
filename="flags.ts"
source={defineCode}
>
{defineCodeHtml}
</CodeBlock>
<p className="text-muted-foreground text-sm">
Declaring a flag
</p>
</div>
<div className="flex h-full flex-col gap-2">
<CodeBlock
filename="app/page.tsx"
source={consumeCode}
>
{consumeCodeHtml}
</CodeBlock>
<p className="text-muted-foreground text-sm">
Using a flag
</p>
</div>
</div>
)
};
47 changes: 47 additions & 0 deletions apps/docs/app/[lang]/(home)/components/flags-config.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { enableBannerFlag, enableDitheredHeroFlag, enableHeroTextFlag, rootFlags } from "./flags";
import { FlagSelect, FlagToggle } from "./toggles";

type FlagsConfigProps = {
code: string;
};

export const FlagsConfig = async ({ code }: FlagsConfigProps) => {
const [bannerFlag, ditheredHeroFlag, heroTextFlag] = await Promise.all([
enableBannerFlag(code, rootFlags),
enableDitheredHeroFlag(code, rootFlags),
enableHeroTextFlag(code, rootFlags),
]);

return (
<div className="rounded-xl bg-background p-4 ring-1 ring-border md:p-6">
<div className="flex flex-col gap-y-1 px-2">
<div className="mb-0.5 text-xl font-semibold tracking-tight">Try the Flags SDK</div>
<span className="text-base">
Set persistent flags for this page
</span>
</div>
<div className="divide-y">
<FlagToggle
value={ditheredHeroFlag}
flagKey={enableDitheredHeroFlag.key}
label={enableDitheredHeroFlag.key}
description={enableDitheredHeroFlag.description}
/>
<FlagSelect
value={heroTextFlag}
flagKey={enableHeroTextFlag.key}
label={enableHeroTextFlag.key}
description={enableHeroTextFlag.description}
options={enableHeroTextFlag.options}
/>
<FlagToggle
value={bannerFlag}
flagKey={enableBannerFlag.key}
label={enableBannerFlag.key}
description={enableBannerFlag.description}
scroll
/>
</div>
</div>
);
}
43 changes: 43 additions & 0 deletions apps/docs/app/[lang]/(home)/components/flags.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { flag } from 'flags/next';

export const enableBannerFlag = flag({
key: 'enable-banner-flag',
description: 'Full-width callout at the top',
options: [false, true],
decide({ cookies }) {
const on = cookies.get(this.key)?.value;
return on !== undefined;
},
});

export const enableDitheredHeroFlag = flag({
key: 'enable-dithered-hero-flag',
description: 'Keep it dithered',
options: [false, true],
decide({ cookies }) {
const on = cookies.get(this.key)?.value;
return on !== undefined;
},
});

export const enableHeroTextFlag = flag<string>({
key: 'swap-hero-text',
description: 'Toggle between headline options for A/B testing',
options: [
'The feature flags toolkit',
'Ship faster with feature flags',
'Flag features, ship apps faster',
],
decide({ cookies }) {
const cookieValue = cookies.get(this.key)?.value;
return cookieValue && this.options?.includes(cookieValue)
? cookieValue
: (this.options![0] as string);
},
});

export const rootFlags = [
enableBannerFlag,
enableHeroTextFlag,
enableDitheredHeroFlag,
] as const;
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
.image {
position: absolute;
inset: 0;
z-index: -10;

height: 100%;
width: 100%;
left: 0;
top: 0;
right: 0;
bottom: 0;
color: transparent;
}

.image[data-theme="dark"] {
display: none;
}

:global(.dark-theme) {
.image[data-theme="dark"] {
display: block;
}

.image[data-theme="light"] {
display: none;
}
}
16 changes: 16 additions & 0 deletions apps/docs/app/[lang]/(home)/components/hero-image/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { clsx } from 'clsx';
import styles from './hero-image.module.css';

const HeroImage = () => {
// Using the raw image tag avoids a flicker that can happen with the Image component
return (
<img
src="https://mxikj9vd8fb4tfe4.public.blob.vercel-storage.com/marketing/light-gradient-mxu3khHWJ11kkIsInB08oGeEapbXuY.png"
alt="Hero"
fetchPriority="high"
className={clsx(styles.image, 'dark:invert')}
/>
);
};

export default HeroImage;
29 changes: 29 additions & 0 deletions apps/docs/app/[lang]/(home)/components/hero.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import type { ReactNode } from "react";
import { Badge } from "@/components/ui/badge";

type HeroProps = {
badge?: string;
title: string;
description: string;
children: ReactNode;
};

export const Hero = ({ badge, title, description, children }: HeroProps) => (
<section className="mt-(--fd-nav-height) space-y-6 px-4 pt-16 pb-16 text-center sm:pt-24">
<div className="mx-auto w-full max-w-4xl space-y-5">
{badge ? (
<Badge className="rounded-full" variant="secondary">
<div className="size-2 rounded-full bg-muted-foreground" />
<p>{badge}</p>
</Badge>
) : null}
<h1 className="text-balance max-w-3xl mx-auto text-center font-semibold text-[40px]! leading-[1.1] tracking-tight sm:text-5xl! lg:font-semibold xl:text-6xl!">
{title}
</h1>
<p className="mx-auto max-w-3xl text-balance text-muted-foreground leading-relaxed sm:text-xl">
{description}
</p>
</div>
{children}
</section>
);
Loading