feat(frontend): new navbar design (#10341)

## Changes 🏗️

<img width="900" height="327" alt="Screenshot 2025-07-10 at 20 12 38"
src="https://github.com/user-attachments/assets/044f00ed-7e05-46b7-a821-ce1cb0ee9298"
/>
<br /><br />

Navbar updated to look pretty from the new designs:
- the logo is now centred instead of on the left
- menu items have been updated to a smaller font-size and less radius
- icons have been updated

I also generated the API files ( _sorry for the noise_ ). I had to do
some border-radius and button updates on the atoms/tokens for it to look
good.

## Checklist 📋

## For code changes:
- [x] I have clearly listed my changes in the PR description
- [x] I have made a test plan
- [x] I have tested my changes according to the test plan:
  - [x] Login/logout
  - [x] The new navbar looks good across screens 

## For configuration changes

No config changes
This commit is contained in:
Ubbe
2025-07-10 22:06:12 +04:00
committed by GitHub
parent 22c76eab61
commit 2fda8dfd32
32 changed files with 923 additions and 1516 deletions

View File

@@ -1,12 +1,13 @@
"use client";
import { useSearchParams } from "next/navigation";
import { GraphID } from "@/lib/autogpt-server-api/types";
import FlowEditor from "@/components/Flow";
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
import { useEffect } from "react";
import LoadingBox from "@/components/ui/loading";
import { GraphID } from "@/lib/autogpt-server-api/types";
import { useSearchParams } from "next/navigation";
import { Suspense, useEffect } from "react";
export default function BuilderPage() {
function BuilderContent() {
const query = useSearchParams();
const { completeStep } = useOnboarding();
@@ -15,12 +16,20 @@ export default function BuilderPage() {
}, [completeStep]);
const _graphVersion = query.get("flowVersion");
const graphVersion = _graphVersion ? parseInt(_graphVersion) : undefined
const graphVersion = _graphVersion ? parseInt(_graphVersion) : undefined;
return (
<FlowEditor
className="flow-container"
flowID={query.get("flowID") as GraphID | null ?? undefined}
flowID={(query.get("flowID") as GraphID | null) ?? undefined}
flowVersion={graphVersion}
/>
);
}
export default function BuilderPage() {
return (
<Suspense fallback={<LoadingBox className="h-[80vh]" />}>
<BuilderContent />
</Suspense>
);
}

View File

@@ -1,67 +1,10 @@
import { Navbar } from "@/components/layout/Navbar/Navbar";
import { ReactNode } from "react";
import { Navbar } from "@/components/agptui/Navbar";
import { IconType } from "@/components/ui/icons";
export default function PlatformLayout({ children }: { children: ReactNode }) {
return (
<>
<Navbar
links={[
{
name: "Marketplace",
href: "/marketplace",
},
{
name: "Library",
href: "/library",
},
{
name: "Build",
href: "/build",
},
]}
menuItemGroups={[
{
items: [
{
icon: IconType.Edit,
text: "Edit profile",
href: "/profile",
},
],
},
{
items: [
{
icon: IconType.LayoutDashboard,
text: "Creator Dashboard",
href: "/profile/dashboard",
},
{
icon: IconType.UploadCloud,
text: "Publish an agent",
},
],
},
{
items: [
{
icon: IconType.Settings,
text: "Settings",
href: "/profile/settings",
},
],
},
{
items: [
{
icon: IconType.LogOut,
text: "Log out",
},
],
},
]}
/>
<Navbar />
<main>{children}</main>
</>
);

View File

@@ -1,15 +1,15 @@
"use client";
import Link from "next/link";
import { Alert, AlertDescription } from "@/components/ui/alert";
import {
ArrowBottomRightIcon,
QuestionMarkCircledIcon,
} from "@radix-ui/react-icons";
import { Alert, AlertDescription } from "@/components/ui/alert";
import { LibraryPageStateProvider } from "./components/state-provider";
import LibraryActionHeader from "./components/LibraryActionHeader/LibraryActionHeader";
import LibraryAgentList from "./components/LibraryAgentList/LibraryAgentList";
import { LibraryPageStateProvider } from "./components/state-provider";
/**
* LibraryPage Component
@@ -17,7 +17,7 @@ import LibraryAgentList from "./components/LibraryAgentList/LibraryAgentList";
*/
export default function LibraryPage() {
return (
<main className="container min-h-screen space-y-4 pb-20 sm:px-8 md:px-12">
<main className="pt-160 container min-h-screen space-y-4 pb-20 pt-16 sm:px-8 md:px-12">
<LibraryPageStateProvider>
<LibraryActionHeader />
<LibraryAgentList />

View File

@@ -6,12 +6,12 @@
* OpenAPI spec version: 0.1
*/
import type { CredentialsMetaInputTitle } from "./credentialsMetaInputTitle";
import type { ProviderName } from "./providerName";
import type { CredentialsMetaInputType } from "./credentialsMetaInputType";
export interface CredentialsMetaInput {
id: string;
title?: CredentialsMetaInputTitle;
/** Provider name for integrations. Can be any string value, including custom provider names. */
provider: string;
provider: ProviderName;
type: CredentialsMetaInputType;
}

View File

@@ -5,12 +5,12 @@
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
import type { ProviderName } from "./providerName";
import type { LibraryAgentTriggerInfoConfigSchema } from "./libraryAgentTriggerInfoConfigSchema";
import type { LibraryAgentTriggerInfoCredentialsInputName } from "./libraryAgentTriggerInfoCredentialsInputName";
export interface LibraryAgentTriggerInfo {
/** Provider name for integrations. Can be any string value, including custom provider names. */
provider: string;
provider: ProviderName;
/** Input schema for the trigger block */
config_schema: LibraryAgentTriggerInfoConfigSchema;
credentials_input_name: LibraryAgentTriggerInfoCredentialsInputName;

View File

@@ -0,0 +1,53 @@
/**
* Generated by orval v7.10.0 🍺
* Do not edit manually.
* AutoGPT Agent Server
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
export type ProviderName = (typeof ProviderName)[keyof typeof ProviderName];
// eslint-disable-next-line @typescript-eslint/no-redeclare
export const ProviderName = {
aiml_api: "aiml_api",
anthropic: "anthropic",
apollo: "apollo",
compass: "compass",
discord: "discord",
d_id: "d_id",
e2b: "e2b",
exa: "exa",
fal: "fal",
generic_webhook: "generic_webhook",
github: "github",
google: "google",
google_maps: "google_maps",
groq: "groq",
http: "http",
hubspot: "hubspot",
ideogram: "ideogram",
jina: "jina",
linear: "linear",
llama_api: "llama_api",
medium: "medium",
mem0: "mem0",
notion: "notion",
nvidia: "nvidia",
ollama: "ollama",
openai: "openai",
openweathermap: "openweathermap",
open_router: "open_router",
pinecone: "pinecone",
reddit: "reddit",
replicate: "replicate",
revid: "revid",
screenshotone: "screenshotone",
slant3d: "slant3d",
smartlead: "smartlead",
smtp: "smtp",
twitter: "twitter",
todoist: "todoist",
unreal_speech: "unreal_speech",
zerobounce: "zerobounce",
} as const;

View File

@@ -5,13 +5,13 @@
* This server is used to execute agents that are created by the AutoGPT system.
* OpenAPI spec version: 0.1
*/
import type { ProviderName } from "./providerName";
import type { WebhookConfig } from "./webhookConfig";
export interface Webhook {
id?: string;
user_id: string;
/** Provider name for integrations. Can be any string value, including custom provider names. */
provider: string;
provider: ProviderName;
credentials_id: string;
webhook_type: string;
resource: string;

View File

@@ -18,9 +18,8 @@
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "The provider to initiate an OAuth flow for",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
"$ref": "#/components/schemas/ProviderName",
"title": "The provider to initiate an OAuth flow for"
}
},
{
@@ -65,9 +64,8 @@
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "The target provider for this OAuth exchange",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
"$ref": "#/components/schemas/ProviderName",
"title": "The target provider for this OAuth exchange"
}
}
],
@@ -135,9 +133,8 @@
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "The provider to list credentials for",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
"$ref": "#/components/schemas/ProviderName",
"title": "The provider to list credentials for"
}
}
],
@@ -176,9 +173,8 @@
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "The provider to create credentials for",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
"$ref": "#/components/schemas/ProviderName",
"title": "The provider to create credentials for"
}
}
],
@@ -257,9 +253,8 @@
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "The provider to retrieve credentials for",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
"$ref": "#/components/schemas/ProviderName",
"title": "The provider to retrieve credentials for"
}
},
{
@@ -320,9 +315,8 @@
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "The provider to delete credentials for",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
"$ref": "#/components/schemas/ProviderName",
"title": "The provider to delete credentials for"
}
},
{
@@ -386,9 +380,8 @@
"in": "path",
"required": true,
"schema": {
"type": "string",
"title": "Provider where the webhook was registered",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
"$ref": "#/components/schemas/ProviderName",
"title": "Provider where the webhook was registered"
}
},
{
@@ -443,86 +436,6 @@
}
}
},
"/api/integrations/providers": {
"get": {
"tags": ["v1", "integrations"],
"summary": "List Providers",
"description": "Get a list of all available provider names.\n\nReturns both statically defined providers (from ProviderName enum)\nand dynamically registered providers (from SDK decorators).\n\nNote: The complete list of provider names is also available as a constant\nin the generated TypeScript client via PROVIDER_NAMES.",
"operationId": "getV1ListProviders",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"items": { "type": "string" },
"type": "array",
"title": "Response Getv1Listproviders"
}
}
}
}
}
}
},
"/api/integrations/providers/names": {
"get": {
"tags": ["v1", "integrations"],
"summary": "Get Provider Names",
"description": "Get all provider names in a structured format.\n\nThis endpoint is specifically designed to expose the provider names\nin the OpenAPI schema so that code generators like Orval can create\nappropriate TypeScript constants.",
"operationId": "getV1GetProviderNames",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProviderNamesResponse"
}
}
}
}
}
}
},
"/api/integrations/providers/constants": {
"get": {
"tags": ["v1", "integrations"],
"summary": "Get Provider Constants",
"description": "Get provider names as constants.\n\nThis endpoint returns a model with provider names as constants,\nspecifically designed for OpenAPI code generation tools to create\nTypeScript constants.",
"operationId": "getV1GetProviderConstants",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/ProviderConstants" }
}
}
}
}
}
},
"/api/integrations/providers/enum-example": {
"get": {
"tags": ["v1", "integrations"],
"summary": "Get Provider Enum Example",
"description": "Example endpoint that uses the CompleteProviderNames enum.\n\nThis endpoint exists to ensure that the CompleteProviderNames enum is included\nin the OpenAPI schema, which will cause Orval to generate it as a\nTypeScript enum/constant.",
"operationId": "getV1GetProviderEnumExample",
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProviderEnumResponse"
}
}
}
}
}
}
},
"/api/analytics/log_raw_metric": {
"post": {
"tags": ["v1", "analytics"],
@@ -3373,6 +3286,48 @@
}
}
},
"/api/library/agents/by-graph/{graph_id}": {
"get": {
"tags": ["v2", "library", "private"],
"summary": "Get Library Agent By Graph Id",
"operationId": "getV2GetLibraryAgentByGraphId",
"parameters": [
{
"name": "graph_id",
"in": "path",
"required": true,
"schema": { "type": "string", "title": "Graph Id" }
},
{
"name": "version",
"in": "query",
"required": false,
"schema": {
"anyOf": [{ "type": "integer" }, { "type": "null" }],
"title": "Version"
}
}
],
"responses": {
"200": {
"description": "Successful Response",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/LibraryAgent" }
}
}
},
"422": {
"description": "Validation Error",
"content": {
"application/json": {
"schema": { "$ref": "#/components/schemas/HTTPValidationError" }
}
}
}
}
}
},
"/api/library/agents/marketplace/{store_listing_version_id}": {
"get": {
"tags": ["v2", "library", "private", "store, library"],
@@ -4146,11 +4101,7 @@
"anyOf": [{ "type": "string" }, { "type": "null" }],
"title": "Title"
},
"provider": {
"type": "string",
"title": "Provider",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
},
"provider": { "$ref": "#/components/schemas/ProviderName" },
"type": {
"type": "string",
"enum": ["api_key", "oauth2", "user_password", "host_scoped"],
@@ -4160,8 +4111,8 @@
"type": "object",
"required": ["id", "provider", "type"],
"title": "CredentialsMetaInput",
"credentials_provider": ["string"],
"credentials_types": ["api_key", "oauth2", "user_password"]
"credentials_provider": [],
"credentials_types": []
},
"CredentialsMetaResponse": {
"properties": {
@@ -4869,11 +4820,7 @@
},
"LibraryAgentTriggerInfo": {
"properties": {
"provider": {
"type": "string",
"title": "Provider",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
},
"provider": { "$ref": "#/components/schemas/ProviderName" },
"config_schema": {
"additionalProperties": true,
"type": "object",
@@ -5288,6 +5235,7 @@
"AGENT_INPUT",
"CONGRATS",
"GET_RESULTS",
"RUN_AGENTS",
"MARKETPLACE_VISIT",
"MARKETPLACE_ADD_AGENT",
"MARKETPLACE_RUN_AGENT",
@@ -5675,44 +5623,51 @@
"required": ["name", "username", "description", "links"],
"title": "ProfileDetails"
},
"ProviderConstants": {
"properties": {
"PROVIDER_NAMES": {
"additionalProperties": { "type": "string" },
"type": "object",
"title": "Provider Names",
"description": "All available provider names as a constant mapping"
}
},
"type": "object",
"title": "ProviderConstants",
"description": "Model that exposes all provider names as a constant in the OpenAPI schema.\nThis is designed to be converted by Orval into a TypeScript constant."
},
"ProviderEnumResponse": {
"properties": {
"provider": {
"type": "string",
"title": "Provider",
"description": "A provider name from the complete list of providers"
}
},
"type": "object",
"required": ["provider"],
"title": "ProviderEnumResponse",
"description": "Response containing a provider from the enum."
},
"ProviderNamesResponse": {
"properties": {
"providers": {
"items": { "type": "string" },
"type": "array",
"title": "Providers",
"description": "List of all available provider names"
}
},
"type": "object",
"title": "ProviderNamesResponse",
"description": "Response containing list of all provider names."
"ProviderName": {
"type": "string",
"enum": [
"aiml_api",
"anthropic",
"apollo",
"compass",
"discord",
"d_id",
"e2b",
"exa",
"fal",
"generic_webhook",
"github",
"google",
"google_maps",
"groq",
"http",
"hubspot",
"ideogram",
"jina",
"linear",
"llama_api",
"medium",
"mem0",
"notion",
"nvidia",
"ollama",
"openai",
"openweathermap",
"open_router",
"pinecone",
"reddit",
"replicate",
"revid",
"screenshotone",
"slant3d",
"smartlead",
"smtp",
"twitter",
"todoist",
"unreal_speech",
"zerobounce"
],
"title": "ProviderName"
},
"RefundRequest": {
"properties": {
@@ -6486,11 +6441,7 @@
"properties": {
"id": { "type": "string", "title": "Id" },
"user_id": { "type": "string", "title": "User Id" },
"provider": {
"type": "string",
"title": "Provider",
"description": "Provider name for integrations. Can be any string value, including custom provider names."
},
"provider": { "$ref": "#/components/schemas/ProviderName" },
"credentials_id": { "type": "string", "title": "Credentials Id" },
"webhook_type": { "type": "string", "title": "Webhook Type" },
"resource": { "type": "string", "title": "Resource" },

View File

@@ -1,131 +0,0 @@
import * as React from "react";
import Link from "next/link";
import { ProfilePopoutMenu } from "./ProfilePopoutMenu";
import { IconType, IconLogIn, IconAutoGPTLogo } from "@/components/ui/icons";
import { MobileNavBar } from "./MobileNavBar";
import { Button } from "./Button";
import Wallet from "./Wallet";
import { ProfileDetails } from "@/lib/autogpt-server-api/types";
import { NavbarLink } from "./NavbarLink";
import BackendAPI from "@/lib/autogpt-server-api";
import { getServerUser } from "@/lib/supabase/server/getServerUser";
// Disable theme toggle for now
// import { ThemeToggle } from "./ThemeToggle";
interface NavLink {
name: string;
href: string;
}
interface NavbarProps {
links: NavLink[];
menuItemGroups: {
groupName?: string;
items: {
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}[];
}[];
}
async function getProfileData() {
const api = new BackendAPI();
const profile = await Promise.resolve(api.getStoreProfile());
return profile;
}
export const Navbar = async ({ links, menuItemGroups }: NavbarProps) => {
const { user } = await getServerUser();
const isLoggedIn = user !== null;
let profile: ProfileDetails | null = null;
if (isLoggedIn) {
profile = await getProfileData();
}
return (
<>
<nav className="sticky top-0 z-40 mx-[16px] hidden h-16 items-center justify-between rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 py-3 pl-6 pr-3 backdrop-blur-[26px] dark:border-gray-700 dark:bg-gray-900 md:inline-flex">
<div className="flex items-center gap-11">
<div className="relative h-10 w-[88.87px]">
<IconAutoGPTLogo className="h-full w-full" />
</div>
{links.map((link) => (
<NavbarLink key={link.name} name={link.name} href={link.href} />
))}
</div>
{/* Profile section */}
<div className="flex items-center gap-4">
{isLoggedIn ? (
<div className="flex items-center gap-4">
{profile && <Wallet />}
<ProfilePopoutMenu
menuItemGroups={menuItemGroups}
userName={profile?.username}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url}
/>
</div>
) : (
<Link href="/login">
<Button
size="sm"
className="flex items-center justify-end space-x-2"
>
<IconLogIn className="h-5 h-[48px] w-5" />
<span>Log In</span>
</Button>
</Link>
)}
{/* <ThemeToggle /> */}
</div>
</nav>
{/* Mobile Navbar - Adjust positioning */}
<>
{isLoggedIn ? (
<div className="fixed right-4 top-4 z-50">
<MobileNavBar
userName={profile?.username}
menuItemGroups={[
{
groupName: "Navigation",
items: links.map((link) => ({
icon:
link.name === "Marketplace"
? IconType.Marketplace
: link.name === "Library"
? IconType.Library
: link.name === "Build"
? IconType.Builder
: link.name === "Monitor"
? IconType.Library
: IconType.LayoutDashboard,
text: link.name,
href: link.href,
})),
},
...menuItemGroups,
]}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url}
/>
</div>
) : (
<Link
href="/login"
className="fixed right-4 top-4 z-50 mt-4 inline-flex h-8 items-center justify-end rounded-lg pr-4 md:hidden"
>
<Button size="sm" className="flex items-center space-x-2">
<IconLogIn className="h-5 w-5" />
<span>Log In</span>
</Button>
</Link>
)}
</>
</>
);
};

View File

@@ -1,66 +0,0 @@
"use client";
import Link from "next/link";
import {
IconShoppingCart,
IconBoxes,
IconLibrary,
IconLaptop,
} from "@/components/ui/icons";
import { usePathname } from "next/navigation";
interface NavbarLinkProps {
name: string;
href: string;
}
export const NavbarLink = ({ name, href }: NavbarLinkProps) => {
const pathname = usePathname();
const parts = pathname.split("/");
const activeLink = "/" + (parts.length > 2 ? parts[2] : parts[1]);
return (
<Link
href={href}
data-testid={`navbar-link-${name.toLowerCase()}`}
className="font-poppins text-[20px] leading-[28px]"
>
<div
className={`h-[48px] px-5 py-4 ${
activeLink === href
? "rounded-2xl bg-neutral-800 dark:bg-neutral-200"
: ""
} flex items-center justify-start gap-3`}
>
{href === "/marketplace" && (
<IconShoppingCart
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
/>
)}
{href === "/build" && (
<IconBoxes
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
/>
)}
{href === "/monitor" && (
<IconLaptop
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
/>
)}
{href === "/library" && (
<IconLibrary
className={`h-6 w-6 ${activeLink === href ? "text-white dark:text-black" : ""}`}
/>
)}
<div
className={`hidden font-poppins text-[20px] font-medium leading-[28px] lg:block ${
activeLink === href
? "text-neutral-50 dark:text-neutral-900"
: "text-neutral-900 dark:text-neutral-50"
}`}
>
{name}
</div>
</div>
</Link>
);
};

View File

@@ -109,7 +109,7 @@ export default function Wallet() {
<button
ref={walletRef}
className={cn(
"relative flex items-center gap-1 rounded-md bg-zinc-200 px-3 py-2 text-sm transition-colors duration-200 hover:bg-zinc-300",
"relative flex items-center gap-1 rounded-md bg-zinc-50 px-3 py-2 text-sm",
)}
onClick={onWalletOpen}
>

View File

@@ -56,7 +56,7 @@ const meta: Meta<typeof Button> = {
};
export default meta;
type Story = StoryObj<typeof meta>;
type Story = StoryObj<typeof Button>;
// Basic variants
export const Primary: Story = {

View File

@@ -1,82 +1,78 @@
import { cn } from "@/lib/utils";
import { CircleNotchIcon } from "@phosphor-icons/react/dist/ssr";
import { cva, type VariantProps } from "class-variance-authority";
import Link, { type LinkProps } from "next/link";
import React from "react";
import { ButtonProps, extendedButtonVariants } from "./helpers";
// Extended button variants based on our design system
const extendedButtonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 font-['Geist'] leading-snug border",
{
variants: {
variant: {
primary:
"bg-zinc-800 border-zinc-800 text-white hover:bg-zinc-900 hover:border-zinc-900 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
secondary:
"bg-zinc-100 border-zinc-100 text-black hover:bg-zinc-300 hover:border-zinc-300 rounded-full disabled:text-zinc-300 disabled:bg-zinc-50 disabled:border-zinc-50 disabled:opacity-1",
destructive:
"bg-red-500 border-red-500 text-white hover:bg-red-600 hover:border-red-600 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
outline:
"bg-transparent border-zinc-700 text-black hover:bg-zinc-100 hover:border-zinc-700 rounded-full disabled:border-zinc-200 disabled:text-zinc-200 disabled:opacity-1",
ghost:
"bg-transparent border-transparent text-black hover:bg-zinc-50 hover:border-zinc-50 rounded-full disabled:text-zinc-200 disabled:opacity-1",
icon: "bg-white text-black border border-zinc-600 hover:bg-zinc-100 rounded-[96px] disabled:opacity-1",
},
size: {
small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem]",
large: "min-w-20 px-4 py-3 text-sm gap-2",
icon: "p-3",
},
},
defaultVariants: {
variant: "primary",
size: "large",
},
},
);
export function Button(props: ButtonProps) {
const {
className,
variant,
size,
loading = false,
leftIcon,
rightIcon,
children,
as = "button",
...restProps
} = props;
export interface ButtonProps
extends React.ButtonHTMLAttributes<HTMLButtonElement>,
VariantProps<typeof extendedButtonVariants> {
loading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
asChild?: boolean;
}
function Button({
className,
variant,
size,
loading = false,
leftIcon,
rightIcon,
children,
disabled,
...props
}: ButtonProps) {
const disabled = "disabled" in props ? props.disabled : false;
const isDisabled = disabled;
const buttonContent = (
<>
{loading && (
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
)}
{!loading && leftIcon}
{children}
{!loading && rightIcon}
</>
);
if (loading) {
return variant === "ghost" ? (
<button
const loadingClassName =
variant === "ghost"
? cn(
extendedButtonVariants({ variant, size, className }),
"pointer-events-none",
)
: cn(
extendedButtonVariants({ variant: "primary", size, className }),
"pointer-events-none border-zinc-500 bg-zinc-500 text-white",
);
return as === "NextLink" ? (
<Link
{...(restProps as LinkProps)}
className={loadingClassName}
aria-disabled="true"
>
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
{children}
</Link>
) : (
<button className={loadingClassName} disabled>
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
{children}
</button>
);
}
if (as === "NextLink") {
return (
<Link
{...(restProps as LinkProps)}
className={cn(
extendedButtonVariants({ variant, size, className }),
"pointer-events-none",
loading && "pointer-events-none",
isDisabled && "pointer-events-none opacity-50",
)}
aria-disabled={isDisabled}
>
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
{children}
</button>
) : (
<button
className={cn(
extendedButtonVariants({ variant: "primary", size, className }),
"pointer-events-none border-zinc-500 bg-zinc-500 text-white",
)}
>
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
{children}
</button>
{buttonContent}
</Link>
);
}
@@ -87,18 +83,9 @@ function Button({
loading && "pointer-events-none",
)}
disabled={isDisabled}
{...props}
{...(restProps as React.ButtonHTMLAttributes<HTMLButtonElement>)}
>
{loading && (
<CircleNotchIcon className="h-4 w-4 animate-spin" weight="bold" />
)}
{!loading && leftIcon}
{children}
{!loading && rightIcon}
{buttonContent}
</button>
);
}
Button.displayName = "Button";
export { Button, extendedButtonVariants };

View File

@@ -0,0 +1,55 @@
import { cva, VariantProps } from "class-variance-authority";
import { LinkProps } from "next/link";
// Extended button variants based on our design system
export const extendedButtonVariants = cva(
"inline-flex items-center justify-center whitespace-nowrap font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-neutral-950 disabled:pointer-events-none disabled:opacity-50 font-sans leading-snug border",
{
variants: {
variant: {
primary:
"bg-zinc-800 border-zinc-800 text-white hover:bg-zinc-900 hover:border-zinc-900 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
secondary:
"bg-zinc-100 border-zinc-100 text-black hover:bg-zinc-300 hover:border-zinc-300 rounded-full disabled:text-zinc-300 disabled:bg-zinc-50 disabled:border-zinc-50 disabled:opacity-1",
destructive:
"bg-red-500 border-red-500 text-white hover:bg-red-600 hover:border-red-600 rounded-full disabled:text-white disabled:bg-zinc-200 disabled:border-zinc-200 disabled:opacity-1",
outline:
"bg-transparent border-zinc-700 text-black hover:bg-zinc-100 hover:border-zinc-700 rounded-full disabled:border-zinc-200 disabled:text-zinc-200 disabled:opacity-1",
ghost:
"bg-transparent border-transparent text-black hover:bg-zinc-50 hover:border-zinc-50 rounded-full disabled:text-zinc-200 disabled:opacity-1",
icon: "bg-white text-black border border-zinc-600 hover:bg-zinc-100 rounded-[96px] disabled:opacity-1",
},
size: {
small: "px-3 py-2 text-sm gap-1.5 h-[2.25rem]",
large: "min-w-20 px-4 py-3 text-sm gap-2",
icon: "p-3",
},
},
defaultVariants: {
variant: "primary",
size: "large",
},
},
);
type BaseButtonProps = {
loading?: boolean;
leftIcon?: React.ReactNode;
rightIcon?: React.ReactNode;
asChild?: boolean;
} & VariantProps<typeof extendedButtonVariants>;
type ButtonAsButton = BaseButtonProps &
React.ButtonHTMLAttributes<HTMLButtonElement> & {
as?: "button";
href?: never;
};
type ButtonAsLink = BaseButtonProps &
Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, keyof LinkProps> &
LinkProps & {
as: "NextLink";
disabled?: boolean;
};
export type ButtonProps = ButtonAsButton | ButtonAsLink;

View File

@@ -0,0 +1,84 @@
import { IconAutoGPTLogo, IconType } from "@/components/ui/icons";
import Wallet from "../../agptui/Wallet";
import { AccountMenu } from "./components/AccountMenu/AccountMenu";
import { LoginButton } from "./components/LoginButton";
import { MobileNavBar } from "./components/MobileNavbar/MobileNavBar";
import { NavbarLink } from "./components/NavbarLink";
import { accountMenuItems, loggedInLinks, loggedOutLinks } from "./helpers";
import { getNavbarAccountData } from "./data";
export async function Navbar() {
const { profile, isLoggedIn } = await getNavbarAccountData();
return (
<>
<nav className="sticky top-0 z-40 hidden h-16 items-center rounded-bl-2xl rounded-br-2xl border border-white/50 bg-[#f3f4f6]/20 p-3 backdrop-blur-[26px] md:inline-flex">
{/* Left section */}
<div className="flex flex-1 items-center gap-6">
{isLoggedIn
? loggedInLinks.map((link) => (
<NavbarLink key={link.name} name={link.name} href={link.href} />
))
: loggedOutLinks.map((link) => (
<NavbarLink key={link.name} name={link.name} href={link.href} />
))}
</div>
{/* Centered logo */}
<div className="absolute left-1/2 top-1/2 h-10 w-[88.87px] -translate-x-1/2 -translate-y-1/2">
<IconAutoGPTLogo className="h-full w-full" />
</div>
{/* Right section */}
<div className="flex flex-1 items-center justify-end gap-4">
{isLoggedIn ? (
<div className="flex items-center gap-4">
{profile && <Wallet />}
<AccountMenu
userName={profile?.username}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url ?? ""}
menuItemGroups={accountMenuItems}
/>
</div>
) : (
<LoginButton />
)}
{/* <ThemeToggle /> */}
</div>
</nav>
{/* Mobile Navbar - Adjust positioning */}
<>
{isLoggedIn ? (
<div className="fixed right-4 top-4 z-50">
<MobileNavBar
userName={profile?.username}
menuItemGroups={[
{
groupName: "Navigation",
items: loggedInLinks.map((link) => ({
icon:
link.name === "Marketplace"
? IconType.Marketplace
: link.name === "Library"
? IconType.Library
: link.name === "Build"
? IconType.Builder
: link.name === "Monitor"
? IconType.Library
: IconType.LayoutDashboard,
text: link.name,
href: link.href,
})),
},
...accountMenuItems,
]}
userEmail={profile?.name}
avatarSrc={profile?.avatar_url ?? ""}
/>
</div>
) : null}
</>
</>
);
}

View File

@@ -1,74 +1,31 @@
import * as React from "react";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
IconType,
IconEdit,
IconLayoutDashboard,
IconUploadCloud,
IconSettings,
IconLogOut,
IconRefresh,
IconMarketplace,
IconLibrary,
IconBuilder,
} from "../ui/icons";
import Link from "next/link";
import { ProfilePopoutMenuLogoutButton } from "./ProfilePopoutMenuLogoutButton";
import { PublishAgentPopout } from "./composite/PublishAgentPopout";
import * as React from "react";
import { PublishAgentPopout } from "../../../../agptui/composite/PublishAgentPopout";
import { getAccountMenuOptionIcon, MenuItemGroup } from "../../helpers";
import { AccountLogoutOption } from "./components/AccountLogoutOption";
interface ProfilePopoutMenuProps {
interface Props {
userName?: string;
userEmail?: string;
avatarSrc?: string;
hideNavBarUsername?: boolean;
menuItemGroups: {
groupName?: string;
items: {
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}[];
}[];
menuItemGroups: MenuItemGroup[];
}
export function ProfilePopoutMenu({
export function AccountMenu({
userName,
userEmail,
avatarSrc,
menuItemGroups,
}: ProfilePopoutMenuProps) {
}: Props) {
const popupId = React.useId();
const getIcon = (icon: IconType) => {
const iconClass = "w-6 h-6";
switch (icon) {
case IconType.LayoutDashboard:
return <IconLayoutDashboard className={iconClass} />;
case IconType.UploadCloud:
return <IconUploadCloud className={iconClass} />;
case IconType.Edit:
return <IconEdit className={iconClass} />;
case IconType.Settings:
return <IconSettings className={iconClass} />;
case IconType.LogOut:
return <IconLogOut className={iconClass} />;
case IconType.Marketplace:
return <IconMarketplace className={iconClass} />;
case IconType.Library:
return <IconLibrary className={iconClass} />;
case IconType.Builder:
return <IconBuilder className={iconClass} />;
default:
return <IconRefresh className={iconClass} />;
}
};
return (
<Popover>
<PopoverTrigger asChild>
@@ -127,7 +84,7 @@ export function ProfilePopoutMenu({
className="inline-flex w-full items-center justify-start gap-2.5"
>
<div className="relative h-6 w-6">
{getIcon(item.icon)}
{getAccountMenuOptionIcon(item.icon)}
</div>
<div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
{item.text}
@@ -135,7 +92,7 @@ export function ProfilePopoutMenu({
</Link>
);
} else if (item.text === "Log out") {
return <ProfilePopoutMenuLogoutButton key={itemIndex} />;
return <AccountLogoutOption key={itemIndex} />;
} else if (item.text === "Publish an agent") {
return (
<PublishAgentPopout
@@ -143,7 +100,7 @@ export function ProfilePopoutMenu({
trigger={
<div className="inline-flex w-full items-center justify-start gap-2.5">
<div className="relative h-6 w-6">
{getIcon(item.icon)}
{getAccountMenuOptionIcon(item.icon)}
</div>
<div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
{item.text}
@@ -165,7 +122,7 @@ export function ProfilePopoutMenu({
tabIndex={0}
>
<div className="relative h-6 w-6">
{getIcon(item.icon)}
{getAccountMenuOptionIcon(item.icon)}
</div>
<div className="font-sans text-base font-medium leading-normal text-neutral-800 dark:text-neutral-200">
{item.text}

View File

@@ -1,14 +1,14 @@
"use client";
import { IconLogOut } from "@/components/ui/icons";
import { LoadingSpinner } from "@/components/ui/loading";
import { useSupabase } from "@/lib/supabase/hooks/useSupabase";
import { cn } from "@/lib/utils";
import * as Sentry from "@sentry/nextjs";
import { useRouter } from "next/navigation";
import { useTransition } from "react";
import { LoadingSpinner } from "../ui/loading";
import { toast } from "../molecules/Toast/use-toast";
import { toast } from "@/components/molecules/Toast/use-toast";
export function ProfilePopoutMenuLogoutButton() {
export function AccountLogoutOption() {
const router = useRouter();
const [isPending, startTransition] = useTransition();
const supabase = useSupabase();

View File

@@ -0,0 +1,29 @@
"use client";
import { Button } from "@/components/atoms/Button/Button";
import { SignInIcon } from "@phosphor-icons/react/dist/ssr";
import { usePathname, useRouter } from "next/navigation";
export function LoginButton() {
const router = useRouter();
const pathname = usePathname();
const isLoginPage = pathname.includes("/login");
if (isLoginPage) return null;
function handleLogin() {
router.push("/login");
}
return (
<Button
onClick={handleLogin}
size="small"
className="flex items-center justify-end space-x-2"
leftIcon={<SignInIcon className="h-5 w-5" />}
variant="secondary"
>
Log In
</Button>
);
}

View File

@@ -1,45 +1,26 @@
"use client";
import * as React from "react";
import Link from "next/link";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Button } from "@/components/ui/button";
import {
Popover,
PopoverTrigger,
PopoverContent,
PopoverPortal,
PopoverTrigger,
} from "@/components/ui/popover";
import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar";
import { Separator } from "@/components/ui/separator";
import {
IconType,
IconMenu,
IconChevronUp,
IconEdit,
IconLayoutDashboard,
IconUploadCloud,
IconSettings,
IconLogOut,
IconMarketplace,
IconLibrary,
IconBuilder,
} from "../ui/icons";
import { AnimatePresence, motion } from "framer-motion";
import { Button } from "@/components/ui/button";
import { usePathname } from "next/navigation";
import * as React from "react";
import { IconChevronUp, IconMenu } from "../../../../ui/icons";
import { MenuItemGroup } from "../../helpers";
import { MobileNavbarMenuItem } from "./components/MobileNavbarMenuItem";
interface MobileNavBarProps {
userName?: string;
userEmail?: string;
avatarSrc?: string;
menuItemGroups: {
groupName?: string;
items: {
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}[];
}[];
menuItemGroups: MenuItemGroup[];
}
const Overlay = React.forwardRef<HTMLDivElement, { children: React.ReactNode }>(
@@ -49,76 +30,15 @@ const Overlay = React.forwardRef<HTMLDivElement, { children: React.ReactNode }>(
</div>
),
);
Overlay.displayName = "Overlay";
const PopoutMenuItem: React.FC<{
icon: IconType;
isActive: boolean;
text: React.ReactNode;
href?: string;
onClick?: () => void;
}> = ({ icon, isActive, text, href, onClick }) => {
const getIcon = (iconType: IconType) => {
const iconClass = "w-6 h-6 relative";
switch (iconType) {
case IconType.Marketplace:
return <IconMarketplace className={iconClass} />;
case IconType.Library:
return <IconLibrary className={iconClass} />;
case IconType.Builder:
return <IconBuilder className={iconClass} />;
case IconType.Edit:
return <IconEdit className={iconClass} />;
case IconType.LayoutDashboard:
return <IconLayoutDashboard className={iconClass} />;
case IconType.UploadCloud:
return <IconUploadCloud className={iconClass} />;
case IconType.Settings:
return <IconSettings className={iconClass} />;
case IconType.LogOut:
return <IconLogOut className={iconClass} />;
default:
return null;
}
};
const content = (
<div className="inline-flex w-full items-center justify-start gap-4 hover:rounded hover:bg-[#e0e0e0] dark:hover:bg-[#3a3a3a]">
{getIcon(icon)}
<div className="relative">
<div
className={`font-sans text-base font-normal leading-7 text-[#474747] dark:text-[#cfcfcf] ${isActive ? "font-semibold text-[#272727] dark:text-[#ffffff]" : "text-[#474747] dark:text-[#cfcfcf]"}`}
>
{text}
</div>
{isActive && (
<div className="absolute bottom-[-4px] left-0 h-[2px] w-full bg-[#272727] dark:bg-[#ffffff]"></div>
)}
</div>
</div>
);
if (onClick)
return (
<div className="w-full" onClick={onClick}>
{content}
</div>
);
if (href)
return (
<Link href={href} className="w-full">
{content}
</Link>
);
return content;
};
export const MobileNavBar: React.FC<MobileNavBarProps> = ({
export function MobileNavBar({
userName,
userEmail,
avatarSrc,
menuItemGroups,
}) => {
}: MobileNavBarProps) {
const [isOpen, setIsOpen] = React.useState(false);
const pathname = usePathname();
const parts = pathname.split("/");
@@ -173,7 +93,7 @@ export const MobileNavBar: React.FC<MobileNavBarProps> = ({
{menuItemGroups.map((group, groupIndex) => (
<React.Fragment key={groupIndex}>
{group.items.map((item, itemIndex) => (
<PopoutMenuItem
<MobileNavbarMenuItem
key={itemIndex}
icon={item.icon}
isActive={item.href === activeLink}
@@ -194,4 +114,4 @@ export const MobileNavBar: React.FC<MobileNavBarProps> = ({
</AnimatePresence>
</Popover>
);
};
}

View File

@@ -0,0 +1,55 @@
import { IconType } from "@/components/ui/icons";
import { cn } from "@/lib/utils";
import Link from "next/link";
import { getAccountMenuOptionIcon } from "../../../helpers";
interface Props {
icon: IconType;
isActive: boolean;
text: string;
href?: string;
onClick?: () => void;
}
export function MobileNavbarMenuItem({
icon,
isActive,
text,
href,
onClick,
}: Props) {
const content = (
<div className="inline-flex w-full items-center justify-start gap-4 hover:rounded hover:bg-[#e0e0e0] dark:hover:bg-[#3a3a3a]">
{getAccountMenuOptionIcon(icon)}
<div className="relative">
<div
className={cn(
"font-sans text-base font-normal leading-7",
isActive
? "font-semibold text-[#272727] dark:text-[#ffffff]"
: "text-[#474747] dark:text-[#cfcfcf]",
)}
>
{text}
</div>
{isActive && (
<div className="absolute bottom-[-4px] left-0 h-[2px] w-full bg-[#272727] dark:bg-[#ffffff]"></div>
)}
</div>
</div>
);
if (onClick)
return (
<div className="w-full" onClick={onClick}>
{content}
</div>
);
if (href)
return (
<Link href={href} className="w-full">
{content}
</Link>
);
return content;
}

View File

@@ -0,0 +1,68 @@
"use client";
import { IconLaptop } from "@/components/ui/icons";
import { cn } from "@/lib/utils";
import {
CubeIcon,
HouseIcon,
StorefrontIcon,
} from "@phosphor-icons/react/dist/ssr";
import Link from "next/link";
import { usePathname } from "next/navigation";
import { Text } from "../../../atoms/Text/Text";
interface Props {
name: string;
href: string;
}
export function NavbarLink({ name, href }: Props) {
const pathname = usePathname();
const isActive = pathname.includes(href);
return (
<Link
href={href}
data-testid={`navbar-link-${name.toLowerCase()}`}
className="font-poppins text-[20px] leading-[28px]"
>
<div
className={cn(
"flex items-center justify-start gap-1 p-2",
isActive &&
"rounded-small bg-neutral-800 p-2 transition-all duration-300 dark:bg-neutral-200",
)}
>
{href === "/marketplace" && (
<StorefrontIcon
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
/>
)}
{href === "/build" && (
<CubeIcon
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
/>
)}
{href === "/monitor" && (
<IconLaptop
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
/>
)}
{href === "/library" && (
<HouseIcon
className={cn("h-6 w-6", isActive && "text-white dark:text-black")}
/>
)}
<Text
variant="body-medium"
className={cn(
"hidden lg:block",
isActive ? "!text-white" : "!text-black",
)}
>
{name}
</Text>
</div>
</Link>
);
}

View File

@@ -0,0 +1,21 @@
import { IconAutoGPTLogo } from "@/components/ui/icons";
import { Skeleton } from "@/components/ui/skeleton";
export function NavbarLoading() {
return (
<nav className="sticky top-0 z-40 hidden h-16 items-center rounded-bl-2xl rounded-br-2xl border border-white/50 bg-white/5 p-3 backdrop-blur-[26px] md:inline-flex">
<div className="flex flex-1 items-center gap-6">
<Skeleton className="h-4 w-20 bg-white/20" />
<Skeleton className="h-4 w-16 bg-white/20" />
<Skeleton className="h-4 w-12 bg-white/20" />
</div>
<div className="absolute left-1/2 top-1/2 h-10 w-[88.87px] -translate-x-1/2 -translate-y-1/2">
<IconAutoGPTLogo className="h-full w-full" />
</div>
<div className="flex flex-1 items-center justify-end gap-4">
<Skeleton className="h-8 w-8 rounded-full bg-white/20" />
<Skeleton className="h-8 w-8 rounded-full bg-white/20" />
</div>
</nav>
);
}

View File

@@ -0,0 +1,29 @@
import { getV2GetUserProfile } from "@/app/api/__generated__/endpoints/store/store";
import { getServerUser } from "@/lib/supabase/server/getServerUser";
export async function getNavbarAccountData() {
const { user } = await getServerUser();
const isLoggedIn = Boolean(user);
if (!isLoggedIn) {
return {
profile: null,
isLoggedIn,
};
}
let profile = null;
try {
const profileResponse = await getV2GetUserProfile();
profile = profileResponse.data || null;
} catch (error) {
console.error("Error fetching profile:", error);
profile = null;
}
return {
profile,
isLoggedIn,
};
}

View File

@@ -0,0 +1,115 @@
import {
IconBuilder,
IconEdit,
IconLayoutDashboard,
IconLibrary,
IconLogOut,
IconMarketplace,
IconRefresh,
IconSettings,
IconType,
IconUploadCloud,
} from "@/components/ui/icons";
type Link = {
name: string;
href: string;
};
export const loggedInLinks: Link[] = [
{
name: "Marketplace",
href: "/marketplace",
},
{
name: "Library",
href: "/library",
},
{
name: "Build",
href: "/build",
},
];
export const loggedOutLinks: Link[] = [
{
name: "Marketplace",
href: "/marketplace",
},
];
export type MenuItemGroup = {
groupName?: string;
items: {
icon: IconType;
text: string;
href?: string;
onClick?: () => void;
}[];
};
export const accountMenuItems: MenuItemGroup[] = [
{
items: [
{
icon: IconType.Edit,
text: "Edit profile",
href: "/profile",
},
],
},
{
items: [
{
icon: IconType.LayoutDashboard,
text: "Creator Dashboard",
href: "/profile/dashboard",
},
{
icon: IconType.UploadCloud,
text: "Publish an agent",
},
],
},
{
items: [
{
icon: IconType.Settings,
text: "Settings",
href: "/profile/settings",
},
],
},
{
items: [
{
icon: IconType.LogOut,
text: "Log out",
},
],
},
];
export function getAccountMenuOptionIcon(icon: IconType) {
const iconClass = "w-6 h-6";
switch (icon) {
case IconType.LayoutDashboard:
return <IconLayoutDashboard className={iconClass} />;
case IconType.UploadCloud:
return <IconUploadCloud className={iconClass} />;
case IconType.Edit:
return <IconEdit className={iconClass} />;
case IconType.Settings:
return <IconSettings className={iconClass} />;
case IconType.LogOut:
return <IconLogOut className={iconClass} />;
case IconType.Marketplace:
return <IconMarketplace className={iconClass} />;
case IconType.Library:
return <IconLibrary className={iconClass} />;
case IconType.Builder:
return <IconBuilder className={iconClass} />;
default:
return <IconRefresh className={iconClass} />;
}
}

View File

@@ -71,6 +71,30 @@ export const colors = {
800: "#0c5a29",
900: "#09441f",
},
purple: {
50: "#f1ebfe",
100: "#d5c0fc",
200: "#c0a1fa",
300: "#a476f8",
400: "#925cf7",
500: "#7733f5",
600: "#6c2edf",
700: "#5424ae",
800: "#411c87",
900: "#321567",
},
pink: {
50: "#fdedf5",
100: "#f9c6df",
200: "#f6abd0",
300: "#f284bb",
400: "#f06dad",
500: "#ec4899",
600: "#d7428b",
700: "#a8336d",
800: "#822854",
900: "#631e40",
},
// Special semantic colors
white: "#fefefe",

View File

@@ -14,54 +14,54 @@ const meta: Meta = {
export default meta;
// Border radius scale data based on Figma design tokens
// Custom naming convention: xs, s, m, l, xl, 2xl, full
// Custom naming convention: xsmall, small, medium, large, xlarge, 2xlarge, full
const borderRadiusScale = [
{
name: "xs",
name: "xsmall",
value: "0.25rem",
rem: "0.25rem",
px: "4px",
class: "rounded-xs",
class: "rounded-xsmall",
description: "Extra small - for subtle rounding",
},
{
name: "s",
name: "small",
value: "0.5rem",
rem: "0.5rem",
px: "8px",
class: "rounded-s",
class: "rounded-small",
description: "Small - for cards and containers",
},
{
name: "m",
name: "medium",
value: "0.75rem",
rem: "0.75rem",
px: "12px",
class: "rounded-m",
class: "rounded-medium",
description: "Medium - for buttons and inputs",
},
{
name: "l",
name: "large",
value: "1rem",
rem: "1rem",
px: "16px",
class: "rounded-l",
class: "rounded-large",
description: "Large - for panels and modals",
},
{
name: "xl",
name: "xlarge",
value: "1.25rem",
rem: "1.25rem",
px: "20px",
class: "rounded-xl",
class: "rounded-xlarge",
description: "Extra large - for hero sections",
},
{
name: "2xl",
name: "2xlarge",
value: "1.5rem",
rem: "1.5rem",
px: "24px",
class: "rounded-2xl",
class: "rounded-2xlarge",
description: "2X large - for major containers",
},
{
@@ -84,10 +84,11 @@ export function AllVariants() {
Border Radius
</Text>
<Text variant="large" className="text-zinc-600">
Our border radius system uses a simplified naming convention (xs, s,
m, l, xl, 2xl, full) based on our Figma design tokens. This creates
visual hierarchy and maintains design consistency across all
components.
Our border radius system uses a descriptive naming convention
(xsmall, small, medium, large, xlarge, 2xlarge, full) based on our
Figma design tokens. This creates visual hierarchy and maintains
design consistency across all components while avoiding conflicts
with Tailwind&apos;s built-in classes.
</Text>
</div>
@@ -137,9 +138,10 @@ export function AllVariants() {
</div>
<Text variant="body" className="mb-4 text-zinc-600">
We use a custom border radius system based on our Figma design
tokens, with simplified naming (xs, s, m, l, xl, 2xl, full) that
provides consistent radius values optimized for our design
system.
tokens, with descriptive naming (xsmall, small, medium, large,
xlarge, 2xlarge, full) that provides consistent radius values
optimized for our design system while avoiding conflicts with
Tailwind&apos;s built-in classes.
</Text>
</div>
</div>
@@ -188,7 +190,8 @@ export function AllVariants() {
<Text variant="body" className="mb-6 text-zinc-600">
All border radius values from our Figma design tokens. Each value
can be applied to all corners or specific corners/sides using our
simplified naming convention.
descriptive naming convention (xsmall, small, medium, large, xlarge,
2xlarge, full).
</Text>
</div>
@@ -232,30 +235,30 @@ export function AllVariants() {
<StoryCode
code={`// Border radius examples - Design System Tokens
<div className="rounded-xs">Extra small rounding (4px)</div>
<div className="rounded-s">Small rounding (8px)</div>
<div className="rounded-m">Medium rounding (12px)</div>
<div className="rounded-l">Large rounding (16px)</div>
<div className="rounded-xl">Extra large rounding (20px)</div>
<div className="rounded-2xl">2X large rounding (24px)</div>
<div className="rounded-xsmall">Extra small rounding (4px)</div>
<div className="rounded-small">Small rounding (8px)</div>
<div className="rounded-medium">Medium rounding (12px)</div>
<div className="rounded-large">Large rounding (16px)</div>
<div className="rounded-xlarge">Extra large rounding (20px)</div>
<div className="rounded-2xlarge">2X large rounding (24px)</div>
<div className="rounded-full">Pill buttons (circular)</div>
// Directional rounding (works with all sizes)
<div className="rounded-t-m">Top corners only</div>
<div className="rounded-r-m">Right corners only</div>
<div className="rounded-b-m">Bottom corners only</div>
<div className="rounded-l-m">Left corners only</div>
<div className="rounded-t-medium">Top corners only</div>
<div className="rounded-r-medium">Right corners only</div>
<div className="rounded-b-medium">Bottom corners only</div>
<div className="rounded-l-medium">Left corners only</div>
// Individual corners
<div className="rounded-tl-m">Top-left corner</div>
<div className="rounded-tr-m">Top-right corner</div>
<div className="rounded-bl-m">Bottom-left corner</div>
<div className="rounded-br-m">Bottom-right corner</div>
<div className="rounded-tl-medium">Top-left corner</div>
<div className="rounded-tr-medium">Top-right corner</div>
<div className="rounded-bl-medium">Bottom-left corner</div>
<div className="rounded-br-medium">Bottom-right corner</div>
// Usage recommendations
<button className="rounded-full">Pill Button</button>
<div className="rounded-m">Card Container</div>
<input className="rounded-s">Input Field</input>`}
<div className="rounded-medium">Card Container</div>
<input className="rounded-small">Input Field</input>`}
/>
</div>
</div>

View File

@@ -36,6 +36,8 @@ const colorCategories = Object.entries(colors)
orange: "Warnings, notifications, and secondary call-to-actions",
yellow: "Highlights, cautions, and attention-grabbing elements",
green: "Success states, confirmations, and positive actions",
purple: "Brand accents, premium features, and creative elements",
pink: "Highlights, special promotions, and playful interactions",
};
return {
@@ -312,6 +314,8 @@ export function AllVariants() {
<div className="bg-green-50 border-green-200 text-green-800">Success</div>
<div className="bg-red-50 border-red-200 text-red-800">Error</div>
<div className="bg-yellow-50 border-yellow-200 text-yellow-800">Warning</div>
<div className="bg-purple-50 border-purple-200 text-purple-800">Premium</div>
<div className="bg-pink-50 border-pink-200 text-pink-800">Special</div>
// ❌ INCORRECT - Don't use these
<div className="bg-blue-500 text-purple-600">❌ Not approved</div>

View File

@@ -11,12 +11,12 @@ const ScrollArea = React.forwardRef<
>(({ className, children, ...props }, ref) => (
<ScrollAreaPrimitive.Root
ref={ref}
className={cn("relative overflow-hidden", className)}
className={cn("relative", className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
className="h-full w-full rounded-[inherit]"
style={{ overflow: "scroll" }}
style={{ overflowX: "hidden", overflowY: "scroll" }}
>
{children}
</ScrollAreaPrimitive.Viewport>

View File

@@ -166,6 +166,8 @@ export async function serverLogout(options: ServerLogoutOptions = {}) {
scope: options.globalLogout ? "global" : "local",
});
revalidatePath("/");
if (error) {
console.error("Error logging out:", error);
}

View File

@@ -24,6 +24,7 @@ export function useSupabase() {
const [isUserLoading, setIsUserLoading] = useState(true);
const lastValidationRef = useRef<number>(0);
const isValidatingRef = useRef(false);
const isLoggedIn = Boolean(user);
const supabase = useMemo(() => {
try {
@@ -50,7 +51,9 @@ export function useSupabase() {
await serverLogout(options);
} catch (error) {
console.error("Error logging out:", error);
router.push("/login");
} finally {
setUser(null);
router.refresh();
}
}
@@ -162,9 +165,9 @@ export function useSupabase() {
}, []);
return {
supabase, // Available for non-auth operations like real-time subscriptions
user,
isLoggedIn: !isUserLoading ? !!user : null,
supabase, // Available for non-auth operations like real-time subscriptions
isLoggedIn,
isUserLoading,
logOut,
validateSession: validateSessionServer,