style: Fix linting issues in UI components

This commit is contained in:
Stijnus 2025-05-05 20:32:53 +02:00
parent 7f694d0e4b
commit 3b0fbe7ccd
16 changed files with 409 additions and 177 deletions

View File

@ -21,6 +21,7 @@ export function RepositoryCard({ repo, onSelect }: RepositoryCardProps) {
'from-pink-500/10 to-purple-500/5', 'from-pink-500/10 to-purple-500/5',
]; ];
const index = name.length % colors.length; const index = name.length % colors.length;
return colors[index]; return colors[index];
}; };
@ -31,10 +32,21 @@ export function RepositoryCard({ repo, onSelect }: RepositoryCardProps) {
const diffTime = Math.abs(now.getTime() - date.getTime()); const diffTime = Math.abs(now.getTime() - date.getTime());
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24)); const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
if (diffDays <= 1) return 'Today'; if (diffDays <= 1) {
if (diffDays <= 2) return 'Yesterday'; return 'Today';
if (diffDays <= 7) return `${diffDays} days ago`; }
if (diffDays <= 30) return `${Math.floor(diffDays / 7)} weeks ago`;
if (diffDays <= 2) {
return 'Yesterday';
}
if (diffDays <= 7) {
return `${diffDays} days ago`;
}
if (diffDays <= 30) {
return `${Math.floor(diffDays / 7)} weeks ago`;
}
return date.toLocaleDateString(undefined, { return date.toLocaleDateString(undefined, {
year: 'numeric', year: 'numeric',

View File

@ -5,6 +5,8 @@ export interface RepositoryDialogContextType {
setShowAuthDialog: React.Dispatch<React.SetStateAction<boolean>>; setShowAuthDialog: React.Dispatch<React.SetStateAction<boolean>>;
} }
// Default context value with a no-op function
// eslint-disable-next-line @typescript-eslint/no-empty-function
export const RepositoryDialogContext = createContext<RepositoryDialogContextType>({ export const RepositoryDialogContext = createContext<RepositoryDialogContextType>({
setShowAuthDialog: () => {}, setShowAuthDialog: () => {},
}); });

View File

@ -1,5 +1,4 @@
import React, { useContext } from 'react'; import React, { useContext } from 'react';
import { motion } from 'framer-motion';
import type { GitHubRepoInfo } from '~/types/GitHub'; import type { GitHubRepoInfo } from '~/types/GitHub';
import { EmptyState, StatusIndicator } from '~/components/ui'; import { EmptyState, StatusIndicator } from '~/components/ui';
import { RepositoryCard } from './RepositoryCard'; import { RepositoryCard } from './RepositoryCard';

View File

@ -1,5 +1,5 @@
import type { GitHubRepoInfo, GitHubContent, RepositoryStats, GitHubUserResponse } from '~/types/GitHub'; import type { GitHubRepoInfo, GitHubContent, RepositoryStats, GitHubUserResponse } from '~/types/GitHub';
import { useState, useEffect, useContext } from 'react'; import { useState, useEffect } from 'react';
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import * as Dialog from '@radix-ui/react-dialog'; import * as Dialog from '@radix-ui/react-dialog';
import { classNames } from '~/utils/classNames'; import { classNames } from '~/utils/classNames';
@ -8,17 +8,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import Cookies from 'js-cookie'; import Cookies from 'js-cookie';
// Import UI components // Import UI components
import { import { Input, SearchInput, Badge, FilterChip } from '~/components/ui';
Input,
SearchInput,
Badge,
FilterChip,
EmptyState,
GradientCard,
TabsWithSlider,
StatusIndicator,
RepositoryStats as RepoStats,
} from '~/components/ui';
// Import the components we've extracted // Import the components we've extracted
import { TabButton } from './TabButton'; import { TabButton } from './TabButton';

View File

@ -3,7 +3,7 @@ import * as Dialog from '@radix-ui/react-dialog';
import { motion } from 'framer-motion'; import { motion } from 'framer-motion';
import type { RepositoryStats } from '~/types/GitHub'; import type { RepositoryStats } from '~/types/GitHub';
import { formatSize } from '~/utils/formatSize'; import { formatSize } from '~/utils/formatSize';
import { RepositoryStats as RepoStats, Badge } from '~/components/ui'; import { RepositoryStats as RepoStats } from '~/components/ui';
interface StatsDialogProps { interface StatsDialogProps {
isOpen: boolean; isOpen: boolean;

View File

@ -24,24 +24,27 @@ export function Breadcrumbs({
maxItems = 0, maxItems = 0,
renderItem, renderItem,
}: BreadcrumbsProps) { }: BreadcrumbsProps) {
const displayItems = maxItems > 0 && items.length > maxItems const displayItems =
? [ maxItems > 0 && items.length > maxItems
...items.slice(0, 1), ? [
{ label: '...', onClick: undefined, href: undefined }, ...items.slice(0, 1),
...items.slice(-Math.max(1, maxItems - 2)), { label: '...', onClick: undefined, href: undefined },
] ...items.slice(-Math.max(1, maxItems - 2)),
: items; ]
: items;
const defaultRenderItem = (item: BreadcrumbItem, index: number, isLast: boolean) => { const defaultRenderItem = (item: BreadcrumbItem, index: number, isLast: boolean) => {
const content = ( const content = (
<div className="flex items-center gap-1.5"> <div className="flex items-center gap-1.5">
{item.icon && <span className={classNames(item.icon, 'w-3.5 h-3.5')} />} {item.icon && <span className={classNames(item.icon, 'w-3.5 h-3.5')} />}
<span className={classNames( <span
isLast className={classNames(
? 'font-medium text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark' isLast
: 'text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary-dark hover:text-bolt-elements-textPrimary dark:hover:text-bolt-elements-textPrimary-dark', ? 'font-medium text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark'
item.onClick || item.href ? 'cursor-pointer' : '' : 'text-bolt-elements-textSecondary dark:text-bolt-elements-textSecondary-dark hover:text-bolt-elements-textPrimary dark:hover:text-bolt-elements-textPrimary-dark',
)}> item.onClick || item.href ? 'cursor-pointer' : '',
)}
>
{item.label} {item.label}
</span> </span>
</div> </div>
@ -49,12 +52,7 @@ export function Breadcrumbs({
if (item.href && !isLast) { if (item.href && !isLast) {
return ( return (
<motion.a <motion.a href={item.href} className="hover:underline" whileHover={{ scale: 1.05 }} whileTap={{ scale: 0.95 }}>
href={item.href}
className="hover:underline"
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{content} {content}
</motion.a> </motion.a>
); );
@ -82,12 +80,17 @@ export function Breadcrumbs({
<ol className="flex items-center gap-1.5"> <ol className="flex items-center gap-1.5">
{displayItems.map((item, index) => { {displayItems.map((item, index) => {
const isLast = index === displayItems.length - 1; const isLast = index === displayItems.length - 1;
return ( return (
<li key={index} className="flex items-center"> <li key={index} className="flex items-center">
{renderItem ? renderItem(item, index, isLast) : defaultRenderItem(item, index, isLast)} {renderItem ? renderItem(item, index, isLast) : defaultRenderItem(item, index, isLast)}
{!isLast && ( {!isLast && (
<span className={classNames(separator, 'w-3 h-3 mx-1 text-bolt-elements-textTertiary dark:text-bolt-elements-textTertiary-dark')} /> <span
className={classNames(
separator,
'w-3 h-3 mx-1 text-bolt-elements-textTertiary dark:text-bolt-elements-textTertiary-dark',
)}
/>
)} )}
</li> </li>
); );

View File

@ -37,11 +37,13 @@ export function CodeBlock({
const lines = code.split('\n'); const lines = code.split('\n');
return ( return (
<div className={classNames( <div
'rounded-lg overflow-hidden border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark', className={classNames(
'bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3', 'rounded-lg overflow-hidden border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark',
className 'bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3',
)}> className,
)}
>
{/* Header */} {/* Header */}
<div className="flex items-center justify-between px-4 py-2 bg-bolt-elements-background-depth-3 dark:bg-bolt-elements-background-depth-4 border-b border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark"> <div className="flex items-center justify-between px-4 py-2 bg-bolt-elements-background-depth-3 dark:bg-bolt-elements-background-depth-4 border-b border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@ -66,36 +68,26 @@ export function CodeBlock({
whileHover={{ scale: 1.05 }} whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }} whileTap={{ scale: 0.95 }}
> >
{copied ? ( {copied ? <span className="i-ph:check w-4 h-4 text-green-500" /> : <span className="i-ph:copy w-4 h-4" />}
<span className="i-ph:check w-4 h-4 text-green-500" />
) : (
<span className="i-ph:copy w-4 h-4" />
)}
</motion.button> </motion.button>
</Tooltip> </Tooltip>
</div> </div>
{/* Code content */} {/* Code content */}
<div className={classNames( <div className={classNames('overflow-auto', 'font-mono text-sm', 'custom-scrollbar')} style={{ maxHeight }}>
'overflow-auto',
'font-mono text-sm',
'custom-scrollbar'
)} style={{ maxHeight }}>
<table className="min-w-full border-collapse"> <table className="min-w-full border-collapse">
<tbody> <tbody>
{lines.map((line, index) => ( {lines.map((line, index) => (
<tr <tr
key={index} key={index}
className={classNames( className={classNames(
highlightLines.includes(index + 1) ? 'bg-purple-500/10 dark:bg-purple-500/20' : '', highlightLines.includes(index + 1) ? 'bg-purple-500/10 dark:bg-purple-500/20' : '',
'hover:bg-bolt-elements-background-depth-3 dark:hover:bg-bolt-elements-background-depth-4' 'hover:bg-bolt-elements-background-depth-3 dark:hover:bg-bolt-elements-background-depth-4',
)} )}
> >
{showLineNumbers && ( {showLineNumbers && (
<td className="py-1 pl-4 pr-2 text-right select-none text-bolt-elements-textTertiary dark:text-bolt-elements-textTertiary-dark border-r border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark"> <td className="py-1 pl-4 pr-2 text-right select-none text-bolt-elements-textTertiary dark:text-bolt-elements-textTertiary-dark border-r border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark">
<span className="inline-block min-w-[1.5rem] text-xs"> <span className="inline-block min-w-[1.5rem] text-xs">{index + 1}</span>
{index + 1}
</span>
</td> </td>
)} )}
<td className="py-1 pl-4 pr-4 text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark whitespace-pre"> <td className="py-1 pl-4 pr-4 text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark whitespace-pre">

View File

@ -32,20 +32,28 @@ const VARIANT_STYLES = {
interface EmptyStateProps { interface EmptyStateProps {
/** Icon class name */ /** Icon class name */
icon?: string; icon?: string;
/** Title text */ /** Title text */
title: string; title: string;
/** Optional description text */ /** Optional description text */
description?: string; description?: string;
/** Primary action button label */ /** Primary action button label */
actionLabel?: string; actionLabel?: string;
/** Primary action button callback */ /** Primary action button callback */
onAction?: () => void; onAction?: () => void;
/** Secondary action button label */ /** Secondary action button label */
secondaryActionLabel?: string; secondaryActionLabel?: string;
/** Secondary action button callback */ /** Secondary action button callback */
onSecondaryAction?: () => void; onSecondaryAction?: () => void;
/** Additional class name */ /** Additional class name */
className?: string; className?: string;
/** Component size variant */ /** Component size variant */
variant?: 'default' | 'compact'; variant?: 'default' | 'compact';
} }

View File

@ -14,110 +14,312 @@ export function FileIcon({ filename, size = 'md', className }: FileIconProps) {
const getIconForExtension = (extension: string): string => { const getIconForExtension = (extension: string): string => {
// Code files // Code files
if (['js', 'jsx', 'ts', 'tsx'].includes(extension)) return 'i-ph:file-js'; if (['js', 'jsx', 'ts', 'tsx'].includes(extension)) {
if (['html', 'htm', 'xhtml'].includes(extension)) return 'i-ph:file-html'; return 'i-ph:file-js';
if (['css', 'scss', 'sass', 'less'].includes(extension)) return 'i-ph:file-css'; }
if (['json', 'jsonc'].includes(extension)) return 'i-ph:brackets-curly';
if (['md', 'markdown'].includes(extension)) return 'i-ph:file-text'; if (['html', 'htm', 'xhtml'].includes(extension)) {
if (['py', 'pyc', 'pyd', 'pyo'].includes(extension)) return 'i-ph:file-py'; return 'i-ph:file-html';
if (['java', 'class', 'jar'].includes(extension)) return 'i-ph:file-java'; }
if (['php'].includes(extension)) return 'i-ph:file-php';
if (['rb', 'ruby'].includes(extension)) return 'i-ph:file-rs'; if (['css', 'scss', 'sass', 'less'].includes(extension)) {
if (['c', 'cpp', 'h', 'hpp', 'cc'].includes(extension)) return 'i-ph:file-cpp'; return 'i-ph:file-css';
if (['go'].includes(extension)) return 'i-ph:file-rs'; }
if (['rs', 'rust'].includes(extension)) return 'i-ph:file-rs';
if (['swift'].includes(extension)) return 'i-ph:file-swift'; if (['json', 'jsonc'].includes(extension)) {
if (['kt', 'kotlin'].includes(extension)) return 'i-ph:file-kotlin'; return 'i-ph:brackets-curly';
if (['dart'].includes(extension)) return 'i-ph:file-dart'; }
if (['md', 'markdown'].includes(extension)) {
return 'i-ph:file-text';
}
if (['py', 'pyc', 'pyd', 'pyo'].includes(extension)) {
return 'i-ph:file-py';
}
if (['java', 'class', 'jar'].includes(extension)) {
return 'i-ph:file-java';
}
if (['php'].includes(extension)) {
return 'i-ph:file-php';
}
if (['rb', 'ruby'].includes(extension)) {
return 'i-ph:file-rs';
}
if (['c', 'cpp', 'h', 'hpp', 'cc'].includes(extension)) {
return 'i-ph:file-cpp';
}
if (['go'].includes(extension)) {
return 'i-ph:file-rs';
}
if (['rs', 'rust'].includes(extension)) {
return 'i-ph:file-rs';
}
if (['swift'].includes(extension)) {
return 'i-ph:file-swift';
}
if (['kt', 'kotlin'].includes(extension)) {
return 'i-ph:file-kotlin';
}
if (['dart'].includes(extension)) {
return 'i-ph:file-dart';
}
// Config files // Config files
if (['yml', 'yaml'].includes(extension)) return 'i-ph:file-cloud'; if (['yml', 'yaml'].includes(extension)) {
if (['xml', 'svg'].includes(extension)) return 'i-ph:file-xml'; return 'i-ph:file-cloud';
if (['toml'].includes(extension)) return 'i-ph:file-text'; }
if (['ini', 'conf', 'config'].includes(extension)) return 'i-ph:file-text';
if (['env', 'env.local', 'env.development', 'env.production'].includes(extension)) return 'i-ph:file-lock'; if (['xml', 'svg'].includes(extension)) {
return 'i-ph:file-xml';
}
if (['toml'].includes(extension)) {
return 'i-ph:file-text';
}
if (['ini', 'conf', 'config'].includes(extension)) {
return 'i-ph:file-text';
}
if (['env', 'env.local', 'env.development', 'env.production'].includes(extension)) {
return 'i-ph:file-lock';
}
// Document files // Document files
if (['pdf'].includes(extension)) return 'i-ph:file-pdf'; if (['pdf'].includes(extension)) {
if (['doc', 'docx'].includes(extension)) return 'i-ph:file-doc'; return 'i-ph:file-pdf';
if (['xls', 'xlsx'].includes(extension)) return 'i-ph:file-xls'; }
if (['ppt', 'pptx'].includes(extension)) return 'i-ph:file-ppt';
if (['txt'].includes(extension)) return 'i-ph:file-text'; if (['doc', 'docx'].includes(extension)) {
return 'i-ph:file-doc';
}
if (['xls', 'xlsx'].includes(extension)) {
return 'i-ph:file-xls';
}
if (['ppt', 'pptx'].includes(extension)) {
return 'i-ph:file-ppt';
}
if (['txt'].includes(extension)) {
return 'i-ph:file-text';
}
// Image files // Image files
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'ico', 'tiff'].includes(extension)) return 'i-ph:file-image'; if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'ico', 'tiff'].includes(extension)) {
return 'i-ph:file-image';
}
// Audio/Video files // Audio/Video files
if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(extension)) return 'i-ph:file-audio'; if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(extension)) {
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) return 'i-ph:file-video'; return 'i-ph:file-audio';
}
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) {
return 'i-ph:file-video';
}
// Archive files // Archive files
if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(extension)) return 'i-ph:file-zip'; if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(extension)) {
return 'i-ph:file-zip';
}
// Special files // Special files
if (filename === 'package.json') return 'i-ph:package'; if (filename === 'package.json') {
if (filename === 'tsconfig.json') return 'i-ph:file-ts'; return 'i-ph:package';
if (filename === 'README.md') return 'i-ph:book-open'; }
if (filename === 'LICENSE') return 'i-ph:scales';
if (filename === '.gitignore') return 'i-ph:git-branch'; if (filename === 'tsconfig.json') {
if (filename.startsWith('Dockerfile')) return 'i-ph:docker-logo'; return 'i-ph:file-ts';
}
if (filename === 'README.md') {
return 'i-ph:book-open';
}
if (filename === 'LICENSE') {
return 'i-ph:scales';
}
if (filename === '.gitignore') {
return 'i-ph:git-branch';
}
if (filename.startsWith('Dockerfile')) {
return 'i-ph:docker-logo';
}
// Default // Default
return 'i-ph:file'; return 'i-ph:file';
}; };
const getIconColorForExtension = (extension: string): string => { const getIconColorForExtension = (extension: string): string => {
// Code files // Code files
if (['js', 'jsx'].includes(extension)) return 'text-yellow-500'; if (['js', 'jsx'].includes(extension)) {
if (['ts', 'tsx'].includes(extension)) return 'text-blue-500'; return 'text-yellow-500';
if (['html', 'htm', 'xhtml'].includes(extension)) return 'text-orange-500'; }
if (['css', 'scss', 'sass', 'less'].includes(extension)) return 'text-blue-400';
if (['json', 'jsonc'].includes(extension)) return 'text-yellow-400'; if (['ts', 'tsx'].includes(extension)) {
if (['md', 'markdown'].includes(extension)) return 'text-gray-500'; return 'text-blue-500';
if (['py', 'pyc', 'pyd', 'pyo'].includes(extension)) return 'text-green-500'; }
if (['java', 'class', 'jar'].includes(extension)) return 'text-red-500';
if (['php'].includes(extension)) return 'text-purple-500'; if (['html', 'htm', 'xhtml'].includes(extension)) {
if (['rb', 'ruby'].includes(extension)) return 'text-red-600'; return 'text-orange-500';
if (['c', 'cpp', 'h', 'hpp', 'cc'].includes(extension)) return 'text-blue-600'; }
if (['go'].includes(extension)) return 'text-cyan-500';
if (['rs', 'rust'].includes(extension)) return 'text-orange-600'; if (['css', 'scss', 'sass', 'less'].includes(extension)) {
if (['swift'].includes(extension)) return 'text-orange-500'; return 'text-blue-400';
if (['kt', 'kotlin'].includes(extension)) return 'text-purple-400'; }
if (['dart'].includes(extension)) return 'text-cyan-400';
if (['json', 'jsonc'].includes(extension)) {
return 'text-yellow-400';
}
if (['md', 'markdown'].includes(extension)) {
return 'text-gray-500';
}
if (['py', 'pyc', 'pyd', 'pyo'].includes(extension)) {
return 'text-green-500';
}
if (['java', 'class', 'jar'].includes(extension)) {
return 'text-red-500';
}
if (['php'].includes(extension)) {
return 'text-purple-500';
}
if (['rb', 'ruby'].includes(extension)) {
return 'text-red-600';
}
if (['c', 'cpp', 'h', 'hpp', 'cc'].includes(extension)) {
return 'text-blue-600';
}
if (['go'].includes(extension)) {
return 'text-cyan-500';
}
if (['rs', 'rust'].includes(extension)) {
return 'text-orange-600';
}
if (['swift'].includes(extension)) {
return 'text-orange-500';
}
if (['kt', 'kotlin'].includes(extension)) {
return 'text-purple-400';
}
if (['dart'].includes(extension)) {
return 'text-cyan-400';
}
// Config files // Config files
if (['yml', 'yaml'].includes(extension)) return 'text-purple-300'; if (['yml', 'yaml'].includes(extension)) {
if (['xml'].includes(extension)) return 'text-orange-300'; return 'text-purple-300';
if (['svg'].includes(extension)) return 'text-green-400'; }
if (['toml'].includes(extension)) return 'text-gray-500';
if (['ini', 'conf', 'config'].includes(extension)) return 'text-gray-500'; if (['xml'].includes(extension)) {
if (['env', 'env.local', 'env.development', 'env.production'].includes(extension)) return 'text-green-500'; return 'text-orange-300';
}
if (['svg'].includes(extension)) {
return 'text-green-400';
}
if (['toml'].includes(extension)) {
return 'text-gray-500';
}
if (['ini', 'conf', 'config'].includes(extension)) {
return 'text-gray-500';
}
if (['env', 'env.local', 'env.development', 'env.production'].includes(extension)) {
return 'text-green-500';
}
// Document files // Document files
if (['pdf'].includes(extension)) return 'text-red-500'; if (['pdf'].includes(extension)) {
if (['doc', 'docx'].includes(extension)) return 'text-blue-600'; return 'text-red-500';
if (['xls', 'xlsx'].includes(extension)) return 'text-green-600'; }
if (['ppt', 'pptx'].includes(extension)) return 'text-red-600';
if (['txt'].includes(extension)) return 'text-gray-500'; if (['doc', 'docx'].includes(extension)) {
return 'text-blue-600';
}
if (['xls', 'xlsx'].includes(extension)) {
return 'text-green-600';
}
if (['ppt', 'pptx'].includes(extension)) {
return 'text-red-600';
}
if (['txt'].includes(extension)) {
return 'text-gray-500';
}
// Image files // Image files
if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'ico', 'tiff'].includes(extension)) return 'text-pink-500'; if (['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'ico', 'tiff'].includes(extension)) {
return 'text-pink-500';
}
// Audio/Video files // Audio/Video files
if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(extension)) return 'text-green-500'; if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(extension)) {
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) return 'text-blue-500'; return 'text-green-500';
}
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) {
return 'text-blue-500';
}
// Archive files // Archive files
if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(extension)) return 'text-yellow-600'; if (['zip', 'rar', '7z', 'tar', 'gz', 'bz2'].includes(extension)) {
return 'text-yellow-600';
}
// Special files // Special files
if (filename === 'package.json') return 'text-red-400'; if (filename === 'package.json') {
if (filename === 'tsconfig.json') return 'text-blue-500'; return 'text-red-400';
if (filename === 'README.md') return 'text-blue-400'; }
if (filename === 'LICENSE') return 'text-gray-500';
if (filename === '.gitignore') return 'text-orange-500'; if (filename === 'tsconfig.json') {
if (filename.startsWith('Dockerfile')) return 'text-blue-500'; return 'text-blue-500';
}
if (filename === 'README.md') {
return 'text-blue-400';
}
if (filename === 'LICENSE') {
return 'text-gray-500';
}
if (filename === '.gitignore') {
return 'text-orange-500';
}
if (filename.startsWith('Dockerfile')) {
return 'text-blue-500';
}
// Default // Default
return 'text-gray-400'; return 'text-gray-400';
}; };
@ -140,7 +342,5 @@ export function FileIcon({ filename, size = 'md', className }: FileIconProps) {
const color = getIconColorForExtension(extension); const color = getIconColorForExtension(extension);
const sizeClass = getSizeClass(size); const sizeClass = getSizeClass(size);
return ( return <span className={classNames(icon, color, sizeClass, className)} />;
<span className={classNames(icon, color, sizeClass, className)} />
);
} }

View File

@ -5,14 +5,19 @@ import { classNames } from '~/utils/classNames';
interface FilterChipProps { interface FilterChipProps {
/** The label text to display */ /** The label text to display */
label: string; label: string;
/** Optional value to display after the label */ /** Optional value to display after the label */
value?: string | number; value?: string | number;
/** Function to call when the remove button is clicked */ /** Function to call when the remove button is clicked */
onRemove?: () => void; onRemove?: () => void;
/** Whether the chip is active/selected */ /** Whether the chip is active/selected */
active?: boolean; active?: boolean;
/** Optional icon to display before the label */ /** Optional icon to display before the label */
icon?: string; icon?: string;
/** Additional class name */ /** Additional class name */
className?: string; className?: string;
} }

View File

@ -14,17 +14,27 @@ const GRADIENT_COLORS = [
'from-pink-500/10 to-purple-500/5', 'from-pink-500/10 to-purple-500/5',
]; ];
interface GradientCardProps extends React.HTMLAttributes<HTMLDivElement> { interface GradientCardProps {
/** Custom gradient class (overrides seed-based gradient) */ /** Custom gradient class (overrides seed-based gradient) */
gradient?: string; gradient?: string;
/** Seed string to determine gradient color */ /** Seed string to determine gradient color */
seed?: string; seed?: string;
/** Whether to apply hover animation effect */ /** Whether to apply hover animation effect */
hoverEffect?: boolean; hoverEffect?: boolean;
/** Whether to apply border effect */ /** Whether to apply border effect */
borderEffect?: boolean; borderEffect?: boolean;
/** Card content */ /** Card content */
children: React.ReactNode; children: React.ReactNode;
/** Additional class name */
className?: string;
/** Additional props */
[key: string]: any;
} }
/** /**
@ -54,7 +64,7 @@ export function GradientCard({
}, },
whileTap: { scale: 0.98 }, whileTap: { scale: 0.98 },
} }
: {}; : undefined;
return ( return (
<motion.div <motion.div
@ -80,8 +90,11 @@ export function GradientCard({
* Calculate a gradient color based on the seed string for visual variety * Calculate a gradient color based on the seed string for visual variety
*/ */
function getGradientColorFromSeed(seedString?: string): string { function getGradientColorFromSeed(seedString?: string): string {
if (!seedString) return GRADIENT_COLORS[0]; if (!seedString) {
return GRADIENT_COLORS[0];
}
const index = seedString.length % GRADIENT_COLORS.length; const index = seedString.length % GRADIENT_COLORS.length;
return GRADIENT_COLORS[index]; return GRADIENT_COLORS[index];
} }

View File

@ -25,7 +25,7 @@ export function RepositoryStats({ stats, className, compact = false }: Repositor
Repository Statistics: Repository Statistics:
</p> </p>
)} )}
<div className={classNames('grid gap-3', compact ? 'grid-cols-2' : 'grid-cols-2 md:grid-cols-3')}> <div className={classNames('grid gap-3', compact ? 'grid-cols-2' : 'grid-cols-2 md:grid-cols-3')}>
{totalFiles !== undefined && ( {totalFiles !== undefined && (
<div className="flex items-center gap-2 text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark"> <div className="flex items-center gap-2 text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark">
@ -33,7 +33,7 @@ export function RepositoryStats({ stats, className, compact = false }: Repositor
<span className={compact ? 'text-xs' : 'text-sm'}>Total Files: {totalFiles.toLocaleString()}</span> <span className={compact ? 'text-xs' : 'text-sm'}>Total Files: {totalFiles.toLocaleString()}</span>
</div> </div>
)} )}
{totalSize !== undefined && ( {totalSize !== undefined && (
<div className="flex items-center gap-2 text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark"> <div className="flex items-center gap-2 text-bolt-elements-textPrimary dark:text-bolt-elements-textPrimary-dark">
<span className="i-ph:database text-purple-500 w-4 h-4" /> <span className="i-ph:database text-purple-500 w-4 h-4" />
@ -53,19 +53,12 @@ export function RepositoryStats({ stats, className, compact = false }: Repositor
.sort(([, a], [, b]) => b - a) .sort(([, a], [, b]) => b - a)
.slice(0, compact ? 3 : 5) .slice(0, compact ? 3 : 5)
.map(([lang, size]) => ( .map(([lang, size]) => (
<Badge <Badge key={lang} variant="subtle" size={compact ? 'sm' : 'md'}>
key={lang}
variant="subtle"
size={compact ? 'sm' : 'md'}
>
{lang} ({formatSize(size)}) {lang} ({formatSize(size)})
</Badge> </Badge>
))} ))}
{Object.keys(languages).length > (compact ? 3 : 5) && ( {Object.keys(languages).length > (compact ? 3 : 5) && (
<Badge <Badge variant="subtle" size={compact ? 'sm' : 'md'}>
variant="subtle"
size={compact ? 'sm' : 'md'}
>
+{Object.keys(languages).length - (compact ? 3 : 5)} more +{Object.keys(languages).length - (compact ? 3 : 5)} more
</Badge> </Badge>
)} )}
@ -77,20 +70,12 @@ export function RepositoryStats({ stats, className, compact = false }: Repositor
<div className={compact ? 'pt-1' : 'pt-2'}> <div className={compact ? 'pt-1' : 'pt-2'}>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">
{hasPackageJson && ( {hasPackageJson && (
<Badge <Badge variant="primary" size={compact ? 'sm' : 'md'} icon="i-ph:package w-3.5 h-3.5">
variant="primary"
size={compact ? 'sm' : 'md'}
icon="i-ph:package w-3.5 h-3.5"
>
package.json package.json
</Badge> </Badge>
)} )}
{hasDependencies && ( {hasDependencies && (
<Badge <Badge variant="primary" size={compact ? 'sm' : 'md'} icon="i-ph:tree-structure w-3.5 h-3.5">
variant="primary"
size={compact ? 'sm' : 'md'}
icon="i-ph:tree-structure w-3.5 h-3.5"
>
Dependencies Dependencies
</Badge> </Badge>
)} )}

View File

@ -6,12 +6,16 @@ import { motion, AnimatePresence } from 'framer-motion';
interface SearchInputProps extends React.InputHTMLAttributes<HTMLInputElement> { interface SearchInputProps extends React.InputHTMLAttributes<HTMLInputElement> {
/** Function to call when the clear button is clicked */ /** Function to call when the clear button is clicked */
onClear?: () => void; onClear?: () => void;
/** Whether to show the clear button when there is input */ /** Whether to show the clear button when there is input */
showClearButton?: boolean; showClearButton?: boolean;
/** Additional class name for the search icon */ /** Additional class name for the search icon */
iconClassName?: string; iconClassName?: string;
/** Additional class name for the container */ /** Additional class name for the container */
containerClassName?: string; containerClassName?: string;
/** Whether the search is loading */ /** Whether the search is loading */
loading?: boolean; loading?: boolean;
} }
@ -47,7 +51,7 @@ export const SearchInput = forwardRef<HTMLInputElement, SearchInputProps>(
{/* Input field */} {/* Input field */}
<Input <Input
ref={ref} ref={ref}
className={classNames('pl-10', hasValue && showClearButton && 'pr-10', className)} className={classNames('pl-10', hasValue && showClearButton ? 'pr-10' : '', className)}
{...props} {...props}
/> />

View File

@ -41,7 +41,7 @@ export function SearchResultItem({
className={classNames( className={classNames(
'p-5 rounded-xl 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 bg-bolt-elements-background-depth-1/50 dark:bg-bolt-elements-background-depth-3/50', 'p-5 rounded-xl 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 bg-bolt-elements-background-depth-1/50 dark:bg-bolt-elements-background-depth-3/50',
onClick ? 'cursor-pointer' : '', onClick ? 'cursor-pointer' : '',
className className,
)} )}
whileHover={{ whileHover={{
scale: 1.01, scale: 1.01,
@ -57,7 +57,12 @@ export function SearchResultItem({
<div className="flex items-start justify-between mb-3 gap-3"> <div className="flex items-start justify-between mb-3 gap-3">
<div className="flex items-start gap-3"> <div className="flex items-start gap-3">
{icon && ( {icon && (
<div className={classNames('w-10 h-10 rounded-xl backdrop-blur-sm flex items-center justify-center shadow-sm', iconBackground)}> <div
className={classNames(
'w-10 h-10 rounded-xl backdrop-blur-sm flex items-center justify-center shadow-sm',
iconBackground,
)}
>
<span className={classNames(icon, 'w-5 h-5', iconColor)} /> <span className={classNames(icon, 'w-5 h-5', iconColor)} />
</div> </div>
)} )}
@ -72,7 +77,7 @@ export function SearchResultItem({
)} )}
</div> </div>
</div> </div>
{actionLabel && onAction && ( {actionLabel && onAction && (
<motion.button <motion.button
onClick={(e) => { onClick={(e) => {

View File

@ -37,12 +37,16 @@ const TEXT_SIZE_CLASSES: Record<SizeType, string> = {
interface StatusIndicatorProps { interface StatusIndicatorProps {
/** The status to display */ /** The status to display */
status: StatusType; status: StatusType;
/** Size of the indicator */ /** Size of the indicator */
size?: SizeType; size?: SizeType;
/** Whether to show a pulsing animation */ /** Whether to show a pulsing animation */
pulse?: boolean; pulse?: boolean;
/** Optional label text */ /** Optional label text */
label?: string; label?: string;
/** Additional class name */ /** Additional class name */
className?: string; className?: string;
} }

View File

@ -5,8 +5,10 @@ import { classNames } from '~/utils/classNames';
interface Tab { interface Tab {
/** Unique identifier for the tab */ /** Unique identifier for the tab */
id: string; id: string;
/** Content to display in the tab */ /** Content to display in the tab */
label: React.ReactNode; label: React.ReactNode;
/** Optional icon to display before the label */ /** Optional icon to display before the label */
icon?: string; icon?: string;
} }
@ -14,16 +16,22 @@ interface Tab {
interface TabsWithSliderProps { interface TabsWithSliderProps {
/** Array of tab objects */ /** Array of tab objects */
tabs: Tab[]; tabs: Tab[];
/** ID of the currently active tab */ /** ID of the currently active tab */
activeTab: string; activeTab: string;
/** Function called when a tab is clicked */ /** Function called when a tab is clicked */
onChange: (tabId: string) => void; onChange: (tabId: string) => void;
/** Additional class name for the container */ /** Additional class name for the container */
className?: string; className?: string;
/** Additional class name for inactive tabs */ /** Additional class name for inactive tabs */
tabClassName?: string; tabClassName?: string;
/** Additional class name for the active tab */ /** Additional class name for the active tab */
activeTabClassName?: string; activeTabClassName?: string;
/** Additional class name for the slider */ /** Additional class name for the slider */
sliderClassName?: string; sliderClassName?: string;
} }
@ -51,8 +59,10 @@ export function TabsWithSlider({
// Update slider position when active tab changes // Update slider position when active tab changes
useEffect(() => { useEffect(() => {
const activeIndex = tabs.findIndex((tab) => tab.id === activeTab); const activeIndex = tabs.findIndex((tab) => tab.id === activeTab);
if (activeIndex !== -1 && tabsRef.current[activeIndex]) { if (activeIndex !== -1 && tabsRef.current[activeIndex]) {
const activeTabElement = tabsRef.current[activeIndex]; const activeTabElement = tabsRef.current[activeIndex];
if (activeTabElement) { if (activeTabElement) {
setSliderDimensions({ setSliderDimensions({
width: activeTabElement.offsetWidth, width: activeTabElement.offsetWidth,