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

View File

@ -5,6 +5,8 @@ export interface RepositoryDialogContextType {
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>({
setShowAuthDialog: () => {},
});

View File

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

View File

@ -1,5 +1,5 @@
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 * as Dialog from '@radix-ui/react-dialog';
import { classNames } from '~/utils/classNames';
@ -8,17 +8,7 @@ import { motion, AnimatePresence } from 'framer-motion';
import Cookies from 'js-cookie';
// Import UI components
import {
Input,
SearchInput,
Badge,
FilterChip,
EmptyState,
GradientCard,
TabsWithSlider,
StatusIndicator,
RepositoryStats as RepoStats,
} from '~/components/ui';
import { Input, SearchInput, Badge, FilterChip } from '~/components/ui';
// Import the components we've extracted
import { TabButton } from './TabButton';

View File

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

View File

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

View File

@ -37,11 +37,13 @@ export function CodeBlock({
const lines = code.split('\n');
return (
<div className={classNames(
'rounded-lg overflow-hidden border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark',
'bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3',
className
)}>
<div
className={classNames(
'rounded-lg overflow-hidden border border-bolt-elements-borderColor dark:border-bolt-elements-borderColor-dark',
'bg-bolt-elements-background-depth-2 dark:bg-bolt-elements-background-depth-3',
className,
)}
>
{/* 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 gap-2">
@ -66,36 +68,26 @@ export function CodeBlock({
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
>
{copied ? (
<span className="i-ph:check w-4 h-4 text-green-500" />
) : (
<span className="i-ph:copy w-4 h-4" />
)}
{copied ? <span className="i-ph:check w-4 h-4 text-green-500" /> : <span className="i-ph:copy w-4 h-4" />}
</motion.button>
</Tooltip>
</div>
{/* Code content */}
<div className={classNames(
'overflow-auto',
'font-mono text-sm',
'custom-scrollbar'
)} style={{ maxHeight }}>
<div className={classNames('overflow-auto', 'font-mono text-sm', 'custom-scrollbar')} style={{ maxHeight }}>
<table className="min-w-full border-collapse">
<tbody>
{lines.map((line, index) => (
<tr
<tr
key={index}
className={classNames(
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 && (
<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">
{index + 1}
</span>
<span className="inline-block min-w-[1.5rem] text-xs">{index + 1}</span>
</td>
)}
<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 {
/** Icon class name */
icon?: string;
/** Title text */
title: string;
/** Optional description text */
description?: string;
/** Primary action button label */
actionLabel?: string;
/** Primary action button callback */
onAction?: () => void;
/** Secondary action button label */
secondaryActionLabel?: string;
/** Secondary action button callback */
onSecondaryAction?: () => void;
/** Additional class name */
className?: string;
/** Component size variant */
variant?: 'default' | 'compact';
}

View File

@ -14,110 +14,312 @@ export function FileIcon({ filename, size = 'md', className }: FileIconProps) {
const getIconForExtension = (extension: string): string => {
// Code files
if (['js', 'jsx', 'ts', 'tsx'].includes(extension)) return 'i-ph:file-js';
if (['html', 'htm', 'xhtml'].includes(extension)) return 'i-ph:file-html';
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 (['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';
if (['js', 'jsx', 'ts', 'tsx'].includes(extension)) {
return 'i-ph:file-js';
}
if (['html', 'htm', 'xhtml'].includes(extension)) {
return 'i-ph:file-html';
}
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 (['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
if (['yml', 'yaml'].includes(extension)) return 'i-ph:file-cloud';
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';
if (['yml', 'yaml'].includes(extension)) {
return 'i-ph:file-cloud';
}
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
if (['pdf'].includes(extension)) return 'i-ph:file-pdf';
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';
if (['pdf'].includes(extension)) {
return 'i-ph:file-pdf';
}
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
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
if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(extension)) return 'i-ph:file-audio';
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) return 'i-ph:file-video';
if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(extension)) {
return 'i-ph:file-audio';
}
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) {
return 'i-ph:file-video';
}
// 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
if (filename === 'package.json') return 'i-ph:package';
if (filename === 'tsconfig.json') 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';
if (filename === 'package.json') {
return 'i-ph:package';
}
if (filename === 'tsconfig.json') {
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
return 'i-ph:file';
};
const getIconColorForExtension = (extension: string): string => {
// Code files
if (['js', 'jsx'].includes(extension)) return 'text-yellow-500';
if (['ts', 'tsx'].includes(extension)) return 'text-blue-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 (['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';
if (['js', 'jsx'].includes(extension)) {
return 'text-yellow-500';
}
if (['ts', 'tsx'].includes(extension)) {
return 'text-blue-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 (['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
if (['yml', 'yaml'].includes(extension)) return 'text-purple-300';
if (['xml'].includes(extension)) 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';
if (['yml', 'yaml'].includes(extension)) {
return 'text-purple-300';
}
if (['xml'].includes(extension)) {
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
if (['pdf'].includes(extension)) return 'text-red-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';
if (['pdf'].includes(extension)) {
return 'text-red-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
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
if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(extension)) return 'text-green-500';
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) return 'text-blue-500';
if (['mp3', 'wav', 'ogg', 'flac', 'aac'].includes(extension)) {
return 'text-green-500';
}
if (['mp4', 'avi', 'mov', 'wmv', 'flv', 'webm', 'mkv'].includes(extension)) {
return 'text-blue-500';
}
// 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
if (filename === 'package.json') return 'text-red-400';
if (filename === 'tsconfig.json') 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';
if (filename === 'package.json') {
return 'text-red-400';
}
if (filename === 'tsconfig.json') {
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
return 'text-gray-400';
};
@ -140,7 +342,5 @@ export function FileIcon({ filename, size = 'md', className }: FileIconProps) {
const color = getIconColorForExtension(extension);
const sizeClass = getSizeClass(size);
return (
<span className={classNames(icon, color, sizeClass, className)} />
);
return <span className={classNames(icon, color, sizeClass, className)} />;
}

View File

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

View File

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

View File

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

View File

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

View File

@ -41,7 +41,7 @@ export function SearchResultItem({
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',
onClick ? 'cursor-pointer' : '',
className
className,
)}
whileHover={{
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 gap-3">
{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)} />
</div>
)}
@ -72,7 +77,7 @@ export function SearchResultItem({
)}
</div>
</div>
{actionLabel && onAction && (
<motion.button
onClick={(e) => {

View File

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

View File

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