Add comprehensive UI redesign with dark mode, pagination, and search functionality#278
Conversation
✅ Deploy Preview for algorithm-datastructures-math-studies ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
|
Warning Rate limit exceeded
⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. 📝 WalkthroughWalkthroughgenerate_index.py が静的インデックス生成を大幅に拡張し、ローカルベンダー資産のコピー/マッピング、カテゴリ別カウントとアイコン挿入、日本語テンプレート(Lab デザイン)への書き換え、ダーク/ライト切替、タブナビ、検索、クライアント側ページネーション、タイムスタンプ埋め込みを追加しました。 Changes
Sequence Diagram(s)sequenceDiagram
participant Gen as generate_index.py
participant FS as Filesystem (public/, repo)
participant Browser as Browser (index.html)
participant User as ユーザー
Gen->>FS: vendored assets を public/vendor にコピー、index.html を生成
FS->>Browser: 生成された index.html を配信(静的ファイル)
User->>Browser: ページ読み込み
Browser->>Browser: 初期レンダリング(タブ, カウント, タイムスタンプ)
User->>Browser: タブ切替 / 検索入力 / ページ操作 / テーマ切替
Browser->>Browser: フィルタリング・ページネーション・localStorage 保存
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 分 Possibly related PRs
詩
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 inconclusive)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
generate_index.py (1)
78-104:⚠️ Potential issue | 🟠 Major
/vendor/...のルート相対パスは、ネストされたHTMLファイルの参照を壊します
rewrite_html_content()が生成する置換先/vendor/...は、公開ディレクトリ内のすべてのHTMLファイル(154+のネストされたファイルを含む)に対して共通の絶対パスを使用しています。これにより以下のシナリオで参照が破損します:
- GitHub Pages のサブパス配信(
https://<user>.github.io/<repo>/)- ファイルの深さが異なる場合の相対参照(例:
public/Algorithm/Backtracking/leetcode/39. Combination Sum/Claude/README.htmlなど)file://プロトコルでの直接開き(OSのルートを指してしまう)各HTMLファイルの出力位置に応じて、vendorディレクトリへの相対パスを動的に計算する必要があります。
修正案(各HTMLファイルから vendor への相対パスを計算して置換)
- def rewrite_html_content(self, content: str) -> str: + def rewrite_html_content(self, content: str, vendor_prefix: str) -> str: """Replaces CDN links with local vendor links.""" replacements = [ # React - ('https://unpkg.com/react@18/umd/react.development.js', '/vendor/react/react.development.js'), + ('https://unpkg.com/react@18/umd/react.development.js', f'{vendor_prefix}/react/react.development.js'), ... ]呼び出し元(136-145行目付近):
- modified_content = self.rewrite_html_content(content) + vendor_prefix = os.path.relpath( + os.path.join(output_dir, "vendor"), + os.path.dirname(dest_path), + ).replace(os.sep, "/") + modified_content = self.rewrite_html_content(content, vendor_prefix)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@generate_index.py` around lines 78 - 104, The replace targets in rewrite_html_content use absolute "/vendor/..." paths which break nested HTML (GitHub Pages subpaths, file://, differing depths); change rewrite_html_content to accept the HTML file's output path (e.g., output_path or html_relpath) or a computed vendor_relative_base and compute a relative vendor prefix (like "../" repeated based on output file depth) then replace vendor URLs with vendor_relative_base + "vendor/..." instead of "/vendor/..."; update the caller(s) that generate each HTML (where rewrite_html_content is invoked) to pass the file's output location so the function can compute correct relative links.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@generate_index.py`:
- Around line 182-851: The large html_template string in generate_index.py
(variable html_template) should be moved to an external template file (e.g.,
index_template.html) and loaded at runtime; replace the inlined html_template
with code that reads the file and performs safe placeholder substitution (use
Python's str.format_map with a dict or preferably use the standard library's
string.Template or Jinja2 to avoid accidental unescaped braces), update
references to {total_count}, {tabs}, {all_files}, {tab_contents}, {timestamp}
accordingly, remove the embedded doubled braces trick by letting the template
engine handle literal braces, and finally run ruff and black on the Python
changes before merge to satisfy formatting/lint guidelines.
- Around line 169-180: The subtitle currently hardcodes "across 6 domains" while
total_count is computed dynamically; compute domain_count from the categories
list you actually render (e.g., len(sorted_categories) or len([c for c in
sorted_categories if you include it]) ) and pass that domain_count into the
template instead of the fixed 6; update the code around
category_icons/total_count/current_time to compute domain_count from
sorted_categories (or structure keys if that better matches rendering) and
ensure the template rendering calls use this domain_count variable wherever
"across 6 domains" is generated (also replace other occurrences noted at lines
~595-602 and ~875-881).
- Around line 860-871: The HTML is inserting unescaped title and path (from
get_html_title()/title and path) into item_html and tabs_html, allowing markup
injection; fix by HTML-escaping display values before interpolation: call
html.escape on title and on the displayed path (but keep href using the
url-encoded encoded_path) and use the escaped variables in the f-strings that
build tabs_html/file_list_html; ensure html is imported and replace uses of raw
title/path in the string building (e.g., where item_html is constructed and the
tab label) with the escaped versions.
- Around line 739-763: openTab clears the search input but only calls
clearSearchFilter(categoryName) for the destination tab, leaving any
data-search-hidden flags on the previous tab and causing cards to remain hidden
when returning; fix by locating the previously active tab before hiding tabs
(e.g. via document.querySelector('.tab-content.active') or iterate all
'.tab-content' elements) and call clearSearchFilter with that previous tab's id
(or call clearSearchFilter for every tab) in addition to calling it for the
destination tab, making sure to perform this after resetting the input and
before renderPage(categoryName); reference openTab, clearSearchFilter,
currentPages, renderPage, and data-search-hidden.
- Around line 678-690: In renderPaginationControls, avoid leaving an empty
.pagination container when totalPages <= 1: before creating or after clearing
the container, if totalPages <= 1 remove any existing container (using the
container variable or tab.querySelector('.pagination')) and return immediately;
alternatively check totalPages <= 1 at the top and delete the existing
.pagination then return—update the logic in renderPaginationControls
(referencing tabId, container, totalPages, currentPage) to ensure no empty
container remains.
---
Outside diff comments:
In `@generate_index.py`:
- Around line 78-104: The replace targets in rewrite_html_content use absolute
"/vendor/..." paths which break nested HTML (GitHub Pages subpaths, file://,
differing depths); change rewrite_html_content to accept the HTML file's output
path (e.g., output_path or html_relpath) or a computed vendor_relative_base and
compute a relative vendor prefix (like "../" repeated based on output file
depth) then replace vendor URLs with vendor_relative_base + "vendor/..." instead
of "/vendor/..."; update the caller(s) that generate each HTML (where
rewrite_html_content is invoked) to pass the file's output location so the
function can compute correct relative links.
| # HTML Template — "Refined Lab" デザイン | ||
| html_template = """<!DOCTYPE html> | ||
| <html lang="en"> | ||
| <html lang="ja"> | ||
| <head> | ||
| <meta charset="UTF-8"> | ||
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | ||
| <title>Algorithm Study Index</title> | ||
| <title>Algorithm Study Lab</title> | ||
| <link rel="preconnect" href="https://fonts.googleapis.com"> | ||
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet"> | ||
| <style> | ||
| body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif; line-height: 1.6; color: #333; max-width: 1200px; margin: 0 auto; padding: 20px; background-color: #f5f5f7; }} | ||
| h1 {{ text-align: center; color: #2c3e50; margin-bottom: 30px; }} | ||
| .tabs {{ display: flex; flex-wrap: wrap; justify-content: center; gap: 10px; margin-bottom: 20px; }} | ||
| .tab-button {{ padding: 10px 20px; border: none; background: #e0e0e0; cursor: pointer; border-radius: 5px; font-weight: 500; font-size: 16px; transition: all 0.3s ease; }} | ||
| .tab-button.active {{ background: #007bff; color: white; box-shadow: 0 2px 5px rgba(0,0,0,0.2); }} | ||
| .tab-button:hover:not(.active) {{ background: #d0d0d0; }} | ||
| .tab-content {{ display: none; animation: fadeIn 0.5s; }} | ||
| *, *::before, *::after {{ box-sizing: border-box; margin: 0; padding: 0; }} | ||
|
|
||
| :root {{ | ||
| --bg-primary: #faf8f5; | ||
| --bg-card: #ffffff; | ||
| --text-primary: #2d3436; | ||
| --text-secondary: #636e72; | ||
| --text-muted: #b2bec3; | ||
| --border-light: #e0dcd7; | ||
| --shadow-color: rgba(0, 0, 0, 0.06); | ||
| --color-all: #6c5ce7; | ||
| --color-algorithm: #0984e3; | ||
| --color-concurrency: #e17055; | ||
| --color-datastructures: #00b894; | ||
| --color-javascript: #f0932b; | ||
| --color-mathematics: #a29bfe; | ||
| --color-sql: #fd79a8; | ||
| --color-all-light: #ede9fe; | ||
| --color-algorithm-light: #dbeafe; | ||
| --color-concurrency-light: #fee2d5; | ||
| --color-datastructures-light: #d1fae5; | ||
| --color-javascript-light: #fef3c7; | ||
| --color-mathematics-light: #e8e5fe; | ||
| --color-sql-light: #fce7f3; | ||
| --radius-sm: 8px; | ||
| --radius-md: 14px; | ||
| --radius-lg: 50px; | ||
| --transition: 0.3s cubic-bezier(0.4, 0, 0.2, 1); | ||
| }} | ||
|
|
||
| [data-theme="dark"] {{ | ||
| --bg-primary: #13111a; | ||
| --bg-card: #1c1a27; | ||
| --text-primary: #e8e6f0; | ||
| --text-secondary: #9b99a9; | ||
| --text-muted: #5c5a6a; | ||
| --border-light: #2e2c3a; | ||
| --shadow-color: rgba(0, 0, 0, 0.4); | ||
| --color-all-light: #2d2654; | ||
| --color-algorithm-light: #0f2a4a; | ||
| --color-concurrency-light: #3a1a10; | ||
| --color-datastructures-light: #0a2e23; | ||
| --color-javascript-light: #3a2a10; | ||
| --color-mathematics-light: #2a2654; | ||
| --color-sql-light: #3a1a2e; | ||
| }} | ||
|
|
||
| body {{ | ||
| font-family: 'Outfit', -apple-system, BlinkMacSystemFont, sans-serif; | ||
| line-height: 1.6; | ||
| color: var(--text-primary); | ||
| max-width: 1280px; | ||
| margin: 0 auto; | ||
| padding: 20px 24px; | ||
| background-color: var(--bg-primary); | ||
| background-image: radial-gradient(circle, var(--border-light) 1px, transparent 1px); | ||
| background-size: 28px 28px; | ||
| min-height: 100vh; | ||
| }} | ||
|
|
||
| /* Theme Toggle */ | ||
| .theme-toggle {{ | ||
| position: fixed; | ||
| top: 20px; | ||
| right: 20px; | ||
| width: 46px; | ||
| height: 46px; | ||
| border-radius: 50%; | ||
| border: 2px solid var(--border-light); | ||
| background: var(--bg-card); | ||
| cursor: pointer; | ||
| font-size: 1.3rem; | ||
| display: flex; | ||
| align-items: center; | ||
| justify-content: center; | ||
| transition: all var(--transition); | ||
| z-index: 100; | ||
| box-shadow: 0 2px 12px var(--shadow-color); | ||
| }} | ||
| .theme-toggle:hover {{ | ||
| transform: rotate(20deg) scale(1.1); | ||
| box-shadow: 0 4px 20px var(--shadow-color); | ||
| }} | ||
|
|
||
| /* Header */ | ||
| .site-header {{ | ||
| text-align: center; | ||
| padding: 48px 20px 32px; | ||
| position: relative; | ||
| }} | ||
| .header-badge {{ | ||
| display: inline-block; | ||
| background: linear-gradient(135deg, var(--color-all), #a29bfe); | ||
| color: white; | ||
| padding: 4px 18px; | ||
| border-radius: 20px; | ||
| font-size: 0.7rem; | ||
| font-weight: 700; | ||
| letter-spacing: 3px; | ||
| text-transform: uppercase; | ||
| margin-bottom: 14px; | ||
| }} | ||
| .site-title {{ | ||
| font-family: 'Outfit', sans-serif; | ||
| font-weight: 700; | ||
| font-size: 2.8rem; | ||
| color: var(--text-primary); | ||
| line-height: 1.15; | ||
| }} | ||
| .title-accent {{ | ||
| background: linear-gradient(135deg, var(--color-all), var(--color-algorithm)); | ||
| -webkit-background-clip: text; | ||
| -webkit-text-fill-color: transparent; | ||
| background-clip: text; | ||
| }} | ||
| .site-subtitle {{ | ||
| color: var(--text-secondary); | ||
| font-size: 1rem; | ||
| margin-top: 10px; | ||
| font-weight: 400; | ||
| }} | ||
|
|
||
| /* Search */ | ||
| .search-container {{ | ||
| max-width: 620px; | ||
| margin: 0 auto 28px; | ||
| position: relative; | ||
| display: flex; | ||
| align-items: center; | ||
| }} | ||
| .search-icon {{ | ||
| position: absolute; | ||
| left: 18px; | ||
| font-size: 1.1rem; | ||
| opacity: 0.4; | ||
| pointer-events: none; | ||
| }} | ||
| .search-input {{ | ||
| width: 100%; | ||
| padding: 14px 90px 14px 48px; | ||
| border: 2px solid var(--border-light); | ||
| border-radius: var(--radius-lg); | ||
| font-family: 'Outfit', sans-serif; | ||
| font-size: 0.95rem; | ||
| background: var(--bg-card); | ||
| color: var(--text-primary); | ||
| transition: border-color var(--transition), box-shadow var(--transition); | ||
| outline: none; | ||
| }} | ||
| .search-input::placeholder {{ color: var(--text-muted); }} | ||
| .search-input:focus {{ | ||
| border-color: var(--color-all); | ||
| box-shadow: 0 0 0 4px var(--color-all-light); | ||
| }} | ||
| .search-count {{ | ||
| position: absolute; | ||
| right: 48px; | ||
| font-size: 0.78rem; | ||
| color: var(--text-muted); | ||
| font-weight: 500; | ||
| }} | ||
| .search-clear {{ | ||
| position: absolute; | ||
| right: 14px; | ||
| background: none; | ||
| border: none; | ||
| font-size: 1.4rem; | ||
| cursor: pointer; | ||
| color: var(--text-muted); | ||
| padding: 4px 8px; | ||
| border-radius: 50%; | ||
| transition: all 0.2s; | ||
| display: none; | ||
| }} | ||
| .search-clear:hover {{ | ||
| color: var(--text-primary); | ||
| background: var(--border-light); | ||
| }} | ||
|
|
||
| /* Tabs */ | ||
| .tabs {{ | ||
| display: flex; | ||
| flex-wrap: wrap; | ||
| justify-content: center; | ||
| gap: 8px; | ||
| margin-bottom: 28px; | ||
| padding: 0 16px; | ||
| }} | ||
| .tab-button {{ | ||
| padding: 10px 20px; | ||
| border: 2px solid var(--border-light); | ||
| background: var(--bg-card); | ||
| cursor: pointer; | ||
| border-radius: var(--radius-lg); | ||
| font-family: 'Outfit', sans-serif; | ||
| font-weight: 600; | ||
| font-size: 0.88rem; | ||
| color: var(--text-secondary); | ||
| transition: all var(--transition); | ||
| display: inline-flex; | ||
| align-items: center; | ||
| gap: 6px; | ||
| white-space: nowrap; | ||
| }} | ||
| .tab-icon {{ font-size: 1.05em; }} | ||
| .tab-count {{ | ||
| background: var(--border-light); | ||
| color: var(--text-muted); | ||
| padding: 1px 8px; | ||
| border-radius: 12px; | ||
| font-size: 0.72rem; | ||
| font-weight: 700; | ||
| transition: all var(--transition); | ||
| }} | ||
| .tab-button:hover:not(.active) {{ | ||
| border-color: var(--text-muted); | ||
| transform: translateY(-1px); | ||
| }} | ||
| .tab-button.active .tab-count {{ | ||
| background: rgba(255,255,255,0.25); | ||
| color: white; | ||
| }} | ||
|
|
||
| /* カテゴリ別アクティブカラー */ | ||
| .tab-button[data-category="all"].active {{ background: var(--color-all); color: white; border-color: var(--color-all); box-shadow: 0 4px 16px rgba(108,92,231,0.35); }} | ||
| .tab-button[data-category="algorithm"].active {{ background: var(--color-algorithm); color: white; border-color: var(--color-algorithm); box-shadow: 0 4px 16px rgba(9,132,227,0.35); }} | ||
| .tab-button[data-category="concurrency"].active {{ background: var(--color-concurrency); color: white; border-color: var(--color-concurrency); box-shadow: 0 4px 16px rgba(225,112,85,0.35); }} | ||
| .tab-button[data-category="datastructures"].active {{ background: var(--color-datastructures); color: white; border-color: var(--color-datastructures); box-shadow: 0 4px 16px rgba(0,184,148,0.35); }} | ||
| .tab-button[data-category="javascript"].active {{ background: var(--color-javascript); color: white; border-color: var(--color-javascript); box-shadow: 0 4px 16px rgba(240,147,43,0.35); }} | ||
| .tab-button[data-category="mathematics"].active {{ background: var(--color-mathematics); color: white; border-color: var(--color-mathematics); box-shadow: 0 4px 16px rgba(162,155,254,0.35); }} | ||
| .tab-button[data-category="sql"].active {{ background: var(--color-sql); color: white; border-color: var(--color-sql); box-shadow: 0 4px 16px rgba(253,121,168,0.35); }} | ||
|
|
||
| /* Tab Content */ | ||
| .tab-content {{ display: none; }} | ||
| .tab-content.active {{ display: block; }} | ||
| @keyframes fadeIn {{ from {{ opacity: 0; }} to {{ opacity: 1; }} }} | ||
| .file-list {{ list-style: none; padding: 0; display: grid; grid-template-columns: repeat(auto-fill, minmax(300px, 1fr)); gap: 15px; }} | ||
| .file-item {{ background: white; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); transition: transform 0.2s; padding: 15px; border: 1px solid #eee; }} | ||
| .file-item:hover {{ transform: translateY(-3px); box-shadow: 0 5px 15px rgba(0,0,0,0.1); border-color: #007bff; }} | ||
| .file-link {{ text-decoration: none; color: #2c3e50; font-weight: 500; display: block; }} | ||
| .file-path {{ font-size: 0.8em; color: #7f8c8d; margin-top: 5px; display: block; word-break: break-all; }} | ||
| footer {{ margin-top: 50px; text-align: center; font-size: 0.9em; color: #777; border-top: 1px solid #ddd; padding-top: 20px; }} | ||
|
|
||
| /* Card Grid */ | ||
| .file-list {{ | ||
| list-style: none; | ||
| padding: 0; | ||
| display: grid; | ||
| grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)); | ||
| gap: 16px; | ||
| }} | ||
| .file-item {{ | ||
| background: var(--bg-card); | ||
| border-radius: var(--radius-md); | ||
| box-shadow: 0 2px 8px var(--shadow-color); | ||
| transition: all 0.35s cubic-bezier(0.4, 0, 0.2, 1); | ||
| border: 1px solid var(--border-light); | ||
| border-left: 4px solid var(--border-light); | ||
| overflow: hidden; | ||
| opacity: 0; | ||
| transform: translateY(16px); | ||
| animation: cardSlideIn 0.4s ease forwards; | ||
| }} | ||
| .file-item[data-category="algorithm"] {{ border-left-color: var(--color-algorithm); }} | ||
| .file-item[data-category="concurrency"] {{ border-left-color: var(--color-concurrency); }} | ||
| .file-item[data-category="datastructures"] {{ border-left-color: var(--color-datastructures); }} | ||
| .file-item[data-category="javascript"] {{ border-left-color: var(--color-javascript); }} | ||
| .file-item[data-category="mathematics"] {{ border-left-color: var(--color-mathematics); }} | ||
| .file-item[data-category="sql"] {{ border-left-color: var(--color-sql); }} | ||
|
|
||
| .file-item:hover {{ | ||
| transform: translateY(-4px) scale(1.01); | ||
| box-shadow: 0 12px 32px rgba(0, 0, 0, 0.12); | ||
| }} | ||
| .file-item[data-category="algorithm"]:hover {{ background: linear-gradient(135deg, var(--color-algorithm-light), var(--bg-card)); }} | ||
| .file-item[data-category="concurrency"]:hover {{ background: linear-gradient(135deg, var(--color-concurrency-light), var(--bg-card)); }} | ||
| .file-item[data-category="datastructures"]:hover {{ background: linear-gradient(135deg, var(--color-datastructures-light), var(--bg-card)); }} | ||
| .file-item[data-category="javascript"]:hover {{ background: linear-gradient(135deg, var(--color-javascript-light), var(--bg-card)); }} | ||
| .file-item[data-category="mathematics"]:hover {{ background: linear-gradient(135deg, var(--color-mathematics-light), var(--bg-card)); }} | ||
| .file-item[data-category="sql"]:hover {{ background: linear-gradient(135deg, var(--color-sql-light), var(--bg-card)); }} | ||
|
|
||
| @keyframes cardSlideIn {{ | ||
| from {{ opacity: 0; transform: translateY(16px); }} | ||
| to {{ opacity: 1; transform: translateY(0); }} | ||
| }} | ||
|
|
||
| .file-link {{ | ||
| text-decoration: none; | ||
| color: var(--text-primary); | ||
| display: block; | ||
| padding: 16px 18px; | ||
| }} | ||
| .card-header {{ | ||
| display: flex; | ||
| align-items: flex-start; | ||
| gap: 8px; | ||
| margin-bottom: 6px; | ||
| }} | ||
| .card-icon {{ | ||
| font-size: 1.25rem; | ||
| flex-shrink: 0; | ||
| line-height: 1.4; | ||
| }} | ||
| .card-title {{ | ||
| font-family: 'Outfit', sans-serif; | ||
| font-weight: 600; | ||
| font-size: 0.92rem; | ||
| line-height: 1.45; | ||
| color: var(--text-primary); | ||
| }} | ||
| .file-path {{ | ||
| font-family: 'JetBrains Mono', monospace; | ||
| font-size: 0.7rem; | ||
| color: var(--text-muted); | ||
| margin-top: 4px; | ||
| display: block; | ||
| word-break: break-all; | ||
| line-height: 1.5; | ||
| }} | ||
|
|
||
| /* Pagination */ | ||
| .pagination {{ | ||
| display: flex; | ||
| justify-content: center; | ||
| align-items: center; | ||
| gap: 6px; | ||
| margin-top: 32px; | ||
| padding-bottom: 24px; | ||
| }} | ||
| .page-button {{ | ||
| padding: 8px 14px; | ||
| border: 1px solid var(--border-light); | ||
| background: var(--bg-card); | ||
| cursor: pointer; | ||
| border-radius: 10px; | ||
| color: var(--text-secondary); | ||
| font-family: 'Outfit', sans-serif; | ||
| font-weight: 600; | ||
| font-size: 0.88rem; | ||
| transition: all 0.2s; | ||
| }} | ||
| .page-button.active {{ | ||
| background: var(--color-all); | ||
| color: white; | ||
| border-color: var(--color-all); | ||
| box-shadow: 0 2px 8px rgba(108, 92, 231, 0.3); | ||
| }} | ||
| .page-button:hover:not(.active):not(:disabled) {{ | ||
| background: var(--color-all-light); | ||
| border-color: var(--color-all); | ||
| color: var(--color-all); | ||
| }} | ||
| .page-button:disabled {{ | ||
| opacity: 0.35; | ||
| cursor: not-allowed; | ||
| }} | ||
| .page-ellipsis {{ | ||
| color: var(--text-muted); | ||
| font-weight: 600; | ||
| padding: 0 4px; | ||
| }} | ||
|
|
||
| .hidden-item {{ display: none !important; }} | ||
|
|
||
| /* Footer */ | ||
| footer {{ | ||
| margin-top: 60px; | ||
| text-align: center; | ||
| font-size: 0.82rem; | ||
| color: var(--text-muted); | ||
| border-top: 1px dashed var(--border-light); | ||
| padding: 24px 20px; | ||
| }} | ||
| .footer-icon {{ margin-right: 4px; }} | ||
|
|
||
| /* No Results */ | ||
| .no-results {{ | ||
| text-align: center; | ||
| padding: 60px 20px; | ||
| color: var(--text-muted); | ||
| font-size: 1.1rem; | ||
| display: none; | ||
| }} | ||
| .no-results-icon {{ font-size: 2.5rem; display: block; margin-bottom: 12px; }} | ||
|
|
||
| /* Responsive */ | ||
| @media (max-width: 768px) {{ | ||
| .site-title {{ font-size: 2rem; }} | ||
| .file-list {{ grid-template-columns: 1fr; gap: 12px; }} | ||
| .tabs {{ gap: 6px; }} | ||
| .tab-button {{ padding: 8px 14px; font-size: 0.8rem; }} | ||
| .search-input {{ padding: 12px 80px 12px 42px; font-size: 0.9rem; }} | ||
| .theme-toggle {{ top: 12px; right: 12px; width: 40px; height: 40px; font-size: 1.1rem; }} | ||
| }} | ||
| @media (max-width: 480px) {{ | ||
| body {{ padding: 12px; }} | ||
| .site-title {{ font-size: 1.6rem; }} | ||
| .site-header {{ padding: 36px 12px 24px; }} | ||
| .tab-button {{ padding: 6px 10px; font-size: 0.75rem; }} | ||
| .tab-count {{ display: none; }} | ||
| .file-list {{ grid-template-columns: 1fr; }} | ||
| }} | ||
| </style> | ||
| </head> | ||
| <body> | ||
| <h1>Algorithm Study Index</h1> | ||
| <button id="themeToggle" class="theme-toggle" aria-label="Toggle dark mode"> | ||
| <span id="themeIcon">\u263E</span> | ||
| </button> | ||
|
|
||
| <header class="site-header"> | ||
| <div class="header-badge">Lab</div> | ||
| <h1 class="site-title"> | ||
| \U0001F9EA Algorithm Study | ||
| <span class="title-accent">Index</span> | ||
| </h1> | ||
| <p class="site-subtitle">{total_count} interactive lessons across 6 domains</p> | ||
| </header> | ||
|
|
||
| <div class="search-container"> | ||
| <span class="search-icon">\U0001F50D</span> | ||
| <input type="text" id="searchInput" class="search-input" | ||
| placeholder="Search problems... (e.g. Binary Search, DP, LeetCode 91)" | ||
| autocomplete="off"> | ||
| <span id="searchCount" class="search-count"></span> | ||
| <button id="searchClear" class="search-clear" aria-label="Clear search">\u00D7</button> | ||
| </div> | ||
|
|
||
| <div class="tabs" id="categoryTabs"> | ||
| <button class="tab-button active" onclick="openTab(event, 'All')">All</button> | ||
| <button class="tab-button active" data-category="all" onclick="openTab(event, 'All')"> | ||
| <span class="tab-icon">\U0001F30D</span> All <span class="tab-count">{total_count}</span> | ||
| </button> | ||
| {tabs} | ||
| </div> | ||
|
|
||
| <div id="All" class="tab-content active"> | ||
| <ul class="file-list"> | ||
| {all_files} | ||
| </ul> | ||
| <div class="no-results"><span class="no-results-icon">\U0001F50E</span>No results found</div> | ||
| </div> | ||
|
|
||
| {tab_contents} | ||
|
|
||
| <footer> | ||
| <span class="footer-icon">\U0001F9EA</span> | ||
| Generated on {timestamp} | ||
| </footer> | ||
|
|
||
| <script> | ||
| const ITEMS_PER_PAGE = 12; | ||
| let currentPages = {{}}; | ||
|
|
||
| /* Pagination */ | ||
| function initPagination() {{ | ||
| document.querySelectorAll('.tab-content').forEach(tab => {{ | ||
| currentPages[tab.id] = 1; | ||
| renderPage(tab.id); | ||
| }}); | ||
| }} | ||
|
|
||
| function renderPage(tabId) {{ | ||
| const tab = document.getElementById(tabId); | ||
| const allItems = Array.from(tab.querySelectorAll('.file-item')); | ||
| const items = allItems.filter(item => item.dataset.searchHidden !== 'true'); | ||
| const hiddenBySearch = allItems.filter(item => item.dataset.searchHidden === 'true'); | ||
|
|
||
| hiddenBySearch.forEach(item => item.classList.add('hidden-item')); | ||
|
|
||
| const totalItems = items.length; | ||
| const totalPages = Math.max(1, Math.ceil(totalItems / ITEMS_PER_PAGE)); | ||
| const currentPage = Math.min(currentPages[tabId] || 1, totalPages); | ||
| currentPages[tabId] = currentPage; | ||
|
|
||
| items.forEach((item, index) => {{ | ||
| const start = (currentPage - 1) * ITEMS_PER_PAGE; | ||
| const end = start + ITEMS_PER_PAGE; | ||
| if (index >= start && index < end) {{ | ||
| item.classList.remove('hidden-item'); | ||
| }} else {{ | ||
| item.classList.add('hidden-item'); | ||
| }} | ||
| }}); | ||
|
|
||
| const noResults = tab.querySelector('.no-results'); | ||
| if (noResults) {{ | ||
| noResults.style.display = totalItems === 0 ? 'block' : 'none'; | ||
| }} | ||
|
|
||
| renderPaginationControls(tabId, totalPages, currentPage); | ||
| applyStaggerAnimation(tabId); | ||
| }} | ||
|
|
||
| function renderPaginationControls(tabId, totalPages, currentPage) {{ | ||
| const tab = document.getElementById(tabId); | ||
| let container = tab.querySelector('.pagination'); | ||
|
|
||
| if (!container) {{ | ||
| container = document.createElement('div'); | ||
| container.className = 'pagination'; | ||
| tab.appendChild(container); | ||
| }} | ||
| while (container.firstChild) container.removeChild(container.firstChild); | ||
|
|
||
| if (totalPages <= 1) return; | ||
|
|
||
| const prevBtn = document.createElement('button'); | ||
| prevBtn.className = 'page-button'; | ||
| prevBtn.textContent = '\u00AB Prev'; | ||
| prevBtn.disabled = currentPage === 1; | ||
| prevBtn.onclick = () => {{ currentPages[tabId]--; renderPage(tabId); window.scrollTo({{ top: 0, behavior: 'smooth' }}); }}; | ||
| container.appendChild(prevBtn); | ||
|
|
||
| for (let i = 1; i <= totalPages; i++) {{ | ||
| if (totalPages > 7) {{ | ||
| if (i !== 1 && i !== totalPages && Math.abs(i - currentPage) > 1) {{ | ||
| if (i === 2 || i === totalPages - 1) {{ | ||
| const el = document.createElement('span'); | ||
| el.className = 'page-ellipsis'; | ||
| el.textContent = '...'; | ||
| container.appendChild(el); | ||
| }} | ||
| continue; | ||
| }} | ||
| }} | ||
| const btn = document.createElement('button'); | ||
| btn.className = 'page-button'; | ||
| btn.textContent = i; | ||
| if (i === currentPage) btn.classList.add('active'); | ||
| btn.onclick = () => {{ currentPages[tabId] = i; renderPage(tabId); window.scrollTo({{ top: 0, behavior: 'smooth' }}); }}; | ||
| container.appendChild(btn); | ||
| }} | ||
|
|
||
| const nextBtn = document.createElement('button'); | ||
| nextBtn.className = 'page-button'; | ||
| nextBtn.textContent = 'Next \u00BB'; | ||
| nextBtn.disabled = currentPage === totalPages; | ||
| nextBtn.onclick = () => {{ currentPages[tabId]++; renderPage(tabId); window.scrollTo({{ top: 0, behavior: 'smooth' }}); }}; | ||
| container.appendChild(nextBtn); | ||
| }} | ||
|
|
||
| /* Stagger Animation */ | ||
| function applyStaggerAnimation(tabId) {{ | ||
| const tab = document.getElementById(tabId); | ||
| const items = tab.querySelectorAll('.file-item:not(.hidden-item)'); | ||
| items.forEach((item, i) => {{ | ||
| item.style.animation = 'none'; | ||
| item.offsetHeight; | ||
| item.style.animation = ''; | ||
| item.style.animationDelay = (i * 0.04) + 's'; | ||
| }}); | ||
| }} | ||
|
|
||
| /* Tab Navigation */ | ||
| function openTab(evt, categoryName) {{ | ||
| var i, tabcontent, tablinks; | ||
| tabcontent = document.getElementsByClassName("tab-content"); | ||
| for (i = 0; i < tabcontent.length; i++) {{ | ||
| tabcontent[i].style.display = "none"; | ||
| tabcontent[i].classList.remove("active"); | ||
| document.querySelectorAll('.tab-content').forEach(tc => {{ | ||
| tc.style.display = 'none'; | ||
| tc.classList.remove('active'); | ||
| }}); | ||
| document.querySelectorAll('.tab-button').forEach(tb => {{ | ||
| tb.classList.remove('active'); | ||
| }}); | ||
|
|
||
| const target = document.getElementById(categoryName); | ||
| target.style.display = 'block'; | ||
| target.classList.add('active'); | ||
| evt.currentTarget.classList.add('active'); | ||
|
|
||
| const input = document.getElementById('searchInput'); | ||
| if (input.value) {{ | ||
| input.value = ''; | ||
| document.getElementById('searchClear').style.display = 'none'; | ||
| document.getElementById('searchCount').textContent = ''; | ||
| clearSearchFilter(categoryName); | ||
| }} | ||
|
|
||
| currentPages[categoryName] = currentPages[categoryName] || 1; | ||
| renderPage(categoryName); | ||
| }} | ||
|
|
||
| function clearSearchFilter(tabId) {{ | ||
| const tab = document.getElementById(tabId); | ||
| tab.querySelectorAll('.file-item').forEach(item => {{ | ||
| delete item.dataset.searchHidden; | ||
| }}); | ||
| }} | ||
|
|
||
| /* Search */ | ||
| function initSearch() {{ | ||
| const input = document.getElementById('searchInput'); | ||
| const clearBtn = document.getElementById('searchClear'); | ||
| const countEl = document.getElementById('searchCount'); | ||
| let debounceTimer; | ||
|
|
||
| input.addEventListener('input', () => {{ | ||
| clearTimeout(debounceTimer); | ||
| debounceTimer = setTimeout(() => filterCards(input.value), 150); | ||
| clearBtn.style.display = input.value ? 'block' : 'none'; | ||
| }}); | ||
|
|
||
| clearBtn.addEventListener('click', () => {{ | ||
| input.value = ''; | ||
| clearBtn.style.display = 'none'; | ||
| countEl.textContent = ''; | ||
| const activeTab = document.querySelector('.tab-content.active'); | ||
| if (activeTab) {{ | ||
| clearSearchFilter(activeTab.id); | ||
| currentPages[activeTab.id] = 1; | ||
| renderPage(activeTab.id); | ||
| }} | ||
| }}); | ||
|
|
||
| function filterCards(query) {{ | ||
| const q = query.toLowerCase().trim(); | ||
| const activeTab = document.querySelector('.tab-content.active'); | ||
| if (!activeTab) return; | ||
|
|
||
| const items = activeTab.querySelectorAll('.file-item'); | ||
| let visible = 0; | ||
|
|
||
| items.forEach(item => {{ | ||
| const text = item.textContent.toLowerCase(); | ||
| const match = !q || text.includes(q); | ||
| if (match) {{ | ||
| delete item.dataset.searchHidden; | ||
| visible++; | ||
| }} else {{ | ||
| item.dataset.searchHidden = 'true'; | ||
| }} | ||
| }}); | ||
|
|
||
| countEl.textContent = q ? visible + ' found' : ''; | ||
| currentPages[activeTab.id] = 1; | ||
| renderPage(activeTab.id); | ||
| }} | ||
| tablinks = document.getElementsByClassName("tab-button"); | ||
| for (i = 0; i < tablinks.length; i++) {{ | ||
| tablinks[i].className = tablinks[i].className.replace(" active", ""); | ||
| }} | ||
|
|
||
| /* Dark Mode */ | ||
| function initTheme() {{ | ||
| const toggle = document.getElementById('themeToggle'); | ||
| const icon = document.getElementById('themeIcon'); | ||
| const saved = localStorage.getItem('algo-study-theme'); | ||
|
|
||
| if (saved === 'dark' || (!saved && window.matchMedia('(prefers-color-scheme: dark)').matches)) {{ | ||
| document.documentElement.setAttribute('data-theme', 'dark'); | ||
| icon.textContent = '\u2600'; | ||
| }} | ||
| document.getElementById(categoryName).style.display = "block"; | ||
| document.getElementById(categoryName).classList.add("active"); | ||
| evt.currentTarget.className += " active"; | ||
|
|
||
| toggle.addEventListener('click', () => {{ | ||
| const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; | ||
| const next = isDark ? 'light' : 'dark'; | ||
| document.documentElement.setAttribute('data-theme', next); | ||
| icon.textContent = next === 'dark' ? '\u2600' : '\u263E'; | ||
| localStorage.setItem('algo-study-theme', next); | ||
| }}); | ||
| }} | ||
|
|
||
| /* Init */ | ||
| window.addEventListener('DOMContentLoaded', () => {{ | ||
| initTheme(); | ||
| initPagination(); | ||
| initSearch(); | ||
| }}); | ||
| </script> | ||
| </body> | ||
| </html>""" | ||
|
|
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
巨大な html_template を Python 文字列に直書きしており、保守と差分レビューが難しい
この規模のHTML/CSS/JSを .format() 前提で {{ }} だらけにする構造は、将来的にテンプレ修正時の事故(未エスケープ { による KeyError 等)を誘発しやすいです。テンプレートを別ファイルへ分離し、Python 側は「読み込み+プレースホルダ置換」に寄せるのを推奨します。
また、Python 部分はマージ前に ruff/black を通して整形・lint しておくと安全です。As per coding guidelines, "Use Python's ruff and black for code formatting and linting".
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@generate_index.py` around lines 182 - 851, The large html_template string in
generate_index.py (variable html_template) should be moved to an external
template file (e.g., index_template.html) and loaded at runtime; replace the
inlined html_template with code that reads the file and performs safe
placeholder substitution (use Python's str.format_map with a dict or preferably
use the standard library's string.Template or Jinja2 to avoid accidental
unescaped braces), update references to {total_count}, {tabs}, {all_files},
{tab_contents}, {timestamp} accordingly, remove the embedded doubled braces
trick by letting the template engine handle literal braces, and finally run ruff
and black on the Python changes before merge to satisfy formatting/lint
guidelines.
|
Note Docstrings generation - SUCCESS |
Docstrings generation was requested by @myoshi2891. * #278 (comment) The following files were modified: * `generate_index.py`
📝 Add docstrings to `dev-from-macmini`
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
generate_index.py (3)
55-58: 🧹 Nitpick | 🔵 Trivialインデントが不揃いです(ruff / black で検出される問題)。
Line 56-58 はインデントにスペースが1つ余分に入っています(Line 40, 97 の
#コメントも同様)。コーディングガイドラインに従い、マージ前にruff/blackを実行してください。As per coding guidelines, "Use Python's ruff and black for code formatting and linting".
♻️ 修正案
for src in prism_plugins: - if os.path.exists(src): - rel_path = os.path.relpath(src, "node_modules/prismjs") - file_map[src] = f"prismjs/{rel_path}" + if os.path.exists(src): + rel_path = os.path.relpath(src, "node_modules/prismjs") + file_map[src] = f"prismjs/{rel_path}"🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@generate_index.py` around lines 55 - 58, Fix inconsistent indentation around the prism_plugins loop: align the for-statement block so the lines using os.path.exists(src), rel_path = os.path.relpath(...), and file_map[src] = f"prismjs/{rel_path}" have the same indentation level as the for line; also correct the mismatched leading space on the nearby inline '#' comments (the ones above and below this block) to match project style, then run ruff/black to autoformat and ensure no remaining spacing issues.
151-167:⚠️ Potential issue | 🟡 Minor
get_html_titleがtry-exceptブロックの外にあり、エンコーディングエラー時にスクリプト全体がクラッシュします。Line 143-154 の
try-exceptはファイル読み書きのエラーをキャッチしますが、Line 166 のget_html_title(filepath)はその外にあります。Line 151 でエンコーディングエラーが発生しフォールバックコピーされた場合、同じファイルに対するget_html_titleも同様に失敗し、ループ全体が中断します。🛡️ 修正案
except Exception as e: print(f"Error processing {filepath}: {e}") # Fallback copy if encoding/read fails (unlikely given check) shutil.copy2(filepath, dest_path) # Indexing Logic parts = rel_path.split(os.sep) if len(parts) > 1: category = parts[0] else: category = "Uncategorized" if category.startswith('.'): continue - title = self.get_html_title(filepath) + try: + title = self.get_html_title(filepath) + except Exception: + title = os.path.basename(filepath) structure[category].append((title, rel_path))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@generate_index.py` around lines 151 - 167, get_html_title(filepath) is called outside the try-except that handles read/encoding failures, so an encoding or parse error there can crash the loop; wrap the title extraction in its own safe block (or move it inside the existing except/try) so failures are handled gracefully: call get_html_title(filepath) inside the try that reads the file or add a small try-except around get_html_title in the loop, on failure set a sensible fallback title (e.g., filename or "Untitled") and then append (title, rel_path) to structure[category]; ensure the code still respects the existing category skip (category.startswith('.')) and that structure is updated only after a successful or fallback title is determined.
11-17: 🧹 Nitpick | 🔵 Trivialファイルの二重読み込みを回避可能です。
generate_index内(Line 143-144)で既にファイル内容を読み込んでいますが、get_html_title(Line 12) で同じファイルを再度読み込んでいます。contentを引数として渡せば不要な I/O を削減できます。♻️ リファクタ案
- def get_html_title(self, filepath: str) -> str: - with open(filepath, 'r', encoding='utf-8') as f: - content = f.read() - match = re.search(r'<title>(.*?)</title>', content, re.IGNORECASE | re.DOTALL) - if match: - return html.unescape(match.group(1).strip()) - return os.path.basename(filepath) + def get_html_title(self, filepath: str, content: str = "") -> str: + if not content: + with open(filepath, 'r', encoding='utf-8') as f: + content = f.read() + match = re.search(r'<title>(.*?)</title>', content, re.IGNORECASE | re.DOTALL) + if match: + return html.unescape(match.group(1).strip()) + return os.path.basename(filepath)呼び出し側(
generate_index内):title = self.get_html_title(filepath, content=content)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@generate_index.py` around lines 11 - 17, get_html_title is re-opening files already read in generate_index; modify get_html_title to accept an optional content: str parameter (e.g., def get_html_title(self, filepath: str, content: Optional[str]=None)) and only open/read filepath when content is None, then update the generate_index call to pass the already-read content (title = self.get_html_title(filepath, content=content)); ensure other callers of get_html_title are updated or still work with the new optional parameter and keep the existing return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Outside diff comments:
In `@generate_index.py`:
- Around line 55-58: Fix inconsistent indentation around the prism_plugins loop:
align the for-statement block so the lines using os.path.exists(src), rel_path =
os.path.relpath(...), and file_map[src] = f"prismjs/{rel_path}" have the same
indentation level as the for line; also correct the mismatched leading space on
the nearby inline '#' comments (the ones above and below this block) to match
project style, then run ruff/black to autoformat and ensure no remaining spacing
issues.
- Around line 151-167: get_html_title(filepath) is called outside the try-except
that handles read/encoding failures, so an encoding or parse error there can
crash the loop; wrap the title extraction in its own safe block (or move it
inside the existing except/try) so failures are handled gracefully: call
get_html_title(filepath) inside the try that reads the file or add a small
try-except around get_html_title in the loop, on failure set a sensible fallback
title (e.g., filename or "Untitled") and then append (title, rel_path) to
structure[category]; ensure the code still respects the existing category skip
(category.startswith('.')) and that structure is updated only after a successful
or fallback title is determined.
- Around line 11-17: get_html_title is re-opening files already read in
generate_index; modify get_html_title to accept an optional content: str
parameter (e.g., def get_html_title(self, filepath: str, content:
Optional[str]=None)) and only open/read filepath when content is None, then
update the generate_index call to pass the already-read content (title =
self.get_html_title(filepath, content=content)); ensure other callers of
get_html_title are updated or still work with the new optional parameter and
keep the existing return behavior.
---
Duplicate comments:
In `@generate_index.py`:
- Around line 865-878: The category value is being interpolated directly into
the onclick JS string and title/path are not HTML-escaped; fix by importing json
at the top and using json.dumps(category) (or an equivalent JS-string escaper)
when building the onclick argument for openTab to ensure a JS-safe string, and
wrap title and path with html.escape before inserting into item_html and
file_list_html; update the string building locations that produce tabs_html,
item_html, all_files_html and tab_contents_html to use the escaped values
(reference symbols: category, title, path, tabs_html, file_list_html,
tab_contents_html, openTab).
- Around line 187-188: The large HTML string assigned to html_template in
generate_index.py should be moved into an external template file and loaded at
runtime to improve maintainability; replace the inline html_template with code
that reads a separate .html (or .j2) file and render it via a template engine
(e.g., Jinja2) using safe lookup (e.g., Jinja2's default values or the get
filter) to avoid KeyError from heavy use of {{ }} placeholders; update the
variable name/reference (html_template) to call the loader/renderer and ensure
tests or callers (where html_template is used) now receive the rendered string
instead of the inline literal.
- Around line 758-764: The current code only calls
clearSearchFilter(categoryName) for the destination tab, leaving the source
tab's data-search-hidden flags set; update the logic so that when clearing the
search input you also clear any data-search-hidden state on the other tab(s).
Concretely, after clearing the input and UI bits (the code that touches
'searchInput', 'searchClear', 'searchCount'), either invoke clearSearchFilter
for the previous/other category in addition to the current categoryName or query
and remove the data-search-hidden attribute from all card elements (selector
matching the per-category cards) so no tab retains hidden cards; reference
clearSearchFilter, categoryName and the data-search-hidden attribute to locate
where to change the DOM-clearing behavior.
- Line 606: The subtitle currently hardcodes "6 domains" in the template string
("{total_count} interactive lessons across 6 domains"); change it to use a
dynamic domain count (e.g., a variable like domain_count or
len(domains)/len(categories) computed where total_count is computed) so the
rendered string becomes "{total_count} interactive lessons across {domain_count}
domains" (and ensure the variable used is in scope where the template is built
and pluralization remains correct).
- Around line 683-694: renderPaginationControls currently creates (and clears) a
.pagination container before checking totalPages <= 1, leaving an empty
container and layout gap; change the logic in renderPaginationControls(tabId,
totalPages, currentPage) so that you either (a) check totalPages <= 1 before
creating/appending the container, or (b) if you must create it first, remove the
container from the tab (tab.removeChild(container) or container.remove()) and
return when totalPages <= 1; target the container variable and the tab element
in that function to ensure no empty .pagination element remains.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
generate_index.py (1)
142-167:⚠️ Potential issue | 🟠 Major
get_html_titleが try-except の外にあり、エンコードエラーでスクリプト全体がクラッシュする
exceptブランチでshutil.copy2によるフォールバックコピーが完了した後、同じファイルをencoding='utf-8'で再オープンするget_html_title(Line 166)が呼ばれます。非 UTF-8 の HTML ファイルがあると同じUnicodeDecodeErrorが発生し、以降のすべての処理が中断されます。🐛 修正案 — フォールバック時は basename を使用
except Exception as e: print(f"Error processing {filepath}: {e}") shutil.copy2(filepath, dest_path) # Indexing Logic parts = rel_path.split(os.sep) if len(parts) > 1: category = parts[0] else: category = "Uncategorized" if category.startswith('.'): continue - title = self.get_html_title(filepath) + try: + title = self.get_html_title(filepath) + except Exception: + title = os.path.basename(filepath) structure[category].append((title, rel_path))🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@generate_index.py` around lines 142 - 167, The code calls get_html_title(filepath) outside the file-read try/except so a UnicodeDecodeError in non-UTF8 files can crash the script; change the flow to determine title after the read/fallback and protect it: call get_html_title on the successfully written file (dest_path) inside the try/except or wrap get_html_title(...) in its own try/except and fall back to os.path.basename(filepath) (or similar) when title extraction fails, ensuring structure[category].append((title, rel_path)) always receives a safe title.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@generate_index.py`:
- Around line 612-614: The search <input> element with id "searchInput" (class
"search-input") lacks an accessible name; add an aria-label attribute (e.g.,
aria-label="Search problems") to that <input> so screen readers can announce its
purpose (do not rely on placeholder text); update the element definition where
id="searchInput" is defined to include the aria-label string.
- Around line 739-744: applyStaggerAnimation currently forces a synchronous
reflow per item because each iteration reads item.offsetHeight; change the
implementation to avoid per-item layout reads by batching the layout read and
using requestAnimationFrame or toggling a CSS class: first loop to set/reset
animation styles without reading layout, perform a single layout read (e.g.,
read container.offsetHeight) inside requestAnimationFrame or before the second
pass, then in a second loop apply item.style.animationDelay (or add a stagger
CSS class) so you only trigger at most one reflow rather than one per item;
target symbols: applyStaggerAnimation, item.offsetHeight,
item.style.animationDelay.
- Around line 195-197: HTML currently includes Google Fonts <link> tags which
leaves font loading CDN-dependent; copy Outfit and JetBrains Mono font files
into vendor/fonts/, add `@font-face` declarations in your main CSS (or a new
vendor/fonts.css) pointing to those local files, update generate_index.py to
inject a local <link rel="stylesheet" href="vendor/fonts.css"> instead of the
Google Fonts URLs (replace the three <link> lines), and if CDN usage was
intentional, document that decision in CLAUDE.md noting the trade-offs for
offline/CI environments.
- Around line 874-886: The category string is injected into HTML and JS without
escaping (used in tabs_html onclick, tab_contents_html id, and displayed text),
so escape it before embedding: call html.escape(category, quote=True) (use a
variable like safe_category) when inserting into id attributes and into the
visible text, and avoid breaking the onclick JS by either using a data-category
attribute (e.g., data-category="{safe_category}") and letting openTab read
event.currentTarget.dataset.category, or if keeping inline onclick, pass an
already JS-escaped value (escape single quotes/backslashes) so openTab(event,
...) cannot be broken; update uses in tabs_html, tab_contents_html, and any
onclick occurrences accordingly.
---
Outside diff comments:
In `@generate_index.py`:
- Around line 142-167: The code calls get_html_title(filepath) outside the
file-read try/except so a UnicodeDecodeError in non-UTF8 files can crash the
script; change the flow to determine title after the read/fallback and protect
it: call get_html_title on the successfully written file (dest_path) inside the
try/except or wrap get_html_title(...) in its own try/except and fall back to
os.path.basename(filepath) (or similar) when title extraction fails, ensuring
structure[category].append((title, rel_path)) always receives a safe title.
| <link rel="preconnect" href="https://fonts.googleapis.com"> | ||
| <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> | ||
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@400;500;600;700&family=JetBrains+Mono:wght@400&display=swap" rel="stylesheet"> |
There was a problem hiding this comment.
🧹 Nitpick | 🔵 Trivial
Google Fonts のみ CDN 依存が残り、ローカルベンダー方針と不整合
React/Babel/Tailwind/PrismJS/FontAwesome はすべてローカルベンダーへコピーしているのに、Outfit と JetBrains Mono だけ Google Fonts CDN に依存しています。ネットワーク接続がない環境(CI や GitHub Pages のオフラインプレビュー等)では fonts が読み込まれず、フォールバックフォントで表示されます。フォントファイルも vendor/fonts/ にコピーし、@font-face でセルフホストするか、または意図的な CDN 依存として CLAUDE.md に明記することを推奨します。
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@generate_index.py` around lines 195 - 197, HTML currently includes Google
Fonts <link> tags which leaves font loading CDN-dependent; copy Outfit and
JetBrains Mono font files into vendor/fonts/, add `@font-face` declarations in
your main CSS (or a new vendor/fonts.css) pointing to those local files, update
generate_index.py to inject a local <link rel="stylesheet"
href="vendor/fonts.css"> instead of the Google Fonts URLs (replace the three
<link> lines), and if CDN usage was intentional, document that decision in
CLAUDE.md noting the trade-offs for offline/CI environments.
Docstrings generation was requested by @myoshi2891. * #278 (comment) The following files were modified: * `generate_index.py`
📝 Add docstrings to `dev-from-macmini`
ページネーションの表示制御の微調整と、タブ切り替え時の検索フィルタ・クリアロジックの改善