Fix close button styling in dialog components to address ghost white issue in dark mode

This commit is contained in:
Stijnus 2025-05-07 14:10:37 +02:00
parent 0775c82e00
commit 306bb29053
7 changed files with 127 additions and 134 deletions

View File

@ -55,6 +55,7 @@ export function GitHubAuthDialog({ isOpen, onClose }: GitHubAuthDialogProps) {
Cookies.set('git:github.com', JSON.stringify({ username: token, password: 'x-oauth-basic' }));
toast.success(`Successfully connected as ${userData.login}`);
setToken('');
onClose();
} else {
if (response.status === 401) {
@ -174,7 +175,7 @@ export function GitHubAuthDialog({ isOpen, onClose }: GitHubAuthDialogProps) {
<Dialog.Close asChild>
<button
onClick={onClose}
className="px-4 py-1.5 bg-[#F5F5F5] hover:bg-[#E5E5E5] dark:bg-[#252525] dark:hover:bg-[#333333] rounded-lg text-[#111111] dark:text-white transition-colors text-sm"
className="px-4 py-1.5 bg-transparent bg-[#F5F5F5] hover:bg-[#E5E5E5] dark:bg-[#252525] dark:hover:bg-[#333333] rounded-lg text-[#111111] dark:text-white transition-colors text-sm"
>
Close
</button>

View File

@ -1,7 +1,11 @@
import * as Dialog from '@radix-ui/react-dialog';
import { useState, useEffect } from 'react';
import { useState, useEffect, useCallback } from 'react';
import { toast } from 'react-toastify';
import { motion } from 'framer-motion';
import { useDebouncedCallback } from 'use-debounce';
import { Octokit } from '@octokit/rest';
// Internal imports
import { getLocalStorage } from '~/lib/persistence';
import { classNames } from '~/utils/classNames';
import type { GitHubUserResponse } from '~/types/GitHub';
@ -10,10 +14,9 @@ import { workbenchStore } from '~/lib/stores/workbench';
import { extractRelativePath } from '~/utils/diff';
import { formatSize } from '~/utils/formatSize';
import type { FileMap, File } from '~/lib/stores/files';
import { Octokit } from '@octokit/rest';
// Import UI components
import { Badge, EmptyState, StatusIndicator, SearchInput, CloseButton } from '~/components/ui';
// UI Components
import { Badge, EmptyState, StatusIndicator, SearchInput } from '~/components/ui';
interface PushToGitHubDialogProps {
isOpen: boolean;
@ -64,6 +67,8 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
}, [isOpen]);
// Filter repositories based on search query
// const debouncedSetRepoSearchQuery = useDebouncedCallback((value: string) => setRepoSearchQuery(value), 300);
useEffect(() => {
if (recentRepos.length === 0) {
setFilteredRepos([]);
@ -86,7 +91,7 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
setFilteredRepos(filtered);
}, [recentRepos, repoSearchQuery]);
const fetchRecentRepos = async (token: string) => {
const fetchRecentRepos = useCallback(async (token: string) => {
if (!token) {
logStore.logError('No GitHub token available');
toast.error('GitHub authentication required');
@ -98,84 +103,77 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
setIsFetchingRepos(true);
console.log('Fetching GitHub repositories with token:', token.substring(0, 5) + '...');
/*
* Log the request details for debugging
* According to GitHub API docs, we can't use both 'type' and 'affiliation' parameters together
*/
const requestUrl =
'https://api.github.com/user/repos?sort=updated&per_page=10&affiliation=owner,organization_member';
console.log('Request URL:', requestUrl);
console.log('Request headers:', {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${token.substring(0, 5)}...`,
});
// Fetch ALL repos by paginating through all pages
let allRepos: GitHubRepo[] = [];
let page = 1;
let hasMore = true;
const response = await fetch(requestUrl, {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${token.trim()}`,
},
});
while (hasMore) {
const requestUrl = `https://api.github.com/user/repos?sort=updated&per_page=100&page=${page}&affiliation=owner,organization_member`;
const response = await fetch(requestUrl, {
headers: {
Accept: 'application/vnd.github.v3+json',
Authorization: `Bearer ${token.trim()}`,
},
});
// Log response status for debugging
console.log('Response status:', response.status, response.statusText);
console.log('Response headers:', {
'X-RateLimit-Limit': response.headers.get('x-ratelimit-limit'),
'X-RateLimit-Remaining': response.headers.get('x-ratelimit-remaining'),
'X-RateLimit-Used': response.headers.get('x-ratelimit-used'),
});
if (!response.ok) {
let errorData: { message?: string } = {};
if (!response.ok) {
let errorData: { message?: string } = {};
try {
errorData = await response.json();
console.error('Error response data:', errorData);
} catch (e) {
errorData = { message: 'Could not parse error response' };
console.error('Could not parse error response:', e);
}
if (response.status === 401) {
toast.error('GitHub token expired. Please reconnect your account.');
// Clear invalid token
const connection = getLocalStorage('github_connection');
if (connection) {
localStorage.removeItem('github_connection');
setUser(null);
}
} else if (response.status === 403 && response.headers.get('x-ratelimit-remaining') === '0') {
// Rate limit exceeded
const resetTime = response.headers.get('x-ratelimit-reset');
const resetDate = resetTime ? new Date(parseInt(resetTime) * 1000).toLocaleTimeString() : 'soon';
toast.error(`GitHub API rate limit exceeded. Limit resets at ${resetDate}`);
} else {
logStore.logError('Failed to fetch GitHub repositories', {
status: response.status,
statusText: response.statusText,
error: errorData,
});
toast.error(`Failed to fetch repositories: ${errorData.message || response.statusText}`);
}
return;
}
try {
errorData = await response.json();
console.error('Error response data:', errorData);
} catch (e) {
errorData = { message: 'Could not parse error response' };
console.error('Could not parse error response:', e);
}
const repos = (await response.json()) as GitHubRepo[];
allRepos = allRepos.concat(repos);
if (response.status === 401) {
toast.error('GitHub token expired. Please reconnect your account.');
// Clear invalid token
const connection = getLocalStorage('github_connection');
if (connection) {
localStorage.removeItem('github_connection');
setUser(null);
if (repos.length < 100) {
hasMore = false;
} else {
page += 1;
}
} else if (response.status === 403 && response.headers.get('x-ratelimit-remaining') === '0') {
// Rate limit exceeded
const resetTime = response.headers.get('x-ratelimit-reset');
const resetDate = resetTime ? new Date(parseInt(resetTime) * 1000).toLocaleTimeString() : 'soon';
toast.error(`GitHub API rate limit exceeded. Limit resets at ${resetDate}`);
} else {
logStore.logError('Failed to fetch GitHub repositories', {
status: response.status,
statusText: response.statusText,
error: errorData,
});
toast.error(`Failed to fetch repositories: ${errorData.message || response.statusText}`);
} catch (parseError) {
console.error('Error parsing JSON response:', parseError);
logStore.logError('Failed to parse GitHub repositories response', { parseError });
toast.error('Failed to parse repository data');
setRecentRepos([]);
return;
}
return;
}
// Clone the response before parsing it to avoid the "body already read" error
const responseClone = response.clone();
try {
const repos = (await responseClone.json()) as GitHubRepo[];
console.log(`Successfully fetched ${repos.length} repositories`);
setRecentRepos(repos);
} catch (parseError) {
console.error('Error parsing JSON response:', parseError);
logStore.logError('Failed to parse GitHub repositories response', { parseError });
toast.error('Failed to parse repository data');
setRecentRepos([]);
}
setRecentRepos(allRepos);
} catch (error) {
console.error('Exception while fetching GitHub repositories:', error);
logStore.logError('Failed to fetch GitHub repositories', { error });
@ -183,9 +181,9 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
} finally {
setIsFetchingRepos(false);
}
};
}, []);
const handleSubmit = async (e: React.FormEvent) => {
async function handleSubmit(e: React.FormEvent) {
e.preventDefault();
const connection = getLocalStorage('github_connection');
@ -257,7 +255,7 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
} finally {
setIsLoading(false);
}
};
}
const handleClose = () => {
setRepoName('');
@ -304,7 +302,13 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
</div>
</div>
<Dialog.Close asChild>
<CloseButton onClick={handleClose} size="md" />
<button
onClick={handleClose}
className="p-2 rounded-lg transition-all duration-200 ease-in-out bg-transparent text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary dark:text-bolt-elements-textTertiary-dark dark:hover:text-bolt-elements-textPrimary-dark hover:bg-bolt-elements-background-depth-2 dark:hover:bg-bolt-elements-background-depth-3 focus:outline-none focus:ring-2 focus:ring-bolt-elements-borderColor dark:focus:ring-bolt-elements-borderColor-dark"
>
<span className="i-ph:x block w-5 h-5" aria-hidden="true" />
<span className="sr-only">Close dialog</span>
</button>
</Dialog.Close>
</div>
@ -412,7 +416,13 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
>
<div className="relative text-center space-y-4">
<Dialog.Close asChild>
<CloseButton onClick={handleClose} className="absolute right-0 top-0" size="md" />
<button
onClick={handleClose}
className="absolute right-0 top-0 p-2 rounded-lg transition-all duration-200 ease-in-out bg-transparent text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary dark:text-bolt-elements-textTertiary-dark dark:hover:text-bolt-elements-textPrimary-dark hover:bg-bolt-elements-background-depth-2 dark:hover:bg-bolt-elements-background-depth-3 focus:outline-none focus:ring-2 focus:ring-bolt-elements-borderColor dark:focus:ring-bolt-elements-borderColor-dark"
>
<span className="i-ph:x block w-5 h-5" aria-hidden="true" />
<span className="sr-only">Close dialog</span>
</button>
</Dialog.Close>
<motion.div
initial={{ scale: 0.8 }}
@ -498,7 +508,13 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
</p>
</div>
<Dialog.Close asChild>
<CloseButton onClick={handleClose} className="ml-auto" size="md" />
<button
onClick={handleClose}
className="ml-auto p-2 rounded-lg transition-all duration-200 ease-in-out bg-transparent text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary dark:text-bolt-elements-textTertiary-dark dark:hover:text-bolt-elements-textPrimary-dark hover:bg-bolt-elements-background-depth-2 dark:hover:bg-bolt-elements-background-depth-3 focus:outline-none focus:ring-2 focus:ring-bolt-elements-borderColor dark:focus:ring-bolt-elements-borderColor-dark"
>
<span className="i-ph:x block w-5 h-5" aria-hidden="true" />
<span className="sr-only">Close dialog</span>
</button>
</Dialog.Close>
</div>
@ -635,7 +651,6 @@ export function PushToGitHubDialog({ isOpen, onClose, onPush }: PushToGitHubDial
<StatusIndicator status="loading" pulse={true} label="Loading repositories..." />
</div>
)}
<div className="p-3 bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 rounded-lg border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark">
<div className="flex items-center gap-2">
<input

View File

@ -7,6 +7,8 @@ interface RepositoryCardProps {
onSelect: () => void;
}
import { useMemo } from 'react';
export function RepositoryCard({ repo, onSelect }: RepositoryCardProps) {
// Calculate a gradient color based on the repository name for visual variety
const getGradientColor = (name: string) => {
@ -55,9 +57,12 @@ export function RepositoryCard({ repo, onSelect }: RepositoryCardProps) {
});
};
const gradient = useMemo(() => getGradientColor(repo.name), [repo.name]);
// const formattedDate = useMemo(() => formatDate(repo.updated_at), [repo.updated_at]);
return (
<motion.div
className={`p-5 rounded-xl bg-gradient-to-br ${getGradientColor(repo.name)} border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark hover:border-purple-500/40 transition-all duration-300 shadow-sm hover:shadow-md`}
className={`p-5 rounded-xl bg-gradient-to-br ${gradient} border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark hover:border-purple-500/40 transition-all duration-300 shadow-sm hover:shadow-md`}
whileHover={{
scale: 1.02,
y: -2,

View File

@ -8,10 +8,9 @@ import { motion, AnimatePresence } from 'framer-motion';
import Cookies from 'js-cookie';
// Import UI components
import { Input, SearchInput, Badge, FilterChip } from '~/components/ui';
import { Input, SearchInput, Badge, FilterChip, Tabs, TabsList, TabsTrigger } from '~/components/ui';
// Import the components we've extracted
import { TabButton } from './TabButton';
import { RepositoryList } from './RepositoryList';
import { StatsDialog } from './StatsDialog';
import { GitHubAuthDialog } from './GitHubAuthDialog';
@ -564,7 +563,7 @@ export function RepositorySelectionDialog({ isOpen, onClose, onSelect }: Reposit
<Dialog.Close
onClick={handleClose}
className={classNames(
'p-2 rounded-lg transition-all duration-200 ease-in-out',
'p-2 rounded-lg transition-all duration-200 ease-in-out bg-transparent',
'text-bolt-elements-textTertiary hover:text-bolt-elements-textPrimary',
'dark:text-bolt-elements-textTertiary-dark dark:hover:text-bolt-elements-textPrimary-dark',
'hover:bg-bolt-elements-background-depth-2 dark:hover:bg-bolt-elements-background-depth-3',
@ -599,18 +598,13 @@ export function RepositorySelectionDialog({ isOpen, onClose, onSelect }: Reposit
<div className="p-5">
{/* Tabs */}
<div className="flex gap-2 mb-6">
<TabButton active={activeTab === 'my-repos'} onClick={() => setActiveTab('my-repos')}>
<span className="i-ph:book-bookmark" />
My Repos
</TabButton>
<TabButton active={activeTab === 'search'} onClick={() => setActiveTab('search')}>
<span className="i-ph:magnifying-glass" />
Search
</TabButton>
<TabButton active={activeTab === 'url'} onClick={() => setActiveTab('url')}>
<span className="i-ph:link" />
URL
</TabButton>
<Tabs value={activeTab} onValueChange={(v) => setActiveTab(v as 'my-repos' | 'search' | 'url')}>
<TabsList>
<TabsTrigger value="my-repos">My Repos</TabsTrigger>
<TabsTrigger value="search">Search</TabsTrigger>
<TabsTrigger value="url">From URL</TabsTrigger>
</TabsList>
</Tabs>
</div>
{activeTab === 'url' ? (

View File

@ -1,36 +0,0 @@
import React from 'react';
import { motion } from 'framer-motion';
import { classNames } from '~/utils/classNames';
interface TabButtonProps {
active: boolean;
onClick: () => void;
children: React.ReactNode;
}
export function TabButton({ active, onClick, children }: TabButtonProps) {
return (
<motion.button
onClick={onClick}
className={classNames(
'px-4 py-2 h-10 rounded-lg transition-all duration-200 flex items-center gap-2 min-w-[120px] justify-center relative overflow-hidden',
active
? 'bg-purple-500 text-white hover:bg-purple-600 shadow-sm shadow-purple-500/20'
: 'bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3 text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark hover:bg-bolt-elements-background-depth-3 dark:hover:bg-bolt-elements-background-depth-4 border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark',
)}
whileHover={{ scale: 1.02 }}
whileTap={{ scale: 0.98 }}
layout
>
{active && (
<motion.span
layoutId="activeTab"
className="absolute inset-0 bg-purple-500 -z-10"
initial={false}
transition={{ type: 'spring', stiffness: 300, damping: 30 }}
/>
)}
<span className={classNames('flex items-center gap-2', active ? 'font-medium' : '')}>{children}</span>
</motion.button>
);
}

View File

@ -149,6 +149,7 @@
"shiki": "^1.24.0",
"tailwind-merge": "^2.2.1",
"unist-util-visit": "^5.0.0",
"use-debounce": "^10.0.4",
"vite-plugin-node-polyfills": "^0.22.0",
"zod": "^3.24.1",
"zustand": "^5.0.3"

View File

@ -326,6 +326,9 @@ importers:
unist-util-visit:
specifier: ^5.0.0
version: 5.0.0
use-debounce:
specifier: ^10.0.4
version: 10.0.4(react@18.3.1)
vite-plugin-node-polyfills:
specifier: ^0.22.0
version: 0.22.0(rollup@4.38.0)(vite@5.4.15(@types/node@22.13.14)(sass-embedded@1.86.0))
@ -7743,6 +7746,12 @@ packages:
'@types/react':
optional: true
use-debounce@10.0.4:
resolution: {integrity: sha512-6Cf7Yr7Wk7Kdv77nnJMf6de4HuDE4dTxKij+RqE9rufDsI6zsbjyAxcH5y2ueJCQAnfgKbzXbZHYlkFwmBlWkw==}
engines: {node: '>= 16.0.0'}
peerDependencies:
react: '*'
use-memo-one@1.1.3:
resolution: {integrity: sha512-g66/K7ZQGYrI6dy8GLpVcMsBp4s17xNkYJVSMvTEevGy3nDxHOfE6z8BVE22+5G5x7t3+bhzrlTDB7ObrEE0cQ==}
peerDependencies:
@ -16864,6 +16873,10 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.20
use-debounce@10.0.4(react@18.3.1):
dependencies:
react: 18.3.1
use-memo-one@1.1.3(react@18.3.1):
dependencies:
react: 18.3.1