diff --git a/src/App.tsx b/src/App.tsx index 6e1f8717..36016a72 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -51,11 +51,12 @@ const SettingsView = React.memo(() => ); SettingsView.displayName = 'SettingsView'; function App() { - const { - isAuthenticated, - currentView, + const { + isAuthenticated, + currentView, selectedCategory, theme, + hasHydrated, searchResults, repositories, setSelectedCategory, @@ -107,7 +108,7 @@ function App() { switch (currentView) { case 'repositories': return ( - +
+ Loading... +
+ + ); + } + if (!isAuthenticated) { return ; } diff --git a/src/components/MarkdownRenderer.tsx b/src/components/MarkdownRenderer.tsx index 00f8e3c2..4c287405 100644 --- a/src/components/MarkdownRenderer.tsx +++ b/src/components/MarkdownRenderer.tsx @@ -18,6 +18,7 @@ interface MarkdownRendererProps { enableHtml?: boolean; baseUrl?: string; headingIds?: Map; + fontSize?: 'small' | 'medium' | 'large'; } const REMARK_PLUGINS = [remarkGfm, remarkBreaks]; @@ -92,10 +93,6 @@ const CodeBlock: React.FC<{ } }, [codeText, uiLanguage]); - const codeLines = codeText.split('\n'); - const lineCount = codeLines.length; - const showLineNumbers = lineCount > 3; - const isBashLike = ['bash', 'sh', 'shell', 'zsh'].includes(normalizedLanguage); const isPowerShell = ['powershell', 'ps1'].includes(normalizedLanguage); const isCmdLike = ['cmd', 'bat'].includes(normalizedLanguage); @@ -141,11 +138,6 @@ const CodeBlock: React.FC<{ )}
- {showLineNumbers && ( - - {lineCount} {uiLanguage === 'zh' ? '行' : 'lines'} - - )}
); @@ -481,7 +448,9 @@ const MarkdownImage: React.FC<{ src?: string; alt?: string; baseUrl?: string }> setIsLoading(false); }, []); - const handleRetry = useCallback(() => { + const handleRetry = useCallback((e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); setHasError(false); setIsLoading(true); setImageSizeKnown(false); @@ -784,7 +753,8 @@ const MarkdownRenderer: React.FC = memo(({ shouldRender = true, enableHtml = false, baseUrl, - headingIds + headingIds, + fontSize = 'medium' }) => { const headingCounterRef = useRef(headingIds?.size ?? 0); const headingTextCountMapRef = useRef(new Map()); @@ -796,6 +766,18 @@ const MarkdownRenderer: React.FC = memo(({ const rehypePlugins = enableHtml ? REHYPE_PLUGINS_WITH_HTML : REHYPE_PLUGINS_NO_HTML; + const getProseClass = useCallback(() => { + switch (fontSize) { + case 'small': + return 'prose prose-sm dark:prose-invert'; + case 'large': + return 'prose prose-lg dark:prose-invert'; + case 'medium': + default: + return 'prose dark:prose-invert'; + } + }, [fontSize]); + const getHeadingId = useCallback((children: React.ReactNode): string | undefined => { if (headingIds && headingIds.size > 0) { const text = extractTextFromChildren(children); @@ -933,7 +915,7 @@ const MarkdownRenderer: React.FC = memo(({ } return ( -
+
= ({ const currentFontSize = FONT_SIZES[fontSizeIndex].value; + const getFontSizeType = useCallback((): 'small' | 'medium' | 'large' => { + switch (fontSizeIndex) { + case 0: + return 'small'; + case 2: + return 'large'; + case 1: + default: + return 'medium'; + } + }, [fontSizeIndex]); + const extractToc = useCallback((content: string): { items: TocItem[], idMap: Map } => { const items: TocItem[] = []; const idMap = new Map(); @@ -477,6 +489,7 @@ export const ReadmeModal: React.FC = ({ enableHtml={true} baseUrl={repository?.html_url} headingIds={headingIdMap} + fontSize={getFontSizeType()} /> ) : (
diff --git a/src/components/RepositoryCard.tsx b/src/components/RepositoryCard.tsx index 71ce7297..f02243c4 100644 --- a/src/components/RepositoryCard.tsx +++ b/src/components/RepositoryCard.tsx @@ -873,28 +873,28 @@ const RepositoryCardComponent: React.FC = ({
- {/* Description with Tooltip */} + {/* Description with Tooltip - Enhanced for Light Mode */}
isTextTruncated && setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)} >

{highlightSearchTerm(displayContent.content, searchQuery)}

- {/* Tooltip - Only show when text is actually truncated */} + {/* Enhanced Tooltip - Optimized for Light Mode Readability */} {isTextTruncated && showTooltip && ( -
-
+
+
{highlightSearchTerm(displayContent.content, searchQuery)}
- {/* Arrow */} -
+ {/* Arrow with Light Mode Optimization */} +
)}
diff --git a/src/components/RepositoryEditModal.tsx b/src/components/RepositoryEditModal.tsx index 4e78de2f..cd6e56ec 100644 --- a/src/components/RepositoryEditModal.tsx +++ b/src/components/RepositoryEditModal.tsx @@ -54,7 +54,7 @@ export const RepositoryEditModal: React.FC = ({ onClose, repository }) => { - const { updateRepository, language, customCategories, hiddenDefaultCategoryIds, defaultCategoryOverrides } = useAppStore(); + const { updateRepository, language, customCategories, hiddenDefaultCategoryIds, defaultCategoryOverrides, theme } = useAppStore(); const [formData, setFormData] = useState({ description: '', @@ -589,12 +589,15 @@ export const RepositoryEditModal: React.FC = ({ if (!repository) return null; - // 统一的卡片样式 - const sectionClass = "p-5 bg-white dark:bg-panel-dark rounded-xl border border-black/[0.06] dark:border-white/[0.04]"; - const labelClass = "flex items-center space-x-2 text-sm font-medium text-gray-900 dark:text-text-primary mb-3"; - const inputClass = "w-full px-3 py-2 bg-light-bg dark:bg-white/[0.04] border border-black/[0.06] dark:border-white/[0.04] rounded-lg text-gray-900 dark:text-text-primary placeholder-gray-400 dark:placeholder-text-tertiary focus:outline-none focus:ring-2 focus:ring-brand-violet focus:border-transparent transition-all"; - const buttonSecondaryClass = "flex items-center space-x-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border transition-all"; - const tagClass = "inline-flex items-center px-2.5 py-1 bg-gray-100 text-gray-700 dark:bg-white/[0.04] dark:text-text-secondary border-transparent rounded-md text-sm border border-black/[0.06] dark:border-white/[0.04] dark:border-white/[0.04]"; + // Unified card styles with enhanced light mode optimization + const sectionClass = "p-5 bg-white dark:bg-panel-dark rounded-xl border border-gray-200/80 dark:border-white/[0.04] shadow-sm"; + const labelClass = "flex items-center space-x-2 text-[13px] font-medium text-gray-900 dark:text-text-primary mb-3"; + const inputClass = "w-full px-4 py-3 bg-gray-50/50 dark:bg-white/[0.04] border border-gray-200 dark:border-white/[0.04] rounded-xl text-gray-900 dark:text-text-primary placeholder-gray-400 dark:placeholder-text-tertiary focus:outline-none focus:ring-2 focus:ring-brand-violet/30 focus:border-brand-violet dark:focus:ring-brand-violet/50 dark:focus:border-brand-violet transition-all duration-200 hover:bg-gray-100/50 dark:hover:bg-white/[0.06] hover:border-gray-300 dark:hover:border-white/[0.08] text-[13px] leading-[1.625]"; + const textareaClass = `${inputClass} resize-y min-h-[120px] max-h-[400px] overflow-y-auto scrollbar-auto`; + const buttonSecondaryClass = "flex items-center space-x-1.5 px-3 py-1.5 text-xs font-medium rounded-lg border transition-all duration-200"; + const tagClass = "inline-flex items-center px-2.5 py-1 bg-gray-100 text-gray-700 dark:bg-white/[0.04] dark:text-text-secondary rounded-md text-sm border border-gray-200/80 dark:border-white/[0.04]"; + const infoBoxClass = "mt-3 p-3.5 bg-gradient-to-br from-gray-50 to-white dark:from-white/[0.02] dark:to-white/[0.04] border border-gray-200/80 dark:border-white/[0.04] rounded-xl text-[12px] leading-[1.5] transition-all duration-200"; + const infoTextClass = "text-gray-700 dark:text-text-secondary flex items-start"; return ( = ({ setFormData(prev => ({ ...prev, description: e.target.value })); setEditIntent(prev => ({ ...prev, description: 'keep-custom' })); }} - className={`${inputClass} resize-none`} - rows={3} + className={textareaClass} + rows={5} placeholder={t('输入自定义描述...', 'Enter custom description...')} /> - {/* Save Effect Info - always visible */} + {/* Save Effect Info - Enhanced for Light Mode */} {editIntent.description === 'clear' ? ( -
-

- +

+

+ {t( '描述已清空,保存后将显示"(无描述)"。即使有AI总结或原始描述也不会显示。', @@ -697,9 +700,9 @@ export const RepositoryEditModal: React.FC = ({

) : editIntent.description === 'reset-to-ai' ? ( -
-

- +

+

+ {t( '保存后将清除自定义描述,显示AI总结。如果AI重新分析,描述可能随之变化。', @@ -709,9 +712,9 @@ export const RepositoryEditModal: React.FC = ({

) : editIntent.description === 'reset-to-original' ? ( -
-

- +

+

+ {t( '保存后将清除自定义描述,显示GitHub原始描述。', @@ -721,9 +724,9 @@ export const RepositoryEditModal: React.FC = ({

) : editIntent.description === 'keep-custom' && (formData.description || '').trim() === '' ? ( -
-

- +

+

+ {repository?.ai_summary ? t('当前编辑为空,保存后将显示AI总结。如需清空请点击"清除描述"。', 'Currently empty. AI summary will be shown after saving. Click "Clear" to explicitly clear.') @@ -734,9 +737,9 @@ export const RepositoryEditModal: React.FC = ({

) : editIntent.description === 'keep-custom' && customStatus.description ? ( -
-

- +

+

+ {t( '保存后将使用此自定义描述,优先级高于AI总结和原始描述。', @@ -746,9 +749,9 @@ export const RepositoryEditModal: React.FC = ({

) : editIntent.description === 'keep-custom' && formData.description.trim() !== '' && !customStatus.description ? ( -
-

- +

+

+ {t( '当前内容与AI总结或原始描述一致,保存后将使用自动推断的来源。', @@ -809,10 +812,10 @@ export const RepositoryEditModal: React.FC = ({

- {/* Feature Tip */} -
-

- + {/* Feature Tip - Enhanced */} +

+

+ {t( '描述优先级:自定义描述 > AI总结 > 原始描述。"重置"会清除自定义并回退到对应来源,"清除"会明确清空描述(不显示任何来源)。', @@ -906,11 +909,11 @@ export const RepositoryEditModal: React.FC = ({

- {/* Custom Category Selection Info */} + {/* Custom Category Selection Info - Enhanced */} {editIntent.category === 'keep-custom' && formData.category && ( -
-

- +

+

+ {t( '已选择自定义分类。保存后仓库将固定显示在此分类中,不会随AI分析结果自动变化。建议同时开启分类锁定以防止同步时被覆盖。', @@ -921,11 +924,11 @@ export const RepositoryEditModal: React.FC = ({

)} - {/* Reset Category Info */} + {/* Reset Category Info - Enhanced */} {editIntent.category === 'reset-to-ai' && ( -
-

- +

+

+ {t( '重置为AI分类将清除自定义分类设置,系统会根据AI标签自动推断分类。如果AI标签变化,分类可能会随之改变。', @@ -937,9 +940,9 @@ export const RepositoryEditModal: React.FC = ({ )} {editIntent.category === 'reset-to-original' && ( -

-

- +

+

+ {t( '重置为默认分类将清除自定义分类设置,系统会根据仓库信息(名称、描述、语言等)自动匹配分类。', @@ -950,11 +953,11 @@ export const RepositoryEditModal: React.FC = ({

)} - {/* Clear Category Warning */} + {/* Clear Category Warning - Enhanced */} {editIntent.category === 'clear' && ( -
-

- +

+

+ {t( '清除分类后,仓库将不再有明确的分类归属。系统会尝试根据AI标签自动匹配分类,如果没有匹配到则可能显示在默认分类中。', @@ -965,8 +968,8 @@ export const RepositoryEditModal: React.FC = ({

)} - {/* Category Lock */} -
+ {/* Category Lock - Enhanced */} +
{formData.categoryLocked && formData.category ? ( @@ -1097,38 +1100,38 @@ export const RepositoryEditModal: React.FC = ({
- {/* Status Alert */} + {/* Status Alert - Enhanced */} {formData.tags.length === 0 && ( -
-

+

+

{editIntent.tags === 'clear' ? ( <> - ⚠️ + ⚠️ {t('标签已清空。保存后将不显示任何标签。', 'Tags cleared. No tags will be shown after saving.')} ) : editIntent.tags === 'reset-to-ai' ? ( <> - + {t('将显示AI标签。', 'AI tags will be shown.')} ) : editIntent.tags === 'reset-to-original' ? ( <> - + {t('将显示GitHub Topics。', 'GitHub Topics will be shown.')} ) : repository?.ai_tags && repository.ai_tags.length > 0 ? ( <> - ⚠️ + ⚠️ {t('当前无自定义标签。保存后将显示AI标签。', 'No custom tags. AI tags will be shown after saving.')} ) : repository?.topics && repository.topics.length > 0 ? ( <> - ⚠️ + ⚠️ {t('当前无自定义标签。保存后将显示GitHub Topics。', 'No custom tags. GitHub Topics will be shown after saving.')} ) : ( <> - ⚠️ + ⚠️ {t('无可用标签。', 'No tags available.')} )} @@ -1160,17 +1163,17 @@ export const RepositoryEditModal: React.FC = ({

- {/* Action Buttons */} -
+ {/* Action Buttons - Enhanced */} +
{isOpen && ( -
+
{sortOptions.map((option) => (
-
-

{t('关于AI搜索', 'About AI Search')}

-

- {activeAIConfig - ? t( - '已配置AI服务时,将调用AI进行语义搜索和智能重排序。未配置时使用本地算法根据仓库名称、描述、标签等多维度进行匹配和排序。', - 'When AI service is configured, it will be called for semantic search and intelligent reranking. Otherwise, local algorithms are used to match and rank based on repository name, description, tags, and other dimensions.' - ) - : t( - '此功能使用本地算法进行智能排序。配置AI服务后可启用语义搜索功能,获得更精准的搜索结果。', - 'This feature uses local algorithms for intelligent ranking. Configure an AI service to enable semantic search for more accurate results.' - )} +

+

+ {t('关于AI搜索', 'About AI Search')}

-
+

+ {activeAIConfig ? t( + 'AI语义搜索模式:使用配置的AI服务进行智能语义理解和重排序。AI将分析查询意图,理解上下文关系,并提供语义相关的搜索结果。支持自然语言查询和概念匹配。', + 'AI semantic search mode: Uses configured AI service for intelligent semantic understanding and reranking. AI analyzes query intent, understands context, and provides semantically relevant results. Supports natural language queries and concept matching.' + ) : t( + '回退模式:基础文本搜索与默认排序。当未配置AI服务时,系统将使用基础文本匹配进行搜索(支持名称、描述、标签、语言等字段),并应用标准的排序和过滤控制。此为轻量级搜索方案,无语义理解能力。', + 'Fallback mode: Basic text search with default sorting. When no AI service is configured, the system uses basic text matching for search (supports name, description, tags, language, etc.) and applies standard sort and filter controls. This is a lightweight search solution without semantic understanding capabilities.' + )} +

+
@@ -975,7 +975,7 @@ export const SearchBar: React.FC = () => {
{/* Sort Controls */} -
+
setSearchFilters({ sortBy: value as 'stars' | 'updated' | 'name' | 'starred' })} diff --git a/src/index.css b/src/index.css index e6e93b22..72f785bf 100644 --- a/src/index.css +++ b/src/index.css @@ -855,3 +855,106 @@ option { @apply bg-white dark:bg-panel-dark text-gray-900 dark:text-text-primary; } + +/* ========== Enhanced Text Area & Input Styles for Light Mode ========== */ + +/* Optimized text selection colors for better readability */ +::selection { + background-color: rgba(94, 106, 210, 0.2); + color: inherit; +} + +.dark ::selection { + background-color: rgba(113, 112, 255, 0.3); +} + +/* Enhanced focus states for form elements */ +textarea:focus, +input:focus, +select:focus { + outline: none; + box-shadow: 0 0 0 3px rgba(94, 106, 210, 0.1); +} + +.dark textarea:focus, +.dark input:focus, +.dark select:focus { + box-shadow: 0 0 0 3px rgba(113, 112, 255, 0.15); +} + +/* Smooth scrolling for text areas */ +textarea { + scroll-behavior: smooth; + -webkit-overflow-scrolling: touch; +} + +/* Enhanced scrollbar styling for WebKit browsers */ +textarea::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +textarea::-webkit-scrollbar-track { + background: transparent; + border-radius: 4px; + margin: 4px 0; +} + +textarea::-webkit-scrollbar-thumb { + background: rgba(156, 163, 175, 0.4); + border-radius: 4px; + border: 2px solid transparent; + background-clip: padding-box; + transition: background-color 0.2s ease; +} + +textarea::-webkit-scrollbar-thumb:hover { + background: rgba(156, 163, 175, 0.6); +} + +.dark textarea::-webkit-scrollbar-thumb { + background: rgba(75, 85, 99, 0.4); +} + +.dark textarea::-webkit-scrollbar-thumb:hover { + background: rgba(75, 85, 99, 0.6); +} + +/* Textarea resize handle styling */ +textarea::-webkit-resizer { + background-image: url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9 1L1 9M9 5L5 9M9 9L9 9' stroke='%239CA3AF' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E"); + background-repeat: no-repeat; + background-position: right bottom; + cursor: se-resize; +} + +.dark textarea::-webkit-resizer { + background-image: url("data:image/svg+xml,%3Csvg width='10' height='10' viewBox='0 0 10 10' fill='none' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M9 1L1 9M9 5L5 9M9 9L9 9' stroke='%234B5563' stroke-width='1.5' stroke-linecap='round'/%3E%3C/svg%3E"); +} + +/* Enhanced placeholder styling */ +::placeholder { + color: #9ca3af; + opacity: 1; +} + +.dark ::placeholder { + color: #8a8f98; +} + +/* Responsive text sizing for better readability */ +@media (max-width: 768px) { + textarea, + input[type="text"] { + font-size: 16px; /* Prevent iOS zoom */ + } +} + +/* Print-friendly styles */ +@media print { + textarea, + input { + border: 1px solid #000; + box-shadow: none; + } +} diff --git a/src/store/useAppStore.ts b/src/store/useAppStore.ts index 9912a231..e06afd63 100644 --- a/src/store/useAppStore.ts +++ b/src/store/useAppStore.ts @@ -145,6 +145,9 @@ interface AppActions { setLanguage: (language: 'zh' | 'en') => void; setSidebarCollapsed: (collapsed: boolean) => void; setReadmeModalOpen: (open: boolean) => void; + + // Hydration state + setHasHydrated: (hydrated: boolean) => void; // Update actions setUpdateNotification: (notification: UpdateNotification | null) => void; @@ -643,6 +646,7 @@ export const useAppStore = create()( collapsedSidebarCategoryCount: 20, assetFilters: defaultPresetFilters, theme: 'dark', + hasHydrated: false, currentView: 'repositories', selectedCategory: 'all', language: 'zh', @@ -1150,6 +1154,9 @@ export const useAppStore = create()( setLanguage: (language) => set({ language }), setSidebarCollapsed: (isSidebarCollapsed) => set({ isSidebarCollapsed }), setReadmeModalOpen: (readmeModalOpen) => set({ readmeModalOpen }), + + // Hydration state + setHasHydrated: (hasHydrated) => set({ hasHydrated }), // Update actions setUpdateNotification: (notification) => set({ updateNotification: notification }), @@ -1488,6 +1495,14 @@ export const useAppStore = create()( ...normalized, }; }, + onRehydrateStorage: (state) => (_rehydratedState, error) => { + if (error) { + console.error('Store hydration failed', error); + } else { + console.log('Store hydration complete'); + } + state.setHasHydrated(true); + }, } ) );