From 44325f9cab85621846d6fa3bd10d4230632b4323 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Fri, 5 Dec 2025 18:31:19 +0100 Subject: [PATCH 01/68] Refactor auth confirmation route and enhance URL handling - Removed unused imports and simplified the confirmation logic in `route.ts`. - Introduced functions to determine if a URL is relative or from an external origin. - Updated the confirmation schema to accept a string for the `next` parameter. - Enhanced logging for the confirmation process and added a redirect to the confirmation page for same-origin requests. - Added `CONFIRM` URL to the `AUTH_URLS` configuration. - Refactored the builds repository to export functions directly. This improves the clarity and maintainability of the authentication flow. --- src/app/(auth)/confirm/page.tsx | 113 +++++++++++ src/app/api/auth/confirm/route.ts | 177 ++++++------------ src/configs/urls.ts | 1 + src/server/api/models/auth.models.ts | 28 +++ .../api/repositories/auth.repository.ts | 80 ++++++++ .../api/repositories/builds.repository.ts | 11 +- src/server/api/routers/auth.ts | 42 +++++ src/server/api/routers/builds.ts | 2 +- src/server/api/routers/index.ts | 2 + 9 files changed, 332 insertions(+), 124 deletions(-) create mode 100644 src/app/(auth)/confirm/page.tsx create mode 100644 src/server/api/models/auth.models.ts create mode 100644 src/server/api/repositories/auth.repository.ts create mode 100644 src/server/api/routers/auth.ts diff --git a/src/app/(auth)/confirm/page.tsx b/src/app/(auth)/confirm/page.tsx new file mode 100644 index 000000000..add699f80 --- /dev/null +++ b/src/app/(auth)/confirm/page.tsx @@ -0,0 +1,113 @@ +'use client' + +import { AUTH_URLS } from '@/configs/urls' +import { AuthFormMessage, AuthMessage } from '@/features/auth/form-message' +import { type OtpType } from '@/server/api/models/auth.models' +import { useTRPC } from '@/trpc/client' +import { Button } from '@/ui/primitives/button' +import { useMutation } from '@tanstack/react-query' +import { useRouter, useSearchParams } from 'next/navigation' +import { useMemo, useState } from 'react' + +const OTP_TYPE_LABELS: Record = { + signup: 'Sign Up', + recovery: 'Password Reset', + invite: 'Team Invitation', + magiclink: 'Sign In', + email: 'Email Verification', + email_change: 'Email Change', +} + +const OTP_TYPE_DESCRIPTIONS: Record = { + signup: 'Complete your account registration', + recovery: 'Reset your password', + invite: 'Accept team invitation', + magiclink: 'Sign in to your account', + email: 'Verify your email address', + email_change: 'Confirm your new email address', +} + +export default function ConfirmPage() { + const router = useRouter() + const searchParams = useSearchParams() + const trpc = useTRPC() + + const [message, setMessage] = useState() + + const params = useMemo(() => { + const tokenHash = searchParams.get('token_hash') ?? '' + const type = searchParams.get('type') as OtpType | null + const next = searchParams.get('next') ?? '' + + return { tokenHash, type, next } + }, [searchParams]) + + const isValidParams = params.tokenHash && params.type && params.next + const typeLabel = params.type ? OTP_TYPE_LABELS[params.type] : 'Verification' + const typeDescription = params.type + ? OTP_TYPE_DESCRIPTIONS[params.type] + : 'Confirm your action' + + const confirmMutation = useMutation( + trpc.auth.confirmEmail.mutationOptions({ + onSuccess: (data) => { + router.push(data.redirectUrl) + }, + onError: (error) => { + setMessage({ error: error.message }) + }, + }) + ) + + const handleConfirm = () => { + if (!isValidParams || !params.type) return + + setMessage(undefined) + confirmMutation.mutate({ + token_hash: params.tokenHash, + type: params.type, + next: params.next, + }) + } + + return ( +
+

{typeLabel}

+

{typeDescription}

+ +
+ +
+ +

+ Changed your mind?{' '} + + . +

+ + {!isValidParams && !message && ( + + )} + + {message && } +
+ ) +} diff --git a/src/app/api/auth/confirm/route.ts b/src/app/api/auth/confirm/route.ts index 4b87c925f..b60a70ed3 100644 --- a/src/app/api/auth/confirm/route.ts +++ b/src/app/api/auth/confirm/route.ts @@ -1,12 +1,11 @@ -import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' +import { AUTH_URLS } from '@/configs/urls' import { l } from '@/lib/clients/logger/logger' -import { createClient } from '@/lib/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' import { redirect } from 'next/navigation' -import { NextRequest, NextResponse } from 'next/server' -import { serializeError } from 'serialize-error' +import { NextRequest } from 'next/server' import { z } from 'zod' +// accepts both absolute URLs (external apps) and relative URLs (same-origin) const confirmSchema = z.object({ token_hash: z.string().min(1), type: z.enum([ @@ -17,12 +16,40 @@ const confirmSchema = z.object({ 'email', 'email_change', ]), - next: z.url(), + next: z.string().min(1), }) const normalizeOrigin = (origin: string) => origin.replace('www.', '').replace(/\/$/, '') +function isRelativeUrl(url: string): boolean { + return url.startsWith('/') && !url.startsWith('//') +} + +function isExternalOrigin(next: string, dashboardOrigin: string): boolean { + // relative URLs are always same-origin + if (isRelativeUrl(next)) { + return false + } + + try { + return ( + normalizeOrigin(new URL(next).origin) !== normalizeOrigin(dashboardOrigin) + ) + } catch { + // invalid URL format - treat as same-origin to be safe + return false + } +} + +/** + * This route acts as an intermediary for email OTP verification. + * + * Email providers may prefetch/scan links, consuming OTP tokens before users click. + * To prevent this: + * - Same-origin requests: Redirect to /confirm page where user must click to verify + * - External origin requests: Redirect to Supabase client flow URL (for external apps) + */ export async function GET(request: NextRequest) { const { searchParams } = new URL(request.url) @@ -51,133 +78,43 @@ export async function GET(request: NextRequest) { ) } - const supabaseTokenHash = result.data.token_hash - const supabaseType = result.data.type - const supabaseRedirectTo = result.data.next - const supabaseClientFlowUrl = new URL( - `${process.env.NEXT_PUBLIC_SUPABASE_URL}/auth/v1/verify?token=${supabaseTokenHash}&type=${supabaseType}&redirect_to=${supabaseRedirectTo}` - ) - - const dashboardUrl = request.nextUrl - - const isDifferentOrigin = - supabaseRedirectTo && - normalizeOrigin(new URL(supabaseRedirectTo).origin) !== - normalizeOrigin(dashboardUrl.origin) + const { token_hash, type, next } = result.data + const dashboardOrigin = request.nextUrl.origin + const isDifferentOrigin = isExternalOrigin(next, dashboardOrigin) l.info({ key: 'auth_confirm:init', context: { - supabase_token_hash: supabaseTokenHash - ? `${supabaseTokenHash.slice(0, 10)}...` - : null, - supabaseType, - supabaseRedirectTo, + tokenHashPrefix: token_hash.slice(0, 10), + type, + next, isDifferentOrigin, - supabaseClientFlowUrl, - requestUrl: request.url, - origin: request.nextUrl.origin, + origin: dashboardOrigin, }, }) - // when the next param is an absolute URL, with a different origin, - // we need to redirect to the supabase client flow url + // external origin: redirect to supabase client flow for the external app to handle if (isDifferentOrigin) { + const supabaseClientFlowUrl = new URL( + `${process.env.NEXT_PUBLIC_SUPABASE_URL}/auth/v1/verify?token=${token_hash}&type=${type}&redirect_to=${next}` + ) throw redirect(supabaseClientFlowUrl.toString()) } - try { - const next = - supabaseType === 'recovery' - ? `${request.nextUrl.origin}${PROTECTED_URLS.RESET_PASSWORD}` - : supabaseRedirectTo - - const redirectUrl = new URL(next) - - const supabase = await createClient() + // same origin: redirect to /confirm page for user to explicitly confirm + const confirmPageUrl = new URL(dashboardOrigin + AUTH_URLS.CONFIRM) + confirmPageUrl.searchParams.set('token_hash', token_hash) + confirmPageUrl.searchParams.set('type', type) + confirmPageUrl.searchParams.set('next', next) - const { error, data } = await supabase.auth.verifyOtp({ - type: supabaseType, - token_hash: supabaseTokenHash, - }) - - if (error) { - l.error({ - key: 'auth_confirm:supabase_error', - message: error.message, - error: serializeError(error), - context: { - supabase_token_hash: supabaseTokenHash - ? `${supabaseTokenHash.slice(0, 10)}...` - : null, - supabaseType, - supabaseRedirectTo, - redirectUrl: redirectUrl.toString(), - }, - }) - - let errorMessage = 'Invalid Token' - if (error.status === 403 && error.code === 'otp_expired') { - errorMessage = 'Email link has expired. Please request a new one.' - } - - return encodedRedirect( - 'error', - dashboardSignInUrl.toString(), - errorMessage - ) - } - - // handle re-auth - if (redirectUrl.pathname === PROTECTED_URLS.ACCOUNT_SETTINGS) { - redirectUrl.searchParams.set('reauth', '1') - - return NextResponse.redirect(redirectUrl.toString()) - } - - l.info({ - key: 'auth_confirm:success', - user_id: data?.user?.id, - context: { - supabaseTokenHash: `${supabaseTokenHash.slice(0, 10)}...`, - supabaseType, - supabaseRedirectTo, - redirectUrl: redirectUrl.toString(), - reauth: redirectUrl.searchParams.get('reauth'), - }, - }) - - return NextResponse.redirect(redirectUrl.toString()) - } catch (e) { - const sE = serializeError(e) as object - - // nextjs internally throws redirects (encodedRedirect with error message in this case) - // and captures them to do the actual redirect. - - // we need to throw the error to let nextjs handle it - if ( - 'message' in sE && - typeof sE.message === 'string' && - sE.message.includes('NEXT_REDIRECT') - ) { - throw e - } - - l.error({ - key: 'AUTH_CONFIRM:ERROR', - message: 'message' in sE ? sE.message : undefined, - error: sE, - context: { - supabaseTokenHash: `${supabaseTokenHash.slice(0, 10)}...`, - supabaseType, - supabaseRedirectTo, - }, - }) + l.info({ + key: 'auth_confirm:redirect_to_confirm_page', + context: { + tokenHashPrefix: token_hash.slice(0, 10), + type, + confirmPageUrl: confirmPageUrl.toString(), + }, + }) - return encodedRedirect( - 'error', - dashboardSignInUrl.toString(), - 'Invalid Token' - ) - } + throw redirect(confirmPageUrl.toString()) } diff --git a/src/configs/urls.ts b/src/configs/urls.ts index ed4af2c4a..a5d6352e9 100644 --- a/src/configs/urls.ts +++ b/src/configs/urls.ts @@ -2,6 +2,7 @@ export const AUTH_URLS = { FORGOT_PASSWORD: '/forgot-password', SIGN_IN: '/sign-in', SIGN_UP: '/sign-up', + CONFIRM: '/confirm', CALLBACK: '/api/auth/callback', CLI: '/auth/cli', } diff --git a/src/server/api/models/auth.models.ts b/src/server/api/models/auth.models.ts new file mode 100644 index 000000000..d5b1d1e80 --- /dev/null +++ b/src/server/api/models/auth.models.ts @@ -0,0 +1,28 @@ +import { relativeUrlSchema } from '@/lib/schemas/url' +import z from 'zod' + +// otp types supported by supabase +export const OtpTypeSchema = z.enum([ + 'signup', + 'recovery', + 'invite', + 'magiclink', + 'email', + 'email_change', +]) + +export type OtpType = z.infer + +// shared schema for client form and tRPC router +export const ConfirmEmailInputSchema = z.object({ + token_hash: z.string().min(1), + type: OtpTypeSchema, + next: relativeUrlSchema, +}) + +export type ConfirmEmailInput = z.infer + +// response types +export interface ConfirmEmailResult { + redirectUrl: string +} diff --git a/src/server/api/repositories/auth.repository.ts b/src/server/api/repositories/auth.repository.ts new file mode 100644 index 000000000..01a2f626f --- /dev/null +++ b/src/server/api/repositories/auth.repository.ts @@ -0,0 +1,80 @@ +import { l } from '@/lib/clients/logger/logger' +import { createClient } from '@/lib/clients/supabase/server' +import { TRPCError } from '@trpc/server' +import { serializeError } from 'serialize-error' +import type { OtpType } from '../models/auth.models' + +interface VerifyOtpResult { + userId: string +} + +/** + * Verifies an OTP token with Supabase Auth + * @throws TRPCError on verification failure + */ +async function verifyOtp( + tokenHash: string, + type: OtpType +): Promise { + const supabase = await createClient() + + const { data, error } = await supabase.auth.verifyOtp({ + type, + token_hash: tokenHash, + }) + + if (error) { + l.error( + { + key: 'auth_repository:verify_otp:error', + error: serializeError(error), + context: { + type, + tokenHashPrefix: tokenHash.slice(0, 10), + errorCode: error.code, + errorStatus: error.status, + }, + }, + `failed to verify OTP: ${error.message}` + ) + + // map supabase errors to user-friendly messages + if (error.status === 403 && error.code === 'otp_expired') { + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Email link has expired. Please request a new one.', + }) + } + + throw new TRPCError({ + code: 'BAD_REQUEST', + message: 'Invalid or expired verification link.', + }) + } + + if (!data.user) { + l.error( + { + key: 'auth_repository:verify_otp:no_user', + context: { + type, + tokenHashPrefix: tokenHash.slice(0, 10), + }, + }, + `failed to verify OTP: no user found for token hash: ${tokenHash.slice(0, 10)}` + ) + + throw new TRPCError({ + code: 'INTERNAL_SERVER_ERROR', + message: 'Verification failed. Please try again.', + }) + } + + return { + userId: data.user.id, + } +} + +export const authRepo = { + verifyOtp, +} diff --git a/src/server/api/repositories/builds.repository.ts b/src/server/api/repositories/builds.repository.ts index cb0cb717f..fe1d7697a 100644 --- a/src/server/api/repositories/builds.repository.ts +++ b/src/server/api/repositories/builds.repository.ts @@ -15,7 +15,7 @@ function isUUID(value: string): boolean { return z.uuid().safeParse(value).success } -export async function resolveTemplateId( +async function resolveTemplateId( templateIdOrAlias: string, teamId: string ): Promise { @@ -50,7 +50,7 @@ interface ListBuildsResult { nextCursor: string | null } -export async function listBuilds( +async function listBuilds( teamId: string, buildIdOrTemplate?: string, statuses: BuildStatusDB[] = ['waiting', 'building', 'uploaded', 'failed'], @@ -131,7 +131,7 @@ export async function listBuilds( // get running build statuses -export async function getRunningStatuses( +async function getRunningStatuses( teamId: string, buildIds: string[] ): Promise { @@ -161,3 +161,8 @@ export async function getRunningStatuses( ), })) } + +export const buildsRepo = { + listBuilds, + getRunningStatuses, +} diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts new file mode 100644 index 000000000..76de048a8 --- /dev/null +++ b/src/server/api/routers/auth.ts @@ -0,0 +1,42 @@ +import { PROTECTED_URLS } from '@/configs/urls' +import { createTRPCRouter } from '../init' +import { + ConfirmEmailInputSchema, + type ConfirmEmailResult, + type OtpType, +} from '../models/auth.models' +import { publicProcedure } from '../procedures' +import { authRepo } from '../repositories/auth.repository' + +/** + * Determines the redirect URL based on OTP type and the original next parameter. + * Handles relative URLs (e.g., /dashboard) + */ +function buildRedirectUrl(type: OtpType, next: string): string { + // recovery flow always goes to account settings with reauth flag + if (type === 'recovery') { + return `${PROTECTED_URLS.RESET_PASSWORD}?reauth=1` + } + + // reauth flow for account settings + if (next.startsWith(PROTECTED_URLS.ACCOUNT_SETTINGS)) { + const hasQuery = next.includes('?') + return hasQuery ? `${next}&reauth=1` : `${next}?reauth=1` + } + + return next +} + +export const authRouter = createTRPCRouter({ + confirmEmail: publicProcedure + .input(ConfirmEmailInputSchema) + .mutation(async ({ input }): Promise => { + const { token_hash, type, next } = input + + await authRepo.verifyOtp(token_hash, type) + + const redirectUrl = buildRedirectUrl(type, next) + + return { redirectUrl } + }), +}) diff --git a/src/server/api/routers/builds.ts b/src/server/api/routers/builds.ts index 81831906c..5156daaad 100644 --- a/src/server/api/routers/builds.ts +++ b/src/server/api/routers/builds.ts @@ -1,7 +1,7 @@ import { SUPABASE_AUTH_HEADERS } from '@/configs/api' import { infra } from '@/lib/clients/api' import { l } from '@/lib/clients/logger/logger' -import * as buildsRepo from '@/server/api/repositories/builds.repository' +import { buildsRepo } from '@/server/api/repositories/builds.repository' import { TRPCError } from '@trpc/server' import { z } from 'zod' import { apiError } from '../errors' diff --git a/src/server/api/routers/index.ts b/src/server/api/routers/index.ts index 85602dca8..02349648c 100644 --- a/src/server/api/routers/index.ts +++ b/src/server/api/routers/index.ts @@ -1,9 +1,11 @@ import { createCallerFactory, createTRPCRouter } from '../init' +import { authRouter } from './auth' import { buildsRouter } from './builds' import { sandboxesRouter } from './sandboxes' import { templatesRouter } from './templates' export const trpcAppRouter = createTRPCRouter({ + auth: authRouter, sandboxes: sandboxesRouter, templates: templatesRouter, builds: buildsRouter, From e739b5a269df8dda24d4ddedabb6a533b1ab5c8c Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Fri, 5 Dec 2025 18:55:32 +0100 Subject: [PATCH 02/68] only allow absolute urls in schema --- src/app/api/auth/confirm/route.ts | 23 ++++------------------- src/server/api/models/auth.models.ts | 3 +-- src/server/api/routers/auth.ts | 13 ++++++++----- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/src/app/api/auth/confirm/route.ts b/src/app/api/auth/confirm/route.ts index b60a70ed3..22c112e53 100644 --- a/src/app/api/auth/confirm/route.ts +++ b/src/app/api/auth/confirm/route.ts @@ -5,7 +5,6 @@ import { redirect } from 'next/navigation' import { NextRequest } from 'next/server' import { z } from 'zod' -// accepts both absolute URLs (external apps) and relative URLs (same-origin) const confirmSchema = z.object({ token_hash: z.string().min(1), type: z.enum([ @@ -16,30 +15,16 @@ const confirmSchema = z.object({ 'email', 'email_change', ]), - next: z.string().min(1), + next: z.httpUrl(), }) const normalizeOrigin = (origin: string) => origin.replace('www.', '').replace(/\/$/, '') -function isRelativeUrl(url: string): boolean { - return url.startsWith('/') && !url.startsWith('//') -} - function isExternalOrigin(next: string, dashboardOrigin: string): boolean { - // relative URLs are always same-origin - if (isRelativeUrl(next)) { - return false - } - - try { - return ( - normalizeOrigin(new URL(next).origin) !== normalizeOrigin(dashboardOrigin) - ) - } catch { - // invalid URL format - treat as same-origin to be safe - return false - } + return ( + normalizeOrigin(new URL(next).origin) !== normalizeOrigin(dashboardOrigin) + ) } /** diff --git a/src/server/api/models/auth.models.ts b/src/server/api/models/auth.models.ts index d5b1d1e80..41bdbf57e 100644 --- a/src/server/api/models/auth.models.ts +++ b/src/server/api/models/auth.models.ts @@ -1,4 +1,3 @@ -import { relativeUrlSchema } from '@/lib/schemas/url' import z from 'zod' // otp types supported by supabase @@ -17,7 +16,7 @@ export type OtpType = z.infer export const ConfirmEmailInputSchema = z.object({ token_hash: z.string().min(1), type: OtpTypeSchema, - next: relativeUrlSchema, + next: z.httpUrl(), }) export type ConfirmEmailInput = z.infer diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts index 76de048a8..7d6f3dee6 100644 --- a/src/server/api/routers/auth.ts +++ b/src/server/api/routers/auth.ts @@ -10,18 +10,21 @@ import { authRepo } from '../repositories/auth.repository' /** * Determines the redirect URL based on OTP type and the original next parameter. - * Handles relative URLs (e.g., /dashboard) */ function buildRedirectUrl(type: OtpType, next: string): string { + const redirectUrl = new URL(next) + // recovery flow always goes to account settings with reauth flag if (type === 'recovery') { - return `${PROTECTED_URLS.RESET_PASSWORD}?reauth=1` + redirectUrl.pathname = PROTECTED_URLS.RESET_PASSWORD + redirectUrl.searchParams.set('reauth', '1') + return redirectUrl.toString() } // reauth flow for account settings - if (next.startsWith(PROTECTED_URLS.ACCOUNT_SETTINGS)) { - const hasQuery = next.includes('?') - return hasQuery ? `${next}&reauth=1` : `${next}?reauth=1` + if (redirectUrl.pathname === PROTECTED_URLS.ACCOUNT_SETTINGS) { + redirectUrl.searchParams.set('reauth', '1') + return redirectUrl.toString() } return next From 80c88f4bd1758d3a07bb7f8b81fd24dc5a211dad Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Fri, 5 Dec 2025 22:38:33 +0100 Subject: [PATCH 03/68] Refactor OTP verification process and enhance confirmation page - Updated the confirmation page to utilize a new `verifyOtp` function for handling OTP verification. - Replaced TRPC mutation with a direct API call for improved clarity and control over the verification process. - Enhanced error handling and user feedback on the confirmation page. - Removed the deprecated `auth` router and adjusted related imports accordingly. - Improved logging for the OTP verification process in the repository. These changes streamline the authentication flow and improve maintainability. --- src/app/(auth)/confirm/page.tsx | 77 ++++++++---- src/app/api/auth/confirm/route.ts | 52 +++++--- src/app/api/auth/verify-otp/route.ts | 117 ++++++++++++++++++ .../api/repositories/auth.repository.ts | 36 +++++- src/server/api/routers/auth.ts | 45 ------- src/server/api/routers/index.ts | 2 - 6 files changed, 240 insertions(+), 89 deletions(-) create mode 100644 src/app/api/auth/verify-otp/route.ts delete mode 100644 src/server/api/routers/auth.ts diff --git a/src/app/(auth)/confirm/page.tsx b/src/app/(auth)/confirm/page.tsx index add699f80..12d03d79e 100644 --- a/src/app/(auth)/confirm/page.tsx +++ b/src/app/(auth)/confirm/page.tsx @@ -1,13 +1,16 @@ 'use client' import { AUTH_URLS } from '@/configs/urls' -import { AuthFormMessage, AuthMessage } from '@/features/auth/form-message' -import { type OtpType } from '@/server/api/models/auth.models' -import { useTRPC } from '@/trpc/client' +import { AuthFormMessage } from '@/features/auth/form-message' +import { + ConfirmEmailInputSchema, + type ConfirmEmailInput, + type OtpType, +} from '@/server/api/models/auth.models' import { Button } from '@/ui/primitives/button' import { useMutation } from '@tanstack/react-query' import { useRouter, useSearchParams } from 'next/navigation' -import { useMemo, useState } from 'react' +import { useMemo, useTransition } from 'react' const OTP_TYPE_LABELS: Record = { signup: 'Sign Up', @@ -27,12 +30,31 @@ const OTP_TYPE_DESCRIPTIONS: Record = { email_change: 'Confirm your new email address', } +interface VerifyOtpResponse { + redirectUrl?: string + error?: string +} + +async function verifyOtp(input: ConfirmEmailInput): Promise { + const response = await fetch('/api/auth/verify-otp', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(input), + }) + + const data: VerifyOtpResponse = await response.json() + + if (!response.ok || data.error) { + throw new Error(data.error || 'Verification failed. Please try again.') + } + + return data +} + export default function ConfirmPage() { const router = useRouter() const searchParams = useSearchParams() - const trpc = useTRPC() - - const [message, setMessage] = useState() + const [isPending, startTransition] = useTransition() const params = useMemo(() => { const tokenHash = searchParams.get('token_hash') ?? '' @@ -42,28 +64,32 @@ export default function ConfirmPage() { return { tokenHash, type, next } }, [searchParams]) - const isValidParams = params.tokenHash && params.type && params.next + const isValidParams = ConfirmEmailInputSchema.safeParse({ + token_hash: params.tokenHash, + type: params.type, + next: params.next, + }).success + const typeLabel = params.type ? OTP_TYPE_LABELS[params.type] : 'Verification' const typeDescription = params.type ? OTP_TYPE_DESCRIPTIONS[params.type] : 'Confirm your action' - const confirmMutation = useMutation( - trpc.auth.confirmEmail.mutationOptions({ - onSuccess: (data) => { - router.push(data.redirectUrl) - }, - onError: (error) => { - setMessage({ error: error.message }) - }, - }) - ) + const mutation = useMutation({ + mutationFn: verifyOtp, + onSuccess: (data) => { + if (data.redirectUrl) { + startTransition(() => { + router.push(data.redirectUrl!) + }) + } + }, + }) const handleConfirm = () => { if (!isValidParams || !params.type) return - setMessage(undefined) - confirmMutation.mutate({ + mutation.mutate({ token_hash: params.tokenHash, type: params.type, next: params.next, @@ -78,7 +104,7 @@ export default function ConfirmPage() {
) } diff --git a/src/app/api/auth/confirm/route.ts b/src/app/api/auth/confirm/route.ts index 22c112e53..e52b5d148 100644 --- a/src/app/api/auth/confirm/route.ts +++ b/src/app/api/auth/confirm/route.ts @@ -67,22 +67,37 @@ export async function GET(request: NextRequest) { const dashboardOrigin = request.nextUrl.origin const isDifferentOrigin = isExternalOrigin(next, dashboardOrigin) - l.info({ - key: 'auth_confirm:init', - context: { - tokenHashPrefix: token_hash.slice(0, 10), - type, - next, - isDifferentOrigin, - origin: dashboardOrigin, + l.info( + { + key: 'auth_confirm:init', + context: { + tokenHashPrefix: token_hash.slice(0, 10), + type, + next, + isDifferentOrigin, + origin: dashboardOrigin, + }, }, - }) + `confirming email with OTP token hash: ${token_hash.slice(0, 10)}` + ) // external origin: redirect to supabase client flow for the external app to handle if (isDifferentOrigin) { const supabaseClientFlowUrl = new URL( - `${process.env.NEXT_PUBLIC_SUPABASE_URL}/auth/v1/verify?token=${token_hash}&type=${type}&redirect_to=${next}` + `${process.env.NEXT_PUBLIC_SUPABASE_URL}/auth/v1/verify?token=${token_hash}&type=${type}&redirect_to=${encodeURIComponent(next)}` + ) + + l.info( + { + key: 'auth_confirm:redirect_to_supabase_client_flow', + context: { + supabaseClientFlowUrl: supabaseClientFlowUrl.toString(), + next, + }, + }, + `redirecting to supabase client flow: ${supabaseClientFlowUrl.toString()}` ) + throw redirect(supabaseClientFlowUrl.toString()) } @@ -92,14 +107,17 @@ export async function GET(request: NextRequest) { confirmPageUrl.searchParams.set('type', type) confirmPageUrl.searchParams.set('next', next) - l.info({ - key: 'auth_confirm:redirect_to_confirm_page', - context: { - tokenHashPrefix: token_hash.slice(0, 10), - type, - confirmPageUrl: confirmPageUrl.toString(), + l.info( + { + key: 'auth_confirm:redirect_to_confirm_page', + context: { + tokenHashPrefix: token_hash.slice(0, 10), + type, + confirmPageUrl: confirmPageUrl.toString(), + }, }, - }) + `redirecting to confirm page: ${confirmPageUrl.toString()}` + ) throw redirect(confirmPageUrl.toString()) } diff --git a/src/app/api/auth/verify-otp/route.ts b/src/app/api/auth/verify-otp/route.ts new file mode 100644 index 000000000..d9cce08c2 --- /dev/null +++ b/src/app/api/auth/verify-otp/route.ts @@ -0,0 +1,117 @@ +import { PROTECTED_URLS } from '@/configs/urls' +import { l } from '@/lib/clients/logger/logger' +import { + ConfirmEmailInputSchema, + type OtpType, +} from '@/server/api/models/auth.models' +import { authRepo } from '@/server/api/repositories/auth.repository' +import { NextRequest, NextResponse } from 'next/server' + +/** + * Determines the redirect URL based on OTP type and the original next parameter. + */ +function buildRedirectUrl(type: OtpType, next: string): string { + const redirectUrl = new URL(next) + + // recovery flow always goes to account settings with reauth flag + if (type === 'recovery') { + redirectUrl.pathname = PROTECTED_URLS.RESET_PASSWORD + redirectUrl.searchParams.set('reauth', '1') + return redirectUrl.toString() + } + + // reauth flow for account settings + if (redirectUrl.pathname === PROTECTED_URLS.ACCOUNT_SETTINGS) { + redirectUrl.searchParams.set('reauth', '1') + return redirectUrl.toString() + } + + return next +} + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + + const result = ConfirmEmailInputSchema.safeParse(body) + + if (!result.success) { + l.error( + { + key: 'verify_otp:invalid_input', + error: result.error.flatten(), + }, + 'invalid input for verify OTP' + ) + + return NextResponse.json( + { error: 'Invalid request' }, + { status: 400 } + ) + } + + const { token_hash, type, next } = result.data + + l.info( + { + key: 'verify_otp:init', + context: { + type, + tokenHashPrefix: token_hash.slice(0, 10), + next, + }, + }, + `verifying OTP token: ${token_hash.slice(0, 10)}` + ) + + const { userId } = await authRepo.verifyOtp(token_hash, type) + + const redirectUrl = buildRedirectUrl(type, next) + + l.info( + { + key: 'verify_otp:success', + user_id: userId, + context: { + type, + redirectUrl, + }, + }, + `OTP verified for user: ${userId}, redirecting to: ${redirectUrl}` + ) + + return NextResponse.json({ redirectUrl }) + } catch (error) { + // handle known errors from repository + if (error && typeof error === 'object' && 'message' in error) { + const message = (error as { message: string }).message + + l.error( + { + key: 'verify_otp:error', + error: message, + }, + `verify OTP failed: ${message}` + ) + + return NextResponse.json( + { error: message }, + { status: 400 } + ) + } + + l.error( + { + key: 'verify_otp:unknown_error', + error: String(error), + }, + 'verify OTP failed with unknown error' + ) + + return NextResponse.json( + { error: 'Verification failed. Please try again.' }, + { status: 500 } + ) + } +} + diff --git a/src/server/api/repositories/auth.repository.ts b/src/server/api/repositories/auth.repository.ts index 01a2f626f..b98e75db0 100644 --- a/src/server/api/repositories/auth.repository.ts +++ b/src/server/api/repositories/auth.repository.ts @@ -9,7 +9,8 @@ interface VerifyOtpResult { } /** - * Verifies an OTP token with Supabase Auth + * Verifies an OTP token with Supabase Auth. + * Creates a session and sets auth cookies on success. * @throws TRPCError on verification failure */ async function verifyOtp( @@ -38,7 +39,6 @@ async function verifyOtp( `failed to verify OTP: ${error.message}` ) - // map supabase errors to user-friendly messages if (error.status === 403 && error.code === 'otp_expired') { throw new TRPCError({ code: 'BAD_REQUEST', @@ -70,6 +70,38 @@ async function verifyOtp( }) } + // verify session was created (cookies should be set by supabase client) + const hasSession = !!data.session + const hasAccessToken = !!data.session?.access_token + const hasRefreshToken = !!data.session?.refresh_token + + l.info( + { + key: 'auth_repository:verify_otp:success', + user_id: data.user.id, + context: { + type, + tokenHashPrefix: tokenHash.slice(0, 10), + hasSession, + hasAccessToken, + hasRefreshToken, + sessionExpiresAt: data.session?.expires_at, + }, + }, + `verified OTP for user: ${data.user.id}, session: ${hasSession}` + ) + + if (!hasSession) { + l.warn( + { + key: 'auth_repository:verify_otp:no_session', + user_id: data.user.id, + context: { type, tokenHashPrefix: tokenHash.slice(0, 10) }, + }, + `OTP verified but no session returned - user may not be signed in` + ) + } + return { userId: data.user.id, } diff --git a/src/server/api/routers/auth.ts b/src/server/api/routers/auth.ts deleted file mode 100644 index 7d6f3dee6..000000000 --- a/src/server/api/routers/auth.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { PROTECTED_URLS } from '@/configs/urls' -import { createTRPCRouter } from '../init' -import { - ConfirmEmailInputSchema, - type ConfirmEmailResult, - type OtpType, -} from '../models/auth.models' -import { publicProcedure } from '../procedures' -import { authRepo } from '../repositories/auth.repository' - -/** - * Determines the redirect URL based on OTP type and the original next parameter. - */ -function buildRedirectUrl(type: OtpType, next: string): string { - const redirectUrl = new URL(next) - - // recovery flow always goes to account settings with reauth flag - if (type === 'recovery') { - redirectUrl.pathname = PROTECTED_URLS.RESET_PASSWORD - redirectUrl.searchParams.set('reauth', '1') - return redirectUrl.toString() - } - - // reauth flow for account settings - if (redirectUrl.pathname === PROTECTED_URLS.ACCOUNT_SETTINGS) { - redirectUrl.searchParams.set('reauth', '1') - return redirectUrl.toString() - } - - return next -} - -export const authRouter = createTRPCRouter({ - confirmEmail: publicProcedure - .input(ConfirmEmailInputSchema) - .mutation(async ({ input }): Promise => { - const { token_hash, type, next } = input - - await authRepo.verifyOtp(token_hash, type) - - const redirectUrl = buildRedirectUrl(type, next) - - return { redirectUrl } - }), -}) diff --git a/src/server/api/routers/index.ts b/src/server/api/routers/index.ts index 02349648c..85602dca8 100644 --- a/src/server/api/routers/index.ts +++ b/src/server/api/routers/index.ts @@ -1,11 +1,9 @@ import { createCallerFactory, createTRPCRouter } from '../init' -import { authRouter } from './auth' import { buildsRouter } from './builds' import { sandboxesRouter } from './sandboxes' import { templatesRouter } from './templates' export const trpcAppRouter = createTRPCRouter({ - auth: authRouter, sandboxes: sandboxesRouter, templates: templatesRouter, builds: buildsRouter, From a1445c378a156de5915c2c82913c0cabb57876d9 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Fri, 5 Dec 2025 22:46:44 +0100 Subject: [PATCH 04/68] Refactor OTP verification response handling and enhance error management - Updated the `VerifyOtpResponse` interface to require a `redirectUrl` for successful OTP verification. - Simplified error handling in the `verifyOtp` function to throw a generic error message when verification fails. - Improved the confirmation page's mutation success handling to directly push the `redirectUrl` without additional checks. - Introduced a new function to build error redirect URLs, enhancing user feedback for invalid verification attempts. These changes streamline the OTP verification process and improve user experience during authentication. --- src/app/(auth)/confirm/page.tsx | 15 ++++----- src/app/api/auth/verify-otp/route.ts | 49 ++++++++++++++-------------- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/src/app/(auth)/confirm/page.tsx b/src/app/(auth)/confirm/page.tsx index 12d03d79e..e74a27210 100644 --- a/src/app/(auth)/confirm/page.tsx +++ b/src/app/(auth)/confirm/page.tsx @@ -31,8 +31,7 @@ const OTP_TYPE_DESCRIPTIONS: Record = { } interface VerifyOtpResponse { - redirectUrl?: string - error?: string + redirectUrl: string } async function verifyOtp(input: ConfirmEmailInput): Promise { @@ -44,8 +43,8 @@ async function verifyOtp(input: ConfirmEmailInput): Promise { const data: VerifyOtpResponse = await response.json() - if (!response.ok || data.error) { - throw new Error(data.error || 'Verification failed. Please try again.') + if (!data.redirectUrl) { + throw new Error('Verification failed. Please try again.') } return data @@ -78,11 +77,9 @@ export default function ConfirmPage() { const mutation = useMutation({ mutationFn: verifyOtp, onSuccess: (data) => { - if (data.redirectUrl) { - startTransition(() => { - router.push(data.redirectUrl!) - }) - } + startTransition(() => { + router.push(data.redirectUrl) + }) }, }) diff --git a/src/app/api/auth/verify-otp/route.ts b/src/app/api/auth/verify-otp/route.ts index d9cce08c2..8dd0604be 100644 --- a/src/app/api/auth/verify-otp/route.ts +++ b/src/app/api/auth/verify-otp/route.ts @@ -1,4 +1,4 @@ -import { PROTECTED_URLS } from '@/configs/urls' +import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' import { l } from '@/lib/clients/logger/logger' import { ConfirmEmailInputSchema, @@ -29,7 +29,18 @@ function buildRedirectUrl(type: OtpType, next: string): string { return next } +/** + * Builds a redirect URL to sign-in with an encoded error message. + */ +function buildErrorRedirectUrl(origin: string, message: string): string { + const url = new URL(origin + AUTH_URLS.SIGN_IN) + url.searchParams.set('error', encodeURIComponent(message)) + return url.toString() +} + export async function POST(request: NextRequest) { + const origin = request.nextUrl.origin + try { const body = await request.json() @@ -44,10 +55,12 @@ export async function POST(request: NextRequest) { 'invalid input for verify OTP' ) - return NextResponse.json( - { error: 'Invalid request' }, - { status: 400 } + const errorRedirectUrl = buildErrorRedirectUrl( + origin, + 'Invalid verification link. Please request a new one.' ) + + return NextResponse.json({ redirectUrl: errorRedirectUrl }) } const { token_hash, type, next } = result.data @@ -68,18 +81,6 @@ export async function POST(request: NextRequest) { const redirectUrl = buildRedirectUrl(type, next) - l.info( - { - key: 'verify_otp:success', - user_id: userId, - context: { - type, - redirectUrl, - }, - }, - `OTP verified for user: ${userId}, redirecting to: ${redirectUrl}` - ) - return NextResponse.json({ redirectUrl }) } catch (error) { // handle known errors from repository @@ -94,10 +95,9 @@ export async function POST(request: NextRequest) { `verify OTP failed: ${message}` ) - return NextResponse.json( - { error: message }, - { status: 400 } - ) + const errorRedirectUrl = buildErrorRedirectUrl(origin, message) + + return NextResponse.json({ redirectUrl: errorRedirectUrl }) } l.error( @@ -108,10 +108,11 @@ export async function POST(request: NextRequest) { 'verify OTP failed with unknown error' ) - return NextResponse.json( - { error: 'Verification failed. Please try again.' }, - { status: 500 } + const errorRedirectUrl = buildErrorRedirectUrl( + origin, + 'Verification failed. Please try again.' ) + + return NextResponse.json({ redirectUrl: errorRedirectUrl }) } } - From f8d126edf3a05f9f5fbad9bb7c99615fe0c54399 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Fri, 5 Dec 2025 23:00:12 +0100 Subject: [PATCH 05/68] Enhance OTP verification process with improved redirect handling and security - Updated the `buildRedirectUrl` function to utilize the dashboard origin for safe redirects, preventing open redirect vulnerabilities. - Introduced a check for external origins in the OTP verification process, rejecting requests from untrusted sources. - Enhanced error logging for invalid OTP requests and improved user feedback with a dedicated error redirect URL. - Simplified the handling of search parameters in the redirect URL construction. These changes strengthen the security and user experience during the OTP verification flow. --- src/app/api/auth/verify-otp/route.ts | 63 ++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/src/app/api/auth/verify-otp/route.ts b/src/app/api/auth/verify-otp/route.ts index 8dd0604be..eedf12f10 100644 --- a/src/app/api/auth/verify-otp/route.ts +++ b/src/app/api/auth/verify-otp/route.ts @@ -6,14 +6,36 @@ import { } from '@/server/api/models/auth.models' import { authRepo } from '@/server/api/repositories/auth.repository' import { NextRequest, NextResponse } from 'next/server' +import { flattenError } from 'zod' + +const normalizeOrigin = (origin: string) => + origin.replace('www.', '').replace(/\/$/, '') + +function isExternalOrigin(next: string, dashboardOrigin: string): boolean { + return ( + normalizeOrigin(new URL(next).origin) !== normalizeOrigin(dashboardOrigin) + ) +} /** - * Determines the redirect URL based on OTP type and the original next parameter. + * Determines the redirect URL based on OTP type. + * Uses dashboard origin to build safe redirect URLs, only preserving the pathname from next. */ -function buildRedirectUrl(type: OtpType, next: string): string { - const redirectUrl = new URL(next) - - // recovery flow always goes to account settings with reauth flag +function buildRedirectUrl( + type: OtpType, + next: string, + dashboardOrigin: string +): string { + const nextUrl = new URL(next) + // always use dashboard origin to prevent open redirects + const redirectUrl = new URL(dashboardOrigin) + redirectUrl.pathname = nextUrl.pathname + // preserve search params from original next URL + nextUrl.searchParams.forEach((value, key) => { + redirectUrl.searchParams.set(key, value) + }) + + // recovery flow always goes to reset password with reauth flag if (type === 'recovery') { redirectUrl.pathname = PROTECTED_URLS.RESET_PASSWORD redirectUrl.searchParams.set('reauth', '1') @@ -26,7 +48,7 @@ function buildRedirectUrl(type: OtpType, next: string): string { return redirectUrl.toString() } - return next + return redirectUrl.toString() } /** @@ -50,7 +72,7 @@ export async function POST(request: NextRequest) { l.error( { key: 'verify_otp:invalid_input', - error: result.error.flatten(), + error: flattenError(result.error), }, 'invalid input for verify OTP' ) @@ -65,6 +87,29 @@ export async function POST(request: NextRequest) { const { token_hash, type, next } = result.data + // reject external origins to prevent open redirect attacks + if (isExternalOrigin(next, origin)) { + l.warn( + { + key: 'verify_otp:external_origin_rejected', + context: { + type, + tokenHashPrefix: token_hash.slice(0, 10), + nextOrigin: new URL(next).origin, + dashboardOrigin: origin, + }, + }, + `rejected verify OTP request with external origin: ${new URL(next).origin}` + ) + + const errorRedirectUrl = buildErrorRedirectUrl( + origin, + 'Invalid verification link. Please request a new one.' + ) + + return NextResponse.json({ redirectUrl: errorRedirectUrl }) + } + l.info( { key: 'verify_otp:init', @@ -77,9 +122,9 @@ export async function POST(request: NextRequest) { `verifying OTP token: ${token_hash.slice(0, 10)}` ) - const { userId } = await authRepo.verifyOtp(token_hash, type) + await authRepo.verifyOtp(token_hash, type) - const redirectUrl = buildRedirectUrl(type, next) + const redirectUrl = buildRedirectUrl(type, next, origin) return NextResponse.json({ redirectUrl }) } catch (error) { From aa6f28be31ccf0c74b0a718c7c223d9194af089c Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Fri, 5 Dec 2025 23:01:40 +0100 Subject: [PATCH 06/68] chore --- src/app/(auth)/confirm/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/(auth)/confirm/page.tsx b/src/app/(auth)/confirm/page.tsx index e74a27210..16761d9b5 100644 --- a/src/app/(auth)/confirm/page.tsx +++ b/src/app/(auth)/confirm/page.tsx @@ -105,7 +105,7 @@ export default function ConfirmPage() { disabled={!isValidParams} className="w-full" > - Confirm + Continue From b9a3e87b63bbb2a88c0dec569f19529547bab39c Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Sat, 6 Dec 2025 11:18:12 +0100 Subject: [PATCH 07/68] fix: possible undefined ui behavior --- src/app/(auth)/confirm/page.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/app/(auth)/confirm/page.tsx b/src/app/(auth)/confirm/page.tsx index 16761d9b5..80e8fea28 100644 --- a/src/app/(auth)/confirm/page.tsx +++ b/src/app/(auth)/confirm/page.tsx @@ -4,6 +4,7 @@ import { AUTH_URLS } from '@/configs/urls' import { AuthFormMessage } from '@/features/auth/form-message' import { ConfirmEmailInputSchema, + OtpTypeSchema, type ConfirmEmailInput, type OtpType, } from '@/server/api/models/auth.models' @@ -57,7 +58,9 @@ export default function ConfirmPage() { const params = useMemo(() => { const tokenHash = searchParams.get('token_hash') ?? '' - const type = searchParams.get('type') as OtpType | null + const typeParam = searchParams.get('type') + const typeResult = OtpTypeSchema.safeParse(typeParam) + const type: OtpType | null = typeResult.success ? typeResult.data : null const next = searchParams.get('next') ?? '' return { tokenHash, type, next } From b4cef9c18206585435a171a5f4531af179cac569 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Sat, 6 Dec 2025 13:12:12 +0100 Subject: [PATCH 08/68] Refactor authentication utility functions and enhance origin checks - Moved the `normalizeOrigin` and `isExternalOrigin` functions to the `auth.ts` utility file for better organization and reusability. - Updated the `confirm` and `verify-otp` routes to utilize the new `isExternalOrigin` function, improving the handling of redirect URLs. - Removed redundant implementations of origin normalization from the routes, streamlining the codebase. These changes improve code maintainability and enhance security checks during the authentication process. --- src/app/api/auth/confirm/route.ts | 11 +---------- src/app/api/auth/verify-otp/route.ts | 10 +--------- src/lib/utils/auth.ts | 19 +++++++++++++++++++ 3 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/app/api/auth/confirm/route.ts b/src/app/api/auth/confirm/route.ts index e52b5d148..9f7923977 100644 --- a/src/app/api/auth/confirm/route.ts +++ b/src/app/api/auth/confirm/route.ts @@ -1,6 +1,6 @@ import { AUTH_URLS } from '@/configs/urls' import { l } from '@/lib/clients/logger/logger' -import { encodedRedirect } from '@/lib/utils/auth' +import { encodedRedirect, isExternalOrigin } from '@/lib/utils/auth' import { redirect } from 'next/navigation' import { NextRequest } from 'next/server' import { z } from 'zod' @@ -18,15 +18,6 @@ const confirmSchema = z.object({ next: z.httpUrl(), }) -const normalizeOrigin = (origin: string) => - origin.replace('www.', '').replace(/\/$/, '') - -function isExternalOrigin(next: string, dashboardOrigin: string): boolean { - return ( - normalizeOrigin(new URL(next).origin) !== normalizeOrigin(dashboardOrigin) - ) -} - /** * This route acts as an intermediary for email OTP verification. * diff --git a/src/app/api/auth/verify-otp/route.ts b/src/app/api/auth/verify-otp/route.ts index eedf12f10..2292b3dff 100644 --- a/src/app/api/auth/verify-otp/route.ts +++ b/src/app/api/auth/verify-otp/route.ts @@ -1,5 +1,6 @@ import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' import { l } from '@/lib/clients/logger/logger' +import { isExternalOrigin } from '@/lib/utils/auth' import { ConfirmEmailInputSchema, type OtpType, @@ -8,15 +9,6 @@ import { authRepo } from '@/server/api/repositories/auth.repository' import { NextRequest, NextResponse } from 'next/server' import { flattenError } from 'zod' -const normalizeOrigin = (origin: string) => - origin.replace('www.', '').replace(/\/$/, '') - -function isExternalOrigin(next: string, dashboardOrigin: string): boolean { - return ( - normalizeOrigin(new URL(next).origin) !== normalizeOrigin(dashboardOrigin) - ) -} - /** * Determines the redirect URL based on OTP type. * Uses dashboard origin to build safe redirect URLs, only preserving the pathname from next. diff --git a/src/lib/utils/auth.ts b/src/lib/utils/auth.ts index d8282dfd4..9c8fa437b 100644 --- a/src/lib/utils/auth.ts +++ b/src/lib/utils/auth.ts @@ -28,3 +28,22 @@ export function encodedRedirect( export function getUserProviders(user: User) { return user.app_metadata.providers as string[] | undefined } + +/** + * Normalizes an origin URL by removing 'www.' prefix and trailing slash. + */ +export function normalizeOrigin(origin: string): string { + return origin.replace('www.', '').replace(/\/$/, '') +} + +/** + * Checks if the redirect URL points to a different origin than the dashboard. + */ +export function isExternalOrigin( + next: string, + dashboardOrigin: string +): boolean { + return ( + normalizeOrigin(new URL(next).origin) !== normalizeOrigin(dashboardOrigin) + ) +} From bfedbe93ffdcff04fba899b23fb14e21550cabd6 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Sat, 6 Dec 2025 13:32:17 +0100 Subject: [PATCH 09/68] chore: cleanup --- src/app/api/auth/confirm/route.ts | 22 ++++++------------- src/app/api/auth/verify-otp/route.ts | 14 ++++-------- src/server/api/models/auth.models.ts | 3 --- .../api/repositories/auth.repository.ts | 22 +++++++++---------- 4 files changed, 22 insertions(+), 39 deletions(-) diff --git a/src/app/api/auth/confirm/route.ts b/src/app/api/auth/confirm/route.ts index 9f7923977..b5400fbe1 100644 --- a/src/app/api/auth/confirm/route.ts +++ b/src/app/api/auth/confirm/route.ts @@ -1,20 +1,14 @@ import { AUTH_URLS } from '@/configs/urls' import { l } from '@/lib/clients/logger/logger' import { encodedRedirect, isExternalOrigin } from '@/lib/utils/auth' +import { OtpTypeSchema } from '@/server/api/models/auth.models' import { redirect } from 'next/navigation' import { NextRequest } from 'next/server' import { z } from 'zod' const confirmSchema = z.object({ token_hash: z.string().min(1), - type: z.enum([ - 'signup', - 'recovery', - 'invite', - 'magiclink', - 'email', - 'email_change', - ]), + type: OtpTypeSchema, next: z.httpUrl(), }) @@ -62,17 +56,16 @@ export async function GET(request: NextRequest) { { key: 'auth_confirm:init', context: { - tokenHashPrefix: token_hash.slice(0, 10), + token_hash_prefix: token_hash.slice(0, 10), type, next, - isDifferentOrigin, + is_different_origin: isDifferentOrigin, origin: dashboardOrigin, }, }, `confirming email with OTP token hash: ${token_hash.slice(0, 10)}` ) - // external origin: redirect to supabase client flow for the external app to handle if (isDifferentOrigin) { const supabaseClientFlowUrl = new URL( `${process.env.NEXT_PUBLIC_SUPABASE_URL}/auth/v1/verify?token=${token_hash}&type=${type}&redirect_to=${encodeURIComponent(next)}` @@ -82,7 +75,7 @@ export async function GET(request: NextRequest) { { key: 'auth_confirm:redirect_to_supabase_client_flow', context: { - supabaseClientFlowUrl: supabaseClientFlowUrl.toString(), + supabase_client_flow_url: supabaseClientFlowUrl.toString(), next, }, }, @@ -92,7 +85,6 @@ export async function GET(request: NextRequest) { throw redirect(supabaseClientFlowUrl.toString()) } - // same origin: redirect to /confirm page for user to explicitly confirm const confirmPageUrl = new URL(dashboardOrigin + AUTH_URLS.CONFIRM) confirmPageUrl.searchParams.set('token_hash', token_hash) confirmPageUrl.searchParams.set('type', type) @@ -102,9 +94,9 @@ export async function GET(request: NextRequest) { { key: 'auth_confirm:redirect_to_confirm_page', context: { - tokenHashPrefix: token_hash.slice(0, 10), + token_hash_prefix: token_hash.slice(0, 10), type, - confirmPageUrl: confirmPageUrl.toString(), + confirm_page_url: confirmPageUrl.toString(), }, }, `redirecting to confirm page: ${confirmPageUrl.toString()}` diff --git a/src/app/api/auth/verify-otp/route.ts b/src/app/api/auth/verify-otp/route.ts index 2292b3dff..0ca59e12d 100644 --- a/src/app/api/auth/verify-otp/route.ts +++ b/src/app/api/auth/verify-otp/route.ts @@ -19,22 +19,18 @@ function buildRedirectUrl( dashboardOrigin: string ): string { const nextUrl = new URL(next) - // always use dashboard origin to prevent open redirects const redirectUrl = new URL(dashboardOrigin) redirectUrl.pathname = nextUrl.pathname - // preserve search params from original next URL nextUrl.searchParams.forEach((value, key) => { redirectUrl.searchParams.set(key, value) }) - // recovery flow always goes to reset password with reauth flag if (type === 'recovery') { redirectUrl.pathname = PROTECTED_URLS.RESET_PASSWORD redirectUrl.searchParams.set('reauth', '1') return redirectUrl.toString() } - // reauth flow for account settings if (redirectUrl.pathname === PROTECTED_URLS.ACCOUNT_SETTINGS) { redirectUrl.searchParams.set('reauth', '1') return redirectUrl.toString() @@ -79,16 +75,15 @@ export async function POST(request: NextRequest) { const { token_hash, type, next } = result.data - // reject external origins to prevent open redirect attacks if (isExternalOrigin(next, origin)) { l.warn( { key: 'verify_otp:external_origin_rejected', context: { type, - tokenHashPrefix: token_hash.slice(0, 10), - nextOrigin: new URL(next).origin, - dashboardOrigin: origin, + token_hash_prefix: token_hash.slice(0, 10), + next_origin: new URL(next).origin, + dashboard_origin: origin, }, }, `rejected verify OTP request with external origin: ${new URL(next).origin}` @@ -107,7 +102,7 @@ export async function POST(request: NextRequest) { key: 'verify_otp:init', context: { type, - tokenHashPrefix: token_hash.slice(0, 10), + token_hash_prefix: token_hash.slice(0, 10), next, }, }, @@ -120,7 +115,6 @@ export async function POST(request: NextRequest) { return NextResponse.json({ redirectUrl }) } catch (error) { - // handle known errors from repository if (error && typeof error === 'object' && 'message' in error) { const message = (error as { message: string }).message diff --git a/src/server/api/models/auth.models.ts b/src/server/api/models/auth.models.ts index 41bdbf57e..3eaf6bf3b 100644 --- a/src/server/api/models/auth.models.ts +++ b/src/server/api/models/auth.models.ts @@ -1,6 +1,5 @@ import z from 'zod' -// otp types supported by supabase export const OtpTypeSchema = z.enum([ 'signup', 'recovery', @@ -12,7 +11,6 @@ export const OtpTypeSchema = z.enum([ export type OtpType = z.infer -// shared schema for client form and tRPC router export const ConfirmEmailInputSchema = z.object({ token_hash: z.string().min(1), type: OtpTypeSchema, @@ -21,7 +19,6 @@ export const ConfirmEmailInputSchema = z.object({ export type ConfirmEmailInput = z.infer -// response types export interface ConfirmEmailResult { redirectUrl: string } diff --git a/src/server/api/repositories/auth.repository.ts b/src/server/api/repositories/auth.repository.ts index b98e75db0..c52f4728f 100644 --- a/src/server/api/repositories/auth.repository.ts +++ b/src/server/api/repositories/auth.repository.ts @@ -31,9 +31,9 @@ async function verifyOtp( error: serializeError(error), context: { type, - tokenHashPrefix: tokenHash.slice(0, 10), - errorCode: error.code, - errorStatus: error.status, + token_hash_prefix: tokenHash.slice(0, 10), + error_code: error.code, + error_status: error.status, }, }, `failed to verify OTP: ${error.message}` @@ -58,10 +58,10 @@ async function verifyOtp( key: 'auth_repository:verify_otp:no_user', context: { type, - tokenHashPrefix: tokenHash.slice(0, 10), + token_hash_prefix: tokenHash.slice(0, 10), }, }, - `failed to verify OTP: no user found for token hash: ${tokenHash.slice(0, 10)}` + `failed to verify OTP: no user found` ) throw new TRPCError({ @@ -81,14 +81,14 @@ async function verifyOtp( user_id: data.user.id, context: { type, - tokenHashPrefix: tokenHash.slice(0, 10), - hasSession, - hasAccessToken, - hasRefreshToken, - sessionExpiresAt: data.session?.expires_at, + token_hash_prefix: tokenHash.slice(0, 10), + has_session: !!data.session, + has_access_token: !!data.session?.access_token, + has_refresh_token: !!data.session?.refresh_token, + session_expires_at: data.session?.expires_at, }, }, - `verified OTP for user: ${data.user.id}, session: ${hasSession}` + `verified OTP for user: ${data.user.id}` ) if (!hasSession) { From a28a81a1adba21d81cff901189f958e004edb7d5 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Sat, 6 Dec 2025 13:44:43 +0100 Subject: [PATCH 10/68] chore: sync lroute and component contract --- src/app/(auth)/confirm/page.tsx | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/src/app/(auth)/confirm/page.tsx b/src/app/(auth)/confirm/page.tsx index 80e8fea28..dbc0fdf9b 100644 --- a/src/app/(auth)/confirm/page.tsx +++ b/src/app/(auth)/confirm/page.tsx @@ -35,6 +35,10 @@ interface VerifyOtpResponse { redirectUrl: string } +/** + * Verifies OTP and returns a redirect URL. + * The API always returns a redirectUrl - errors redirect to sign-in with encoded error params. + */ async function verifyOtp(input: ConfirmEmailInput): Promise { const response = await fetch('/api/auth/verify-otp', { method: 'POST', @@ -42,13 +46,7 @@ async function verifyOtp(input: ConfirmEmailInput): Promise { body: JSON.stringify(input), }) - const data: VerifyOtpResponse = await response.json() - - if (!data.redirectUrl) { - throw new Error('Verification failed. Please try again.') - } - - return data + return response.json() } export default function ConfirmPage() { @@ -124,7 +122,7 @@ export default function ConfirmPage() { .

- {!isValidParams && !mutation.error && ( + {!isValidParams && ( )} - - {mutation.error && ( - - )} ) } From 3ebd3063b4d83584d6ab1f73ec3535f87b4f1a22 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Sat, 6 Dec 2025 14:10:56 +0100 Subject: [PATCH 11/68] improve: confirm page prose --- src/app/(auth)/confirm/page.tsx | 35 ++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/app/(auth)/confirm/page.tsx b/src/app/(auth)/confirm/page.tsx index dbc0fdf9b..071c34228 100644 --- a/src/app/(auth)/confirm/page.tsx +++ b/src/app/(auth)/confirm/page.tsx @@ -14,21 +14,31 @@ import { useRouter, useSearchParams } from 'next/navigation' import { useMemo, useTransition } from 'react' const OTP_TYPE_LABELS: Record = { - signup: 'Sign Up', - recovery: 'Password Reset', + signup: 'Welcome to E2B', + recovery: 'Password Recovery', invite: 'Team Invitation', magiclink: 'Sign In', - email: 'Email Verification', - email_change: 'Email Change', + email: 'Verify Email', + email_change: 'Confirm Email Change', } const OTP_TYPE_DESCRIPTIONS: Record = { - signup: 'Complete your account registration', - recovery: 'Reset your password', - invite: 'Accept team invitation', - magiclink: 'Sign in to your account', - email: 'Verify your email address', - email_change: 'Confirm your new email address', + signup: 'Click below to verify your email and create your account.', + recovery: + 'Click below to sign in. You will be forwarded to the account settings page where you can change your password.', + invite: 'Click below to accept the invitation and join the team.', + magiclink: 'Click below to sign in to your account.', + email: 'Click below to verify your email address.', + email_change: 'Click below to confirm your new email address.', +} + +const OTP_TYPE_BUTTON_LABELS: Record = { + signup: 'Create Account', + recovery: 'Sign In', + invite: 'Join Team', + magiclink: 'Sign In', + email: 'Verify Email', + email_change: 'Confirm Email', } interface VerifyOtpResponse { @@ -74,6 +84,9 @@ export default function ConfirmPage() { const typeDescription = params.type ? OTP_TYPE_DESCRIPTIONS[params.type] : 'Confirm your action' + const buttonLabel = params.type + ? OTP_TYPE_BUTTON_LABELS[params.type] + : 'Continue' const mutation = useMutation({ mutationFn: verifyOtp, @@ -106,7 +119,7 @@ export default function ConfirmPage() { disabled={!isValidParams} className="w-full" > - Continue + {buttonLabel} From d96ae85596484467bd2e44b3209a83ac7292c5cc Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Tue, 9 Dec 2025 21:38:59 +0100 Subject: [PATCH 12/68] wip: integrate components --- bun.lock | 16 +- package.json | 8 +- .../dashboard/sandboxes/list/table-body.tsx | 10 +- .../sandboxes/list/table-filters.tsx | 10 +- .../components/range-label.tsx | 4 - .../settings/webhooks/add-edit-dialog.tsx | 6 +- .../settings/webhooks/edit-secret-dialog.tsx | 2 +- .../dashboard/settings/webhooks/table-row.tsx | 2 +- .../dashboard/sidebar/create-team-dialog.tsx | 40 +++-- src/features/dashboard/sidebar/toggle.tsx | 7 +- .../dashboard/templates/builds/header.tsx | 6 +- .../templates/list/table-filters.tsx | 2 +- src/lib/hooks/use-clipboard.ts | 24 +-- src/ui/brand.tsx | 2 +- src/ui/copy-button.tsx | 61 ++++--- src/ui/data-table.tsx | 17 +- src/ui/icons.tsx | 2 +- src/ui/not-found.tsx | 6 +- src/ui/number-input.tsx | 8 +- src/ui/polling-button.tsx | 3 +- src/ui/primitives/badge.tsx | 34 ++-- src/ui/primitives/button.tsx | 112 ++++++------- src/ui/primitives/calendar.tsx | 4 +- src/ui/primitives/dialog.tsx | 43 +++-- src/ui/primitives/input.tsx | 2 +- src/ui/primitives/popover.tsx | 81 +++++---- src/ui/primitives/radio-group.tsx | 42 ++++- src/ui/primitives/sidebar.tsx | 2 +- src/ui/primitives/sonner.tsx | 24 +-- src/ui/survey.tsx | 12 +- src/ui/table-filter-button.tsx | 3 +- src/ui/theme-switcher.tsx | 2 +- src/ui/time-input.tsx | 154 +++++++++--------- src/ui/time-range-picker.tsx | 2 +- 34 files changed, 404 insertions(+), 349 deletions(-) diff --git a/bun.lock b/bun.lock index c38694ae4..53aaa39cf 100644 --- a/bun.lock +++ b/bun.lock @@ -20,16 +20,16 @@ "@opentelemetry/semantic-conventions": "^1.36.0", "@radix-ui/react-avatar": "^1.1.4", "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-dialog": "^1.1.7", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-label": "^2.1.3", - "@radix-ui/react-popover": "^1.1.7", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.4", "@radix-ui/react-select": "^2.1.7", "@radix-ui/react-separator": "^1.1.3", "@radix-ui/react-slider": "^1.2.4", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-toast": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.0", @@ -74,7 +74,7 @@ "immer": "^10.1.1", "lucide-react": "^0.525.0", "micromatch": "^4.0.8", - "motion": "^12.18.1", + "motion": "^12.23.25", "nanoid": "^5.0.9", "next": "^16.0.7", "next-safe-action": "^8.0.11", @@ -156,11 +156,11 @@ }, }, "overrides": { + "@nodelib/fs.scandir": "2.1.5", + "@nodelib/fs.stat": "2.0.5", "@shikijs/core": "2.3.2", "@shikijs/themes": "2.3.2", "shiki": "2.3.2", - "@nodelib/fs.stat": "2.0.5", - "@nodelib/fs.scandir": "2.1.5", "whatwg-url": "^13", }, "packages": { @@ -1772,7 +1772,7 @@ "fraction.js": ["fraction.js@4.3.7", "", {}, "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew=="], - "framer-motion": ["framer-motion@12.23.24", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w=="], + "framer-motion": ["framer-motion@12.23.25", "", { "dependencies": { "motion-dom": "^12.23.23", "motion-utils": "^12.23.6", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-gUHGl2e4VG66jOcH0JHhuJQr6ZNwrET9g31ZG0xdXzT0CznP7fHX4P8Bcvuc4MiUB90ysNnWX2ukHRIggkl6hQ=="], "fs-extra": ["fs-extra@4.0.3", "", { "dependencies": { "graceful-fs": "^4.1.2", "jsonfile": "^4.0.0", "universalify": "^0.1.0" } }, "sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg=="], @@ -2208,7 +2208,7 @@ "module-details-from-path": ["module-details-from-path@1.0.4", "", {}, "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w=="], - "motion": ["motion@12.23.24", "", { "dependencies": { "framer-motion": "^12.23.24", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Rc5E7oe2YZ72N//S3QXGzbnXgqNrTESv8KKxABR20q2FLch9gHLo0JLyYo2hZ238bZ9Gx6cWhj9VO0IgwbMjCw=="], + "motion": ["motion@12.23.25", "", { "dependencies": { "framer-motion": "^12.23.25", "tslib": "^2.4.0" }, "peerDependencies": { "@emotion/is-prop-valid": "*", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@emotion/is-prop-valid", "react", "react-dom"] }, "sha512-Fk5Y1kcgxYiTYOUjmwfXQAP7tP+iGqw/on1UID9WEL/6KpzxPr9jY2169OsjgZvXJdpraKXy0orkjaCVIl5fgQ=="], "motion-dom": ["motion-dom@12.23.23", "", { "dependencies": { "motion-utils": "^12.23.6" } }, "sha512-n5yolOs0TQQBRUFImrRfs/+6X4p3Q4n1dUEqt/H58Vx7OW6RF+foWEgmTVDhIWJIMXOuNNL0apKH2S16en9eiA=="], diff --git a/package.json b/package.json index 56daeb0c2..c000df7e9 100644 --- a/package.json +++ b/package.json @@ -57,16 +57,16 @@ "@opentelemetry/semantic-conventions": "^1.36.0", "@radix-ui/react-avatar": "^1.1.4", "@radix-ui/react-checkbox": "^1.3.3", - "@radix-ui/react-dialog": "^1.1.7", + "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.7", "@radix-ui/react-label": "^2.1.3", - "@radix-ui/react-popover": "^1.1.7", + "@radix-ui/react-popover": "^1.1.15", "@radix-ui/react-radio-group": "^1.3.8", "@radix-ui/react-scroll-area": "^1.2.4", "@radix-ui/react-select": "^2.1.7", "@radix-ui/react-separator": "^1.1.3", "@radix-ui/react-slider": "^1.2.4", - "@radix-ui/react-slot": "^1.2.3", + "@radix-ui/react-slot": "^1.2.4", "@radix-ui/react-tabs": "^1.1.12", "@radix-ui/react-toast": "^1.2.7", "@radix-ui/react-tooltip": "^1.2.0", @@ -111,7 +111,7 @@ "immer": "^10.1.1", "lucide-react": "^0.525.0", "micromatch": "^4.0.8", - "motion": "^12.18.1", + "motion": "^12.23.25", "nanoid": "^5.0.9", "next": "^16.0.7", "next-safe-action": "^8.0.11", diff --git a/src/features/dashboard/sandboxes/list/table-body.tsx b/src/features/dashboard/sandboxes/list/table-body.tsx index 97f388f5a..eebc82db4 100644 --- a/src/features/dashboard/sandboxes/list/table-body.tsx +++ b/src/features/dashboard/sandboxes/list/table-body.tsx @@ -2,8 +2,8 @@ import { Sandbox } from '@/types/api.types' import { DataTableBody } from '@/ui/data-table' import Empty from '@/ui/empty' import { Button } from '@/ui/primitives/button' +import { AddIcon, CloseIcon } from '@/ui/primitives/icons' import { Row } from '@tanstack/react-table' -import { ExternalLink, X } from 'lucide-react' import { memo, useMemo } from 'react' import { useSandboxTableStore } from './stores/table-store' import { SandboxesTable, SandboxWithMetrics } from './table-config' @@ -43,8 +43,8 @@ export const TableBody = memo(function TableBody({ title="No Results Found" description="No sandboxes match your current filters" message={ - } className="h-[70%] max-md:w-screen" @@ -57,10 +57,10 @@ export const TableBody = memo(function TableBody({ title="No Sandboxes Yet" description="Running Sandboxes can be observed here" message={ - } diff --git a/src/features/dashboard/sandboxes/list/table-filters.tsx b/src/features/dashboard/sandboxes/list/table-filters.tsx index b4e64a4a3..8cbcb159f 100644 --- a/src/features/dashboard/sandboxes/list/table-filters.tsx +++ b/src/features/dashboard/sandboxes/list/table-filters.tsx @@ -118,7 +118,7 @@ const TemplateFilter = memo(function TemplateFilter() { className="w-full" /> diff --git a/src/features/dashboard/sandboxes/monitoring/charts/concurrent-chart/components/range-label.tsx b/src/features/dashboard/sandboxes/monitoring/charts/concurrent-chart/components/range-label.tsx index e15bb0aaa..bd35e0908 100644 --- a/src/features/dashboard/sandboxes/monitoring/charts/concurrent-chart/components/range-label.tsx +++ b/src/features/dashboard/sandboxes/monitoring/charts/concurrent-chart/components/range-label.tsx @@ -10,8 +10,6 @@ export function RangeLabel({ label, copyValue }: RangeLabelProps) {
@@ -24,8 +22,6 @@ export function RangeLabel({ label, copyValue }: RangeLabelProps) { diff --git a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx b/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx index 707e78646..cf416772f 100644 --- a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx @@ -238,7 +238,7 @@ export default function WebhookAddEditDialog({ {currentStep === 1 ? ( diff --git a/src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx b/src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx index 49cd336ae..2bb3d5326 100644 --- a/src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/edit-secret-dialog.tsx @@ -173,7 +173,7 @@ export default function WebhookEditSecretDialog({ type="submit" disabled={!isSecretValid} className="w-full" - variant="outline" + variant="secondary" > Confirm diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index 669b438f1..040d0fa21 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -100,7 +100,7 @@ export default function WebhookTableRow({ - diff --git a/src/features/dashboard/sidebar/create-team-dialog.tsx b/src/features/dashboard/sidebar/create-team-dialog.tsx index 7c09a1505..486d45d6a 100644 --- a/src/features/dashboard/sidebar/create-team-dialog.tsx +++ b/src/features/dashboard/sidebar/create-team-dialog.tsx @@ -26,6 +26,7 @@ import { FormMessage, } from '@/ui/primitives/form' import { Input } from '@/ui/primitives/input' +import { Loader } from '@/ui/primitives/loader' import { zodResolver } from '@hookform/resolvers/zod' import { useHookFormAction } from '@next-safe-action/adapter-react-hook-form/hooks' import { useRouter } from 'next/navigation' @@ -112,21 +113,30 @@ export function CreateTeamDialog({
- - + {isExecuting ? ( +
+ + Creating Team... +
+ ) : ( + <> + + + + )}
diff --git a/src/features/dashboard/sidebar/toggle.tsx b/src/features/dashboard/sidebar/toggle.tsx index b4bb83864..fc7f0be11 100644 --- a/src/features/dashboard/sidebar/toggle.tsx +++ b/src/features/dashboard/sidebar/toggle.tsx @@ -49,12 +49,7 @@ export default function DashboardSidebarToggle() { )} - diff --git a/src/features/dashboard/templates/list/table-filters.tsx b/src/features/dashboard/templates/list/table-filters.tsx index 0d50cafb9..d81b660af 100644 --- a/src/features/dashboard/templates/list/table-filters.tsx +++ b/src/features/dashboard/templates/list/table-filters.tsx @@ -160,7 +160,7 @@ const TemplatesTableFilters = React.forwardRef< > - diff --git a/src/lib/hooks/use-clipboard.ts b/src/lib/hooks/use-clipboard.ts index bb9768d21..01349a4c7 100644 --- a/src/lib/hooks/use-clipboard.ts +++ b/src/lib/hooks/use-clipboard.ts @@ -1,4 +1,6 @@ -import { useCallback, useState } from 'react' +"use client"; + +import { useState, useCallback } from "react"; /** * Hook for copying text to clipboard with temporary success state @@ -8,25 +10,25 @@ import { useCallback, useState } from 'react' export const useClipboard = ( duration: number = 3000 ): [boolean, (text: string) => Promise] => { - const [wasCopied, setWasCopied] = useState(false) + const [wasCopied, setWasCopied] = useState(false); const copy = useCallback( async (text: string) => { try { - await navigator.clipboard.writeText(text) - setWasCopied(true) + await navigator.clipboard.writeText(text); + setWasCopied(true); // Reset wasCopied after duration setTimeout(() => { - setWasCopied(false) - }, duration) + setWasCopied(false); + }, duration); } catch (err) { - console.error('Failed to copy text:', err) - setWasCopied(false) + console.error("Failed to copy text:", err); + setWasCopied(false); } }, [duration] - ) + ); - return [wasCopied, copy] -} + return [wasCopied, copy]; +}; diff --git a/src/ui/brand.tsx b/src/ui/brand.tsx index 1dbe6b0ab..57539d57f 100644 --- a/src/ui/brand.tsx +++ b/src/ui/brand.tsx @@ -1,4 +1,4 @@ -import { cn } from '@/lib/utils' +import { cn } from '@/lib/utils/ui' import { Badge, BadgeProps } from '@/ui/primitives/badge' export const E2BLogo = ({ diff --git a/src/ui/copy-button.tsx b/src/ui/copy-button.tsx index f814e4bcb..6df4e8723 100644 --- a/src/ui/copy-button.tsx +++ b/src/ui/copy-button.tsx @@ -1,39 +1,54 @@ 'use client' import { useClipboard } from '@/lib/hooks/use-clipboard' +import { EASE_APPEAR } from '@/lib/utils/ui' import { Button, ButtonProps } from '@/ui/primitives/button' -import { CheckIcon } from 'lucide-react' +import { CheckIcon, CopyIcon } from '@/ui/primitives/icons' +import { AnimatePresence, motion } from 'motion/react' import { FC } from 'react' -import { CopyIcon } from './primitives/icons' interface CopyButtonProps extends ButtonProps { value: string onCopy?: () => void } -const CopyButton: FC = ({ - value, - onCopy, - onClick, - ...props -}) => { - const [wasCopied, copy] = useClipboard() - - const handleClick = (e: React.MouseEvent) => { - e.stopPropagation() - e.preventDefault() - copy(value) - onCopy?.() - onClick?.(e) - } +const CopyButton: FC = ({ value, onCopy, ...props }) => { + const [wasCopied, copy] = useClipboard(1000) return ( - ) } diff --git a/src/ui/data-table.tsx b/src/ui/data-table.tsx index 58803b483..e45fdf8b0 100644 --- a/src/ui/data-table.tsx +++ b/src/ui/data-table.tsx @@ -162,7 +162,6 @@ const DataTable = React.forwardRef( className={cn( // Base table styles from table.tsx 'w-full caption-bottom', - 'font-mono', // Div table styles 'w-fit', className @@ -248,32 +247,32 @@ function DataTablePagination({
-
- - - - -
-
- Hours - handleTimeChange('hours', value)} - min={0} - max={23} - step={1} - disabled={disabled} - inputClassName="h-8 w-11 text-center border-r-0" - buttonClassName="h-[1rem]" - /> -
-
- Minutes - handleTimeChange('minutes', value)} - min={0} - max={59} - step={1} - disabled={disabled} - inputClassName="h-8 w-11 text-center border-r-0" - buttonClassName="h-[1rem]" - /> -
-
- Seconds - handleTimeChange('seconds', value)} - min={0} - max={59} - step={1} - disabled={disabled} - inputClassName="h-8 w-11 text-center" - buttonClassName="h-[1rem]" - /> + + +
-
- + +
+
+ Hours + handleTimeChange('hours', value)} + min={0} + max={23} + step={1} + disabled={disabled} + inputClassName="h-8 w-11 text-center border-r-0" + buttonClassName="h-[1rem]" + /> +
+
+ Minutes + handleTimeChange('minutes', value)} + min={0} + max={59} + step={1} + disabled={disabled} + inputClassName="h-8 w-11 text-center border-r-0" + buttonClassName="h-[1rem]" + /> +
+
+ Seconds + handleTimeChange('seconds', value)} + min={0} + max={59} + step={1} + disabled={disabled} + inputClassName="h-8 w-11 text-center" + buttonClassName="h-[1rem]" + /> +
+
+
+ )} ) diff --git a/src/ui/time-range-picker.tsx b/src/ui/time-range-picker.tsx index 36dec25cb..650055b3c 100644 --- a/src/ui/time-range-picker.tsx +++ b/src/ui/time-range-picker.tsx @@ -294,7 +294,7 @@ export function TimeRangePicker({ type="submit" disabled={!form.formState.isDirty || !form.formState.isValid} className="w-fit self-end mt-auto" - variant="outline" + variant="secondary" > Apply From 76632868a47ed101ff3181c0da547f205267f0a0 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Mon, 15 Dec 2025 16:40:48 +0100 Subject: [PATCH 13/68] wip --- .../dashboard/account/user-access-token.tsx | 5 ++- .../dashboard/members/member-table-row.tsx | 2 +- .../dashboard/sandbox/header/started-at.tsx | 7 +--- .../dashboard/sandbox/header/template-id.tsx | 7 +--- .../dashboard/sandbox/header/title.tsx | 1 - .../sandbox/inspect/viewer-header.tsx | 14 +++---- .../dashboard/settings/general/info-card.tsx | 4 +- .../dashboard/settings/general/name-card.tsx | 39 +++++++++++++------ .../settings/keys/create-api-key-dialog.tsx | 7 +++- .../dashboard/usage/usage-metric-chart.tsx | 2 +- src/ui/primitives/button.tsx | 17 +++++++- src/ui/primitives/loader.tsx | 2 + 12 files changed, 65 insertions(+), 42 deletions(-) diff --git a/src/features/dashboard/account/user-access-token.tsx b/src/features/dashboard/account/user-access-token.tsx index d322e26c0..f0e85a415 100644 --- a/src/features/dashboard/account/user-access-token.tsx +++ b/src/features/dashboard/account/user-access-token.tsx @@ -45,7 +45,7 @@ export default function UserAccessToken({ className }: UserAccessTokenProps) { /> diff --git a/src/features/dashboard/members/member-table-row.tsx b/src/features/dashboard/members/member-table-row.tsx index 9c5755c15..7e08ee824 100644 --- a/src/features/dashboard/members/member-table-row.tsx +++ b/src/features/dashboard/members/member-table-row.tsx @@ -93,7 +93,7 @@ export default function MemberTableRow({ loading: isRemoving, }} trigger={ - } diff --git a/src/features/dashboard/sandbox/header/started-at.tsx b/src/features/dashboard/sandbox/header/started-at.tsx index b224910b0..da764afdf 100644 --- a/src/features/dashboard/sandbox/header/started-at.tsx +++ b/src/features/dashboard/sandbox/header/started-at.tsx @@ -36,12 +36,7 @@ export default function StartedAt() {

{prefix}, {timeStr}

- + ) } diff --git a/src/features/dashboard/sandbox/header/template-id.tsx b/src/features/dashboard/sandbox/header/template-id.tsx index d235ea179..c45b9b62a 100644 --- a/src/features/dashboard/sandbox/header/template-id.tsx +++ b/src/features/dashboard/sandbox/header/template-id.tsx @@ -15,12 +15,7 @@ export default function TemplateId() { return (

{value}

- +
) } diff --git a/src/features/dashboard/sandbox/header/title.tsx b/src/features/dashboard/sandbox/header/title.tsx index 216d1de0a..5de144df3 100644 --- a/src/features/dashboard/sandbox/header/title.tsx +++ b/src/features/dashboard/sandbox/header/title.tsx @@ -16,7 +16,6 @@ export default function Title() { diff --git a/src/features/dashboard/sandbox/inspect/viewer-header.tsx b/src/features/dashboard/sandbox/inspect/viewer-header.tsx index 7a8c4fd55..cc6c75220 100644 --- a/src/features/dashboard/sandbox/inspect/viewer-header.tsx +++ b/src/features/dashboard/sandbox/inspect/viewer-header.tsx @@ -27,20 +27,16 @@ export default function SandboxInspectViewerHeader({ {name} {fileContentState?.type === 'text' && ( - + )} - - diff --git a/src/features/dashboard/settings/general/info-card.tsx b/src/features/dashboard/settings/general/info-card.tsx index 91976ec53..1ad84c66d 100644 --- a/src/features/dashboard/settings/general/info-card.tsx +++ b/src/features/dashboard/settings/general/info-card.tsx @@ -31,12 +31,12 @@ export function InfoCard({ className }: InfoCardProps) {
E-Mail {team.email} - +
Team ID {team.id} - +
diff --git a/src/features/dashboard/settings/general/name-card.tsx b/src/features/dashboard/settings/general/name-card.tsx index 96a51a1d5..caff38633 100644 --- a/src/features/dashboard/settings/general/name-card.tsx +++ b/src/features/dashboard/settings/general/name-card.tsx @@ -8,9 +8,10 @@ import { useToast, } from '@/lib/hooks/use-toast' import { exponentialSmoothing } from '@/lib/utils' +import { cn } from '@/lib/utils/ui' import { updateTeamNameAction } from '@/server/team/team-actions' import { UpdateTeamNameSchema } from '@/server/team/types' -import { Button } from '@/ui/primitives/button' +import { Button, buttonVariants } from '@/ui/primitives/button' import { Card, CardContent, @@ -26,9 +27,11 @@ import { FormMessage, } from '@/ui/primitives/form' import { Input } from '@/ui/primitives/input' +import { Loader } from '@/ui/primitives/loader' import { zodResolver } from '@hookform/resolvers/zod' import { useHookFormOptimisticAction } from '@next-safe-action/adapter-react-hook-form/hooks' import { AnimatePresence, motion } from 'motion/react' +import { useMemo } from 'react' interface NameCardProps { className?: string @@ -37,7 +40,7 @@ interface NameCardProps { export function NameCard({ className }: NameCardProps) { 'use no memo' - const { team } = useDashboard() + const { team, setTeam } = useDashboard() const { toast } = useToast() @@ -69,8 +72,13 @@ export function NameCard({ className }: NameCardProps) { }, } }, - onSuccess: async () => { + onSuccess: async ({ data }) => { toast(defaultSuccessToast(USER_MESSAGES.teamNameUpdated.message)) + + setTeam({ + ...team, + name: data.name, + }) }, onError: ({ error }) => { if (!error.serverError) return @@ -87,6 +95,9 @@ export function NameCard({ className }: NameCardProps) { const { watch } = form + const name = watch('name') + const isNameDirty = useMemo(() => name !== team.name, [name, team.name]) + return ( @@ -135,14 +146,20 @@ export function NameCard({ className }: NameCardProps) { )} /> - + {isExecuting ? ( +
+ {' '} + Saving... +
+ ) : ( + + )} diff --git a/src/features/dashboard/settings/keys/create-api-key-dialog.tsx b/src/features/dashboard/settings/keys/create-api-key-dialog.tsx index 98c48c3c1..e2ebce923 100644 --- a/src/features/dashboard/settings/keys/create-api-key-dialog.tsx +++ b/src/features/dashboard/settings/keys/create-api-key-dialog.tsx @@ -124,7 +124,10 @@ const CreateApiKeyDialog: FC = ({ children }) => { /> - @@ -154,7 +157,7 @@ const CreateApiKeyDialog: FC = ({ children }) => { - + diff --git a/src/features/dashboard/usage/usage-metric-chart.tsx b/src/features/dashboard/usage/usage-metric-chart.tsx index d10fe1c60..b6fb9f741 100644 --- a/src/features/dashboard/usage/usage-metric-chart.tsx +++ b/src/features/dashboard/usage/usage-metric-chart.tsx @@ -98,7 +98,7 @@ function UsageMetricChartContent({ - From 6cab25b988c8917ad66108736bc1b01dfaed27a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Thu, 5 Feb 2026 13:58:31 +0100 Subject: [PATCH 16/68] feat: update UI registry not found screen --- src/ui/not-found.tsx | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/ui/not-found.tsx b/src/ui/not-found.tsx index 253bf31f2..5111a0e19 100644 --- a/src/ui/not-found.tsx +++ b/src/ui/not-found.tsx @@ -26,17 +26,17 @@ export default function NotFound() { changed, or is temporarily unavailable.

- -
+ +
@@ -44,9 +44,9 @@ export default function NotFound() { From 06d1d1077cc8410a9d2043dd0db3e8ac47923c2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Thu, 5 Feb 2026 14:23:20 +0100 Subject: [PATCH 17/68] fix: sizing of icon button variant, remove redundant sizing from icon classes --- src/app/dashboard/unauthorized.tsx | 6 +++--- src/ui/not-found.tsx | 6 +++--- src/ui/primitives/button.tsx | 2 +- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/app/dashboard/unauthorized.tsx b/src/app/dashboard/unauthorized.tsx index 8f3fb8c3c..39c4d767b 100644 --- a/src/app/dashboard/unauthorized.tsx +++ b/src/app/dashboard/unauthorized.tsx @@ -35,13 +35,13 @@ export default function Unauthorized() {
@@ -51,7 +51,7 @@ export default function Unauthorized() { onClick={() => window.history.back()} className="w-full" > - + Go Back diff --git a/src/ui/not-found.tsx b/src/ui/not-found.tsx index 5111a0e19..f3a434923 100644 --- a/src/ui/not-found.tsx +++ b/src/ui/not-found.tsx @@ -30,13 +30,13 @@ export default function NotFound() {
@@ -46,7 +46,7 @@ export default function NotFound() { onClick={() => window.history.back()} className="w-full" > - + Go Back diff --git a/src/ui/primitives/button.tsx b/src/ui/primitives/button.tsx index 2de7181c5..61440f272 100644 --- a/src/ui/primitives/button.tsx +++ b/src/ui/primitives/button.tsx @@ -58,7 +58,7 @@ const buttonVariants = cva( size: { default: '[&_svg]:size-4 h-9 py-1.5 gap-1 [&:has(svg)]:pr-3 [&:has(svg)]:pl-2.5 px-4', - icon: 'h-9 w-9 px-2.5 py-1.5 [&_svg]:size-5', + icon: 'h-9 w-9 px-2.5 py-1.5 [&_svg]:size-4', 'icon-sm': 'h-7 w-7 px-2.5 py-1.5 [&_svg]:size-4', 'icon-xs': 'size-4.5 [&_svg]:size-3', 'icon-lg': 'h-12 w-12 px-2.5 py-1.5 [&_svg]:size-6', From 8775bdef686ae34854ab8e8b14dfa48265bdcf1a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Thu, 5 Feb 2026 14:46:24 +0100 Subject: [PATCH 18/68] feat: update ui buttons + spacing in Account screen --- src/features/dashboard/account/email-settings.tsx | 2 +- src/features/dashboard/account/name-settings.tsx | 2 +- src/features/dashboard/account/password-settings.tsx | 2 +- src/features/dashboard/account/reauth-dialog.tsx | 2 +- src/features/dashboard/account/user-access-token.tsx | 10 ++++------ src/ui/alert-dialog.tsx | 2 +- 6 files changed, 9 insertions(+), 11 deletions(-) diff --git a/src/features/dashboard/account/email-settings.tsx b/src/features/dashboard/account/email-settings.tsx index 2b69464c8..a71830605 100644 --- a/src/features/dashboard/account/email-settings.tsx +++ b/src/features/dashboard/account/email-settings.tsx @@ -159,7 +159,7 @@ export function EmailSettings({ className }: EmailSettingsProps) { Has to be a valid e-mail address.

diff --git a/src/ui/alert-dialog.tsx b/src/ui/alert-dialog.tsx index 26b84292b..51cdc51b8 100644 --- a/src/ui/alert-dialog.tsx +++ b/src/ui/alert-dialog.tsx @@ -51,7 +51,7 @@ export const AlertDialog: FC = ({ {children &&
{children}
} - + {originalValue !== null && ( From 4fc1de964e041f1d872a8728e1b1212a2b0df80e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Thu, 5 Feb 2026 18:17:23 +0100 Subject: [PATCH 20/68] feat: introduce a link button variant, apply everywhere --- src/features/dashboard/build/header-cells.tsx | 6 ++---- .../dashboard/sandboxes/list/table-cells.tsx | 5 ++--- .../dashboard/templates/builds/table-cells.tsx | 9 +++------ src/ui/primitives/button.tsx | 18 ++++++++++++++++-- 4 files changed, 23 insertions(+), 15 deletions(-) diff --git a/src/features/dashboard/build/header-cells.tsx b/src/features/dashboard/build/header-cells.tsx index ddb8e5238..9e75b9ec3 100644 --- a/src/features/dashboard/build/header-cells.tsx +++ b/src/features/dashboard/build/header-cells.tsx @@ -30,10 +30,8 @@ export function Template({ return ( ) } diff --git a/src/features/dashboard/templates/builds/table-cells.tsx b/src/features/dashboard/templates/builds/table-cells.tsx index 8c0c97f2c..21a8aedb0 100644 --- a/src/features/dashboard/templates/builds/table-cells.tsx +++ b/src/features/dashboard/templates/builds/table-cells.tsx @@ -48,11 +48,9 @@ export function Template({ return ( ) } diff --git a/src/ui/primitives/button.tsx b/src/ui/primitives/button.tsx index 61440f272..260d04343 100644 --- a/src/ui/primitives/button.tsx +++ b/src/ui/primitives/button.tsx @@ -4,9 +4,16 @@ import { cva, type VariantProps } from 'class-variance-authority' import * as React from 'react' import { Loader } from './loader' +const linkStyles = [ + 'text-fg-secondary underline underline-offset-2', + 'hover:opacity-80', // hover + 'data-[display-state=hover]:opacity-80', // duplicated hover, for display purposes + 'disabled:opacity-50', // disabled +] + const buttonVariants = cva( [ - 'inline-flex items-center cursor-pointer justify-center whitespace-nowrap prose-body-highlight!', + 'inline-flex items-center cursor-pointer justify-center whitespace-nowrap', 'transition-colors [&_svg]:transition-colors disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0', '[&_svg]:text-icon-tertiary', ].join(' '), @@ -14,6 +21,7 @@ const buttonVariants = cva( variants: { variant: { primary: [ + 'prose-body-highlight', '[&_svg]:text-icon-inverted', 'bg-bg-inverted text-fg-inverted', 'hover:bg-bg-inverted-hover', // hover @@ -22,6 +30,7 @@ const buttonVariants = cva( 'data-[state=open]:bg-bg-inverted-hover', ].join(' '), secondary: [ + 'prose-body-highlight', 'border', 'hover:border-stroke-active', // hover 'data-[display-state=hover]:border-stroke-active', // duplicated hover, for display purposes @@ -31,6 +40,7 @@ const buttonVariants = cva( 'data-[state=open]:bg-bg-1', ].join(' '), tertiary: [ + 'prose-body-highlight', 'text-fg', 'hover:text-fg hover:underline', // hover 'data-[display-state=hover]:text-fg data-[display-state=hover]:underline', // duplicated hover, for display purposes @@ -39,6 +49,7 @@ const buttonVariants = cva( 'disabled:opacity-65 text-fg-tertiary', // disabled ].join(' '), quaternary: [ + 'prose-body-highlight', 'text-fg-tertiary', 'hover:text-fg', // hover 'data-[display-state=hover]:text-fg', // duplicated hover, for display purposes @@ -47,6 +58,7 @@ const buttonVariants = cva( 'disabled:opacity-65', // disabled ].join(' '), error: [ + 'prose-body-highlight', '[&_svg]:text-icon-inverted', 'bg-accent-error-highlight text-fg-inverted', 'hover:bg-accent-error-highlight/90', // hover @@ -54,6 +66,8 @@ const buttonVariants = cva( 'disabled:text-fg-tertiary disabled:bg-fill disabled:[&_svg]:text-icon-tertiary', // disabled 'data-[state=open]:bg-accent-error-highlight/90', ].join(' '), + link: ['prose-body', ...linkStyles].join(' '), + 'link-table': ['prose-table', ...linkStyles].join(' '), }, size: { default: @@ -62,7 +76,7 @@ const buttonVariants = cva( 'icon-sm': 'h-7 w-7 px-2.5 py-1.5 [&_svg]:size-4', 'icon-xs': 'size-4.5 [&_svg]:size-3', 'icon-lg': 'h-12 w-12 px-2.5 py-1.5 [&_svg]:size-6', - none: '', + none: 'gap-1', }, }, defaultVariants: { From 0998a17fa9e2aa3b7c4d403e64a6b2a34ca8366a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Sun, 22 Feb 2026 11:20:37 +0100 Subject: [PATCH 21/68] feat: introduce iconbutton component and conform copybutton to it --- src/ui/copy-button.tsx | 15 +++----- src/ui/primitives/icon-button.tsx | 61 +++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+), 9 deletions(-) create mode 100644 src/ui/primitives/icon-button.tsx diff --git a/src/ui/copy-button.tsx b/src/ui/copy-button.tsx index 6df4e8723..6ee49e38e 100644 --- a/src/ui/copy-button.tsx +++ b/src/ui/copy-button.tsx @@ -2,12 +2,12 @@ import { useClipboard } from '@/lib/hooks/use-clipboard' import { EASE_APPEAR } from '@/lib/utils/ui' -import { Button, ButtonProps } from '@/ui/primitives/button' +import { IconButton, IconButtonProps } from '@/ui/primitives/icon-button' import { CheckIcon, CopyIcon } from '@/ui/primitives/icons' import { AnimatePresence, motion } from 'motion/react' import { FC } from 'react' -interface CopyButtonProps extends ButtonProps { +interface CopyButtonProps extends IconButtonProps { value: string onCopy?: () => void } @@ -16,10 +16,7 @@ const CopyButton: FC = ({ value, onCopy, ...props }) => { const [wasCopied, copy] = useClipboard(1000) return ( - + ) } diff --git a/src/ui/primitives/icon-button.tsx b/src/ui/primitives/icon-button.tsx new file mode 100644 index 000000000..3cfcb4b83 --- /dev/null +++ b/src/ui/primitives/icon-button.tsx @@ -0,0 +1,61 @@ +import { cn } from '@/lib/utils/ui' +import { Slot } from '@radix-ui/react-slot' +import { cva, type VariantProps } from 'class-variance-authority' +import * as React from 'react' + +const iconButtonVariants = cva( + [ + 'inline-flex items-center cursor-pointer justify-center', + 'transition-colors [&_svg]:transition-colors disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0', + '[&_svg]:text-icon-tertiary', + ].join(' '), + { + variants: { + variant: { + secondary: [ + 'h-9 w-9 [&_svg]:size-4', + 'border', + 'hover:border-stroke-active', + 'data-[display-state=hover]:border-stroke-active', + 'active:bg-bg-1', + 'data-[display-state=active]:bg-bg-1', + 'disabled:opacity-50', + 'data-[state=open]:bg-bg-1', + ].join(' '), + tertiary: [ + 'size-4 [&_svg]:size-4', + 'hover:[&_svg]:text-icon', + 'data-[display-state=hover]:[&_svg]:text-icon', + 'active:[&_svg]:text-icon', + 'data-[display-state=active]:[&_svg]:text-icon', + 'disabled:opacity-50', + ].join(' '), + }, + }, + defaultVariants: { + variant: 'tertiary', + }, + } +) + +export interface IconButtonProps + extends React.ButtonHTMLAttributes, + VariantProps { + asChild?: boolean +} + +const IconButton = React.forwardRef( + ({ className, variant, asChild = false, ...props }, ref) => { + const Comp = asChild ? Slot : 'button' + return ( + + ) + } +) +IconButton.displayName = 'IconButton' + +export { IconButton, iconButtonVariants } From b7f678deb6afa0f731c5fe26ea89da2e0c3040d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Sun, 22 Feb 2026 11:33:40 +0100 Subject: [PATCH 22/68] fix: icon button shrinking in flex containers --- src/ui/primitives/icon-button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/primitives/icon-button.tsx b/src/ui/primitives/icon-button.tsx index 3cfcb4b83..474cc2d0b 100644 --- a/src/ui/primitives/icon-button.tsx +++ b/src/ui/primitives/icon-button.tsx @@ -5,7 +5,7 @@ import * as React from 'react' const iconButtonVariants = cva( [ - 'inline-flex items-center cursor-pointer justify-center', + 'inline-flex items-center cursor-pointer justify-center shrink-0', 'transition-colors [&_svg]:transition-colors disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0', '[&_svg]:text-icon-tertiary', ].join(' '), From 2d3b17035a6d882f7abb9aa820521e6a05da13d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Sun, 22 Feb 2026 11:34:35 +0100 Subject: [PATCH 23/68] fix: copy button on account screen --- src/features/dashboard/account/user-access-token.tsx | 1 - src/features/dashboard/settings/general/info-card.tsx | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/features/dashboard/account/user-access-token.tsx b/src/features/dashboard/account/user-access-token.tsx index e7a8426db..78b6e63d2 100644 --- a/src/features/dashboard/account/user-access-token.tsx +++ b/src/features/dashboard/account/user-access-token.tsx @@ -70,7 +70,6 @@ export default function UserAccessToken({ className }: UserAccessTokenProps) { )} Team Slug {team.slug} - +
From 9f558902af003522870eba93e5e7495e1d28b971 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Sun, 22 Feb 2026 11:39:43 +0100 Subject: [PATCH 24/68] feat: tweak template build page --- src/features/dashboard/build/header.tsx | 2 -- src/features/dashboard/build/logs.tsx | 4 ++-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/features/dashboard/build/header.tsx b/src/features/dashboard/build/header.tsx index ea349c933..1363c28cc 100644 --- a/src/features/dashboard/build/header.tsx +++ b/src/features/dashboard/build/header.tsx @@ -125,8 +125,6 @@ function StatusBanner({ status, statusMessage }: StatusBannerProps) { )} diff --git a/src/features/dashboard/build/logs.tsx b/src/features/dashboard/build/logs.tsx index 9f52fd8a6..23501bed2 100644 --- a/src/features/dashboard/build/logs.tsx +++ b/src/features/dashboard/build/logs.tsx @@ -285,8 +285,8 @@ function LevelFilter({ level, onLevelChange }: LevelFilterProps) { diff --git a/src/features/dashboard/members/danger-zone.tsx b/src/features/dashboard/members/danger-zone.tsx index e42e5ce2c..6a1f22977 100644 --- a/src/features/dashboard/members/danger-zone.tsx +++ b/src/features/dashboard/members/danger-zone.tsx @@ -65,7 +65,7 @@ async function DangerZoneContent({ teamId }: { teamId: string }) { confirm="Leave" onConfirm={() => {}} trigger={ - } diff --git a/src/features/dashboard/members/member-table-row.tsx b/src/features/dashboard/members/member-table-row.tsx index 7e08ee824..0f0d37666 100644 --- a/src/features/dashboard/members/member-table-row.tsx +++ b/src/features/dashboard/members/member-table-row.tsx @@ -10,7 +10,8 @@ import { removeTeamMemberAction } from '@/server/team/team-actions' import { TeamMember } from '@/server/team/types' import { AlertDialog } from '@/ui/alert-dialog' import { Avatar, AvatarFallback, AvatarImage } from '@/ui/primitives/avatar' -import { Button } from '@/ui/primitives/button' +import { IconButton } from '@/ui/primitives/icon-button' +import { TrashIcon } from '@/ui/primitives/icons' import { TableCell, TableRow } from '@/ui/primitives/table' import { useAction } from 'next-safe-action/hooks' import { useRouter } from 'next/navigation' @@ -90,12 +91,12 @@ export default function MemberTableRow({ confirm="Remove" onConfirm={() => handleRemoveMember(member.info.id)} confirmProps={{ - loading: isRemoving, + loading: isRemoving ? 'Removing...' : undefined, }} trigger={ - + + + } open={removeDialogOpen} onOpenChange={setRemoveDialogOpen} From a452404cb60fa65621592ab0964da2458f6fddc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Sun, 22 Feb 2026 12:09:51 +0100 Subject: [PATCH 26/68] feat: tweak couple of complex components including a button primitive --- src/ui/alert-popover.tsx | 3 +-- src/ui/json-popover.tsx | 9 +++------ src/ui/polling-button.tsx | 13 ++++++------- 3 files changed, 10 insertions(+), 15 deletions(-) diff --git a/src/ui/alert-popover.tsx b/src/ui/alert-popover.tsx index 107bf017c..93eb5e853 100644 --- a/src/ui/alert-popover.tsx +++ b/src/ui/alert-popover.tsx @@ -44,12 +44,11 @@ export const AlertPopover: FC = ({ {children &&
{children}
}
- + - diff --git a/src/app/(auth)/sign-up/page.tsx b/src/app/(auth)/sign-up/page.tsx index 626f76ff7..14bcf848c 100644 --- a/src/app/(auth)/sign-up/page.tsx +++ b/src/app/(auth)/sign-up/page.tsx @@ -181,7 +181,7 @@ export default function SignUp() {
- +
diff --git a/src/ui/code-block.tsx b/src/ui/code-block.tsx index 5df0986e9..1d51277e2 100644 --- a/src/ui/code-block.tsx +++ b/src/ui/code-block.tsx @@ -129,9 +129,7 @@ export const CodeBlock = forwardRef( ) : ( allowCopy && ( ) diff --git a/src/ui/data-table.tsx b/src/ui/data-table.tsx index a950b8bc7..0a2514843 100644 --- a/src/ui/data-table.tsx +++ b/src/ui/data-table.tsx @@ -257,32 +257,32 @@ function DataTablePagination({
diff --git a/src/ui/live.tsx b/src/ui/live.tsx index 169c4586c..d27ba89ea 100644 --- a/src/ui/live.tsx +++ b/src/ui/live.tsx @@ -51,7 +51,6 @@ export function LiveBadge({ return (
- - +
@@ -27,7 +27,7 @@ export const TableFilterButton = React.forwardRef< {value} )} - + ) }) diff --git a/src/ui/theme-switcher.tsx b/src/ui/theme-switcher.tsx index e42e6dc92..746fcf963 100644 --- a/src/ui/theme-switcher.tsx +++ b/src/ui/theme-switcher.tsx @@ -1,7 +1,7 @@ 'use client' import useIsMounted from '@/lib/hooks/use-is-mounted' -import { Button } from '@/ui/primitives/button' +import { IconButton } from '@/ui/primitives/icon-button' import { DropdownMenu, DropdownMenuContent, @@ -24,18 +24,16 @@ const ThemeSwitcher = ({ className }: ThemeSwitcherProps) => { return null } - const ICON_SIZE = 16 - return ( - + -
+
- + +
@@ -196,20 +193,17 @@ export const TimeInput = memo(function TimeInput({ 'placeholder:prose-label' )} /> -
+
{getTimezoneIdentifier()} - + +
From b98addebc6cba2c0f8a91b3716b209122b209243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Tue, 24 Feb 2026 11:01:26 +0100 Subject: [PATCH 38/68] fix: remove mocks --- src/features/dashboard/sandbox/header/metadata.tsx | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/features/dashboard/sandbox/header/metadata.tsx b/src/features/dashboard/sandbox/header/metadata.tsx index ee7da40bb..8a6baa332 100644 --- a/src/features/dashboard/sandbox/header/metadata.tsx +++ b/src/features/dashboard/sandbox/header/metadata.tsx @@ -8,10 +8,7 @@ import { useSandboxContext } from '../context' export default function Metadata() { const { sandboxInfo } = useSandboxContext() - // TODO: remove mock metadata - const mockMetadata = { env: 'production', version: '1.2.3', user: 'test-user' } - - if (!sandboxInfo?.metadata && !mockMetadata) { + if (!sandboxInfo?.metadata) { return ( N/A @@ -21,7 +18,7 @@ export default function Metadata() { return ( Metadata From b5ae9b23b8ad69059ca3b0066cb665b840be2a04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Tue, 24 Feb 2026 11:21:38 +0100 Subject: [PATCH 39/68] feat: integrate ui in remaining primitives --- src/features/dashboard/sandbox/inspect/root-path-input.tsx | 2 +- src/ui/primitives/calendar.tsx | 4 ++-- src/ui/primitives/sidebar.tsx | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/features/dashboard/sandbox/inspect/root-path-input.tsx b/src/features/dashboard/sandbox/inspect/root-path-input.tsx index f80c3fcce..99a67518e 100644 --- a/src/features/dashboard/sandbox/inspect/root-path-input.tsx +++ b/src/features/dashboard/sandbox/inspect/root-path-input.tsx @@ -82,7 +82,7 @@ export default function RootPathInput({ disabled={isPending || !isDirty} type="submit" > - Go {isPending ? : } + Go {isPending ? : } ) diff --git a/src/ui/primitives/calendar.tsx b/src/ui/primitives/calendar.tsx index 8af2c369f..9be4ca2f9 100644 --- a/src/ui/primitives/calendar.tsx +++ b/src/ui/primitives/calendar.tsx @@ -255,7 +255,7 @@ function CalendarDayButton({ + Date: Tue, 24 Feb 2026 11:58:02 +0100 Subject: [PATCH 41/68] feat: update icon-button and integrate survey --- src/ui/primitives/icon-button.tsx | 10 ++++++++-- src/ui/survey.tsx | 27 +++++++++++++-------------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/ui/primitives/icon-button.tsx b/src/ui/primitives/icon-button.tsx index b776c333e..84e903a13 100644 --- a/src/ui/primitives/icon-button.tsx +++ b/src/ui/primitives/icon-button.tsx @@ -21,6 +21,7 @@ const iconButtonVariants = cva( 'data-[display-state=active]:bg-bg-1', // display active 'disabled:opacity-50', // disabled 'data-[state=open]:bg-bg-1', // open (e.g. popover trigger) + 'data-[selected=true]:bg-bg-1 data-[selected=true]:border-stroke-active data-[selected=true]:[&_svg]:text-fg', // selected ].join(' '), tertiary: [ 'size-4 [&_svg]:size-4', @@ -33,9 +34,14 @@ const iconButtonVariants = cva( 'disabled:opacity-50', // disabled ].join(' '), }, + size: { + default: '', + xl: 'size-14 [&_svg]:size-7', + }, }, defaultVariants: { variant: 'tertiary', + size: 'default', }, } ) @@ -47,11 +53,11 @@ export interface IconButtonProps } const IconButton = React.forwardRef( - ({ className, variant, asChild = false, ...props }, ref) => { + ({ className, variant, size, asChild = false, ...props }, ref) => { const Comp = asChild ? Slot : 'button' return ( diff --git a/src/ui/survey.tsx b/src/ui/survey.tsx index fa3073d0b..6950b5a10 100644 --- a/src/ui/survey.tsx +++ b/src/ui/survey.tsx @@ -1,5 +1,6 @@ import { cn } from '@/lib/utils' import { Button } from '@/ui/primitives/button' +import { IconButton } from '@/ui/primitives/icon-button' import { CardContent, CardDescription, @@ -71,19 +72,16 @@ export function SurveyContent({ ? // Emoji ratings (question.scale === 3 ? EMOJIS_3 : EMOJIS_5).map( (emoji, emojiIndex) => ( - + ) ) : // Numeric ratings @@ -104,9 +102,10 @@ export function SurveyContent({ variant={ responses[currentQuestionIndex] === String(num) ? 'primary' - : 'secondary' + : 'tertiary' } - size="icon-lg" + size="none" + className="size-9" onClick={() => setResponses((prev) => ({ ...prev, From f626b32bd2e9b81579e2993eba5af43ade6ace2c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Tue, 24 Feb 2026 12:02:15 +0100 Subject: [PATCH 42/68] refactor: remove unused sizes from button --- src/ui/primitives/button.tsx | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/ui/primitives/button.tsx b/src/ui/primitives/button.tsx index ed1f63338..25935e37f 100644 --- a/src/ui/primitives/button.tsx +++ b/src/ui/primitives/button.tsx @@ -72,10 +72,6 @@ const buttonVariants = cva( size: { default: '[&_svg]:size-4 h-9 py-1.5 gap-1 [&:has(svg)]:pr-3 [&:has(svg)]:pl-2.5 px-4', - icon: 'h-9 w-9 px-2.5 py-1.5 [&_svg]:size-4', - 'icon-sm': 'h-7 w-7 px-2.5 py-1.5 [&_svg]:size-4', - 'icon-xs': 'size-4.5 [&_svg]:size-3', - 'icon-lg': 'h-12 w-12 px-2.5 py-1.5 [&_svg]:size-6', none: 'gap-1 [&_svg]:size-4', }, }, From 5faeea711b5fb9f40d6d2a248f07a33d81966c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Tue, 24 Feb 2026 12:11:00 +0100 Subject: [PATCH 43/68] feat: increase in loading state of a button --- src/ui/primitives/button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/primitives/button.tsx b/src/ui/primitives/button.tsx index 25935e37f..0d6afcef6 100644 --- a/src/ui/primitives/button.tsx +++ b/src/ui/primitives/button.tsx @@ -96,7 +96,7 @@ const Button = React.forwardRef(
{loading} From dec28de6c5765a665f966824b315629a43465039 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Tue, 24 Feb 2026 12:18:32 +0100 Subject: [PATCH 44/68] feat: make loading state of a button always respect its variant --- src/ui/primitives/button.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/ui/primitives/button.tsx b/src/ui/primitives/button.tsx index 0d6afcef6..64822a816 100644 --- a/src/ui/primitives/button.tsx +++ b/src/ui/primitives/button.tsx @@ -93,14 +93,14 @@ const Button = React.forwardRef( ({ className, variant, size, asChild = false, loading, ...props }, ref) => { if (loading) { return ( -
{loading} -
+ ) } From 6e7f3e9c6c58085d9bed09ddaa7e83252d3cfb45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Thu, 26 Feb 2026 16:15:57 +0100 Subject: [PATCH 45/68] feat: improve the UI/UX for CopyButtonInline --- src/ui/copy-button-inline.tsx | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/ui/copy-button-inline.tsx b/src/ui/copy-button-inline.tsx index f11499901..bf815db0a 100644 --- a/src/ui/copy-button-inline.tsx +++ b/src/ui/copy-button-inline.tsx @@ -1,6 +1,6 @@ import { useClipboard } from '@/lib/hooks/use-clipboard' import { cn } from '@/lib/utils/ui' -import { useRef, useState } from 'react' +import { Check, Copy } from 'lucide-react' export default function CopyButtonInline({ value, @@ -11,33 +11,35 @@ export default function CopyButtonInline({ children: React.ReactNode className?: string }) { - const [wasCopied, copy] = useClipboard() - const buttonRef = useRef(null) - const [capturedWidth, setCapturedWidth] = useState(null) + const [wasCopied, copy] = useClipboard(2000) const handleClick = (e: React.MouseEvent) => { e.stopPropagation() - if (buttonRef.current && !wasCopied) { - setCapturedWidth(buttonRef.current.offsetWidth) - } copy(value) } return ( - {wasCopied ? 'Copied!' : children} + {children} + ) } From 4bbd64c700d5c8825c6a215a15da8c8f9e1cddfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Thu, 5 Mar 2026 22:10:57 +0100 Subject: [PATCH 46/68] feat: tweak styling of copybuttoninline --- src/ui/copy-button-inline.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/ui/copy-button-inline.tsx b/src/ui/copy-button-inline.tsx index bf815db0a..8b7c73942 100644 --- a/src/ui/copy-button-inline.tsx +++ b/src/ui/copy-button-inline.tsx @@ -1,6 +1,6 @@ import { useClipboard } from '@/lib/hooks/use-clipboard' import { cn } from '@/lib/utils/ui' -import { Check, Copy } from 'lucide-react' +import { CheckIcon, CopyIcon } from '@/ui/primitives/icons' export default function CopyButtonInline({ value, @@ -22,7 +22,7 @@ export default function CopyButtonInline({ @@ -35,9 +35,9 @@ export default function CopyButtonInline({ aria-hidden="true" > {wasCopied ? ( - + ) : ( - + )} From 5597b641b0c740c9145cef2bf73a8aa5d4966cfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Thu, 5 Mar 2026 22:11:12 +0100 Subject: [PATCH 47/68] feat: tweak styling of template build header --- src/features/dashboard/build/header-cells.tsx | 8 ++++---- src/features/dashboard/build/header.tsx | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/features/dashboard/build/header-cells.tsx b/src/features/dashboard/build/header-cells.tsx index 58ae7c517..6ac553614 100644 --- a/src/features/dashboard/build/header-cells.tsx +++ b/src/features/dashboard/build/header-cells.tsx @@ -82,9 +82,9 @@ export function RanFor({ const formattedTimestamp = formatCompactDate(finishedAt) return ( - + In {formatDurationCompact(duration)}{' '} - + ยท {formattedTimestamp} @@ -97,9 +97,9 @@ export function StartedAt({ timestamp }: { timestamp: number }) { const formattedTimestamp = formatCompactDate(timestamp) return ( - + {formatTimeAgoCompact(elapsed)}{' '} - + ยท {formattedTimestamp} diff --git a/src/features/dashboard/build/header.tsx b/src/features/dashboard/build/header.tsx index 1363c28cc..60fb4cb61 100644 --- a/src/features/dashboard/build/header.tsx +++ b/src/features/dashboard/build/header.tsx @@ -30,7 +30,7 @@ export default function BuildHeader({ {buildId} From c126358339855618d41c6bf18444a803880fbf85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Fri, 6 Mar 2026 19:47:37 +0100 Subject: [PATCH 48/68] feat: update the design of the sidebar --- src/configs/sidebar.ts | 48 ++++++++---------- .../dashboard/sidebar/blocked-banner.tsx | 2 +- src/features/dashboard/sidebar/command.tsx | 6 +-- src/features/dashboard/sidebar/content.tsx | 4 +- src/features/dashboard/sidebar/footer.tsx | 32 ++++-------- src/features/dashboard/sidebar/menu.tsx | 19 ++----- src/ui/primitives/icons.tsx | 25 ++++++++++ src/ui/primitives/kbd.tsx | 2 +- src/ui/primitives/sidebar.tsx | 50 ++++++++++++++----- 9 files changed, 104 insertions(+), 84 deletions(-) diff --git a/src/configs/sidebar.ts b/src/configs/sidebar.ts index 4ec746f18..884d523c4 100644 --- a/src/configs/sidebar.ts +++ b/src/configs/sidebar.ts @@ -1,16 +1,16 @@ -import { GaugeIcon, WebhookIcon } from '@/ui/primitives/icons' import { - Activity, - Box, - Container, - CreditCard, - Key, - LucideProps, - Settings, - UserRoundCog, - Users, -} from 'lucide-react' -import { ForwardRefExoticComponent, JSX, RefAttributes } from 'react' + AccountSettingsIcon, + CardIcon, + GaugeIcon, + KeyIcon, + PersonsIcon, + SandboxIcon, + SettingsIcon, + TemplateIcon, + UsageIcon, + WebhookIcon, +} from '@/ui/primitives/icons' +import { JSX } from 'react' import { INCLUDE_ARGUS, INCLUDE_BILLING } from './flags' import { PROTECTED_URLS } from './urls' @@ -21,12 +21,8 @@ type SidebarNavArgs = { export type SidebarNavItem = { label: string href: (args: SidebarNavArgs) => string - icon: - | ForwardRefExoticComponent< - Omit & RefAttributes - > - // eslint-disable-next-line @typescript-eslint/no-explicit-any - | ((...args: any[]) => JSX.Element) + // eslint-disable-next-line @typescript-eslint/no-explicit-any + icon: (...args: any[]) => JSX.Element group?: string activeMatch?: string } @@ -36,13 +32,13 @@ export const SIDEBAR_MAIN_LINKS: SidebarNavItem[] = [ { label: 'Sandboxes', href: (args) => PROTECTED_URLS.SANDBOXES(args.teamIdOrSlug!), - icon: Box, + icon: SandboxIcon, activeMatch: `/dashboard/*/sandboxes/**`, }, { label: 'Templates', href: (args) => PROTECTED_URLS.TEMPLATES(args.teamIdOrSlug!), - icon: Container, + icon: TemplateIcon, activeMatch: `/dashboard/*/templates/**`, }, @@ -64,21 +60,21 @@ export const SIDEBAR_MAIN_LINKS: SidebarNavItem[] = [ { label: 'General', href: (args) => PROTECTED_URLS.GENERAL(args.teamIdOrSlug!), - icon: Settings, + icon: SettingsIcon, group: 'team', activeMatch: `/dashboard/*/general`, }, { label: 'API Keys', href: (args) => PROTECTED_URLS.KEYS(args.teamIdOrSlug!), - icon: Key, + icon: KeyIcon, group: 'team', activeMatch: `/dashboard/*/keys`, }, { label: 'Members', href: (args) => PROTECTED_URLS.MEMBERS(args.teamIdOrSlug!), - icon: Users, + icon: PersonsIcon, group: 'team', activeMatch: `/dashboard/*/members`, }, @@ -90,7 +86,7 @@ export const SIDEBAR_MAIN_LINKS: SidebarNavItem[] = [ label: 'Usage', href: (args: SidebarNavArgs) => PROTECTED_URLS.USAGE(args.teamIdOrSlug!), - icon: Activity, + icon: UsageIcon, group: 'billing', activeMatch: `/dashboard/*/usage/**`, }, @@ -106,7 +102,7 @@ export const SIDEBAR_MAIN_LINKS: SidebarNavItem[] = [ label: 'Billing', href: (args: SidebarNavArgs) => PROTECTED_URLS.BILLING(args.teamIdOrSlug!), - icon: CreditCard, + icon: CardIcon, group: 'billing', activeMatch: `/dashboard/*/billing/**`, }, @@ -118,7 +114,7 @@ export const SIDEBAR_EXTRA_LINKS: SidebarNavItem[] = [ { label: 'Account Settings', href: () => PROTECTED_URLS.ACCOUNT_SETTINGS, - icon: UserRoundCog, + icon: AccountSettingsIcon, }, ] diff --git a/src/features/dashboard/sidebar/blocked-banner.tsx b/src/features/dashboard/sidebar/blocked-banner.tsx index 4ed2e4888..5fd6e7b40 100644 --- a/src/features/dashboard/sidebar/blocked-banner.tsx +++ b/src/features/dashboard/sidebar/blocked-banner.tsx @@ -55,7 +55,7 @@ export default function TeamBlockageAlert({ exit={{ opacity: 0, filter: 'blur(8px)' }} transition={{ duration: 0.4, ease: exponentialSmoothing(4) }} > - +
Team is Blocked diff --git a/src/features/dashboard/sidebar/command.tsx b/src/features/dashboard/sidebar/command.tsx index 7e766dee8..968fa6bb6 100644 --- a/src/features/dashboard/sidebar/command.tsx +++ b/src/features/dashboard/sidebar/command.tsx @@ -17,7 +17,6 @@ import { SidebarMenuItem, useSidebar, } from '@/ui/primitives/sidebar' -import { ChevronRight } from 'lucide-react' import { useRouter } from 'next/navigation' import { useState } from 'react' import { useDashboard } from '../context' @@ -53,14 +52,13 @@ export default function DashboardSidebarCommand({ tooltip="Go to..." variant={isSidebarOpen ? 'outline' : 'default'} className={cn( - 'text-fg-tertiary h-10 relative transition-all', + 'text-fg-tertiary relative transition-all pl-3 !font-normal', 'group-data-[collapsible=icon]:border-x-0 group-data-[collapsible=icon]:border-y group-data-[collapsible=icon]:!w-full group-data-[collapsible=icon]:!p-0', className )} onClick={() => setOpen(true)} > - - Go to + > Go to @@ -86,7 +86,7 @@ export default function DashboardSidebarContent() { > - + GitHub - + - + Documentation - + @@ -80,13 +70,12 @@ export default function DashboardSidebarFooter() { trigger={ - + Feedback } @@ -109,13 +98,12 @@ export default function DashboardSidebarFooter() { trigger={ - + Report Issue } diff --git a/src/features/dashboard/sidebar/menu.tsx b/src/features/dashboard/sidebar/menu.tsx index 85e37abf4..9baf07edc 100644 --- a/src/features/dashboard/sidebar/menu.tsx +++ b/src/features/dashboard/sidebar/menu.tsx @@ -20,13 +20,7 @@ import { useDashboard } from '../context' import { CreateTeamDialog } from './create-team-dialog' import DashboardSidebarMenuTeams from './menu-teams' -interface DashboardSidebarMenuProps { - className?: string -} - -export default function DashboardSidebarMenu({ - className, -}: DashboardSidebarMenuProps) { +export default function DashboardSidebarMenu() { const { team } = useDashboard() const [createTeamOpen, setCreateTeamOpen] = useState(false) @@ -41,16 +35,11 @@ export default function DashboardSidebarMenu({
- + TEAM diff --git a/src/ui/primitives/icons.tsx b/src/ui/primitives/icons.tsx index c23459b26..0e30b800d 100644 --- a/src/ui/primitives/icons.tsx +++ b/src/ui/primitives/icons.tsx @@ -1035,6 +1035,31 @@ export const AlertIcon = ({ className, ...props }: IconProps) => ( ) +export const BugIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const FeedbackIcon = ({ className, ...props }: IconProps) => ( + + + + +) + export const CodeChevronIcon = ({ className, ...props }: IconProps) => ( - {index > 0 && '+'} + {index > 0 && ' '} {formattedKey} ) diff --git a/src/ui/primitives/sidebar.tsx b/src/ui/primitives/sidebar.tsx index 30cd190cc..89286037f 100644 --- a/src/ui/primitives/sidebar.tsx +++ b/src/ui/primitives/sidebar.tsx @@ -2,7 +2,7 @@ import { Slot } from '@radix-ui/react-slot' import { VariantProps, cva } from 'class-variance-authority' -import { PanelLeftIcon } from 'lucide-react' +import { CollapseLeftIcon } from '@/ui/primitives/icons' import * as React from 'react' import { useIsMobile } from '@/lib/hooks/use-mobile' @@ -291,7 +291,7 @@ function SidebarTrigger({ }} {...props} > - + Toggle Sidebar ) @@ -427,7 +427,7 @@ function SidebarGroupLabel({ data-slot="sidebar-group-label" data-sidebar="group-label" className={cn( - 'text-fg-tertiary prose-label-highlight ring-ring flex h-8 shrink-0 items-center px-2 font-mono text-xs uppercase outline-hidden focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', + 'text-fg-tertiary prose-label-highlight ring-ring flex shrink-0 items-start px-2 pb-1.5 uppercase outline-hidden focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', SIDEBAR_TRANSITION_CLASSNAMES, 'group-data-[collapsible=icon]:opacity-0 group-data-[collapsible=icon]:h-0 duration-150', className @@ -479,7 +479,7 @@ function SidebarMenu({ className, ...props }: React.ComponentProps<'ul'>) {
    ) @@ -497,16 +497,41 @@ function SidebarMenuItem({ className, ...props }: React.ComponentProps<'li'>) { } export const sidebarMenuButtonVariants = cva( - 'peer/menu-button cursor-pointer whitespace-nowrap prose-body-highlight flex w-full items-center gap-2 overflow-hidden p-2 text-left outline-hidden ring-ring transition-[width,height,padding] focus-visible:bg-bg-highlight disabled:pointer-events-none disabled:opacity-50 group-has-data-[sidebar=menu-action]/menu-item:pr-8 aria-disabled:pointer-events-none aria-disabled:opacity-50 data-[active=true]:bg-bg-highlight data-[active=true]:text-accent-main-highlight data-[state=open]:hover:bg-bg-highlight data-[state=open]:hover:text-accent-main-highlight group-data-[collapsible=icon]:size-9! group-data-[collapsible=icon]:p-2! [&>span:last-child]:truncate [&>svg]:size-4 [&>svg]:shrink-0', + [ + // Layout + 'peer/menu-button flex w-full items-center gap-2 overflow-hidden p-2 text-left', + // Typography + 'prose-body-highlight whitespace-nowrap [&>span:last-child]:truncate', + // Icons + '[&>svg]:size-5 [&>svg]:shrink-0', + // Cursor & interaction + 'cursor-pointer outline-hidden ring-ring', + // Transitions + 'transition-[width,height,padding]', + // Hover & Focus + 'hover:bg-bg-hover', + // Focus + 'focus-visible:bg-bg-hover', + // Active/selected state + 'data-[active=true]:bg-bg-highlight data-[active=true]:text-accent-main-highlight data-[active=true]:hover:bg-bg-highlight', + // Open state (e.g. dropdown trigger) + 'data-[state=open]:bg-bg-hover', + // Disabled + 'disabled:pointer-events-none disabled:opacity-50', + 'aria-disabled:pointer-events-none aria-disabled:opacity-50', + // Menu action padding + 'group-has-data-[sidebar=menu-action]/menu-item:pr-8', + // Collapsed icon mode + 'group-data-[collapsible=icon]:size-9! group-data-[collapsible=icon]:p-2!', + ].join(' '), { variants: { variant: { - default: 'hover:bg-bg-hover', - active: 'bg-bg-hover !text-accent-main-highlight prose-body-highlight', + default: '', inverted: 'bg-bg-inverted text-fg-inverted border', outline: [ 'border border-stroke bg-transparent', - 'hover:text-accent-main-highlight ', + 'hover:bg-bg-hover', 'active:translate-y-[1px] active:shadow-none', ].join(' '), ghost: [ @@ -529,9 +554,8 @@ export const sidebarMenuButtonVariants = cva( ].join(' '), }, size: { - default: 'h-8 ', - sm: 'h-7 text-xs', - lg: 'h-12 group-data-[collapsible=icon]:p-0!', + default: 'h-9', + switcher: 'h-12 group-data-[collapsible=icon]:h-9 group-data-[collapsible=icon]:p-0! group-data-[collapsible=icon]:border-0', }, }, defaultVariants: { @@ -612,7 +636,7 @@ function SidebarMenuAction({ 'after:absolute after:-inset-2 md:after:hidden', 'peer-data-[size=sm]/menu-button:top-1', 'peer-data-[size=default]/menu-button:top-1.5', - 'peer-data-[size=lg]/menu-button:top-2.5', + 'peer-data-[size=switcher]/menu-button:top-2.5', 'group-data-[collapsible=icon]:hidden', showOnHover && 'peer-data-[active=true]/menu-button:text-accent-main-highlight group-focus-within/menu-item:opacity-100 group-hover/menu-item:opacity-100 data-[state=open]:opacity-100 md:opacity-0', @@ -636,7 +660,7 @@ function SidebarMenuBadge({ 'peer-hover/menu-button:text-accent-main-highlight peer-data-[active=true]/menu-button:text-accent-main-highlight ', 'peer-data-[size=sm]/menu-button:top-1', 'peer-data-[size=default]/menu-button:top-1.5', - 'peer-data-[size=lg]/menu-button:top-2.5', + 'peer-data-[size=switcher]/menu-button:top-2.5', 'group-data-[collapsible=icon]:hidden', className )} From 8ca133a17673f966b3ffc1b726bf8e8daa85ef91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Tue, 10 Mar 2026 11:40:18 +0100 Subject: [PATCH 49/68] feat: replace all remaining sidebar icons + tweak header gap --- src/features/dashboard/layouts/header.tsx | 2 +- src/features/dashboard/sidebar/blocked-banner.tsx | 8 ++++---- src/features/dashboard/sidebar/menu.tsx | 10 +++++----- src/features/dashboard/sidebar/sidebar-mobile.tsx | 4 ++-- src/features/dashboard/sidebar/toggle.tsx | 6 +++--- src/ui/primitives/sidebar.tsx | 10 +++++++--- 6 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/features/dashboard/layouts/header.tsx b/src/features/dashboard/layouts/header.tsx index ee5bdcae8..6a9aac67a 100644 --- a/src/features/dashboard/layouts/header.tsx +++ b/src/features/dashboard/layouts/header.tsx @@ -36,7 +36,7 @@ export default function DashboardLayoutHeader({ className )} > -
    +

    diff --git a/src/features/dashboard/sidebar/blocked-banner.tsx b/src/features/dashboard/sidebar/blocked-banner.tsx index 5fd6e7b40..654d48f0f 100644 --- a/src/features/dashboard/sidebar/blocked-banner.tsx +++ b/src/features/dashboard/sidebar/blocked-banner.tsx @@ -3,7 +3,7 @@ import { PROTECTED_URLS } from '@/configs/urls' import { cn, exponentialSmoothing } from '@/lib/utils' import { SidebarMenuButton, SidebarMenuItem } from '@/ui/primitives/sidebar' -import { AlertOctagonIcon } from 'lucide-react' +import { WarningIcon } from '@/ui/primitives/icons' import { AnimatePresence, motion } from 'motion/react' import { useRouter } from 'next/navigation' import { useMemo } from 'react' @@ -44,7 +44,7 @@ export default function TeamBlockageAlert({ 'bg-accent-error-bg text-accent-error-highlight border-accent-error-bg', }} onClick={handleClick} - className={cn('h-12 bg-accent-error-bg', { + className={cn('h-9 bg-accent-error-bg', { 'cursor-default': !handleClick, })} asChild @@ -55,9 +55,9 @@ export default function TeamBlockageAlert({ exit={{ opacity: 0, filter: 'blur(8px)' }} transition={{ duration: 0.4, ease: exponentialSmoothing(4) }} > - +
    - + Team is Blocked {team.blocked_reason && ( diff --git a/src/features/dashboard/sidebar/menu.tsx b/src/features/dashboard/sidebar/menu.tsx index 9baf07edc..8070bb7dc 100644 --- a/src/features/dashboard/sidebar/menu.tsx +++ b/src/features/dashboard/sidebar/menu.tsx @@ -13,7 +13,7 @@ import { DropdownMenuTrigger, } from '@/ui/primitives/dropdown-menu' import { SidebarMenuButton, SidebarMenuItem } from '@/ui/primitives/sidebar' -import { ChevronsUpDown, LogOut, Plus, UserRoundCog } from 'lucide-react' +import { AccountSettingsIcon, AddIcon, LogoutIcon, UnpackIcon } from '@/ui/primitives/icons' import Link from 'next/link' import { useState } from 'react' import { useDashboard } from '../context' @@ -62,7 +62,7 @@ export default function DashboardSidebarMenu() { {team.transformed_default_name || team.name}
    - + setCreateTeamOpen(true)} > - Create New Team + Create New Team @@ -88,7 +88,7 @@ export default function DashboardSidebarMenu() { asChild > - Account Settings + Account Settings @@ -97,7 +97,7 @@ export default function DashboardSidebarMenu() { className="font-sans prose-label-highlight" onSelect={handleLogout} > - Log Out + Log Out diff --git a/src/features/dashboard/sidebar/sidebar-mobile.tsx b/src/features/dashboard/sidebar/sidebar-mobile.tsx index 2d6244262..69f049458 100644 --- a/src/features/dashboard/sidebar/sidebar-mobile.tsx +++ b/src/features/dashboard/sidebar/sidebar-mobile.tsx @@ -1,6 +1,6 @@ import { cn } from '@/lib/utils' import { Drawer, DrawerContent, DrawerTrigger } from '@/ui/primitives/drawer' -import { Sidebar as SidebarIcon } from 'lucide-react' +import { MenuIcon } from '@/ui/primitives/icons' import Sidebar from './sidebar' interface SidebarMobileProps { @@ -11,7 +11,7 @@ export default function SidebarMobile({ className }: SidebarMobileProps) { return ( - + diff --git a/src/features/dashboard/sidebar/toggle.tsx b/src/features/dashboard/sidebar/toggle.tsx index 16c001feb..de1608fc7 100644 --- a/src/features/dashboard/sidebar/toggle.tsx +++ b/src/features/dashboard/sidebar/toggle.tsx @@ -7,7 +7,7 @@ import ClientOnly from '@/ui/client-only' import { IconButton } from '@/ui/primitives/icon-button' import { useSidebar } from '@/ui/primitives/sidebar' import ShortcutTooltip from '@/ui/shortcut-tooltip' -import { ArrowLeftToLine, ArrowRightFromLine } from 'lucide-react' +import { CollapseLeftIcon, ExpandRightIcon } from '@/ui/primitives/icons' import { AnimatePresence, motion } from 'motion/react' export default function DashboardSidebarToggle() { @@ -51,9 +51,9 @@ export default function DashboardSidebarToggle() { {isOpen ? ( - + ) : ( - + )} diff --git a/src/ui/primitives/sidebar.tsx b/src/ui/primitives/sidebar.tsx index 89286037f..54c6a9294 100644 --- a/src/ui/primitives/sidebar.tsx +++ b/src/ui/primitives/sidebar.tsx @@ -2,7 +2,7 @@ import { Slot } from '@radix-ui/react-slot' import { VariantProps, cva } from 'class-variance-authority' -import { CollapseLeftIcon } from '@/ui/primitives/icons' +import { CollapseLeftIcon, MenuIcon } from '@/ui/primitives/icons' import * as React from 'react' import { useIsMobile } from '@/lib/hooks/use-mobile' @@ -276,7 +276,7 @@ function SidebarTrigger({ onClick, ...props }: React.ComponentProps) { - const { toggleSidebar } = useSidebar() + const { isMobile, toggleSidebar } = useSidebar() return ( ) From 778aa64821e5b8f338492e727fa7f9f0ed1e3e45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Tue, 10 Mar 2026 12:33:23 +0100 Subject: [PATCH 50/68] fix: prevent layout jumping when collapsing the sidebar --- src/features/dashboard/sidebar/footer.tsx | 4 ++-- src/features/dashboard/sidebar/menu.tsx | 4 ++-- src/ui/primitives/sidebar.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/features/dashboard/sidebar/footer.tsx b/src/features/dashboard/sidebar/footer.tsx index 498d3ec9f..137ff3b61 100644 --- a/src/features/dashboard/sidebar/footer.tsx +++ b/src/features/dashboard/sidebar/footer.tsx @@ -71,7 +71,7 @@ export default function DashboardSidebarFooter() { @@ -99,7 +99,7 @@ export default function DashboardSidebarFooter() { diff --git a/src/features/dashboard/sidebar/menu.tsx b/src/features/dashboard/sidebar/menu.tsx index 8070bb7dc..8b258bb0f 100644 --- a/src/features/dashboard/sidebar/menu.tsx +++ b/src/features/dashboard/sidebar/menu.tsx @@ -30,7 +30,7 @@ export default function DashboardSidebarMenu() { return ( <> - + ) { data-sidebar="group" className={cn( 'relative flex w-full min-w-0 flex-col px-3 pt-3 transition-all duration-100', - 'group-data-[collapsible=icon]:p-2 group-data-[collapsible=icon]:first:border-t-0 group-data-[collapsible=icon]:border-t group-data-[collapsible=icon]:border-stroke', + 'group-data-[collapsible=icon]:px-2 group-data-[collapsible=icon]:pb-3 group-data-[collapsible=icon]:first:border-t-0 group-data-[collapsible=icon]:border-t group-data-[collapsible=icon]:border-stroke', className )} {...props} @@ -433,7 +433,7 @@ function SidebarGroupLabel({ className={cn( 'text-fg-tertiary prose-label-highlight ring-ring flex shrink-0 items-start px-2 pb-1.5 uppercase outline-hidden focus-visible:ring-2 [&>svg]:size-4 [&>svg]:shrink-0', SIDEBAR_TRANSITION_CLASSNAMES, - 'group-data-[collapsible=icon]:opacity-0 group-data-[collapsible=icon]:h-0 duration-150', + 'group-data-[collapsible=icon]:opacity-0 group-data-[collapsible=icon]:h-0 group-data-[collapsible=icon]:p-0 duration-150', className )} {...props} From 732e1543a4b34c11add7b85eb4f0fb359d76b1c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Mon, 16 Mar 2026 22:29:08 +0100 Subject: [PATCH 51/68] feat: update the default card variant styling --- src/ui/primitives/card.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/primitives/card.tsx b/src/ui/primitives/card.tsx index 9834e5e6a..38f851a5c 100644 --- a/src/ui/primitives/card.tsx +++ b/src/ui/primitives/card.tsx @@ -6,7 +6,7 @@ export const cardVariants = cva('', { variants: { variant: { default: 'bg-bg text-fg', - layer: 'bg-bg-hover/60 backdrop-blur-lg border border-stroke', + layer: 'bg-bg-1 border border-stroke', slate: '', }, }, From 1e6dfeaafa30f43d96046f4bf23e4903293c5709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Mon, 16 Mar 2026 22:47:03 +0100 Subject: [PATCH 52/68] feat: tweak the look of the account switcher dropdown --- src/features/dashboard/sidebar/menu-teams.tsx | 8 ++++---- src/features/dashboard/sidebar/menu.tsx | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/features/dashboard/sidebar/menu-teams.tsx b/src/features/dashboard/sidebar/menu-teams.tsx index 8bcaa46da..4dfcabf8f 100644 --- a/src/features/dashboard/sidebar/menu-teams.tsx +++ b/src/features/dashboard/sidebar/menu-teams.tsx @@ -97,15 +97,15 @@ export default function DashboardSidebarMenuTeams() { return ( {user?.email && ( - {user.email} + {user.email} )} {teams && teams.length > 0 ? ( teams.map((team) => ( - - + + - + {team.name?.charAt(0).toUpperCase() || '?'} diff --git a/src/features/dashboard/sidebar/menu.tsx b/src/features/dashboard/sidebar/menu.tsx index 8b258bb0f..99ff3e767 100644 --- a/src/features/dashboard/sidebar/menu.tsx +++ b/src/features/dashboard/sidebar/menu.tsx @@ -67,24 +67,24 @@ export default function DashboardSidebarMenu() { setCreateTeamOpen(true)} > Create New Team - + - + @@ -94,7 +94,7 @@ export default function DashboardSidebarMenu() { Log Out From 67865d266896abe1d91109e1e36ed3598cc552d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Mon, 16 Mar 2026 22:49:02 +0100 Subject: [PATCH 53/68] fix: font sizing in account switcher dropdown --- src/features/dashboard/sidebar/menu-teams.tsx | 2 +- src/features/dashboard/sidebar/menu.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/features/dashboard/sidebar/menu-teams.tsx b/src/features/dashboard/sidebar/menu-teams.tsx index 4dfcabf8f..5e466a2d5 100644 --- a/src/features/dashboard/sidebar/menu-teams.tsx +++ b/src/features/dashboard/sidebar/menu-teams.tsx @@ -109,7 +109,7 @@ export default function DashboardSidebarMenuTeams() { {team.name?.charAt(0).toUpperCase() || '?'} - + {team.transformed_default_name || team.name} diff --git a/src/features/dashboard/sidebar/menu.tsx b/src/features/dashboard/sidebar/menu.tsx index 99ff3e767..5eb2e595a 100644 --- a/src/features/dashboard/sidebar/menu.tsx +++ b/src/features/dashboard/sidebar/menu.tsx @@ -74,7 +74,7 @@ export default function DashboardSidebarMenu() { setCreateTeamOpen(true)} > Create New Team @@ -84,7 +84,7 @@ export default function DashboardSidebarMenu() { @@ -94,7 +94,7 @@ export default function DashboardSidebarMenu() { Log Out From fd6b59a2ed7699c428a07d77800af22d3b5258cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Mon, 16 Mar 2026 23:31:14 +0100 Subject: [PATCH 54/68] feat: replace lucide icons with our custom ones (part 1) --- .../dashboard/[teamIdOrSlug]/keys/page.tsx | 4 ++-- .../[teamIdOrSlug]/webhooks/page.tsx | 4 ++-- src/features/dashboard/build/header-cells.tsx | 1 - .../dashboard/sandbox/header/header.tsx | 2 +- .../dashboard/sandbox/header/metadata.tsx | 6 +++--- .../sandbox/header/remaining-time.tsx | 5 +++-- .../sandbox/inspect/incompatible.tsx | 9 +++++---- .../dashboard/sandbox/inspect/not-found.tsx | 5 +++-- .../sandbox/inspect/stopped-banner.tsx | 4 ++-- .../sandbox/inspect/viewer-header.tsx | 8 +++++--- .../dashboard/sandboxes/list/header.tsx | 5 +++-- .../dashboard/sandboxes/list/table-cells.tsx | 7 ++++--- .../sandboxes/list/table-filters.tsx | 6 +++--- .../dashboard/sandboxes/monitoring/header.tsx | 4 ++-- .../monitoring/time-picker/index.tsx | 4 ++-- .../dashboard/settings/keys/table-row.tsx | 4 ++-- .../settings/webhooks/add-edit-dialog.tsx | 4 ++-- .../dashboard/settings/webhooks/table-row.tsx | 20 +++++++++---------- .../templates/builds/table-cells.tsx | 1 - .../dashboard/templates/list/header.tsx | 3 ++- .../dashboard/templates/list/table-body.tsx | 7 ++++--- .../dashboard/templates/list/table-cells.tsx | 10 ++++++---- .../templates/list/table-filters.tsx | 4 ++-- .../dashboard/usage/usage-metric-chart.tsx | 4 ++-- .../usage/usage-time-range-controls.tsx | 6 +++--- src/ui/docs-code-block.tsx | 6 +++--- src/ui/error-indicator.tsx | 4 ++-- src/ui/error-tooltip.tsx | 6 +++--- src/ui/external-icon.tsx | 4 ++-- src/ui/help-tooltip.tsx | 2 +- src/ui/number-input.tsx | 6 +++--- src/ui/polling-button.tsx | 4 ++-- src/ui/primitives/calendar.tsx | 2 +- src/ui/primitives/command.tsx | 4 ++-- src/ui/primitives/dropdown-menu.tsx | 4 ++-- src/ui/primitives/sheet.tsx | 4 ++-- src/ui/primitives/toast.tsx | 4 ++-- src/ui/table-filter-button.tsx | 4 ++-- 38 files changed, 100 insertions(+), 91 deletions(-) diff --git a/src/app/dashboard/[teamIdOrSlug]/keys/page.tsx b/src/app/dashboard/[teamIdOrSlug]/keys/page.tsx index 56139bc48..cdfeda239 100644 --- a/src/app/dashboard/[teamIdOrSlug]/keys/page.tsx +++ b/src/app/dashboard/[teamIdOrSlug]/keys/page.tsx @@ -9,7 +9,7 @@ import { CardHeader, CardTitle, } from '@/ui/primitives/card' -import { Plus } from 'lucide-react' +import { AddIcon } from '@/ui/primitives/icons' interface KeysPageClientProps { params: Promise<{ @@ -38,7 +38,7 @@ export default async function KeysPage({ params }: KeysPageClientProps) {

    diff --git a/src/app/dashboard/[teamIdOrSlug]/webhooks/page.tsx b/src/app/dashboard/[teamIdOrSlug]/webhooks/page.tsx index 0d5da845b..60ee03b12 100644 --- a/src/app/dashboard/[teamIdOrSlug]/webhooks/page.tsx +++ b/src/app/dashboard/[teamIdOrSlug]/webhooks/page.tsx @@ -9,7 +9,7 @@ import { CardDescription, CardHeader, } from '@/ui/primitives/card' -import { Plus } from 'lucide-react' +import { AddIcon } from '@/ui/primitives/icons' import { notFound } from 'next/navigation' interface WebhooksPageClientProps { @@ -43,7 +43,7 @@ export default async function WebhooksPage({
    diff --git a/src/features/dashboard/build/header-cells.tsx b/src/features/dashboard/build/header-cells.tsx index 6ac553614..61ddab251 100644 --- a/src/features/dashboard/build/header-cells.tsx +++ b/src/features/dashboard/build/header-cells.tsx @@ -8,7 +8,6 @@ import { import { cn } from '@/lib/utils/ui' import CopyButtonInline from '@/ui/copy-button-inline' import { Button } from '@/ui/primitives/button' -import { ArrowUpRight } from 'lucide-react' import { useRouter } from 'next/navigation' import { useEffect, useState } from 'react' import { useTemplateTableStore } from '../templates/list/stores/table-store' diff --git a/src/features/dashboard/sandbox/header/header.tsx b/src/features/dashboard/sandbox/header/header.tsx index c9d9f57d8..b03e4b3f7 100644 --- a/src/features/dashboard/sandbox/header/header.tsx +++ b/src/features/dashboard/sandbox/header/header.tsx @@ -1,7 +1,7 @@ import { COOKIE_KEYS } from '@/configs/cookies' import { PROTECTED_URLS } from '@/configs/urls' import { SandboxInfo } from '@/types/api.types' -import { ChevronLeftIcon } from 'lucide-react' +import { ChevronLeftIcon } from '@/ui/primitives/icons' import { cookies } from 'next/headers' import Link from 'next/link' import { DetailsItem, DetailsRow } from '../../layouts/details-row' diff --git a/src/features/dashboard/sandbox/header/metadata.tsx b/src/features/dashboard/sandbox/header/metadata.tsx index 8a6baa332..20ad4ca6e 100644 --- a/src/features/dashboard/sandbox/header/metadata.tsx +++ b/src/features/dashboard/sandbox/header/metadata.tsx @@ -2,7 +2,7 @@ import { JsonPopover } from '@/ui/json-popover' import { Badge } from '@/ui/primitives/badge' -import { Braces, CircleSlash } from 'lucide-react' +import { BlockIcon, MetadataIcon } from '@/ui/primitives/icons' import { useSandboxContext } from '../context' export default function Metadata() { @@ -11,7 +11,7 @@ export default function Metadata() { if (!sandboxInfo?.metadata) { return ( - N/A + N/A ) } @@ -20,7 +20,7 @@ export default function Metadata() { - + Metadata ) diff --git a/src/features/dashboard/sandbox/header/remaining-time.tsx b/src/features/dashboard/sandbox/header/remaining-time.tsx index 1d5bf70ee..9488e5917 100644 --- a/src/features/dashboard/sandbox/header/remaining-time.tsx +++ b/src/features/dashboard/sandbox/header/remaining-time.tsx @@ -3,7 +3,8 @@ import { cn } from '@/lib/utils' import { Badge } from '@/ui/primitives/badge' import { IconButton } from '@/ui/primitives/icon-button' -import { RefreshCw, Square } from 'lucide-react' +import { RefreshIcon } from '@/ui/primitives/icons' +import { Square } from 'lucide-react' import { motion } from 'motion/react' import { useCallback, useEffect, useState } from 'react' import { useSandboxContext } from '../context' @@ -64,7 +65,7 @@ export default function RemainingTime() { onClick={refetchSandboxInfo} disabled={isSandboxInfoLoading} > - diff --git a/src/features/dashboard/sandbox/inspect/incompatible.tsx b/src/features/dashboard/sandbox/inspect/incompatible.tsx index cd08363a1..28e4f1393 100644 --- a/src/features/dashboard/sandbox/inspect/incompatible.tsx +++ b/src/features/dashboard/sandbox/inspect/incompatible.tsx @@ -14,7 +14,8 @@ import { CardHeader, CardTitle, } from '@/ui/primitives/card' -import { AlertTriangle, ArrowUpRight, ChevronLeft } from 'lucide-react' +import { WarningIcon } from '@/ui/primitives/icons' +import { ChevronLeftIcon, ExternalLinkIcon } from '@/ui/primitives/icons' import { motion } from 'motion/react' import Link from 'next/link' import { useEffect } from 'react' @@ -55,7 +56,7 @@ export default function SandboxInspectIncompatible({
    - + Incompatible template
    @@ -118,7 +119,7 @@ export default function SandboxInspectIncompatible({ asChild > - + Back to sandboxes @@ -129,7 +130,7 @@ export default function SandboxInspectIncompatible({ > Documentation{' '} - + diff --git a/src/features/dashboard/sandbox/inspect/not-found.tsx b/src/features/dashboard/sandbox/inspect/not-found.tsx index 4ca7f0503..c5ad70098 100644 --- a/src/features/dashboard/sandbox/inspect/not-found.tsx +++ b/src/features/dashboard/sandbox/inspect/not-found.tsx @@ -13,7 +13,8 @@ import { CardHeader, CardTitle, } from '@/ui/primitives/card' -import { ArrowLeft, ArrowUp, Home, RefreshCw } from 'lucide-react' +import { RefreshIcon } from '@/ui/primitives/icons' +import { ArrowLeft, ArrowUp, Home } from 'lucide-react' import { useParams, useRouter } from 'next/navigation' import { useCallback, useEffect, useState, useTransition } from 'react' import { serializeError } from 'serialize-error' @@ -119,7 +120,7 @@ export default function SandboxInspectNotFound() { className="w-full" disabled={isResetPending} > - - + {showWatcherError ? 'Live filesystem updates disabled' diff --git a/src/features/dashboard/sandbox/inspect/viewer-header.tsx b/src/features/dashboard/sandbox/inspect/viewer-header.tsx index e3727ca14..2fa4e49c6 100644 --- a/src/features/dashboard/sandbox/inspect/viewer-header.tsx +++ b/src/features/dashboard/sandbox/inspect/viewer-header.tsx @@ -1,6 +1,8 @@ import CopyButton from '@/ui/copy-button' import { IconButton } from '@/ui/primitives/icon-button' -import { Download, FileIcon, RefreshCcw, X } from 'lucide-react' +import { RefreshIcon } from '@/ui/primitives/icons' +import { CloseIcon } from '@/ui/primitives/icons' +import { Download, FileIcon } from 'lucide-react' import { motion } from 'motion/react' import { FileContentState } from './filesystem/store' @@ -46,12 +48,12 @@ export default function SandboxInspectViewerHeader({ bounce: 0, }} > - + - +
) diff --git a/src/features/dashboard/sandboxes/list/header.tsx b/src/features/dashboard/sandboxes/list/header.tsx index fdd9a8d30..5c2609151 100644 --- a/src/features/dashboard/sandboxes/list/header.tsx +++ b/src/features/dashboard/sandboxes/list/header.tsx @@ -1,6 +1,7 @@ import { PollingButton } from '@/ui/polling-button' import { Badge } from '@/ui/primitives/badge' -import { Circle, ListFilter } from 'lucide-react' +import { FilterIcon } from '@/ui/primitives/icons' +import { Circle } from 'lucide-react' import { sandboxesPollingIntervals, useSandboxTableStore, @@ -56,7 +57,7 @@ export function SandboxesHeader({ {showFilteredRowCount && ( {table.getFilteredRowModel().rows.length} filtered - + )}
diff --git a/src/features/dashboard/sandboxes/list/table-cells.tsx b/src/features/dashboard/sandboxes/list/table-cells.tsx index fc7173372..2294e9ee8 100644 --- a/src/features/dashboard/sandboxes/list/table-cells.tsx +++ b/src/features/dashboard/sandboxes/list/table-cells.tsx @@ -27,7 +27,8 @@ import { } from '@/ui/primitives/dropdown-menu' import { Loader } from '@/ui/primitives/loader' import { CellContext } from '@tanstack/react-table' -import { ArrowUpRight, MoreVertical, Trash2 } from 'lucide-react' +import { IndicatorDotsIcon } from '@/ui/primitives/icons' +import { TrashIcon } from '@/ui/primitives/icons' import { useAction } from 'next-safe-action/hooks' import { useRouter } from 'next/navigation' import React, { useMemo } from 'react' @@ -89,7 +90,7 @@ export function ActionsCell({ row }: CellContext) { {isKilling ? ( ) : ( - + )} @@ -98,7 +99,7 @@ export function ActionsCell({ row }: CellContext) { Danger Zone - + Kill diff --git a/src/features/dashboard/sandboxes/list/table-filters.tsx b/src/features/dashboard/sandboxes/list/table-filters.tsx index 8cbcb159f..b6d9df88b 100644 --- a/src/features/dashboard/sandboxes/list/table-filters.tsx +++ b/src/features/dashboard/sandboxes/list/table-filters.tsx @@ -19,7 +19,7 @@ import { Input } from '@/ui/primitives/input' import { Label } from '@/ui/primitives/label' import { Separator } from '@/ui/primitives/separator' import { TableFilterButton } from '@/ui/table-filter-button' -import { ListFilter, Plus } from 'lucide-react' +import { AddIcon, FilterIcon } from '@/ui/primitives/icons' import * as React from 'react' import { memo, useCallback } from 'react' import { useDebounceValue } from 'usehooks-ts' @@ -122,7 +122,7 @@ const TemplateFilter = memo(function TemplateFilter() { onClick={handleSubmit} disabled={!localValue.trim()} > - +
@@ -267,7 +267,7 @@ const SandboxesTableFilters = memo(function SandboxesTableFilters({ diff --git a/src/features/dashboard/sandboxes/monitoring/header.tsx b/src/features/dashboard/sandboxes/monitoring/header.tsx index 5098c2da9..666a75ea6 100644 --- a/src/features/dashboard/sandboxes/monitoring/header.tsx +++ b/src/features/dashboard/sandboxes/monitoring/header.tsx @@ -6,7 +6,7 @@ import { getTeamLimits } from '@/server/team/get-team-limits' import ErrorTooltip from '@/ui/error-tooltip' import { SemiLiveBadge } from '@/ui/live' import { Skeleton } from '@/ui/primitives/skeleton' -import { AlertTriangle } from 'lucide-react' +import { WarningIcon } from '@/ui/primitives/icons' import { Suspense } from 'react' import { ConcurrentSandboxesClient, @@ -39,7 +39,7 @@ function BaseErrorTooltip({ children }: { children: React.ReactNode }) { - + Failed diff --git a/src/features/dashboard/sandboxes/monitoring/time-picker/index.tsx b/src/features/dashboard/sandboxes/monitoring/time-picker/index.tsx index b1ca00007..656ce538b 100644 --- a/src/features/dashboard/sandboxes/monitoring/time-picker/index.tsx +++ b/src/features/dashboard/sandboxes/monitoring/time-picker/index.tsx @@ -1,7 +1,7 @@ 'use client' import { AnimatePresence, motion } from 'framer-motion' -import { ChevronRight } from 'lucide-react' +import { ChevronRightIcon } from '@/ui/primitives/icons' import { ReactNode, memo, useCallback, useEffect, useRef } from 'react' import { cn } from '@/lib/utils' @@ -253,7 +253,7 @@ export const TimePicker = memo(function TimePicker({ Custom - + diff --git a/src/features/dashboard/settings/keys/table-row.tsx b/src/features/dashboard/settings/keys/table-row.tsx index 409f8a4d6..739db90b3 100644 --- a/src/features/dashboard/settings/keys/table-row.tsx +++ b/src/features/dashboard/settings/keys/table-row.tsx @@ -21,7 +21,7 @@ import { DropdownMenuTrigger, } from '@/ui/primitives/dropdown-menu' import { TableCell, TableRow } from '@/ui/primitives/table' -import { MoreHorizontal } from 'lucide-react' +import { IndicatorDotsIcon } from '@/ui/primitives/icons' import { motion } from 'motion/react' import { useAction } from 'next-safe-action/hooks' import { useState } from 'react' @@ -135,7 +135,7 @@ export default function ApiKeyTableRow({ ease: exponentialSmoothing(5), }} > - + diff --git a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx b/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx index 4053562b4..965222e8c 100644 --- a/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx +++ b/src/features/dashboard/settings/webhooks/add-edit-dialog.tsx @@ -21,7 +21,7 @@ import { CheckIcon } from '@/ui/primitives/icons' import { Loader } from '@/ui/primitives/loader' import { zodResolver } from '@hookform/resolvers/zod' import { useHookFormAction } from '@next-safe-action/adapter-react-hook-form/hooks' -import { PlusIcon } from 'lucide-react' +import { AddIcon } from '@/ui/primitives/icons' import { useState } from 'react' import { useDashboard } from '../../context' import { WebhookAddEditDialogSteps } from './add-edit-dialog-steps' @@ -261,7 +261,7 @@ export default function WebhookAddEditDialog({ className="w-full" disabled={!isStep2Valid} > - + Add diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index 37c1748f7..592900be5 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -9,14 +9,14 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/ui/primitives/dropdown-menu' -import { TrashIcon } from '@/ui/primitives/icons' -import { TableCell, TableRow } from '@/ui/primitives/table' import { - Lock, - MoreHorizontal, - Pencil, - Webhook as WebhookIcon, -} from 'lucide-react' + EditIcon, + IndicatorDotsIcon, + PrivateIcon, + TrashIcon, + WebhookIcon, +} from '@/ui/primitives/icons' +import { TableCell, TableRow } from '@/ui/primitives/table' import { useState } from 'react' import WebhookAddEditDialog from './add-edit-dialog' import WebhookDeleteDialog from './delete-dialog' @@ -101,19 +101,19 @@ export default function WebhookTableRow({ - + e.preventDefault()}> - Edit + Edit e.preventDefault()}> - Rotate Secret + Rotate Secret diff --git a/src/features/dashboard/templates/builds/table-cells.tsx b/src/features/dashboard/templates/builds/table-cells.tsx index 4f56eed1c..9ad8700f5 100644 --- a/src/features/dashboard/templates/builds/table-cells.tsx +++ b/src/features/dashboard/templates/builds/table-cells.tsx @@ -17,7 +17,6 @@ import { Badge } from '@/ui/primitives/badge' import { Button } from '@/ui/primitives/button' import { CheckIcon, CloseIcon } from '@/ui/primitives/icons' import { Loader } from '@/ui/primitives/loader' -import { ArrowUpRight } from 'lucide-react' import { useRouter } from 'next/navigation' import { useEffect, useState } from 'react' diff --git a/src/features/dashboard/templates/list/header.tsx b/src/features/dashboard/templates/list/header.tsx index 347cb7d10..172214300 100644 --- a/src/features/dashboard/templates/list/header.tsx +++ b/src/features/dashboard/templates/list/header.tsx @@ -1,7 +1,8 @@ import { Template } from '@/types/api.types' import { Badge } from '@/ui/primitives/badge' import { Table } from '@tanstack/react-table' -import { Hexagon, ListFilter } from 'lucide-react' +import { FilterIcon } from '@/ui/primitives/icons' +import { Hexagon } from 'lucide-react' import { Suspense } from 'react' import TemplatesTableFilters from './table-filters' import { SearchInput } from './table-search' diff --git a/src/features/dashboard/templates/list/table-body.tsx b/src/features/dashboard/templates/list/table-body.tsx index ea82e4bda..a6a464025 100644 --- a/src/features/dashboard/templates/list/table-body.tsx +++ b/src/features/dashboard/templates/list/table-body.tsx @@ -3,7 +3,8 @@ import { DataTableBody, DataTableCell, DataTableRow } from '@/ui/data-table' import Empty from '@/ui/empty' import { Button } from '@/ui/primitives/button' import { flexRender, Row, Table } from '@tanstack/react-table' -import { ExternalLink, X } from 'lucide-react' +import { CloseIcon } from '@/ui/primitives/icons' +import { ExternalLinkIcon } from '@/ui/primitives/icons' import { useMemo } from 'react' import { useTemplateTableStore } from './stores/table-store' @@ -46,7 +47,7 @@ export function TemplatesTableBody({ description="No templates match your current filters" message={ } className="h-[70%] max-md:w-screen" @@ -62,7 +63,7 @@ export function TemplatesTableBody({ } diff --git a/src/features/dashboard/templates/list/table-cells.tsx b/src/features/dashboard/templates/list/table-cells.tsx index bee82b29d..a2734c940 100644 --- a/src/features/dashboard/templates/list/table-cells.tsx +++ b/src/features/dashboard/templates/list/table-cells.tsx @@ -29,7 +29,9 @@ import { import { Loader } from '@/ui/primitives/loader_d' import { useMutation, useQueryClient } from '@tanstack/react-query' import { CellContext } from '@tanstack/react-table' -import { Check, Copy, Lock, LockOpen, MoreVertical } from 'lucide-react' +import { CheckIcon, CopyIcon } from '@/ui/primitives/icons' +import { IndicatorDotsIcon } from '@/ui/primitives/icons' +import { Lock, LockOpen } from 'lucide-react' import { useMemo, useState } from 'react' import ResourceUsage from '../../common/resource-usage' import { useDashboard } from '../../context' @@ -231,7 +233,7 @@ export function ActionsCell({ {isUpdating ? ( ) : ( - + )} @@ -349,9 +351,9 @@ export function TemplateNameCell({ aria-hidden="true" > {wasCopied ? ( - + ) : ( - + )} )} diff --git a/src/features/dashboard/templates/list/table-filters.tsx b/src/features/dashboard/templates/list/table-filters.tsx index 521f9821f..185f999b5 100644 --- a/src/features/dashboard/templates/list/table-filters.tsx +++ b/src/features/dashboard/templates/list/table-filters.tsx @@ -18,7 +18,7 @@ import { import { Label } from '@/ui/primitives/label' import { Separator } from '@/ui/primitives/separator' import { TableFilterButton } from '@/ui/table-filter-button' -import { ListFilter } from 'lucide-react' +import { FilterIcon } from '@/ui/primitives/icons' import * as React from 'react' import { useDebounceValue } from 'usehooks-ts' import { useTemplateTableStore } from './stores/table-store' @@ -158,7 +158,7 @@ const TemplatesTableFilters = React.forwardRef< diff --git a/src/features/dashboard/usage/usage-metric-chart.tsx b/src/features/dashboard/usage/usage-metric-chart.tsx index 40bedb97f..ce5309635 100644 --- a/src/features/dashboard/usage/usage-metric-chart.tsx +++ b/src/features/dashboard/usage/usage-metric-chart.tsx @@ -11,7 +11,7 @@ import { } from '@/ui/primitives/card' import { Dialog, DialogContent } from '@/ui/primitives/dialog' import { DialogTitle } from '@radix-ui/react-dialog' -import { Maximize2 } from 'lucide-react' +import { UnpackIcon } from '@/ui/primitives/icons' import { useState } from 'react' import ComputeUsageChart from './compute-usage-chart' import { useUsageCharts } from './usage-charts-context' @@ -104,7 +104,7 @@ function UsageMetricChartContent({ )} aria-label="Expand chart to fullscreen" > - + )} diff --git a/src/features/dashboard/usage/usage-time-range-controls.tsx b/src/features/dashboard/usage/usage-time-range-controls.tsx index 835ce58b2..56b698633 100644 --- a/src/features/dashboard/usage/usage-time-range-controls.tsx +++ b/src/features/dashboard/usage/usage-time-range-controls.tsx @@ -13,7 +13,7 @@ import { import { Separator } from '@/ui/primitives/separator' import { TimeRangePicker, type TimeRangeValues } from '@/ui/time-range-picker' import { TimeRangePresets, type TimeRangePreset } from '@/ui/time-range-presets' -import { ChevronLeft, ChevronRight } from 'lucide-react' +import { ChevronLeftIcon, ChevronRightIcon } from '@/ui/primitives/icons' import { useCallback, useMemo, useState } from 'react' import { TIME_RANGE_PRESETS } from './constants' import { @@ -137,7 +137,7 @@ export function UsageTimeRangeControls({ className="border-r-0 px-2" title="Move back by one-quarter of the range" > - + - + ) diff --git a/src/ui/docs-code-block.tsx b/src/ui/docs-code-block.tsx index 676c753ad..1ba2f9651 100644 --- a/src/ui/docs-code-block.tsx +++ b/src/ui/docs-code-block.tsx @@ -1,5 +1,5 @@ 'use client' -import { Check, Copy } from 'lucide-react' +import { CheckIcon, CopyIcon } from './primitives/icons' import { type ButtonHTMLAttributes, type HTMLAttributes, @@ -172,8 +172,8 @@ function CopyButton({ onClick={onCopy} {...props} > - - + diff --git a/src/ui/error-indicator.tsx b/src/ui/error-indicator.tsx index 247a2ea27..638b048ad 100644 --- a/src/ui/error-indicator.tsx +++ b/src/ui/error-indicator.tsx @@ -1,7 +1,7 @@ 'use client' import { cn } from '@/lib/utils' -import { RefreshCcw } from 'lucide-react' +import { RefreshIcon } from './primitives/icons' import { useRouter } from 'next/navigation' import { useTransition } from 'react' import { Button } from './primitives/button' @@ -54,7 +54,7 @@ export function ErrorIndicator({ onClick={() => startTransition(() => router.refresh())} className="w-full max-w-md gap-1" > - Refresh diff --git a/src/ui/error-tooltip.tsx b/src/ui/error-tooltip.tsx index 42e6eea6e..b0574a711 100644 --- a/src/ui/error-tooltip.tsx +++ b/src/ui/error-tooltip.tsx @@ -1,4 +1,4 @@ -import { AlertTriangle } from 'lucide-react' +import { WarningIcon } from './primitives/icons' import { Tooltip, TooltipContent, @@ -17,11 +17,11 @@ export default function ErrorTooltip({ children, trigger }: ErrorTooltipProps) { {trigger || ( - + )} - + {children} diff --git a/src/ui/external-icon.tsx b/src/ui/external-icon.tsx index 347ad2415..3f846ef15 100644 --- a/src/ui/external-icon.tsx +++ b/src/ui/external-icon.tsx @@ -1,5 +1,5 @@ import { cn } from '@/lib/utils' -import { ChevronRight } from 'lucide-react' +import { ChevronRightIcon } from './primitives/icons' interface ExternalIconProps { className?: string @@ -7,7 +7,7 @@ interface ExternalIconProps { export default function ExternalIcon({ className }: ExternalIconProps) { return ( - = max} > - + Increase - + Decrease diff --git a/src/ui/polling-button.tsx b/src/ui/polling-button.tsx index 28d304487..c76022da5 100644 --- a/src/ui/polling-button.tsx +++ b/src/ui/polling-button.tsx @@ -8,7 +8,7 @@ import { SelectTrigger, } from '@/ui/primitives/select' import { Separator } from '@/ui/primitives/separator' -import { RefreshCw } from 'lucide-react' +import { RefreshIcon } from './primitives/icons' export interface PollingInterval { value: number @@ -58,7 +58,7 @@ export function PollingButton({ disabled={concatenatedIsRefreshing} className="mr-2" > - diff --git a/src/ui/primitives/calendar.tsx b/src/ui/primitives/calendar.tsx index 9be4ca2f9..14cfb4afe 100644 --- a/src/ui/primitives/calendar.tsx +++ b/src/ui/primitives/calendar.tsx @@ -4,7 +4,7 @@ import { ChevronDownIcon, ChevronLeftIcon, ChevronRightIcon, -} from 'lucide-react' +} from './icons' import * as React from 'react' import { DayButton, DayPicker, getDefaultClassNames } from 'react-day-picker' diff --git a/src/ui/primitives/command.tsx b/src/ui/primitives/command.tsx index 45690f6ae..b781b87c2 100644 --- a/src/ui/primitives/command.tsx +++ b/src/ui/primitives/command.tsx @@ -2,7 +2,7 @@ import { DialogTitle, type DialogProps } from '@radix-ui/react-dialog' import { Command as CommandPrimitive } from 'cmdk' -import { Search } from 'lucide-react' +import { SearchIcon } from './icons' import * as React from 'react' import { cn } from '@/lib/utils' @@ -41,7 +41,7 @@ const CommandInput = React.forwardRef< React.ComponentPropsWithoutRef >(({ className, ...props }, ref) => (
- + {children} - + )) DropdownMenuSubTrigger.displayName = diff --git a/src/ui/primitives/sheet.tsx b/src/ui/primitives/sheet.tsx index 895c3ae57..590e3d046 100644 --- a/src/ui/primitives/sheet.tsx +++ b/src/ui/primitives/sheet.tsx @@ -2,7 +2,7 @@ import * as SheetPrimitive from '@radix-ui/react-dialog' import { cva, type VariantProps } from 'class-variance-authority' -import { X } from 'lucide-react' +import { CloseIcon } from './icons' import * as React from 'react' import { cn } from '@/lib/utils' @@ -66,7 +66,7 @@ const SheetContent = React.forwardRef< > {children} - + Close diff --git a/src/ui/primitives/toast.tsx b/src/ui/primitives/toast.tsx index 82dad21aa..b9482d524 100644 --- a/src/ui/primitives/toast.tsx +++ b/src/ui/primitives/toast.tsx @@ -2,7 +2,7 @@ import * as ToastPrimitives from '@radix-ui/react-toast' import { cva, type VariantProps } from 'class-variance-authority' -import { X } from 'lucide-react' +import { CloseIcon } from './icons' import * as React from 'react' import { cn } from '@/lib/utils' @@ -88,7 +88,7 @@ const ToastClose = React.forwardRef< toast-close="" {...props} > - + )) ToastClose.displayName = ToastPrimitives.Close.displayName diff --git a/src/ui/table-filter-button.tsx b/src/ui/table-filter-button.tsx index 6b0d91410..a1c45a2db 100644 --- a/src/ui/table-filter-button.tsx +++ b/src/ui/table-filter-button.tsx @@ -1,4 +1,4 @@ -import { X } from 'lucide-react' +import { CloseIcon } from './primitives/icons' import React from 'react' import { Button } from './primitives/button' @@ -27,7 +27,7 @@ export const TableFilterButton = React.forwardRef< {value} )} - + ) }) From 5b10ec95eb5b0934630569c039994d89a3ffada7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojta=20B=C3=B6hm?= Date: Tue, 17 Mar 2026 18:33:15 +0100 Subject: [PATCH 55/68] feat: replace lucide icons with our custom ones (part 2) --- src/app/dashboard/unauthorized.tsx | 5 ++- src/features/auth/form-message.tsx | 7 ++-- .../concurrent-sandboxes-addon-dialog.tsx | 17 +++----- .../sandbox/header/remaining-time.tsx | 5 +-- .../dashboard/sandbox/header/status.tsx | 8 +--- .../dashboard/sandbox/inspect/dir.tsx | 5 ++- .../dashboard/sandbox/inspect/file.tsx | 5 ++- .../dashboard/sandboxes/list/header.tsx | 4 +- .../dashboard/templates/list/header.tsx | 1 - .../dashboard/templates/list/table-cells.tsx | 10 ++--- src/ui/primitives/icons.tsx | 40 +++++++++++++++++++ src/ui/time-input.tsx | 5 ++- 12 files changed, 72 insertions(+), 40 deletions(-) diff --git a/src/app/dashboard/unauthorized.tsx b/src/app/dashboard/unauthorized.tsx index 39c4d767b..c9673ba9a 100644 --- a/src/app/dashboard/unauthorized.tsx +++ b/src/app/dashboard/unauthorized.tsx @@ -10,7 +10,8 @@ import { CardFooter, CardHeader, } from '@/ui/primitives/card' -import { ArrowLeft, HomeIcon, ShieldX, UsersIcon } from 'lucide-react' +import { PersonsIcon } from '@/ui/primitives/icons' +import { ArrowLeft, HomeIcon, ShieldX } from 'lucide-react' import Link from 'next/link' export default function Unauthorized() { @@ -41,7 +42,7 @@ export default function Unauthorized() { diff --git a/src/features/auth/form-message.tsx b/src/features/auth/form-message.tsx index 54a28c1b1..4ddb92009 100644 --- a/src/features/auth/form-message.tsx +++ b/src/features/auth/form-message.tsx @@ -2,7 +2,8 @@ import { cn } from '@/lib/utils' import { Alert, AlertDescription } from '@/ui/primitives/alert' -import { AlertCircle, CheckCircle2, Info } from 'lucide-react' +import { AlertIcon, InfoIcon } from '@/ui/primitives/icons' +import { CheckCircle2 } from 'lucide-react' import { motion } from 'motion/react' // TODO: this type is used in more places than just authentication @@ -37,7 +38,7 @@ export function AuthFormMessage({ )} {'error' in message && ( - + {decodeURIComponent(message.error!)} @@ -45,7 +46,7 @@ export function AuthFormMessage({ )} {'message' in message && ( - + {decodeURIComponent(message.message!)} diff --git a/src/features/dashboard/billing/concurrent-sandboxes-addon-dialog.tsx b/src/features/dashboard/billing/concurrent-sandboxes-addon-dialog.tsx index 42890f2bb..78c3e5e0c 100644 --- a/src/features/dashboard/billing/concurrent-sandboxes-addon-dialog.tsx +++ b/src/features/dashboard/billing/concurrent-sandboxes-addon-dialog.tsx @@ -21,12 +21,7 @@ import { useStripe, } from '@stripe/react-stripe-js' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { - AlertCircle, - ArrowRight, - CircleDollarSign, - CreditCard, -} from 'lucide-react' +import { AlertIcon, ArrowRightIcon, CardIcon, CreditsIcon } from '@/ui/primitives/icons' import { useRouter } from 'next/navigation' import { useState } from 'react' import { useDashboard } from '../context' @@ -195,7 +190,7 @@ function DialogContent_Inner({ disabled={isProcessing} > Increase Concurrency Limit - + ) : !showPaymentForm ? ( @@ -225,7 +220,7 @@ function PaymentAuthFailedAlert() { variant="warning" className="animate-in fade-in slide-in-from-top-2 duration-300" > - + Payment authentication failed in the last attempt. Please select a new payment method or enter the same card details again to retry. @@ -262,14 +257,14 @@ function AddonFeaturesList({
  • - +

    Raises current subscription by ${monthlyPriceCents / 100}/month

  • - +

    Pay ${(amountDueCents / 100).toFixed(2)} now for the remaining time of the month @@ -380,7 +375,7 @@ function PaymentElementForm({ disabled={!stripe || isProcessing} > Increase Concurrency Limit - + ) : ( diff --git a/src/features/dashboard/sandbox/header/remaining-time.tsx b/src/features/dashboard/sandbox/header/remaining-time.tsx index 9488e5917..4b6b6e6ac 100644 --- a/src/features/dashboard/sandbox/header/remaining-time.tsx +++ b/src/features/dashboard/sandbox/header/remaining-time.tsx @@ -3,8 +3,7 @@ import { cn } from '@/lib/utils' import { Badge } from '@/ui/primitives/badge' import { IconButton } from '@/ui/primitives/icon-button' -import { RefreshIcon } from '@/ui/primitives/icons' -import { Square } from 'lucide-react' +import { DotIcon, RefreshIcon } from '@/ui/primitives/icons' import { motion } from 'motion/react' import { useCallback, useEffect, useState } from 'react' import { useSandboxContext } from '../context' @@ -46,7 +45,7 @@ export default function RemainingTime() { if (!isRunning) { return ( - Stopped + Stopped ) } diff --git a/src/features/dashboard/sandbox/header/status.tsx b/src/features/dashboard/sandbox/header/status.tsx index 2eb70507b..34a7f602a 100644 --- a/src/features/dashboard/sandbox/header/status.tsx +++ b/src/features/dashboard/sandbox/header/status.tsx @@ -1,7 +1,7 @@ 'use client' import { Badge } from '@/ui/primitives/badge' -import { Circle, Square } from 'lucide-react' +import { DotIcon } from '@/ui/primitives/icons' import { useSandboxContext } from '../context' export default function Status() { @@ -9,11 +9,7 @@ export default function Status() { return ( - {isRunning ? ( - - ) : ( - - )} + {isRunning ? 'Running' : 'Stopped'} ) diff --git a/src/features/dashboard/sandbox/inspect/dir.tsx b/src/features/dashboard/sandbox/inspect/dir.tsx index b786fbb5a..e217d65ed 100644 --- a/src/features/dashboard/sandbox/inspect/dir.tsx +++ b/src/features/dashboard/sandbox/inspect/dir.tsx @@ -2,7 +2,8 @@ import { cn } from '@/lib/utils' import { DataTableRow } from '@/ui/data-table' -import { AlertCircle, FolderClosed, FolderOpen } from 'lucide-react' +import { AlertIcon } from '@/ui/primitives/icons' +import { FolderClosed, FolderOpen } from 'lucide-react' import { AnimatePresence, motion } from 'motion/react' import SandboxInspectEmptyNode from './empty' import { FilesystemNode } from './filesystem/types' @@ -63,7 +64,7 @@ export default function SandboxInspectDir({ dir }: SandboxInspectDirProps) { /> {hasError && ( - + {error} )} diff --git a/src/features/dashboard/sandbox/inspect/file.tsx b/src/features/dashboard/sandbox/inspect/file.tsx index c6ef4e1df..80b65ed32 100644 --- a/src/features/dashboard/sandbox/inspect/file.tsx +++ b/src/features/dashboard/sandbox/inspect/file.tsx @@ -2,7 +2,8 @@ import { cn } from '@/lib/utils' import { DataTableRow } from '@/ui/data-table' -import { AlertCircle, FileIcon } from 'lucide-react' +import { AlertIcon } from '@/ui/primitives/icons' +import { FileIcon } from 'lucide-react' import { FilesystemNode } from './filesystem/types' import { useFile } from './hooks/use-file' import NodeLabel from './node-label' @@ -42,7 +43,7 @@ export default function SandboxInspectFile({ file }: SandboxInspectFileProps) { {hasError && ( - + {error} )} diff --git a/src/features/dashboard/sandboxes/list/header.tsx b/src/features/dashboard/sandboxes/list/header.tsx index 5c2609151..194a8a07b 100644 --- a/src/features/dashboard/sandboxes/list/header.tsx +++ b/src/features/dashboard/sandboxes/list/header.tsx @@ -1,7 +1,7 @@ import { PollingButton } from '@/ui/polling-button' import { Badge } from '@/ui/primitives/badge' import { FilterIcon } from '@/ui/primitives/icons' -import { Circle } from 'lucide-react' +import { DotIcon } from '@/ui/primitives/icons' import { sandboxesPollingIntervals, useSandboxTableStore, @@ -52,7 +52,7 @@ export function SandboxesHeader({

    {table.getCoreRowModel().rows.length} running - + {showFilteredRowCount && ( diff --git a/src/features/dashboard/templates/list/header.tsx b/src/features/dashboard/templates/list/header.tsx index 172214300..d532818e7 100644 --- a/src/features/dashboard/templates/list/header.tsx +++ b/src/features/dashboard/templates/list/header.tsx @@ -2,7 +2,6 @@ import { Template } from '@/types/api.types' import { Badge } from '@/ui/primitives/badge' import { Table } from '@tanstack/react-table' import { FilterIcon } from '@/ui/primitives/icons' -import { Hexagon } from 'lucide-react' import { Suspense } from 'react' import TemplatesTableFilters from './table-filters' import { SearchInput } from './table-search' diff --git a/src/features/dashboard/templates/list/table-cells.tsx b/src/features/dashboard/templates/list/table-cells.tsx index a2734c940..85d2d35ae 100644 --- a/src/features/dashboard/templates/list/table-cells.tsx +++ b/src/features/dashboard/templates/list/table-cells.tsx @@ -29,9 +29,7 @@ import { import { Loader } from '@/ui/primitives/loader_d' import { useMutation, useQueryClient } from '@tanstack/react-query' import { CellContext } from '@tanstack/react-table' -import { CheckIcon, CopyIcon } from '@/ui/primitives/icons' -import { IndicatorDotsIcon } from '@/ui/primitives/icons' -import { Lock, LockOpen } from 'lucide-react' +import { CheckIcon, CopyIcon, IndicatorDotsIcon, PrivateIcon, UnlockIcon } from '@/ui/primitives/icons' import { useMemo, useState } from 'react' import ResourceUsage from '../../common/resource-usage' import { useDashboard } from '../../context' @@ -246,12 +244,12 @@ export function ActionsCell({ > {template.public ? ( <> - + Set Internal ) : ( <> - + Set Public )} @@ -441,7 +439,7 @@ export function VisibilityCell({ size="sm" className={cn('uppercase bg-fill', !isPublic && 'pl-[3]')} > - {!isPublic && } + {!isPublic && } {isPublic ? 'Public' : 'Internal'} ) diff --git a/src/ui/primitives/icons.tsx b/src/ui/primitives/icons.tsx index 0e30b800d..9eaa2d718 100644 --- a/src/ui/primitives/icons.tsx +++ b/src/ui/primitives/icons.tsx @@ -1188,6 +1188,20 @@ export const PrivateIcon = ({ className, ...props }: IconProps) => ( ) +export const UnlockIcon = ({ className, ...props }: IconProps) => ( + + + + + +) + export const EnterpriseIcon = ({ className, ...props }: IconProps) => ( ( ) +export const ArrowRightIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const ArrowLeftIcon = ({ className, ...props }: IconProps) => ( + + + + +) + export const InvoiceIcon = ({ className, ...props }: IconProps) => ( - +
    From 85b3d19f702a98fcd824107d1fff3ac015660896 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20B=C3=B6hm?= Date: Mon, 13 Apr 2026 23:04:48 +0100 Subject: [PATCH 56/68] feat: finish replacing lucide icons with custom ones --- src/app/(auth)/auth/cli/page.tsx | 6 +- src/app/dashboard/unauthorized.tsx | 7 +- src/features/auth/form-message.tsx | 5 +- .../dashboard/account/user-access-token.tsx | 8 +- .../dashboard/sandbox/inspect/dir.tsx | 7 +- .../dashboard/sandbox/inspect/file.tsx | 3 +- .../dashboard/sandbox/inspect/not-found.tsx | 9 +- .../sandbox/inspect/parent-dir-item.tsx | 4 +- .../sandbox/inspect/root-path-input.tsx | 4 +- .../sandbox/inspect/viewer-header.tsx | 6 +- .../dashboard/sandbox/inspect/viewer.tsx | 6 +- .../settings/general/profile-picture-card.tsx | 10 +- src/ui/data-table.tsx | 14 +- src/ui/icons.tsx | 4 +- src/ui/not-found.tsx | 6 +- src/ui/primitives/icons.tsx | 175 ++++++++++++++++++ src/ui/primitives/select.tsx | 4 +- src/ui/theme-switcher.tsx | 12 +- src/ui/time-input.tsx | 3 +- 19 files changed, 228 insertions(+), 65 deletions(-) diff --git a/src/app/(auth)/auth/cli/page.tsx b/src/app/(auth)/auth/cli/page.tsx index 330a3e00d..3ac0d783e 100644 --- a/src/app/(auth)/auth/cli/page.tsx +++ b/src/app/(auth)/auth/cli/page.tsx @@ -5,7 +5,7 @@ import { encodedRedirect } from '@/lib/utils/auth' import { generateE2BUserAccessToken } from '@/lib/utils/server' import { getDefaultTeamRelation } from '@/server/auth/get-default-team' import { Alert, AlertDescription, AlertTitle } from '@/ui/primitives/alert' -import { CloudIcon, LaptopIcon, Link2Icon } from 'lucide-react' +import { CloudIcon, SystemIcon, LinkIcon } from '@/ui/primitives/icons' import { redirect } from 'next/navigation' import { Suspense } from 'react' import { serializeError } from 'serialize-error' @@ -50,10 +50,10 @@ function CLIIcons() { return (

    - + - + diff --git a/src/app/dashboard/unauthorized.tsx b/src/app/dashboard/unauthorized.tsx index c9673ba9a..428d83ef1 100644 --- a/src/app/dashboard/unauthorized.tsx +++ b/src/app/dashboard/unauthorized.tsx @@ -10,8 +10,7 @@ import { CardFooter, CardHeader, } from '@/ui/primitives/card' -import { PersonsIcon } from '@/ui/primitives/icons' -import { ArrowLeft, HomeIcon, ShieldX } from 'lucide-react' +import { PersonsIcon, ArrowLeftIcon, HomeIcon, ShieldXIcon } from '@/ui/primitives/icons' import Link from 'next/link' export default function Unauthorized() { @@ -21,7 +20,7 @@ export default function Unauthorized() {

    - +
    403 Access denied. @@ -52,7 +51,7 @@ export default function Unauthorized() { onClick={() => window.history.back()} className="w-full" > - + Go Back diff --git a/src/features/auth/form-message.tsx b/src/features/auth/form-message.tsx index 4ddb92009..9c074ec74 100644 --- a/src/features/auth/form-message.tsx +++ b/src/features/auth/form-message.tsx @@ -2,8 +2,7 @@ import { cn } from '@/lib/utils' import { Alert, AlertDescription } from '@/ui/primitives/alert' -import { AlertIcon, InfoIcon } from '@/ui/primitives/icons' -import { CheckCircle2 } from 'lucide-react' +import { AlertIcon, InfoIcon, SuccessIcon } from '@/ui/primitives/icons' import { motion } from 'motion/react' // TODO: this type is used in more places than just authentication @@ -30,7 +29,7 @@ export function AuthFormMessage({ > {'success' in message && ( - + {decodeURIComponent(message.success!)} diff --git a/src/features/dashboard/account/user-access-token.tsx b/src/features/dashboard/account/user-access-token.tsx index 1772037cb..27932b08f 100644 --- a/src/features/dashboard/account/user-access-token.tsx +++ b/src/features/dashboard/account/user-access-token.tsx @@ -6,7 +6,7 @@ import CopyButton from '@/ui/copy-button' import { IconButton } from '@/ui/primitives/icon-button' import { Input } from '@/ui/primitives/input' import { Loader } from '@/ui/primitives/loader_d' -import { Eye, EyeOff } from 'lucide-react' +import { EyeIcon, EyeOffIcon } from '@/ui/primitives/icons' import { useAction } from 'next-safe-action/hooks' import { useState } from 'react' @@ -60,12 +60,12 @@ export default function UserAccessToken({ className }: UserAccessTokenProps) { ) : token ? ( isVisible ? ( - + ) : ( - + ) ) : ( - + )} {isExpanded && isLoaded ? ( - + ) : ( - + )} setRootPath('')} disabled={isPending && pendingPath === ''} > - + Reset
  • @@ -139,7 +138,7 @@ export default function SandboxInspectNotFound() { } className="w-full" > - + Back to Sandboxes )} diff --git a/src/features/dashboard/sandbox/inspect/parent-dir-item.tsx b/src/features/dashboard/sandbox/inspect/parent-dir-item.tsx index 6491ff0fc..68ffe5e74 100644 --- a/src/features/dashboard/sandbox/inspect/parent-dir-item.tsx +++ b/src/features/dashboard/sandbox/inspect/parent-dir-item.tsx @@ -2,7 +2,7 @@ import { cn } from '@/lib/utils' import { DataTableRow } from '@/ui/data-table' -import { FolderUp } from 'lucide-react' +import { FolderUpIcon } from '@/ui/primitives/icons' import { useRouter } from 'next/navigation' import path from 'path' import { useTransition } from 'react' @@ -55,7 +55,7 @@ export default function SandboxInspectParentDirItem({ } }} > - + .. ) diff --git a/src/features/dashboard/sandbox/inspect/root-path-input.tsx b/src/features/dashboard/sandbox/inspect/root-path-input.tsx index 99a67518e..4f3f99535 100644 --- a/src/features/dashboard/sandbox/inspect/root-path-input.tsx +++ b/src/features/dashboard/sandbox/inspect/root-path-input.tsx @@ -6,7 +6,7 @@ import { cn } from '@/lib/utils' import { Button } from '@/ui/primitives/button' import { Input } from '@/ui/primitives/input' import { Loader } from '@/ui/primitives/loader_d' -import { ArrowRight } from 'lucide-react' +import { ArrowRightIcon } from '@/ui/primitives/icons' import { useRouter } from 'next/navigation' import { useEffect, useState, useTransition } from 'react' import { serializeError } from 'serialize-error' @@ -82,7 +82,7 @@ export default function RootPathInput({ disabled={isPending || !isDirty} type="submit" > - Go {isPending ? : } + Go {isPending ? : } ) diff --git a/src/features/dashboard/sandbox/inspect/viewer-header.tsx b/src/features/dashboard/sandbox/inspect/viewer-header.tsx index 2fa4e49c6..21f1e578e 100644 --- a/src/features/dashboard/sandbox/inspect/viewer-header.tsx +++ b/src/features/dashboard/sandbox/inspect/viewer-header.tsx @@ -1,8 +1,6 @@ import CopyButton from '@/ui/copy-button' import { IconButton } from '@/ui/primitives/icon-button' -import { RefreshIcon } from '@/ui/primitives/icons' -import { CloseIcon } from '@/ui/primitives/icons' -import { Download, FileIcon } from 'lucide-react' +import { RefreshIcon, CloseIcon, DownloadIcon, FileIcon } from '@/ui/primitives/icons' import { motion } from 'motion/react' import { FileContentState } from './filesystem/store' @@ -33,7 +31,7 @@ export default function SandboxInspectViewerHeader({ )} - + diff --git a/src/features/dashboard/sandbox/inspect/viewer.tsx b/src/features/dashboard/sandbox/inspect/viewer.tsx index da718a664..c216b2d39 100644 --- a/src/features/dashboard/sandbox/inspect/viewer.tsx +++ b/src/features/dashboard/sandbox/inspect/viewer.tsx @@ -7,7 +7,7 @@ import { Button } from '@/ui/primitives/button' import { Drawer, DrawerContent } from '@/ui/primitives/drawer' import { ScrollArea, ScrollBar } from '@/ui/primitives/scroll-area' import { AnimatePresence } from 'framer-motion' -import { Download } from 'lucide-react' +import { DownloadIcon } from '@/ui/primitives/icons' import { useEffect, useState } from 'react' import ShikiHighlighter, { Language } from 'react-shiki' @@ -157,7 +157,7 @@ function TextContent({ This file is empty. ) @@ -218,7 +218,7 @@ function UnreadableContent({ state, onDownload }: UnreadableContentProps) { ) diff --git a/src/features/dashboard/settings/general/profile-picture-card.tsx b/src/features/dashboard/settings/general/profile-picture-card.tsx index 5e96dd4f5..2807388e8 100644 --- a/src/features/dashboard/settings/general/profile-picture-card.tsx +++ b/src/features/dashboard/settings/general/profile-picture-card.tsx @@ -13,7 +13,7 @@ import { Avatar, AvatarFallback, AvatarImage } from '@/ui/primitives/avatar' import { Badge } from '@/ui/primitives/badge' import { cardVariants } from '@/ui/primitives/card' import { AnimatePresence, motion } from 'framer-motion' -import { ChevronsUp, ImagePlusIcon, Loader2, Pencil } from 'lucide-react' +import { SpinnerIcon, UploadIcon, PhotoIcon, EditIcon } from '@/ui/primitives/icons' import { useAction } from 'next-safe-action/hooks' import { useRef, useState } from 'react' @@ -109,10 +109,10 @@ export function ProfilePictureCard({ className }: ProfilePictureCardProps) { alt={`${team.name}'s profile picture`} /> - + Upload{' '} - + @@ -139,7 +139,7 @@ export function ProfilePictureCard({ className }: ProfilePictureCardProps) { exit="initial" transition={{ duration: 0.2, ease: exponentialSmoothing(5) }} > - + ) : isUploading ? ( - + ) : null} diff --git a/src/ui/data-table.tsx b/src/ui/data-table.tsx index 0a2514843..245ee35b4 100644 --- a/src/ui/data-table.tsx +++ b/src/ui/data-table.tsx @@ -9,11 +9,7 @@ import { } from '@/ui/primitives/select' import { Separator } from '@/ui/primitives/separator' import { Cell, Header } from '@tanstack/react-table' -import { - ArrowDownWideNarrow, - ArrowUpDown, - ArrowUpNarrowWide, -} from 'lucide-react' +import { SortAscIcon, SortDescIcon } from '@/ui/primitives/icons' import * as React from 'react' interface DataTableColumnHeaderProps @@ -69,14 +65,14 @@ function DataTableHead({ {sorting === undefined ? ( // Show the arrow for the next state based on sortDescFirst header.column.columnDef.sortDescFirst ? ( - + ) : ( - + ) ) : sorting ? ( - + ) : ( - + )} )} diff --git a/src/ui/icons.tsx b/src/ui/icons.tsx index a847a2c16..d399cc40a 100644 --- a/src/ui/icons.tsx +++ b/src/ui/icons.tsx @@ -1,6 +1,6 @@ import { cn } from '@/lib/utils' import type { LucideIcon } from 'lucide-react' -import { TerminalIcon } from 'lucide-react' +import { TerminalCustomIcon } from '@/ui/primitives/icons' import { type HTMLAttributes } from 'react' import { IconBaseProps } from 'react-icons/lib' @@ -21,7 +21,7 @@ export function IconContainer({ {Icon ? ( ) : ( - + )} ) diff --git a/src/ui/not-found.tsx b/src/ui/not-found.tsx index f3a434923..659e3d70c 100644 --- a/src/ui/not-found.tsx +++ b/src/ui/not-found.tsx @@ -1,7 +1,7 @@ 'use client' import { PROTECTED_URLS } from '@/configs/urls' -import { ArrowLeft, HomeIcon, LayoutDashboard } from 'lucide-react' +import { ArrowLeftIcon, HomeIcon, DashboardIcon } from './primitives/icons' import Link from 'next/link' import { Button } from './primitives/button' import { @@ -36,7 +36,7 @@ export default function NotFound() { @@ -46,7 +46,7 @@ export default function NotFound() { onClick={() => window.history.back()} className="w-full" > - + Go Back
    diff --git a/src/ui/primitives/icons.tsx b/src/ui/primitives/icons.tsx index 9eaa2d718..495d2fcd1 100644 --- a/src/ui/primitives/icons.tsx +++ b/src/ui/primitives/icons.tsx @@ -1421,6 +1421,161 @@ export const ArrowLeftIcon = ({ className, ...props }: IconProps) => ( ) +export const ArrowUpIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const HomeIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const MoonIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const SunIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const SystemIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const CalendarIcon = ({ className, ...props }: IconProps) => ( + + + + + + +) + +export const CloudIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const LinkIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const SelectIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const SuccessIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const EyeIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const EyeOffIcon = ({ className, ...props }: IconProps) => ( + + + + + + + +) + +export const DownloadIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const FolderIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const FolderOpenIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const FolderUpIcon = ({ className, ...props }: IconProps) => ( + + + + + +) + +export const FileIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const SpinnerIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const TerminalCustomIcon = ({ className, ...props }: IconProps) => ( + + + + +) + +export const SortIcon = ({ className, ...props }: IconProps) => ( + + + + + + +) + +export const SortAscIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const SortDescIcon = ({ className, ...props }: IconProps) => ( + + + +) + export const InvoiceIcon = ({ className, ...props }: IconProps) => ( ( ) +export const DashboardIcon = ({ className, ...props }: IconProps) => ( + + + + + +) + +export const ShieldXIcon = ({ className, ...props }: IconProps) => ( + + + +) + +export const UploadIcon = ({ className, ...props }: IconProps) => ( + + + +) + export const WebhookIcon = ({ className, ...props }: IconProps) => ( {children} - diff --git a/src/ui/theme-switcher.tsx b/src/ui/theme-switcher.tsx index 746fcf963..1b2ce5284 100644 --- a/src/ui/theme-switcher.tsx +++ b/src/ui/theme-switcher.tsx @@ -9,7 +9,7 @@ import { DropdownMenuRadioItem, DropdownMenuTrigger, } from '@/ui/primitives/dropdown-menu' -import { Laptop, Moon, Sun } from 'lucide-react' +import { MoonIcon, SunIcon, SystemIcon } from '@/ui/primitives/icons' import { useTheme } from 'next-themes' interface ThemeSwitcherProps { @@ -29,9 +29,9 @@ const ThemeSwitcher = ({ className }: ThemeSwitcherProps) => { {resolvedTheme === 'light' ? ( - + ) : ( - + )} @@ -44,21 +44,21 @@ const ThemeSwitcher = ({ className }: ThemeSwitcherProps) => { className="flex items-center gap-2" value="light" > - + Light - + Dark - + System diff --git a/src/ui/time-input.tsx b/src/ui/time-input.tsx index f5545adb7..3885ee923 100644 --- a/src/ui/time-input.tsx +++ b/src/ui/time-input.tsx @@ -1,7 +1,6 @@ 'use client' -import { TimeIcon } from './primitives/icons' -import { CalendarIcon } from 'lucide-react' +import { TimeIcon, CalendarIcon } from './primitives/icons' import { memo, useCallback, useEffect, useState } from 'react' import { cn } from '@/lib/utils' From 3e724d5dbc0b54e53dbed59eb0b6d47b7381ff5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20B=C3=B6hm?= Date: Mon, 13 Apr 2026 23:06:43 +0100 Subject: [PATCH 57/68] refactor: replace LucideIcon with custom Icon --- src/ui/icons.tsx | 5 ++--- src/ui/primitives/icons.tsx | 4 +++- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/ui/icons.tsx b/src/ui/icons.tsx index d399cc40a..79e78b706 100644 --- a/src/ui/icons.tsx +++ b/src/ui/icons.tsx @@ -1,6 +1,5 @@ import { cn } from '@/lib/utils' -import type { LucideIcon } from 'lucide-react' -import { TerminalCustomIcon } from '@/ui/primitives/icons' +import { TerminalCustomIcon, type Icon } from '@/ui/primitives/icons' import { type HTMLAttributes } from 'react' import { IconBaseProps } from 'react-icons/lib' @@ -8,7 +7,7 @@ export function IconContainer({ icon: Icon, ...props }: HTMLAttributes & { - icon?: LucideIcon + icon?: Icon }): React.ReactElement { return (
    { +export interface IconProps extends React.SVGProps { className?: string height?: number width?: number } +export type Icon = React.ComponentType + export const SandboxIcon = ({ className, ...props }: IconProps) => ( Date: Mon, 13 Apr 2026 23:07:46 +0100 Subject: [PATCH 58/68] refactor: get rid of lucide-react package --- bun.lock | 3 --- package.json | 1 - 2 files changed, 4 deletions(-) diff --git a/bun.lock b/bun.lock index a1e644071..74d68b3c2 100644 --- a/bun.lock +++ b/bun.lock @@ -76,7 +76,6 @@ "file-type": "^21.3.0", "geist": "^1.3.1", "immer": "^10.1.1", - "lucide-react": "^0.525.0", "micromatch": "^4.0.8", "motion": "^12.23.25", "nanoid": "^5.0.9", @@ -2134,8 +2133,6 @@ "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], - "lucide-react": ["lucide-react@0.525.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Tm1txJ2OkymCGkvwoHt33Y2JpN5xucVq1slHcgE6Lk0WjDfjgKWor5CdVER8U6DvcfMwh4M8XxmpTiyzfmfDYQ=="], - "lz-string": ["lz-string@1.5.0", "", { "bin": { "lz-string": "bin/bin.js" } }, "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ=="], "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], diff --git a/package.json b/package.json index d8269cddf..bd603e61b 100644 --- a/package.json +++ b/package.json @@ -113,7 +113,6 @@ "file-type": "^21.3.0", "geist": "^1.3.1", "immer": "^10.1.1", - "lucide-react": "^0.525.0", "micromatch": "^4.0.8", "motion": "^12.23.25", "nanoid": "^5.0.9", From fd07ef102cd075fc0018d0ccad553a345f01c76f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20B=C3=B6hm?= Date: Mon, 13 Apr 2026 23:11:31 +0100 Subject: [PATCH 59/68] feat: make icons 16x16 by default --- src/ui/primitives/icons.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/primitives/icons.tsx b/src/ui/primitives/icons.tsx index f1c93b3e7..edb64fdfa 100644 --- a/src/ui/primitives/icons.tsx +++ b/src/ui/primitives/icons.tsx @@ -1,7 +1,7 @@ import { cn } from '@/lib/utils/index' import React from 'react' -const DEFAULT_CLASS_NAMES = 'size-6' +const DEFAULT_CLASS_NAMES = 'size-4' export interface IconProps extends React.SVGProps { className?: string From 87416f20fc34130fbecea2f82084ce002d2661ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20B=C3=B6hm?= Date: Mon, 13 Apr 2026 23:24:55 +0100 Subject: [PATCH 60/68] fix: incorrect transparent error tooltip --- src/styles/theme.css | 3 +++ src/ui/error-tooltip.tsx | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/styles/theme.css b/src/styles/theme.css index 75f4c92f2..93d71efe2 100644 --- a/src/styles/theme.css +++ b/src/styles/theme.css @@ -88,6 +88,7 @@ --accent-error-highlight: #ff4400; --accent-error-bg: rgba(255, 68, 0, 0.16); --accent-error-bg-large: rgba(255, 68, 0, 0.08); + --accent-error-bg-large-solid: #faebe6; --accent-secondary-error-highlight: #ff8763; --accent-secondary-error-bg: rgb(255, 135, 99, 0.16); @@ -187,6 +188,7 @@ --accent-error-highlight: #f54545; --accent-error-bg: rgba(245, 69, 69, 0.16); --accent-error-bg-large: rgba(245, 69, 69, 0.08); + --accent-error-bg-large-solid: #1d0f0f; --accent-secondary-error-highlight: #ff8763; --accent-secondary-error-bg: rgba(255, 135, 99, 0.16); @@ -297,6 +299,7 @@ --color-accent-error-highlight: var(--accent-error-highlight); --color-accent-error-bg: var(--accent-error-bg); --color-accent-error-bg-large: var(--accent-error-bg-large); + --color-accent-error-bg-large-solid: var(--accent-error-bg-large-solid); --color-accent-secondary-error-highlight: var( --accent-secondary-error-highlight diff --git a/src/ui/error-tooltip.tsx b/src/ui/error-tooltip.tsx index b0574a711..30f7fb166 100644 --- a/src/ui/error-tooltip.tsx +++ b/src/ui/error-tooltip.tsx @@ -20,7 +20,7 @@ export default function ErrorTooltip({ children, trigger }: ErrorTooltipProps) { )} - + {children} From 3250f30be0a9256bbc301c90aebf22d848cc6f62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20B=C3=B6hm?= Date: Tue, 14 Apr 2026 10:10:52 +0100 Subject: [PATCH 61/68] refactor: use classname instead of direct unsuported props --- src/app/(auth)/auth/cli/page.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/(auth)/auth/cli/page.tsx b/src/app/(auth)/auth/cli/page.tsx index 3ac0d783e..aeaa74e6e 100644 --- a/src/app/(auth)/auth/cli/page.tsx +++ b/src/app/(auth)/auth/cli/page.tsx @@ -50,13 +50,13 @@ function CLIIcons() { return (

    - + - + - +

    ) From cbbf24df91dc45a9593152eaaa624cee3f34f529 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20B=C3=B6hm?= Date: Tue, 14 Apr 2026 10:17:13 +0100 Subject: [PATCH 62/68] refactor: nitpicks --- src/ui/polling-button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/polling-button.tsx b/src/ui/polling-button.tsx index c76022da5..06bf2058c 100644 --- a/src/ui/polling-button.tsx +++ b/src/ui/polling-button.tsx @@ -52,7 +52,7 @@ export function PollingButton({ const concatenatedIsRefreshing = isRefreshingProp || isRefreshing return ( -
    +
    Date: Tue, 14 Apr 2026 10:27:35 +0100 Subject: [PATCH 63/68] fix: dangling old icon reference --- src/features/dashboard/sandbox/inspect/viewer.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/features/dashboard/sandbox/inspect/viewer.tsx b/src/features/dashboard/sandbox/inspect/viewer.tsx index c216b2d39..c77b51218 100644 --- a/src/features/dashboard/sandbox/inspect/viewer.tsx +++ b/src/features/dashboard/sandbox/inspect/viewer.tsx @@ -229,7 +229,7 @@ function UnreadableContent({ state, onDownload }: UnreadableContentProps) { This file is not readable.
    ) From 3f9f7112184e09f3f3e12c94b264f4907bb66902 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20B=C3=B6hm?= Date: Tue, 14 Apr 2026 10:29:20 +0100 Subject: [PATCH 64/68] fix: out of range animation value --- src/ui/copy-button.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ui/copy-button.tsx b/src/ui/copy-button.tsx index 6ee49e38e..63631f7ec 100644 --- a/src/ui/copy-button.tsx +++ b/src/ui/copy-button.tsx @@ -28,7 +28,7 @@ const CopyButton: FC = ({ value, onCopy, ...props }) => { From 06c621891c2fe53e481eab89ccb5941b2c362f20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vojt=C4=9Bch=20B=C3=B6hm?= Date: Tue, 14 Apr 2026 12:37:29 +0100 Subject: [PATCH 65/68] refactor: formatting --- bun.lock | 2 + src/app/(auth)/auth/cli/page.tsx | 2 +- src/app/(auth)/confirm/page.tsx | 4 +- src/app/(auth)/sign-in/page.tsx | 5 +- src/app/dashboard/unauthorized.tsx | 7 +- src/configs/sidebar.ts | 2 +- src/features/auth/form-message.tsx | 2 +- src/features/auth/oauth-provider-buttons.tsx | 8 +- .../dashboard/account/user-access-token.tsx | 8 +- src/features/dashboard/billing/addons.tsx | 6 +- .../concurrent-sandboxes-addon-dialog.tsx | 9 +- .../dashboard/billing/select-plan.tsx | 4 +- src/features/dashboard/build/header-cells.tsx | 8 +- src/features/dashboard/limits/limit-form.tsx | 2 +- .../dashboard/sandbox/header/kill-button.tsx | 6 +- .../dashboard/sandbox/header/metadata.tsx | 4 +- .../sandbox/header/remaining-time.tsx | 8 +- .../dashboard/sandbox/header/status.tsx | 8 +- .../dashboard/sandbox/inspect/dir.tsx | 2 +- .../sandbox/inspect/incompatible.tsx | 6 +- .../dashboard/sandbox/inspect/not-found.tsx | 7 +- .../sandbox/inspect/parent-dir-item.tsx | 2 +- .../sandbox/inspect/root-path-input.tsx | 2 +- .../sandbox/inspect/viewer-header.tsx | 9 +- .../dashboard/sandbox/inspect/viewer.tsx | 2 +- .../sandboxes/list/table-filters.tsx | 2 +- .../dashboard/sandboxes/monitoring/header.tsx | 2 +- .../monitoring/time-picker/index.tsx | 3 +- .../dashboard/settings/general/name-card.tsx | 2 +- .../settings/general/profile-picture-card.tsx | 7 +- .../dashboard/settings/keys/table-row.tsx | 4 +- .../dashboard/settings/webhooks/table-row.tsx | 5 +- .../dashboard/sidebar/blocked-banner.tsx | 2 +- src/features/dashboard/sidebar/command.tsx | 4 +- src/features/dashboard/sidebar/footer.tsx | 8 +- src/features/dashboard/sidebar/menu-teams.tsx | 9 +- src/features/dashboard/sidebar/menu.tsx | 12 +- src/features/dashboard/sidebar/toggle.tsx | 8 +- .../dashboard/templates/list/table-cells.tsx | 16 +- .../templates/list/table-filters.tsx | 7 +- .../dashboard/usage/usage-metric-chart.tsx | 6 +- .../usage/usage-time-range-controls.tsx | 2 +- src/lib/hooks/use-clipboard.ts | 24 +- src/ui/alert-popover.tsx | 6 +- src/ui/copy-button.tsx | 4 +- src/ui/data-table.tsx | 2 +- src/ui/docs-code-block.tsx | 6 +- src/ui/error-indicator.tsx | 2 +- src/ui/icons.tsx | 4 +- src/ui/json-popover.tsx | 5 +- src/ui/not-found.tsx | 2 +- src/ui/number-input.tsx | 14 +- src/ui/primitives/badge.tsx | 2 +- src/ui/primitives/button.tsx | 2 +- src/ui/primitives/calendar.tsx | 9 +- src/ui/primitives/command.tsx | 3 +- src/ui/primitives/dialog.tsx | 4 +- src/ui/primitives/dropdown-menu.tsx | 3 +- src/ui/primitives/icon-button.tsx | 2 +- src/ui/primitives/icons.tsx | 612 +++++++++++++++--- src/ui/primitives/radio-group.tsx | 2 +- src/ui/primitives/sheet.tsx | 3 +- src/ui/primitives/sidebar.tsx | 6 +- src/ui/primitives/sonner.tsx | 20 +- src/ui/primitives/toast.tsx | 3 +- src/ui/survey.tsx | 2 +- src/ui/table-filter-button.tsx | 2 +- src/ui/theme-switcher.tsx | 2 +- src/ui/time-input.tsx | 14 +- 69 files changed, 721 insertions(+), 263 deletions(-) diff --git a/bun.lock b/bun.lock index 6e819fa9d..ecd7a16e0 100644 --- a/bun.lock +++ b/bun.lock @@ -152,6 +152,8 @@ }, }, "overrides": { + "@nodelib/fs.scandir": "2.1.5", + "@nodelib/fs.stat": "2.0.5", "@shikijs/core": "2.3.2", "@shikijs/themes": "2.3.2", "shiki": "2.3.2", diff --git a/src/app/(auth)/auth/cli/page.tsx b/src/app/(auth)/auth/cli/page.tsx index 5b23dc65f..b6ad3aafd 100644 --- a/src/app/(auth)/auth/cli/page.tsx +++ b/src/app/(auth)/auth/cli/page.tsx @@ -1,4 +1,3 @@ -import { CloudIcon, SystemIcon, LinkIcon } from '@/ui/primitives/icons' import { redirect } from 'next/navigation' import { Suspense } from 'react' import { AUTH_URLS, PROTECTED_URLS } from '@/configs/urls' @@ -8,6 +7,7 @@ import { createClient } from '@/core/shared/clients/supabase/server' import { encodedRedirect } from '@/lib/utils/auth' import { generateE2BUserAccessToken } from '@/lib/utils/server' import { Alert, AlertDescription, AlertTitle } from '@/ui/primitives/alert' +import { CloudIcon, LinkIcon, SystemIcon } from '@/ui/primitives/icons' // Types type CLISearchParams = Promise<{ diff --git a/src/app/(auth)/confirm/page.tsx b/src/app/(auth)/confirm/page.tsx index 559ba7c8d..6bdb0c085 100644 --- a/src/app/(auth)/confirm/page.tsx +++ b/src/app/(auth)/confirm/page.tsx @@ -115,7 +115,9 @@ export default function ConfirmPage() {
    diff --git a/src/app/dashboard/unauthorized.tsx b/src/app/dashboard/unauthorized.tsx index 5268e8912..24d4c45a5 100644 --- a/src/app/dashboard/unauthorized.tsx +++ b/src/app/dashboard/unauthorized.tsx @@ -11,7 +11,12 @@ import { CardFooter, CardHeader, } from '@/ui/primitives/card' -import { PersonsIcon, ArrowLeftIcon, HomeIcon, ShieldXIcon } from '@/ui/primitives/icons' +import { + ArrowLeftIcon, + HomeIcon, + PersonsIcon, + ShieldXIcon, +} from '@/ui/primitives/icons' export default function Unauthorized() { return ( diff --git a/src/configs/sidebar.ts b/src/configs/sidebar.ts index 8886a71bb..7ca6f3ed0 100644 --- a/src/configs/sidebar.ts +++ b/src/configs/sidebar.ts @@ -1,3 +1,4 @@ +import { JSX } from 'react' import { AccountSettingsIcon, CardIcon, @@ -10,7 +11,6 @@ import { UsageIcon, WebhookIcon, } from '@/ui/primitives/icons' -import { JSX } from 'react' import { INCLUDE_ARGUS, INCLUDE_BILLING } from './flags' import { PROTECTED_URLS } from './urls' diff --git a/src/features/auth/form-message.tsx b/src/features/auth/form-message.tsx index 2f0312afd..f79a6b4ac 100644 --- a/src/features/auth/form-message.tsx +++ b/src/features/auth/form-message.tsx @@ -1,9 +1,9 @@ 'use client' -import { AlertIcon, InfoIcon, SuccessIcon } from '@/ui/primitives/icons' import { motion } from 'motion/react' import { cn } from '@/lib/utils' import { Alert, AlertDescription } from '@/ui/primitives/alert' +import { AlertIcon, InfoIcon, SuccessIcon } from '@/ui/primitives/icons' // TODO: this type is used in more places than just authentication // -> should probably be renamed / moved to a more appropriate location diff --git a/src/features/auth/oauth-provider-buttons.tsx b/src/features/auth/oauth-provider-buttons.tsx index 526bdc342..4f6dc6f38 100644 --- a/src/features/auth/oauth-provider-buttons.tsx +++ b/src/features/auth/oauth-provider-buttons.tsx @@ -15,7 +15,9 @@ export function OAuthProviders() {
    ) diff --git a/src/features/dashboard/billing/addons.tsx b/src/features/dashboard/billing/addons.tsx index 97afc428e..70e2e123c 100644 --- a/src/features/dashboard/billing/addons.tsx +++ b/src/features/dashboard/billing/addons.tsx @@ -117,11 +117,7 @@ function AvailableAddons({
    ) : ( - )} diff --git a/src/features/dashboard/billing/concurrent-sandboxes-addon-dialog.tsx b/src/features/dashboard/billing/concurrent-sandboxes-addon-dialog.tsx index 9d7e7bdbb..a2f2a16be 100644 --- a/src/features/dashboard/billing/concurrent-sandboxes-addon-dialog.tsx +++ b/src/features/dashboard/billing/concurrent-sandboxes-addon-dialog.tsx @@ -7,7 +7,6 @@ import { useStripe, } from '@stripe/react-stripe-js' import { useMutation, useQueryClient } from '@tanstack/react-query' -import { AlertIcon, ArrowRightIcon, CardIcon, CreditsIcon } from '@/ui/primitives/icons' import { useRouter } from 'next/navigation' import { useState } from 'react' import { DASHBOARD_TEAMS_LIST_QUERY_OPTIONS } from '@/core/application/teams/queries' @@ -23,7 +22,13 @@ import { DialogHeader, DialogTitle, } from '@/ui/primitives/dialog' -import { SandboxIcon } from '@/ui/primitives/icons' +import { + AlertIcon, + ArrowRightIcon, + CardIcon, + CreditsIcon, + SandboxIcon, +} from '@/ui/primitives/icons' import { Loader } from '@/ui/primitives/loader' import { useDashboard } from '../context' import { diff --git a/src/features/dashboard/billing/select-plan.tsx b/src/features/dashboard/billing/select-plan.tsx index c97f375e5..5fd02aafa 100644 --- a/src/features/dashboard/billing/select-plan.tsx +++ b/src/features/dashboard/billing/select-plan.tsx @@ -198,9 +198,7 @@ function PlanCard({
    {isCurrentPlan ? ( - + ) : (
    diff --git a/src/features/dashboard/build/header-cells.tsx b/src/features/dashboard/build/header-cells.tsx index e57df0cbb..423013c16 100644 --- a/src/features/dashboard/build/header-cells.tsx +++ b/src/features/dashboard/build/header-cells.tsx @@ -85,9 +85,7 @@ export function RanFor({ className="whitespace-nowrap text-fg-secondary group/time" > In {formatDurationCompact(duration)}{' '} - - ยท {formattedTimestamp} - + ยท {formattedTimestamp} ) } @@ -103,9 +101,7 @@ export function StartedAt({ timestamp }: { timestamp: number }) { className="whitespace-nowrap text-fg-secondary group/time" > {formatTimeAgoCompact(elapsed)}{' '} - - ยท {formattedTimestamp} - + ยท {formattedTimestamp} ) } diff --git a/src/features/dashboard/limits/limit-form.tsx b/src/features/dashboard/limits/limit-form.tsx index 53ea70210..4e5bd7d35 100644 --- a/src/features/dashboard/limits/limit-form.tsx +++ b/src/features/dashboard/limits/limit-form.tsx @@ -189,7 +189,7 @@ export default function LimitForm({ {originalValue !== null && ( diff --git a/src/features/dashboard/sandbox/header/metadata.tsx b/src/features/dashboard/sandbox/header/metadata.tsx index 423790a14..708e14420 100644 --- a/src/features/dashboard/sandbox/header/metadata.tsx +++ b/src/features/dashboard/sandbox/header/metadata.tsx @@ -17,9 +17,7 @@ export default function Metadata() { } return ( - + Metadata diff --git a/src/features/dashboard/sandbox/header/remaining-time.tsx b/src/features/dashboard/sandbox/header/remaining-time.tsx index 77878cefd..56d61b383 100644 --- a/src/features/dashboard/sandbox/header/remaining-time.tsx +++ b/src/features/dashboard/sandbox/header/remaining-time.tsx @@ -1,12 +1,12 @@ 'use client' -import { IconButton } from '@/ui/primitives/icon-button' -import { DotIcon, RefreshIcon } from '@/ui/primitives/icons' import { motion } from 'motion/react' import { useCallback, useEffect, useState } from 'react' import { cn } from '@/lib/utils' import { Badge } from '@/ui/primitives/badge' import { Button } from '@/ui/primitives/button' +import { IconButton } from '@/ui/primitives/icon-button' +import { DotIcon, RefreshIcon } from '@/ui/primitives/icons' import { useSandboxContext } from '../context' export default function RemainingTime() { @@ -66,7 +66,9 @@ export default function RemainingTime() { disabled={isSandboxInfoLoading} > diff --git a/src/features/dashboard/sandbox/header/status.tsx b/src/features/dashboard/sandbox/header/status.tsx index 4409f433b..da92dd441 100644 --- a/src/features/dashboard/sandbox/header/status.tsx +++ b/src/features/dashboard/sandbox/header/status.tsx @@ -19,7 +19,13 @@ export default function Status() { return ( - + {isRunning ? 'Running' : 'Stopped'} ) diff --git a/src/features/dashboard/sandbox/inspect/dir.tsx b/src/features/dashboard/sandbox/inspect/dir.tsx index 5b56aadb7..beda65082 100644 --- a/src/features/dashboard/sandbox/inspect/dir.tsx +++ b/src/features/dashboard/sandbox/inspect/dir.tsx @@ -1,9 +1,9 @@ 'use client' -import { AlertIcon, FolderIcon, FolderOpenIcon } from '@/ui/primitives/icons' import { AnimatePresence, motion } from 'motion/react' import { cn } from '@/lib/utils' import { DataTableRow } from '@/ui/data-table' +import { AlertIcon, FolderIcon, FolderOpenIcon } from '@/ui/primitives/icons' import SandboxInspectEmptyNode from './empty-node' import type { FilesystemNode } from './filesystem/types' import { useDirectory } from './hooks/use-directory' diff --git a/src/features/dashboard/sandbox/inspect/incompatible.tsx b/src/features/dashboard/sandbox/inspect/incompatible.tsx index 892cea096..46d6ad417 100644 --- a/src/features/dashboard/sandbox/inspect/incompatible.tsx +++ b/src/features/dashboard/sandbox/inspect/incompatible.tsx @@ -17,7 +17,11 @@ import { CardHeader, CardTitle, } from '@/ui/primitives/card' -import { ChevronLeftIcon, ExternalLinkIcon, WarningIcon } from '@/ui/primitives/icons' +import { + ChevronLeftIcon, + ExternalLinkIcon, + WarningIcon, +} from '@/ui/primitives/icons' interface SandboxInspectIncompatibleProps { templateNameOrId?: string diff --git a/src/features/dashboard/sandbox/inspect/not-found.tsx b/src/features/dashboard/sandbox/inspect/not-found.tsx index fdadf17bc..05648b47d 100644 --- a/src/features/dashboard/sandbox/inspect/not-found.tsx +++ b/src/features/dashboard/sandbox/inspect/not-found.tsx @@ -1,6 +1,5 @@ 'use client' -import { ArrowLeftIcon, ArrowUpIcon, HomeIcon, RefreshIcon } from '@/ui/primitives/icons' import { useParams, useRouter } from 'next/navigation' import { useCallback, useEffect, useState, useTransition } from 'react' import { PROTECTED_URLS } from '@/configs/urls' @@ -8,6 +7,12 @@ import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger' import { useSandboxInspectAnalytics } from '@/lib/hooks/use-analytics' import { cn } from '@/lib/utils' import { Button } from '@/ui/primitives/button' +import { + ArrowLeftIcon, + ArrowUpIcon, + HomeIcon, + RefreshIcon, +} from '@/ui/primitives/icons' import { useSandboxContext } from '../context' import SandboxInspectEmptyFrame from './empty' diff --git a/src/features/dashboard/sandbox/inspect/parent-dir-item.tsx b/src/features/dashboard/sandbox/inspect/parent-dir-item.tsx index 35d40fb15..b7087c763 100644 --- a/src/features/dashboard/sandbox/inspect/parent-dir-item.tsx +++ b/src/features/dashboard/sandbox/inspect/parent-dir-item.tsx @@ -1,11 +1,11 @@ 'use client' -import { FolderUpIcon } from '@/ui/primitives/icons' import { useRouter } from 'next/navigation' import path from 'path' import { useTransition } from 'react' import { cn } from '@/lib/utils' import { DataTableRow } from '@/ui/data-table' +import { FolderUpIcon } from '@/ui/primitives/icons' interface SandboxInspectParentDirItemProps { rootPath: string diff --git a/src/features/dashboard/sandbox/inspect/root-path-input.tsx b/src/features/dashboard/sandbox/inspect/root-path-input.tsx index b98804cd2..d32d6c9aa 100644 --- a/src/features/dashboard/sandbox/inspect/root-path-input.tsx +++ b/src/features/dashboard/sandbox/inspect/root-path-input.tsx @@ -6,9 +6,9 @@ import { l, serializeErrorForLog } from '@/core/shared/clients/logger/logger' import { useSandboxInspectAnalytics } from '@/lib/hooks/use-analytics' import { cn } from '@/lib/utils' import { Button } from '@/ui/primitives/button' +import { ArrowRightIcon } from '@/ui/primitives/icons' import { Input } from '@/ui/primitives/input' import { Loader } from '@/ui/primitives/loader_d' -import { ArrowRightIcon } from '@/ui/primitives/icons' interface RootPathInputProps { className?: string diff --git a/src/features/dashboard/sandbox/inspect/viewer-header.tsx b/src/features/dashboard/sandbox/inspect/viewer-header.tsx index a4af9c7fc..32e37fdc3 100644 --- a/src/features/dashboard/sandbox/inspect/viewer-header.tsx +++ b/src/features/dashboard/sandbox/inspect/viewer-header.tsx @@ -1,8 +1,13 @@ -import { IconButton } from '@/ui/primitives/icon-button' -import { RefreshIcon, CloseIcon, DownloadIcon, FileIcon } from '@/ui/primitives/icons' import { motion } from 'motion/react' import CopyButton from '@/ui/copy-button' import { Button } from '@/ui/primitives/button' +import { IconButton } from '@/ui/primitives/icon-button' +import { + CloseIcon, + DownloadIcon, + FileIcon, + RefreshIcon, +} from '@/ui/primitives/icons' import type { FileContentState } from './filesystem/store' interface SandboxInspectViewerHeaderProps { diff --git a/src/features/dashboard/sandbox/inspect/viewer.tsx b/src/features/dashboard/sandbox/inspect/viewer.tsx index ead8c9cb6..8d12123dc 100644 --- a/src/features/dashboard/sandbox/inspect/viewer.tsx +++ b/src/features/dashboard/sandbox/inspect/viewer.tsx @@ -8,8 +8,8 @@ import { useIsMobile } from '@/lib/hooks/use-mobile' import { cn } from '@/lib/utils' import { Button } from '@/ui/primitives/button' import { Drawer, DrawerContent } from '@/ui/primitives/drawer' -import { ScrollArea, ScrollBar } from '@/ui/primitives/scroll-area' import { DownloadIcon } from '@/ui/primitives/icons' +import { ScrollArea, ScrollBar } from '@/ui/primitives/scroll-area' import { MAX_VIEWABLE_FILE_SIZE_BYTES, diff --git a/src/features/dashboard/sandboxes/list/table-filters.tsx b/src/features/dashboard/sandboxes/list/table-filters.tsx index e6f718410..b127d3a99 100644 --- a/src/features/dashboard/sandboxes/list/table-filters.tsx +++ b/src/features/dashboard/sandboxes/list/table-filters.tsx @@ -19,11 +19,11 @@ import { DropdownMenuSubTrigger, DropdownMenuTrigger, } from '@/ui/primitives/dropdown-menu' +import { AddIcon, FilterIcon } from '@/ui/primitives/icons' import { Input } from '@/ui/primitives/input' import { Label } from '@/ui/primitives/label' import { Separator } from '@/ui/primitives/separator' import { TableFilterButton } from '@/ui/table-filter-button' -import { AddIcon, FilterIcon } from '@/ui/primitives/icons' // Components const RunningSinceFilter = memo(function RunningSinceFilter() { diff --git a/src/features/dashboard/sandboxes/monitoring/header.tsx b/src/features/dashboard/sandboxes/monitoring/header.tsx index e518c1822..2e038efe0 100644 --- a/src/features/dashboard/sandboxes/monitoring/header.tsx +++ b/src/features/dashboard/sandboxes/monitoring/header.tsx @@ -4,8 +4,8 @@ import { getTeamMetricsMax } from '@/core/server/functions/sandboxes/get-team-me import { getNowMemo } from '@/lib/utils/server' import ErrorTooltip from '@/ui/error-tooltip' import { SemiLiveBadge } from '@/ui/live' -import { Skeleton } from '@/ui/primitives/skeleton' import { WarningIcon } from '@/ui/primitives/icons' +import { Skeleton } from '@/ui/primitives/skeleton' import { ConcurrentSandboxesClient, MaxConcurrentSandboxesClient, diff --git a/src/features/dashboard/sandboxes/monitoring/time-picker/index.tsx b/src/features/dashboard/sandboxes/monitoring/time-picker/index.tsx index 1ad20062d..947d04109 100644 --- a/src/features/dashboard/sandboxes/monitoring/time-picker/index.tsx +++ b/src/features/dashboard/sandboxes/monitoring/time-picker/index.tsx @@ -1,9 +1,7 @@ 'use client' import { AnimatePresence, motion } from 'framer-motion' -import { ChevronRightIcon } from '@/ui/primitives/icons' import { memo, type ReactNode, useCallback, useEffect, useRef } from 'react' - import { cn } from '@/lib/utils' import { tryParseDatetime } from '@/lib/utils/formatting' import type { TimeframeState } from '@/lib/utils/timeframe' @@ -15,6 +13,7 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/ui/primitives/dropdown-menu' +import { ChevronRightIcon } from '@/ui/primitives/icons' import { RadioGroup, RadioGroupItem } from '@/ui/primitives/radio-group' import { MAX_DAYS_AGO, TIME_OPTIONS } from './constants' diff --git a/src/features/dashboard/settings/general/name-card.tsx b/src/features/dashboard/settings/general/name-card.tsx index ba2042a1a..3a39fa5cb 100644 --- a/src/features/dashboard/settings/general/name-card.tsx +++ b/src/features/dashboard/settings/general/name-card.tsx @@ -4,6 +4,7 @@ import { zodResolver } from '@hookform/resolvers/zod' import { useHookFormOptimisticAction } from '@next-safe-action/adapter-react-hook-form/hooks' import { useQueryClient } from '@tanstack/react-query' import { AnimatePresence, motion } from 'motion/react' +import { useMemo } from 'react' import { USER_MESSAGES } from '@/configs/user-messages' import { getTransformedDefaultTeamName } from '@/core/modules/teams/utils' import { updateTeamNameAction } from '@/core/server/actions/team-actions' @@ -34,7 +35,6 @@ import { } from '@/ui/primitives/form' import { Input } from '@/ui/primitives/input' import { Loader } from '@/ui/primitives/loader' -import { useMemo } from 'react' interface NameCardProps { className?: string diff --git a/src/features/dashboard/settings/general/profile-picture-card.tsx b/src/features/dashboard/settings/general/profile-picture-card.tsx index f70a561fe..c41c952dc 100644 --- a/src/features/dashboard/settings/general/profile-picture-card.tsx +++ b/src/features/dashboard/settings/general/profile-picture-card.tsx @@ -17,7 +17,12 @@ import { useTRPC } from '@/trpc/client' import { Avatar, AvatarFallback, AvatarImage } from '@/ui/primitives/avatar' import { Badge } from '@/ui/primitives/badge' import { cardVariants } from '@/ui/primitives/card' -import { SpinnerIcon, UploadIcon, PhotoIcon, EditIcon } from '@/ui/primitives/icons' +import { + EditIcon, + PhotoIcon, + SpinnerIcon, + UploadIcon, +} from '@/ui/primitives/icons' interface ProfilePictureCardProps { className?: string diff --git a/src/features/dashboard/settings/keys/table-row.tsx b/src/features/dashboard/settings/keys/table-row.tsx index a35f124c9..01508dc1e 100644 --- a/src/features/dashboard/settings/keys/table-row.tsx +++ b/src/features/dashboard/settings/keys/table-row.tsx @@ -14,7 +14,6 @@ import { } from '@/lib/hooks/use-toast' import { exponentialSmoothing } from '@/lib/utils' import { AlertDialog } from '@/ui/alert-dialog' -import { IconButton } from '@/ui/primitives/icon-button' import { DropdownMenu, DropdownMenuContent, @@ -23,8 +22,9 @@ import { DropdownMenuLabel, DropdownMenuTrigger, } from '@/ui/primitives/dropdown-menu' -import { TableCell, TableRow } from '@/ui/primitives/table' +import { IconButton } from '@/ui/primitives/icon-button' import { IndicatorDotsIcon } from '@/ui/primitives/icons' +import { TableCell, TableRow } from '@/ui/primitives/table' interface TableRowProps { apiKey: TeamAPIKey diff --git a/src/features/dashboard/settings/webhooks/table-row.tsx b/src/features/dashboard/settings/webhooks/table-row.tsx index 7f6c11a9e..839599747 100644 --- a/src/features/dashboard/settings/webhooks/table-row.tsx +++ b/src/features/dashboard/settings/webhooks/table-row.tsx @@ -2,7 +2,6 @@ import { useState } from 'react' import { Badge } from '@/ui/primitives/badge' -import { IconButton } from '@/ui/primitives/icon-button' import { DropdownMenu, DropdownMenuContent, @@ -10,6 +9,7 @@ import { DropdownMenuItem, DropdownMenuTrigger, } from '@/ui/primitives/dropdown-menu' +import { IconButton } from '@/ui/primitives/icon-button' import { EditIcon, IndicatorDotsIcon, @@ -113,7 +113,8 @@ export default function WebhookTableRow({ e.preventDefault()}> - Rotate Secret + Rotate + Secret diff --git a/src/features/dashboard/sidebar/blocked-banner.tsx b/src/features/dashboard/sidebar/blocked-banner.tsx index 7dd791c4e..63d9f1ff6 100644 --- a/src/features/dashboard/sidebar/blocked-banner.tsx +++ b/src/features/dashboard/sidebar/blocked-banner.tsx @@ -1,11 +1,11 @@ 'use client' -import { WarningIcon } from '@/ui/primitives/icons' import { AnimatePresence, motion } from 'motion/react' import { useRouter } from 'next/navigation' import { useMemo } from 'react' import { PROTECTED_URLS } from '@/configs/urls' import { cn, exponentialSmoothing } from '@/lib/utils' +import { WarningIcon } from '@/ui/primitives/icons' import { SidebarMenuButton, SidebarMenuItem } from '@/ui/primitives/sidebar' import { useDashboard } from '../context' diff --git a/src/features/dashboard/sidebar/command.tsx b/src/features/dashboard/sidebar/command.tsx index ac1ce4e78..1ee3635d2 100644 --- a/src/features/dashboard/sidebar/command.tsx +++ b/src/features/dashboard/sidebar/command.tsx @@ -58,7 +58,9 @@ export default function DashboardSidebarCommand({ )} onClick={() => setOpen(true)} > - > Go to + + > Go to + {user?.email && ( - {user.email} + + {user.email} + )} {teams.length > 0 ? ( teams.map((team) => ( - + diff --git a/src/features/dashboard/sidebar/menu.tsx b/src/features/dashboard/sidebar/menu.tsx index abd8908f8..fb11531dd 100644 --- a/src/features/dashboard/sidebar/menu.tsx +++ b/src/features/dashboard/sidebar/menu.tsx @@ -15,8 +15,13 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/ui/primitives/dropdown-menu' +import { + AccountSettingsIcon, + AddIcon, + LogoutIcon, + UnpackIcon, +} from '@/ui/primitives/icons' import { SidebarMenuButton, SidebarMenuItem } from '@/ui/primitives/sidebar' -import { AccountSettingsIcon, AddIcon, LogoutIcon, UnpackIcon } from '@/ui/primitives/icons' import { useDashboard } from '../context' import { CreateTeamDialog } from './create-team-dialog' import DashboardSidebarMenuTeams from './menu-teams' @@ -34,10 +39,7 @@ export default function DashboardSidebarMenu() { - + - {isOpen ? ( - - ) : ( - - )} + {isOpen ? : }
    diff --git a/src/features/dashboard/templates/list/table-cells.tsx b/src/features/dashboard/templates/list/table-cells.tsx index 2c1f63566..7892bc7f2 100644 --- a/src/features/dashboard/templates/list/table-cells.tsx +++ b/src/features/dashboard/templates/list/table-cells.tsx @@ -20,7 +20,6 @@ import { E2BBadge } from '@/ui/brand' import HelpTooltip from '@/ui/help-tooltip' import { Badge } from '@/ui/primitives/badge' import { Button } from '@/ui/primitives/button' -import { IconButton } from '@/ui/primitives/icon-button' import { DropdownMenu, DropdownMenuContent, @@ -30,8 +29,15 @@ import { DropdownMenuSeparator, DropdownMenuTrigger, } from '@/ui/primitives/dropdown-menu' +import { IconButton } from '@/ui/primitives/icon-button' +import { + CheckIcon, + CopyIcon, + IndicatorDotsIcon, + PrivateIcon, + UnlockIcon, +} from '@/ui/primitives/icons' import { Loader } from '@/ui/primitives/loader_d' -import { CheckIcon, CopyIcon, IndicatorDotsIcon, PrivateIcon, UnlockIcon } from '@/ui/primitives/icons' import ResourceUsage from '../../common/resource-usage' import { useDashboard } from '../../context' @@ -228,11 +234,7 @@ export function ActionsCell({ className="size-5" disabled={isUpdating || isDeleting || 'isDefault' in template} > - {isUpdating ? ( - - ) : ( - - )} + {isUpdating ? : } diff --git a/src/features/dashboard/templates/list/table-filters.tsx b/src/features/dashboard/templates/list/table-filters.tsx index a1a1ec9af..7efadf9bd 100644 --- a/src/features/dashboard/templates/list/table-filters.tsx +++ b/src/features/dashboard/templates/list/table-filters.tsx @@ -17,10 +17,10 @@ import { DropdownMenuSubTrigger, DropdownMenuTrigger, } from '@/ui/primitives/dropdown-menu' +import { FilterIcon } from '@/ui/primitives/icons' import { Label } from '@/ui/primitives/label' import { Separator } from '@/ui/primitives/separator' import { TableFilterButton } from '@/ui/table-filter-button' -import { FilterIcon } from '@/ui/primitives/icons' import { useTemplateTableStore } from './stores/table-store' // Components @@ -111,10 +111,7 @@ const ResourcesFilter = () => { className="w-full" /> {localValues.memory > 0 && ( - )} diff --git a/src/features/dashboard/usage/usage-metric-chart.tsx b/src/features/dashboard/usage/usage-metric-chart.tsx index 96774be34..b8eb922cf 100644 --- a/src/features/dashboard/usage/usage-metric-chart.tsx +++ b/src/features/dashboard/usage/usage-metric-chart.tsx @@ -4,7 +4,6 @@ import { DialogTitle } from '@radix-ui/react-dialog' import { useState } from 'react' import { AnimatedMetricDisplay } from '@/features/dashboard/sandboxes/monitoring/charts/animated-metric-display' import { cn } from '@/lib/utils' -import { IconButton } from '@/ui/primitives/icon-button' import { Card, CardContent, @@ -12,6 +11,7 @@ import { cardVariants, } from '@/ui/primitives/card' import { Dialog, DialogContent } from '@/ui/primitives/dialog' +import { IconButton } from '@/ui/primitives/icon-button' import { UnpackIcon } from '@/ui/primitives/icons' import ComputeUsageChart from './compute-usage-chart' import { useUsageCharts } from './usage-charts-context' @@ -142,9 +142,7 @@ export function UsageMetricChart({ open={isFullscreen} onOpenChange={(open) => !open && setFullscreenMetric(null)} > - + {/* title just here to avoid accessibility dev error from radix */} {METRIC_CONFIGS[metric].title} diff --git a/src/features/dashboard/usage/usage-time-range-controls.tsx b/src/features/dashboard/usage/usage-time-range-controls.tsx index 40439cd77..a1e11be61 100644 --- a/src/features/dashboard/usage/usage-time-range-controls.tsx +++ b/src/features/dashboard/usage/usage-time-range-controls.tsx @@ -6,6 +6,7 @@ import { findMatchingPreset } from '@/lib/utils/time-range' import { formatTimeframeAsISO8601Interval } from '@/lib/utils/timeframe' import CopyButton from '@/ui/copy-button' import { Button } from '@/ui/primitives/button' +import { ChevronLeftIcon, ChevronRightIcon } from '@/ui/primitives/icons' import { Popover, PopoverContent, @@ -15,7 +16,6 @@ import { Separator } from '@/ui/primitives/separator' import { TimeRangePicker, type TimeRangeValues } from '@/ui/time-range-picker' import { parseTimeRangeValuesToTimestamps } from '@/ui/time-range-picker.logic' import { type TimeRangePreset, TimeRangePresets } from '@/ui/time-range-presets' -import { ChevronLeftIcon, ChevronRightIcon } from '@/ui/primitives/icons' import { TIME_RANGE_PRESETS } from './constants' import { determineSamplingMode, diff --git a/src/lib/hooks/use-clipboard.ts b/src/lib/hooks/use-clipboard.ts index 01349a4c7..834922960 100644 --- a/src/lib/hooks/use-clipboard.ts +++ b/src/lib/hooks/use-clipboard.ts @@ -1,6 +1,6 @@ -"use client"; +'use client' -import { useState, useCallback } from "react"; +import { useCallback, useState } from 'react' /** * Hook for copying text to clipboard with temporary success state @@ -10,25 +10,25 @@ import { useState, useCallback } from "react"; export const useClipboard = ( duration: number = 3000 ): [boolean, (text: string) => Promise] => { - const [wasCopied, setWasCopied] = useState(false); + const [wasCopied, setWasCopied] = useState(false) const copy = useCallback( async (text: string) => { try { - await navigator.clipboard.writeText(text); - setWasCopied(true); + await navigator.clipboard.writeText(text) + setWasCopied(true) // Reset wasCopied after duration setTimeout(() => { - setWasCopied(false); - }, duration); + setWasCopied(false) + }, duration) } catch (err) { - console.error("Failed to copy text:", err); - setWasCopied(false); + console.error('Failed to copy text:', err) + setWasCopied(false) } }, [duration] - ); + ) - return [wasCopied, copy]; -}; + return [wasCopied, copy] +} diff --git a/src/ui/alert-popover.tsx b/src/ui/alert-popover.tsx index 49f75f709..d47cc911a 100644 --- a/src/ui/alert-popover.tsx +++ b/src/ui/alert-popover.tsx @@ -47,11 +47,7 @@ export const AlertPopover: FC = ({ - diff --git a/src/ui/copy-button.tsx b/src/ui/copy-button.tsx index 63631f7ec..825b7e346 100644 --- a/src/ui/copy-button.tsx +++ b/src/ui/copy-button.tsx @@ -1,11 +1,11 @@ 'use client' +import { AnimatePresence, motion } from 'motion/react' +import { FC } from 'react' import { useClipboard } from '@/lib/hooks/use-clipboard' import { EASE_APPEAR } from '@/lib/utils/ui' import { IconButton, IconButtonProps } from '@/ui/primitives/icon-button' import { CheckIcon, CopyIcon } from '@/ui/primitives/icons' -import { AnimatePresence, motion } from 'motion/react' -import { FC } from 'react' interface CopyButtonProps extends IconButtonProps { value: string diff --git a/src/ui/data-table.tsx b/src/ui/data-table.tsx index d9c89d0b5..d317e4943 100644 --- a/src/ui/data-table.tsx +++ b/src/ui/data-table.tsx @@ -2,6 +2,7 @@ import type { Cell, Header } from '@tanstack/react-table' import * as React from 'react' import { cn } from '@/lib/utils' import { Button } from '@/ui/primitives/button' +import { SortAscIcon, SortDescIcon } from '@/ui/primitives/icons' import { Select, SelectContent, @@ -10,7 +11,6 @@ import { SelectValue, } from '@/ui/primitives/select' import { Separator } from '@/ui/primitives/separator' -import { SortAscIcon, SortDescIcon } from '@/ui/primitives/icons' interface DataTableColumnHeaderProps extends React.HTMLAttributes { diff --git a/src/ui/docs-code-block.tsx b/src/ui/docs-code-block.tsx index efbad1664..9cb029a1a 100644 --- a/src/ui/docs-code-block.tsx +++ b/src/ui/docs-code-block.tsx @@ -1,6 +1,5 @@ 'use client' import type { ScrollAreaViewportProps } from '@radix-ui/react-scroll-area' -import { CheckIcon, CopyIcon } from './primitives/icons' import { type ButtonHTMLAttributes, forwardRef, @@ -12,6 +11,7 @@ import { import { useClipboard } from '@/lib/hooks/use-clipboard' import { cn } from '@/lib/utils' import { buttonVariants } from './primitives/button' +import { CheckIcon, CopyIcon } from './primitives/icons' import { ScrollArea, ScrollBar, ScrollViewport } from './primitives/scroll-area' export type CodeBlockProps = HTMLAttributes & { @@ -171,7 +171,9 @@ function CopyButton({ onClick={onCopy} {...props} > - + diff --git a/src/ui/error-indicator.tsx b/src/ui/error-indicator.tsx index 71ffc6916..cf40fc404 100644 --- a/src/ui/error-indicator.tsx +++ b/src/ui/error-indicator.tsx @@ -1,6 +1,5 @@ 'use client' -import { RefreshIcon } from './primitives/icons' import { useRouter } from 'next/navigation' import { useTransition } from 'react' import { cn } from '@/lib/utils' @@ -13,6 +12,7 @@ import { CardHeader, CardTitle, } from './primitives/card' +import { RefreshIcon } from './primitives/icons' interface ErrorIndicatorProps { title?: string diff --git a/src/ui/icons.tsx b/src/ui/icons.tsx index 79e78b706..d0b01f48f 100644 --- a/src/ui/icons.tsx +++ b/src/ui/icons.tsx @@ -1,7 +1,7 @@ -import { cn } from '@/lib/utils' -import { TerminalCustomIcon, type Icon } from '@/ui/primitives/icons' import { type HTMLAttributes } from 'react' import { IconBaseProps } from 'react-icons/lib' +import { cn } from '@/lib/utils' +import { type Icon, TerminalCustomIcon } from '@/ui/primitives/icons' export function IconContainer({ icon: Icon, diff --git a/src/ui/json-popover.tsx b/src/ui/json-popover.tsx index 85c9f0858..dc96d9330 100644 --- a/src/ui/json-popover.tsx +++ b/src/ui/json-popover.tsx @@ -35,7 +35,10 @@ export function JsonPopover({ diff --git a/src/features/dashboard/layouts/header.tsx b/src/features/dashboard/layouts/header.tsx index 4282ba2a1..7af21ea37 100644 --- a/src/features/dashboard/layouts/header.tsx +++ b/src/features/dashboard/layouts/header.tsx @@ -48,8 +48,6 @@ export default function DashboardLayoutHeader({ {copyableValue && ( diff --git a/src/features/dashboard/members/add-member-dialog.tsx b/src/features/dashboard/members/add-member-dialog.tsx index 61494b43a..cfb47eee1 100644 --- a/src/features/dashboard/members/add-member-dialog.tsx +++ b/src/features/dashboard/members/add-member-dialog.tsx @@ -1,6 +1,5 @@ 'use client' -import { Plus } from 'lucide-react' import { useState } from 'react' import { AddMemberForm } from '@/features/dashboard/members/add-member-form' import { Button } from '@/ui/primitives/button' @@ -11,6 +10,7 @@ import { DialogTitle, DialogTrigger, } from '@/ui/primitives/dialog' +import { AddIcon } from '@/ui/primitives/icons' export const AddMemberDialog = () => { const [open, setOpen] = useState(false) @@ -18,13 +18,8 @@ export const AddMemberDialog = () => { return ( - diff --git a/src/features/dashboard/members/member-table-row.tsx b/src/features/dashboard/members/member-table-row.tsx index c4ebbfb8a..ace66960b 100644 --- a/src/features/dashboard/members/member-table-row.tsx +++ b/src/features/dashboard/members/member-table-row.tsx @@ -160,7 +160,6 @@ const NameCell = ({ You diff --git a/src/features/dashboard/members/members-page-content.tsx b/src/features/dashboard/members/members-page-content.tsx index b43e7e772..66e343c4f 100644 --- a/src/features/dashboard/members/members-page-content.tsx +++ b/src/features/dashboard/members/members-page-content.tsx @@ -1,10 +1,10 @@ 'use client' -import { Search } from 'lucide-react' import { useMemo, useState } from 'react' import type { TeamMember } from '@/core/modules/teams/models' import { cn } from '@/lib/utils' import { pluralize } from '@/lib/utils/formatting' +import { SearchIcon } from '@/ui/primitives/icons' import { Input } from '@/ui/primitives/input' import { AddMemberDialog } from './add-member-dialog' import MemberTable from './member-table' @@ -37,7 +37,7 @@ const MembersPageContent = ({
    - diff --git a/src/features/dashboard/members/remove-member-dialog.tsx b/src/features/dashboard/members/remove-member-dialog.tsx index db40343c6..b8676d1d3 100644 --- a/src/features/dashboard/members/remove-member-dialog.tsx +++ b/src/features/dashboard/members/remove-member-dialog.tsx @@ -54,18 +54,16 @@ export const RemoveMemberDialog = ({
    ))} @@ -260,7 +260,7 @@ export default function ContactSupportDialog({ diff --git a/src/features/dashboard/sandbox/header/ended-at.tsx b/src/features/dashboard/sandbox/header/ended-at.tsx index acb89ce57..a807172fe 100644 --- a/src/features/dashboard/sandbox/header/ended-at.tsx +++ b/src/features/dashboard/sandbox/header/ended-at.tsx @@ -46,12 +46,7 @@ export default function EndedAt() {

    {prefix}, {timeStr}

    - +
    ) } diff --git a/src/features/dashboard/sandbox/monitoring/components/chart-overlays.tsx b/src/features/dashboard/sandbox/monitoring/components/chart-overlays.tsx index 2c3424adc..51bc27a39 100644 --- a/src/features/dashboard/sandbox/monitoring/components/chart-overlays.tsx +++ b/src/features/dashboard/sandbox/monitoring/components/chart-overlays.tsx @@ -1,6 +1,10 @@ -import { PowerIcon, SquareIcon } from 'lucide-react' import { cn } from '@/lib/utils' -import { type AddIcon, PausedIcon, RunningIcon } from '@/ui/primitives/icons' +import { + AddIcon, + BlockIcon, + PausedIcon, + RunningIcon, +} from '@/ui/primitives/icons' import { withOpacity } from '../utils/chart-colors' import type { CrosshairMarker, @@ -20,14 +24,11 @@ const MARKER_BORDER_OPACITY = 0.12 import { formatHoverTimestamp } from '../utils/formatters' -const SANDBOX_LIFECYCLE_EVENT_ICON_MAP: Record< - string, - typeof AddIcon | typeof PowerIcon -> = { - [SANDBOX_LIFECYCLE_EVENT_CREATED]: PowerIcon, +const SANDBOX_LIFECYCLE_EVENT_ICON_MAP: Record = { + [SANDBOX_LIFECYCLE_EVENT_CREATED]: AddIcon, [SANDBOX_LIFECYCLE_EVENT_PAUSED]: PausedIcon, [SANDBOX_LIFECYCLE_EVENT_RESUMED]: RunningIcon, - [SANDBOX_LIFECYCLE_EVENT_KILLED]: SquareIcon, + [SANDBOX_LIFECYCLE_EVENT_KILLED]: BlockIcon, } function LifecycleEventOverlayGroup({ diff --git a/src/features/dashboard/sandbox/monitoring/components/monitoring-time-range-controls.tsx b/src/features/dashboard/sandbox/monitoring/components/monitoring-time-range-controls.tsx index 757019df3..fedbb7e3d 100644 --- a/src/features/dashboard/sandbox/monitoring/components/monitoring-time-range-controls.tsx +++ b/src/features/dashboard/sandbox/monitoring/components/monitoring-time-range-controls.tsx @@ -1,11 +1,11 @@ 'use client' -import { RotateCcw } from 'lucide-react' import { useCallback, useMemo, useState } from 'react' import { cn } from '@/lib/utils' import { LiveDot } from '@/ui/live' import { Button } from '@/ui/primitives/button' -import { TimeIcon } from '@/ui/primitives/icons' +import { IconButton } from '@/ui/primitives/icon-button' +import { RefreshIcon, TimeIcon } from '@/ui/primitives/icons' import { Popover, PopoverContent, @@ -189,8 +189,7 @@ export default function SandboxMonitoringTimeRangeControls({ + +
    ) : null} diff --git a/src/features/dashboard/sandboxes/list/open-sandbox-dialog.tsx b/src/features/dashboard/sandboxes/list/open-sandbox-dialog.tsx index 9d9efb71b..4c5bff11e 100644 --- a/src/features/dashboard/sandboxes/list/open-sandbox-dialog.tsx +++ b/src/features/dashboard/sandboxes/list/open-sandbox-dialog.tsx @@ -90,7 +90,7 @@ export function OpenSandboxDialog() { return ( - + @@ -146,8 +146,8 @@ export function OpenSandboxDialog() {