Add .env.development configuration for Hexclave API and update setup-page with new documentation links and environment variable handling. Introduce codePanelShellClasses for consistent styling in code blocks.

This commit is contained in:
Developing-Gamer 2026-06-24 12:20:00 -07:00
parent f6fcead1c5
commit a8732b0c4a
5 changed files with 150 additions and 751 deletions

View File

@ -1,5 +1,5 @@
NEXT_PUBLIC_HEXCLAVE_API_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}02
NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}26
NEXT_PUBLIC_HEXCLAVE_DOCS_BASE_URL=http://localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}04
NEXT_PUBLIC_HEXCLAVE_HOSTED_HANDLER_DOMAIN_SUFFIX=.localhost:${NEXT_PUBLIC_HEXCLAVE_PORT_PREFIX:-81}09
NEXT_PUBLIC_HEXCLAVE_IS_LOCAL_EMULATOR=false

View File

@ -1,17 +1,17 @@
'use client';
import { CodeBlock } from '@/components/code-block';
import { CodeBlock, codePanelShellClasses } from '@/components/code-block';
import { DesignButton } from "@/components/design-components";
import { APIEnvKeys, NextJsEnvKeys, ViteEnvKeys, codePanelShellClasses } from '@/components/env-keys';
import { EnvFileViewer } from '@/components/env-keys';
import { InlineCode } from '@/components/inline-code';
import { StyledLink } from '@/components/link';
import { Tabs, TabsContent, TabsList, TabsTrigger, Typography, cn } from "@/components/ui";
import { Tabs, TabsList, TabsTrigger, Typography, cn } from "@/components/ui";
import { getPublicEnvVar } from '@/lib/env';
import { useThemeWatcher } from '@/lib/theme';
import { BookIcon, XIcon } from "@phosphor-icons/react";
import { remindersPrompt } from '@hexclave/shared/dist/ai/unified-prompts/reminders';
import { use } from "@hexclave/shared/dist/utils/react";
import { deindent } from '@hexclave/shared/dist/utils/strings';
import dynamic from "next/dynamic";
import Image from 'next/image';
import { Suspense, useRef, useState } from "react";
import type { GlobeMethods } from 'react-globe.gl';
import { PageLayout } from "../page-layout";
@ -22,38 +22,63 @@ import styles from './setup-page.module.css';
const countriesPromise = import('./country-data.geo.json');
const Globe = dynamic(() => import('react-globe.gl').then((mod) => mod.default), { ssr: false });
const commandClasses = "text-red-600 dark:text-red-400";
const nameClasses = "text-green-600 dark:text-green-500";
type SetupFramework = 'nextjs' | 'tanstackStart' | 'react' | 'javascript' | 'python';
type SetupMode = "recommended" | "manual";
const SETUP_PROMPT = deindent`
Install and set up Hexclave in this project by following these instructions:
const PROD_DOCS_BASE_URL = 'https://docs.hexclave.com';
const PROD_API_BASE_URL = 'https://api.hexclave.com';
Read https://skill.hexclave.com and follow the setup instructions it gives for this project's specific framework and language.
function getSetupDocsBaseUrl() {
return getPublicEnvVar('NEXT_PUBLIC_STACK_DOCS_BASE_URL') ?? PROD_DOCS_BASE_URL;
}
Follow skill.hexclave.com as written, but make sure to use the cloud setup, not the local dashboard setup.
function getManualSetupDocsUrl() {
const docsBaseUrl = getSetupDocsBaseUrl().replace(/\/$/, '');
return `${docsBaseUrl}/guides/getting-started/setup`;
}
Do not change the dev script in package.json, even if the skill mentions doing that.
function getSetupApiBaseUrl() {
return getPublicEnvVar('NEXT_PUBLIC_STACK_API_URL') ?? PROD_API_BASE_URL;
}
After setup finishes, verify that the Hexclave MCP server is registered in your AI client config name: \`hexclave\`, transport: \`http\`, URL: \`https://mcp.hexclave.com/mcp\`. If it is not registered, add it manually so future agents have live access to Hexclave docs and APIs.
function buildCloudSetupPrompt(options: {
docsBaseUrl: string,
projectId: string,
apiBaseUrl: string,
}) {
const { docsBaseUrl, projectId, apiBaseUrl } = options;
const normalizedDocsBaseUrl = docsBaseUrl.replace(/\/$/, '');
const reminders = remindersPrompt.replaceAll(PROD_DOCS_BASE_URL, normalizedDocsBaseUrl);
Once setup is done, tell me to add the Hexclave environment variables to .env.local. After that, setup is complete.
`;
return deindent`
Install and set up Hexclave in this project by following these instructions:
Read https://skill.hexclave.com and follow the setup instructions it gives for this project's specific framework and language.
Follow skill.hexclave.com as written, but make sure to use the cloud setup, not the local dashboard setup.
Do not change the dev script in package.json. In cloud setup, there's no need for that.
Use these Hexclave project values when creating environment variables:
- Hexclave API URL: ${apiBaseUrl}
- Hexclave project ID: ${projectId}
Create the framework-specific public environment variables for the Hexclave API URL and project ID. For example, Next.js uses NEXT_PUBLIC_HEXCLAVE_API_URL and NEXT_PUBLIC_HEXCLAVE_PROJECT_ID, while Vite-based frameworks use VITE_HEXCLAVE_API_URL and VITE_HEXCLAVE_PROJECT_ID. If the Hexclave docs for this framework specify different environment variable names, use the docs' framework-specific names with the values above.
After setup finishes, verify that the Hexclave MCP server is registered in your AI client config name: \`hexclave\`, transport: \`http\`, URL: \`https://mcp.hexclave.com/mcp\`. If it is not registered, add it manually so future agents have live access to Hexclave docs and APIs.
Once setup is done, tell me to add the Hexclave secret server key from the dashboard to my environment file. After that, setup is complete.
${reminders}
`;
}
export default function SetupPage(props: { toMetrics: () => void }) {
const adminApp = useAdminApp();
const [selectedFramework, setSelectedFramework] = useState<SetupFramework>('nextjs');
const [setupMode, setSetupMode] = useState<SetupMode>("recommended");
const [keys, setKeys] = useState<{ projectId: string, publishableClientKey?: string, secretServerKey: string } | null>(null);
const projectConfig = adminApp.useProject().useConfig();
const requirePublishableClientKey = projectConfig.project.requirePublishableClientKey;
const publishableClientKeyValue = keys?.publishableClientKey ?? "...";
const optionalPublishableClientKeyProp = (indent: string) =>
requirePublishableClientKey ? `\n${indent}publishableClientKey: "${publishableClientKeyValue}",` : "";
const optionalPublishableClientKeyHeader = (indent: string) =>
requirePublishableClientKey ? `\n${indent}'x-hexclave-publishable-client-key': "${publishableClientKeyValue}",` : "";
const onGenerateKeys = async () => {
const newKey = await adminApp.createInternalApiKey({
@ -71,513 +96,12 @@ export default function SetupPage(props: { toMetrics: () => void }) {
});
};
const nextJsSteps = [
{
step: 2,
title: "Install Hexclave",
content: <>
<Typography>
In a new or existing Next.js project, install Hexclave as a dependency into your project:
</Typography>
<CodeBlock
language="bash"
content={`npx @hexclave/cli@latest init`}
customRender={
<div className="p-4 font-mono text-sm">
<span className={commandClasses}>pnpx</span> <span className={nameClasses}>@hexclave/cli@latest</span> init
</div>
}
title="Terminal"
icon="terminal"
/>
</>
},
{
step: 3,
title: "Create Keys",
content: <>
<Typography>
Put these keys in the <InlineCode>.env.local</InlineCode> file.
</Typography>
<HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="next" />
</>
},
{
step: 4,
title: "Done",
content: <>
<Typography>
If you start your Next.js app with npm run dev and navigate to <StyledLink href="http://localhost:3000/handler/signup">http://localhost:3000/handler/signup</StyledLink>, you will see the sign-up page.
</Typography>
</>
},
];
const reactSteps = [
{
step: 2,
title: "Install Hexclave",
content: <>
<Typography>
In a new or existing React project, install Hexclave&apos;s dependencies:
</Typography>
<CodeBlock
language="bash"
content={`npm install @hexclave/react`}
customRender={
<div className="p-4 font-mono text-sm">
<span className={commandClasses}>npm install</span> <span className={nameClasses}>@hexclave/react</span>
</div>
}
title="Terminal"
icon="terminal"
/>
</>
},
{
step: 3,
title: "Create Keys",
content: <HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="raw" />
},
{
step: 4,
title: "Create hexclave/client.ts file",
content: <>
<Typography>
Create a new file called <InlineCode>hexclave/client.ts</InlineCode> and add the following code. Here we use react-router-dom as an example.
</Typography>
<CodeBlock
language="tsx"
content={deindent`
import { HexclaveClientApp } from "@hexclave/react";
import { useNavigate } from "react-router-dom";
export const hexclaveClientApp = new HexclaveClientApp({
// You should store these in environment variables
projectId: "${keys?.projectId ?? "..."}",${optionalPublishableClientKeyProp(" ")}
tokenStore: "cookie",
redirectMethod: {
useNavigate,
}
});
`}
title="hexclave/client.ts"
icon="code"
/>
</>
},
{
step: 5,
title: "Update App.tsx",
content: <>
<Typography>
Update your App.tsx file to wrap the entire app with a <InlineCode>HexclaveProvider</InlineCode> and <InlineCode>HexclaveTheme</InlineCode> and add a <InlineCode>HexclaveHandler</InlineCode> component to handle the authentication flow.
</Typography>
<CodeBlock
language="tsx"
maxHeight={300}
content={deindent`
import { HexclaveHandler, HexclaveProvider, HexclaveTheme } from "@hexclave/react";
import { Suspense } from "react";
import { BrowserRouter, Route, Routes, useLocation } from "react-router-dom";
import { hexclaveClientApp } from "./hexclave/client";
function HandlerRoutes() {
const location = useLocation();
return (
<HexclaveHandler app={hexclaveClientApp} location={location.pathname} fullPage />
);
}
export default function App() {
return (
<Suspense fallback={"Loading..."}>
<BrowserRouter>
<HexclaveProvider app={hexclaveClientApp}>
<HexclaveTheme>
<Routes>
<Route path="/handler/*" element={<HandlerRoutes />} />
<Route path="/" element={<div>hello world</div>} />
</Routes>
</HexclaveTheme>
</HexclaveProvider>
</BrowserRouter>
</Suspense>
);
}
`}
title="App.tsx"
icon="code"
/>
</>
},
{
step: 6,
title: "Done",
content: <>
<Typography>
If you start your React app with npm run dev and navigate to <StyledLink href="http://localhost:5173/handler/signup">http://localhost:5173/handler/signup</StyledLink>, you will see the sign-up page.
</Typography>
</>
}
];
const tanstackStartSteps = [
{
step: 2,
title: "Install Hexclave",
content: <>
<Typography>
In a new or existing TanStack Start project, install the alpha Hexclave package:
</Typography>
<CodeBlock
language="bash"
content={`npm install @hexclave/tanstack-start`}
customRender={
<div className="p-4 font-mono text-sm">
<span className={commandClasses}>npm install</span> <span className={nameClasses}>@hexclave/tanstack-start</span>
</div>
}
title="Terminal"
icon="terminal"
/>
</>
},
{
step: 3,
title: "Create Keys",
content: <>
<Typography>
Put these keys in your TanStack Start environment file.
</Typography>
<HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="vite" />
</>
},
{
step: 4,
title: "Create hexclave/client.ts file",
content: <>
<Typography>
Create a new file called <InlineCode>src/hexclave/client.ts</InlineCode> and initialize Hexclave with cookie storage.
</Typography>
<CodeBlock
language="tsx"
content={deindent`
import { HexclaveClientApp } from "@hexclave/tanstack-start";
export const hexclaveClientApp = new HexclaveClientApp({
projectId: import.meta.env.VITE_HEXCLAVE_PROJECT_ID,
tokenStore: "cookie",
redirectMethod: "window",
});
`}
title="src/hexclave/client.ts"
icon="code"
/>
</>
},
{
step: 5,
title: "Update the root route",
content: <>
<Typography>
Wrap your TanStack Start root route with <InlineCode>HexclaveProvider</InlineCode> and <InlineCode>HexclaveTheme</InlineCode>.
</Typography>
<CodeBlock
language="tsx"
maxHeight={300}
content={deindent`
import { HexclaveProvider, HexclaveTheme } from "@hexclave/tanstack-start";
import { createRootRoute, HeadContent, Outlet, Scripts } from "@tanstack/react-router";
import { hexclaveClientApp } from "../hexclave/client";
export const Route = createRootRoute({
component: RootComponent,
shellComponent: RootDocument,
});
function RootComponent() {
return (
<HexclaveProvider app={hexclaveClientApp}>
<HexclaveTheme>
<Outlet />
</HexclaveTheme>
</HexclaveProvider>
);
}
function RootDocument({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<HeadContent />
</head>
<body>
{children}
<Scripts />
</body>
</html>
);
}
`}
title="src/routes/__root.tsx"
icon="code"
/>
</>
},
{
step: 6,
title: "Add the handler route",
content: <>
<Typography>
Create a splat route for Hexclave&apos;s built-in auth pages.
</Typography>
<CodeBlock
language="tsx"
content={deindent`
import { HexclaveHandler } from "@hexclave/tanstack-start";
import { createFileRoute, useLocation } from "@tanstack/react-router";
export const Route = createFileRoute("/handler/$")({
ssr: false,
component: HandlerPage,
});
function HandlerPage() {
const { pathname } = useLocation();
return <HexclaveHandler fullPage location={pathname} />;
}
`}
title="src/routes/handler/$.tsx"
icon="code"
/>
<Typography>
If you start your TanStack Start app and navigate to <StyledLink href="http://localhost:3000/handler/sign-up">http://localhost:3000/handler/sign-up</StyledLink>, you will see the sign-up page.
</Typography>
</>
},
];
const javascriptSteps = [
{
step: 2,
title: "Install Hexclave",
content: <>
<Typography>
Install Hexclave using npm:
</Typography>
<CodeBlock
language="bash"
content={`npm install @hexclave/js`}
customRender={
<div className="p-4 font-mono text-sm">
<span className={commandClasses}>npm install</span> <span className={nameClasses}>@hexclave/js</span>
</div>
}
title="Terminal"
icon="terminal"
/>
</>
},
{
step: 3,
title: "Create Keys",
content: <HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="raw" />
},
{
step: 4,
title: "Initialize the app",
content: <>
<Typography>
Create a new file for your Hexclave app initialization:
</Typography>
<Tabs defaultValue="server">
<TabsList>
<TabsTrigger value="server">Server</TabsTrigger>
<TabsTrigger value="client">Client</TabsTrigger>
</TabsList>
<TabsContent value="server">
<CodeBlock
language="typescript"
content={deindent`
import { HexclaveServerApp } from "@hexclave/js";
const hexclaveServerApp = new HexclaveServerApp({
// You should store these in environment variables based on your project setup
projectId: "${keys?.projectId ?? "..."}",${optionalPublishableClientKeyProp(" ")}
secretServerKey: "${keys?.secretServerKey ?? "..."}",
tokenStore: "memory",
});
`}
title="hexclave/server.ts"
icon="code"
/>
</TabsContent>
<TabsContent value="client">
<CodeBlock
language="typescript"
content={deindent`
import { HexclaveClientApp } from "@hexclave/js";
const hexclaveClientApp = new HexclaveClientApp({
// You should store these in environment variables
projectId: "your-project-id",${optionalPublishableClientKeyProp(" ")}
tokenStore: "cookie",
});
`}
title="hexclave/client.ts"
icon="code"
/>
</TabsContent>
</Tabs>
</>
},
{
step: 5,
title: "Example usage",
content: <>
<Tabs defaultValue="server">
<TabsList>
<TabsTrigger value="server">Server</TabsTrigger>
<TabsTrigger value="client">Client</TabsTrigger>
</TabsList>
<TabsContent value="server">
<CodeBlock
language="typescript"
content={deindent`
import { hexclaveServerApp } from "@/hexclave/server";
const user = await hexclaveServerApp.getUser("user_id");
await user.update({
displayName: "New Display Name",
});
const team = await hexclaveServerApp.createTeam({
name: "New Team",
});
await team.addUser(user.id);
`}
title="Example server usage"
icon="code"
/>
</TabsContent>
<TabsContent value="client">
<CodeBlock
language="typescript"
content={deindent`
import { hexclaveClientApp } from "@/hexclave/client";
await hexclaveClientApp.signInWithCredential({
email: "test@example.com",
password: "password123",
});
const user = await hexclaveClientApp.getUser();
await user.update({
displayName: "New Display Name",
});
await user.signOut();
`}
title="Example client usage"
icon="code"
/>
</TabsContent>
</Tabs>
</>
}
];
const pythonSteps = [
{
step: 2,
title: "Install requests",
content: <>
<Typography>
Install the requests library to make HTTP requests to the Hexclave API:
</Typography>
<CodeBlock
language="bash"
content={`pip install requests`}
customRender={
<div className="p-4 font-mono text-sm">
<span className={commandClasses}>pip install</span> <span className={nameClasses}>requests</span>
</div>
}
title="Terminal"
icon="terminal"
/>
</>
},
{
step: 3,
title: "Create Keys",
content: <HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="raw" />
},
{
step: 4,
title: "Create helper function",
content: <>
<Typography>
Create a helper function to make requests to the Hexclave API:
</Typography>
<CodeBlock
language="python"
content={deindent`
import requests
def stack_auth_request(method, endpoint, **kwargs):
res = requests.request(
method,
f'https://api.hexclave.com/{endpoint}',
headers={
'x-hexclave-access-type': 'server',
# You should store these in environment variables
'x-hexclave-project-id': "${keys?.projectId ?? "..."}",${optionalPublishableClientKeyHeader(" ")}
'x-hexclave-secret-server-key': "${keys?.secretServerKey ?? "..."}",
**kwargs.pop('headers', {}),
},
**kwargs,
)
if res.status_code >= 400:
raise Exception(f"Hexclave API request failed with {res.status_code}: {res.text}")
return res.json()
`}
title="stack_auth.py"
icon="code"
/>
</>
},
{
step: 5,
title: "Make requests",
content: <>
<Typography>
You can now make requests to the Hexclave API:
</Typography>
<CodeBlock
language="python"
content={deindent`
# Get current project info
print(stack_auth_request('GET', '/api/v1/projects/current'))
# Get user info with access token
print(stack_auth_request('GET', '/api/v1/users/me', headers={
'x-hexclave-access-token': access_token,
}))
`}
title="example.py"
icon="code"
/>
</>
}
];
const selectedKeyType = selectedFramework === 'nextjs' ? 'next' : selectedFramework === 'tanstackStart' ? 'vite' : 'raw';
const selectedInstallPrompt = buildCloudSetupPrompt({
docsBaseUrl: getSetupDocsBaseUrl(),
projectId: adminApp.projectId,
apiBaseUrl: getSetupApiBaseUrl(),
});
const manualSetupDocsUrl = getManualSetupDocsUrl();
return (
<PageLayout width={1000}>
@ -606,7 +130,7 @@ export default function SetupPage(props: { toMetrics: () => void }) {
variant='outline'
size='sm'
onClick={() => {
window.open('https://docs.hexclave.com/', '_blank');
window.open(getSetupDocsBaseUrl(), '_blank');
}}
>
<BookIcon className="w-4 h-4 mr-2" />
@ -617,13 +141,7 @@ export default function SetupPage(props: { toMetrics: () => void }) {
</div>
<div className="flex justify-end mt-8 mx-4">
<Tabs value={setupMode} onValueChange={(value) => {
if (value === "manual" || value === "recommended") {
setSetupMode(value);
return;
}
throw new Error(`Unexpected setup mode: ${value}`);
}}>
<Tabs value={setupMode} onValueChange={(value) => setSetupMode(value === "manual" ? "manual" : "recommended")}>
<TabsList>
<TabsTrigger value="recommended">Recommended</TabsTrigger>
<TabsTrigger value="manual">Manual setup</TabsTrigger>
@ -631,122 +149,73 @@ export default function SetupPage(props: { toMetrics: () => void }) {
</Tabs>
</div>
<div className="flex flex-col mt-4 mx-4">
<ol className="relative text-gray-500 border-s border-gray-200 dark:border-gray-700 dark:text-gray-400 ">
{(setupMode === "recommended" ? [
{
step: 1,
title: "Copy Setup Prompt",
content: <div className="flex min-w-0 flex-col gap-4">
<CodeBlock
language="text"
content={SETUP_PROMPT}
title="Prompt for your AI agent"
icon="code"
maxHeight={260}
/>
</div>,
},
{
step: 2,
title: "Create Keys",
content: <>
<Typography>
Add these to your project&apos;s <InlineCode>.env.local</InlineCode> file.
</Typography>
<HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} type="next" />
</>,
},
{
step: 3,
title: "Done",
content: <>
<Typography>
After starting your dev server, navigate to <StyledLink href="http://localhost:3000/handler/signup">http://localhost:3000/handler/signup</StyledLink>, you will see the sign-up page.
</Typography>
</>,
},
] : [
{
step: 1,
title: "Select your framework",
content: <div>
<div className="flex gap-4 flex-wrap">
{([{
id: 'nextjs',
name: 'Next.js',
reverseIfDark: true,
imgSrc: '/next-logo.svg',
}, {
id: 'tanstackStart',
name: 'TanStack Start',
reverseIfDark: false,
imgSrc: '/tanstack-start-logo.png',
}, {
id: 'react',
name: 'React',
reverseIfDark: false,
imgSrc: '/react-logo.svg',
}, {
id: 'javascript',
name: 'JavaScript',
reverseIfDark: false,
imgSrc: '/javascript-logo.svg',
}, {
id: 'python',
name: 'Python',
reverseIfDark: false,
imgSrc: '/python-logo.svg',
}] as const).map(({ name, imgSrc: src, reverseIfDark, id }) => (
<DesignButton
key={id}
variant="plain"
className={cn(
"h-24 w-24 flex flex-col items-center justify-center gap-2 rounded-2xl border transition-all duration-150 hover:transition-none shadow-sm",
id === selectedFramework
? "bg-white/95 dark:bg-background/90 ring-1 ring-black/[0.08] dark:ring-white/[0.08] border-black/[0.1] dark:border-white/[0.1] shadow"
: "bg-white/40 dark:bg-background/20 ring-1 ring-black/[0.04] dark:ring-white/[0.04] border-transparent hover:bg-white/70 dark:hover:bg-background/40 hover:ring-black/[0.06] dark:hover:ring-white/[0.06]"
)}
onClick={() => setSelectedFramework(id)}
>
<Image
src={src}
alt={name}
className={reverseIfDark ? "dark:invert" : undefined}
width="0"
height="0"
sizes="100vw"
style={{ width: '30px', height: 'auto' }}
/>
<span className="max-w-full px-1 text-center text-xs font-medium leading-tight text-foreground/90 whitespace-normal">
{name}
</span>
</DesignButton>
))}
{setupMode === "recommended" ? (
<div className="flex flex-col mt-4 mx-4">
<ol className="relative text-gray-500 border-s border-gray-200 dark:border-gray-700 dark:text-gray-400 ">
{[
{
step: 1,
title: "Copy Setup Prompt",
content: <div className="flex min-w-0 flex-col gap-4">
<CodeBlock
language="text"
content={selectedInstallPrompt}
customRender={
<pre className="max-h-[480px] overflow-y-auto whitespace-pre-wrap break-words p-4 text-sm leading-6 text-foreground">
{selectedInstallPrompt}
</pre>
}
title="Prompt for your AI agent"
icon="code"
maxHeight={480}
/>
</div>,
},
{
step: 2,
title: "Create Keys",
content: <>
<Typography>
Add this server-only key to your project&apos;s <InlineCode>.env.local</InlineCode> file.
</Typography>
<HexclaveKeys keys={keys} onGenerateKeys={onGenerateKeys} />
</>,
},
{
step: 3,
title: "Done",
content: <SetupRecommendedDoneStep onExploreDashboard={props.toMetrics} />,
},
].map((item) => (
<li key={item.step} className={cn("ms-6 flex flex-col lg:flex-row gap-10 mb-20")}>
<div className="flex flex-col justify-center gap-2 max-w-[180px] min-w-[180px]">
<span className={`absolute flex items-center justify-center w-8 h-8 bg-zinc-100 dark:bg-zinc-800 rounded-full -start-4 ring-4 ring-white dark:ring-zinc-900`}>
<span className={`text-zinc-500 dark:text-zinc-400 font-semibold`}>{item.step}</span>
</span>
<h3 className="font-medium leading-tight">{item.title}</h3>
</div>
</div>,
},
...(selectedFramework === 'nextjs' ? nextJsSteps : []),
...(selectedFramework === 'tanstackStart' ? tanstackStartSteps : []),
...(selectedFramework === 'react' ? reactSteps : []),
...(selectedFramework === 'javascript' ? javascriptSteps : []),
...(selectedFramework === 'python' ? pythonSteps : []),
]).map((item) => (
<li key={item.step} className={cn("ms-6 flex flex-col lg:flex-row gap-10 mb-20")}>
<div className="flex flex-col justify-center gap-2 max-w-[180px] min-w-[180px]">
<span className={`absolute flex items-center justify-center w-8 h-8 bg-zinc-100 dark:bg-zinc-800 rounded-full -start-4 ring-4 ring-white dark:ring-zinc-900`}>
<span className={`text-zinc-500 dark:text-zinc-400 font-semibold`}>{item.step}</span>
</span>
<h3 className="font-medium leading-tight">{item.title}</h3>
</div>
<div className="flex min-w-0 flex-grow flex-col gap-4">
{item.content}
</div>
</li>
))}
</ol>
</div>
<div className="flex min-w-0 flex-grow flex-col gap-4">
{item.content}
</div>
</li>
))}
</ol>
</div>
) : (
<div className="mx-4 mt-12 flex flex-col items-center gap-4 py-16 text-center">
<Typography>
Manual setup steps live in the documentation so they stay up to date with every framework and SDK change.
</Typography>
<DesignButton
onClick={() => {
window.open(manualSetupDocsUrl, '_blank');
}}
>
<BookIcon className="w-4 h-4 mr-2" />
Open manual setup docs
</DesignButton>
</div>
)}
</PageLayout>
);
}
@ -826,10 +295,24 @@ function GlobeIllustrationInner() {
);
}
function SetupRecommendedDoneStep(props: { onExploreDashboard: () => void }) {
return (
<div className="flex flex-col gap-4">
<Typography>
Hooray! Setup completed.
</Typography>
<div>
<DesignButton onClick={props.onExploreDashboard}>
Explore Dashboard
</DesignButton>
</div>
</div>
);
}
function HexclaveKeys(props: {
keys: { projectId: string, publishableClientKey?: string, secretServerKey: string } | null,
onGenerateKeys: () => Promise<void>,
type: 'next' | 'vite' | 'raw',
}) {
if (!props.keys) {
return (
@ -845,24 +328,7 @@ function HexclaveKeys(props: {
return (
<div className="w-full flex flex-col gap-3">
{props.type === 'next' ? (
<NextJsEnvKeys
projectId={props.keys.projectId}
publishableClientKey={props.keys.publishableClientKey}
secretServerKey={props.keys.secretServerKey}
/>
) : props.type === 'vite' ? (
<ViteEnvKeys
projectId={props.keys.projectId}
secretServerKey={props.keys.secretServerKey}
/>
) : (
<APIEnvKeys
projectId={props.keys.projectId}
publishableClientKey={props.keys.publishableClientKey}
secretServerKey={props.keys.secretServerKey}
/>
)}
<EnvFileViewer filename=".env.local" value={`HEXCLAVE_SECRET_SERVER_KEY=${props.keys.secretServerKey}`} />
<Typography type="label" variant="secondary">
{`Save these keys securely - they won't be shown again after leaving this page.`}

View File

@ -4,86 +4,22 @@ import { CopyButton, SimpleTooltip } from "@/components/ui";
import { useThemeWatcher } from '@/lib/theme';
import { cn } from '@/lib/utils';
import { CodeIcon, TerminalWindowIcon } from "@phosphor-icons/react";
import type { CSSProperties, ReactNode } from 'react';
import type { ReactNode } from 'react';
import { PrismLight as SyntaxHighlighter } from 'react-syntax-highlighter';
import bash from 'react-syntax-highlighter/dist/esm/languages/prism/bash';
import python from 'react-syntax-highlighter/dist/esm/languages/prism/python';
import sql from 'react-syntax-highlighter/dist/esm/languages/prism/sql';
import tsx from 'react-syntax-highlighter/dist/esm/languages/prism/tsx';
import typescript from 'react-syntax-highlighter/dist/esm/languages/prism/typescript';
import { dark, prism } from 'react-syntax-highlighter/dist/esm/styles/prism';
Object.entries({ tsx, bash, typescript, python, sql }).forEach(([key, value]) => {
SyntaxHighlighter.registerLanguage(key, value);
});
const codeThemeBase: Record<string, CSSProperties> = {
'code[class*="language-"]': {
background: 'transparent',
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
textShadow: 'none',
},
'pre[class*="language-"]': {
background: 'transparent',
fontFamily: 'Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace',
textShadow: 'none',
},
};
export const codePanelShellClasses = "overflow-hidden transition-all duration-150 hover:transition-none rounded-xl bg-white/90 dark:bg-background/60 dark:backdrop-blur-xl ring-1 ring-black/[0.06] hover:ring-black/[0.1] dark:ring-white/[0.06] dark:hover:ring-white/[0.1] shadow-none border border-black/[0.06] dark:border-white/[0.06]";
const lightCodeTheme: Record<string, CSSProperties> = {
...codeThemeBase,
'code[class*="language-"]': {
...codeThemeBase['code[class*="language-"]'],
color: '#334155',
},
'pre[class*="language-"]': {
...codeThemeBase['pre[class*="language-"]'],
color: '#334155',
},
comment: { color: '#94a3b8', fontStyle: 'italic' },
punctuation: { color: '#475569' },
property: { color: '#075985' },
tag: { color: '#1d4ed8' },
boolean: { color: '#b45309', fontWeight: 600 },
number: { color: '#b45309', fontWeight: 600 },
constant: { color: '#6d28d9', fontWeight: 600 },
selector: { color: '#047857' },
string: { color: '#047857' },
builtin: { color: '#0e7490', fontWeight: 600 },
operator: { color: '#0f766e', fontWeight: 600 },
variable: { color: '#334155' },
atrule: { color: '#6d28d9', fontWeight: 600 },
function: { color: '#1d4ed8', fontWeight: 600 },
'class-name': { color: '#a16207', fontWeight: 600 },
keyword: { color: '#6d28d9', fontWeight: 600 },
};
const darkCodeTheme: Record<string, CSSProperties> = {
...codeThemeBase,
'code[class*="language-"]': {
...codeThemeBase['code[class*="language-"]'],
color: '#eef6ff',
},
'pre[class*="language-"]': {
...codeThemeBase['pre[class*="language-"]'],
color: '#eef6ff',
},
comment: { color: '#7c8798', fontStyle: 'italic' },
punctuation: { color: '#c6d3e1' },
property: { color: '#38bdf8' },
tag: { color: '#60a5fa' },
boolean: { color: '#fbbf24', fontWeight: 600 },
number: { color: '#fbbf24', fontWeight: 600 },
constant: { color: '#d8b4fe', fontWeight: 600 },
selector: { color: '#4ade80' },
string: { color: '#4ade80' },
builtin: { color: '#22d3ee', fontWeight: 600 },
operator: { color: '#22d3ee', fontWeight: 600 },
variable: { color: '#eef6ff' },
atrule: { color: '#d8b4fe', fontWeight: 600 },
function: { color: '#60a5fa', fontWeight: 600 },
'class-name': { color: '#fde047', fontWeight: 600 },
keyword: { color: '#d8b4fe', fontWeight: 600 },
};
export const codePanelHeaderClasses = "text-muted-foreground font-medium pl-4 pr-2 text-sm flex justify-between items-center bg-black/[0.015] dark:bg-white/[0.015] py-2.5 border-b border-black/[0.06] dark:border-white/[0.06]";
type CodeBlockProps = {
language: string,
@ -116,11 +52,11 @@ export function CodeBlock(props: CodeBlockProps) {
return (
<div className={cn(
"overflow-hidden transition-all duration-150 hover:transition-none",
!props.fullWidth && "rounded-xl",
props.neutralBackground
? "bg-background border border-black/[0.08] dark:border-white/[0.06] shadow-sm"
: "bg-white/90 dark:bg-background/60 dark:backdrop-blur-xl ring-1 ring-black/[0.06] hover:ring-black/[0.1] dark:ring-white/[0.06] dark:hover:ring-white/[0.1] shadow-none border border-black/[0.06] dark:border-white/[0.06]"
? "overflow-hidden transition-all duration-150 hover:transition-none rounded-xl bg-background border border-black/[0.08] dark:border-white/[0.06] shadow-sm"
: !props.fullWidth
? codePanelShellClasses
: "overflow-hidden transition-all duration-150 hover:transition-none bg-white/90 dark:bg-background/60 dark:backdrop-blur-xl ring-1 ring-black/[0.06] hover:ring-black/[0.1] dark:ring-white/[0.06] dark:hover:ring-white/[0.1] shadow-none border border-black/[0.06] dark:border-white/[0.06]"
)}>
<div className={cn(
"text-muted-foreground font-medium pl-4 pr-2 text-sm flex justify-between items-center bg-black/[0.015] dark:bg-white/[0.015]",
@ -143,7 +79,7 @@ export function CodeBlock(props: CodeBlockProps) {
<div className="overflow-x-auto">
{props.customRender ?? <SyntaxHighlighter
language={props.language}
style={theme === 'dark' ? darkCodeTheme : lightCodeTheme}
style={theme === 'dark' ? dark : prism}
customStyle={{
background: 'transparent',
padding: '1em',

View File

@ -1,5 +1,6 @@
"use client";
import { codePanelHeaderClasses, codePanelShellClasses } from '@/components/code-block';
import { getPublicEnvVar } from '@/lib/env';
import { Button, CopyButton, CopyField, Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui";
import React, { useState } from "react";
@ -11,10 +12,6 @@ type EnvFileViewerProps = {
value: string;
}
export const codePanelShellClasses = "overflow-hidden transition-all duration-150 hover:transition-none rounded-xl bg-white/90 dark:bg-background/60 dark:backdrop-blur-xl ring-1 ring-black/[0.06] hover:ring-black/[0.1] dark:ring-white/[0.06] dark:hover:ring-white/[0.1] shadow-none border border-black/[0.06] dark:border-white/[0.06]";
const codePanelHeaderClasses = "text-muted-foreground font-medium pl-4 pr-2 text-sm flex justify-between items-center bg-black/[0.015] dark:bg-white/[0.015] py-2.5 border-b border-black/[0.06] dark:border-white/[0.06]";
export function EnvFileViewer({ filename, value }: EnvFileViewerProps) {
const [revealAll, setRevealAll] = useState(false);
@ -83,7 +80,7 @@ export function EnvFileViewer({ filename, value }: EnvFileViewerProps) {
);
}
function getEnvFileContent(props: {
export function getEnvFileContent(props: {
projectId: string,
publishableClientKey?: string,
secretServerKey?: string,

0
gist-id.txt Normal file
View File