diff --git a/DESIGN.md b/DESIGN.md index dd35e50a..1548dfd5 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -75,6 +75,7 @@ Layout and sizing: - The impeccable skill reference rule "cap line length at ~65-75ch" does NOT apply here. Ignore it. Readability at wide widths is carried by vertical rhythm, leading, and the modular type scale instead. - If you believe a width cap is actually necessary for some specific element, ask first with a concrete reason before adding it. - Body type floor is 16px (`--text-base: 1rem`). Content-heavy passages may go to 1.125rem. +- Absolute minimum font size is 12px (`0.75rem`) for ANY text, including pills, badges, tags, captions, footnotes. Anything smaller hits Chrome's default minimum-font-size floor and renders inconsistently across browsers and user accessibility settings. Use `var(--text-xs)` (`0.8rem`) as the smallest token. - When in doubt about any type size, pick one step larger than what the impeccable skill's scale references suggest. The user has repeatedly corrected sizes upward (11+ separate requests across 8 sessions). Never reduce an existing size unprompted. Footer, meta rows, expand content, labels, headings all trend too small by default. - Row numbers in the table: left-align, no leading zeros. The user tried zero-padding and rejected it. - Adjacent heading levels differ by at least 0.25rem of rendered size. diff --git a/README.md b/README.md index 5e0652f1..dde54a73 100644 --- a/README.md +++ b/README.md @@ -838,7 +838,7 @@ _Libraries for working with graphical user interface applications._ - [nicegui](https://github.com/zauberzeug/nicegui) - An easy-to-use, Python-based UI framework, which shows up in your web browser. - [pywebview](https://github.com/r0x0r/pywebview/) - A lightweight cross-platform native wrapper around a webview component. - Terminal - - [curses](https://docs.python.org/3/library/curses.html) - Built-in wrapper for [ncurses](http://www.gnu.org/software/ncurses/) used to create terminal GUI applications. + - [curses](https://docs.python.org/3/library/curses.html) - (Python standard library) The built-in wrapper for [ncurses](http://www.gnu.org/software/ncurses/) used to create terminal GUI applications. - [urwid](https://github.com/urwid/urwid) - A library for creating terminal GUI applications with strong support for widgets, events, rich colors, etc. - Wrappers - [gooey](https://github.com/chriskiehl/Gooey) - Turn command line programs into a full GUI application with one line. diff --git a/uv.lock b/uv.lock index a097aafb..635a3db0 100644 --- a/uv.lock +++ b/uv.lock @@ -3,7 +3,7 @@ revision = 3 requires-python = ">=3.13" [options] -exclude-newer = "2026-04-30T04:22:17.540198Z" +exclude-newer = "2026-04-30T04:38:39.45925Z" exclude-newer-span = "P3D" [[package]] diff --git a/website/build.py b/website/build.py index 1e5585c9..db42c176 100644 --- a/website/build.py +++ b/website/build.py @@ -118,6 +118,84 @@ def build_robots_txt() -> str: return f"User-agent: *\nContent-Signal: search=yes, ai-input=yes, ai-train=yes\nAllow: /\n\nSitemap: {SITEMAP_URL}\n" +WEBSITE_ID = f"{SITE_URL}#website" +ISPARTOF_WEBSITE = {"@type": "WebSite", "@id": WEBSITE_ID} + + +def _website_node() -> dict: + return { + "@type": "WebSite", + "@id": WEBSITE_ID, + "name": "Awesome Python", + "url": SITE_URL, + } + + +def _item_list_payload(entries: Sequence[TemplateEntry]) -> dict: + return { + "@type": "ItemList", + "numberOfItems": len(entries), + "itemListElement": [ + { + "@type": "ListItem", + "position": i, + "name": entry["name"], + "url": entry["url"], + } + for i, entry in enumerate(entries, start=1) + ], + } + + +def build_homepage_json_ld(entries: Sequence[TemplateEntry], total_categories: int) -> dict: + description = ( + "An opinionated guide to the best Python frameworks, libraries, and tools. " + f"Explore {len(entries)} curated projects across {total_categories} categories, " + "from AI and agents to data science and web development." + ) + return { + "@context": "https://schema.org", + "@graph": [ + _website_node(), + { + "@type": "CollectionPage", + "@id": SITE_URL, + "name": "Awesome Python", + "url": SITE_URL, + "description": description, + "isPartOf": ISPARTOF_WEBSITE, + "mainEntity": _item_list_payload(entries), + }, + ], + } + + +def category_meta_description(name: str, entry_count: int, description: str) -> str: + count_sentence = f"Explore {entry_count} curated Python projects in {name}." + if description: + lead = description if description.endswith((".", "!", "?")) else f"{description}." + return f"{lead} {count_sentence}" + return f"{count_sentence} Part of the Awesome Python catalog." + + +def build_category_json_ld(name: str, url: str, description: str, entries: Sequence[TemplateEntry]) -> dict: + return { + "@context": "https://schema.org", + "@graph": [ + _website_node(), + { + "@type": "CollectionPage", + "@id": url, + "name": f"{name} Python Libraries", + "url": url, + "description": description, + "isPartOf": ISPARTOF_WEBSITE, + "mainEntity": _item_list_payload(entries), + }, + ], + } + + def category_path(category: ParsedSection) -> str: return f"/categories/{category['slug']}/" @@ -378,6 +456,10 @@ def build(repo_root: Path) -> None: site_dir.mkdir(parents=True) filter_urls_json = json.dumps(filter_urls, sort_keys=True, ensure_ascii=False).replace("", "<\\/") + homepage_json_ld = json.dumps( + build_homepage_json_ld(entries, len(categories)), + ensure_ascii=False, + ).replace("", "<\\/") tpl_index = env.get_template("index.html") (site_dir / "index.html").write_text( @@ -393,6 +475,7 @@ def build(repo_root: Path) -> None: category_urls=category_urls, filter_urls=filter_urls, filter_urls_json=filter_urls_json, + homepage_json_ld=homepage_json_ld, ), encoding="utf-8", ) @@ -411,10 +494,20 @@ def build(repo_root: Path) -> None: group_categories: Sequence[ParsedSection] | None = None, ) -> None: page_dir.mkdir(parents=True, exist_ok=True) + category_description = category_meta_description( + category["name"], len(entries), category["description"] + ) + category_json_ld = json.dumps( + build_category_json_ld( + category["name"], category_url, category_description, entries + ), + ensure_ascii=False, + ).replace("", "<\\/") (page_dir / "index.html").write_text( tpl_category.render( category=category, category_url=category_url, + category_description=category_description, entries=entries, total_categories=len(categories), category_urls=category_urls, @@ -423,6 +516,7 @@ def build(repo_root: Path) -> None: filter_urls_json=filter_urls_json, parent_category=parent_category, group_categories=group_categories, + category_json_ld=category_json_ld, ), encoding="utf-8", ) diff --git a/website/static/main.js b/website/static/main.js index 5da89699..d5b337b3 100644 --- a/website/static/main.js +++ b/website/static/main.js @@ -113,7 +113,12 @@ document rows.forEach(function (row, i) { row._origIndex = i; - row._expandRow = row.nextElementSibling; + let next = row.nextElementSibling; + if (next && next.classList.contains("desc-row")) { + row._descRow = next; + next = next.nextElementSibling; + } + row._expandRow = next; }); function collapseAll() { @@ -127,6 +132,7 @@ function collapseAll() { function applyFilters() { const query = searchInput ? searchInput.value.toLowerCase().trim() : ""; + const descRowsVisible = !isIndexDocument || activeFilter !== null; let visibleCount = 0; collapseAll(); @@ -142,9 +148,11 @@ function applyFilters() { if (show && query) { if (!row._searchText) { let text = row.textContent.toLowerCase(); - const next = row.nextElementSibling; - if (next && next.classList.contains("expand-row")) { - text += " " + next.textContent.toLowerCase(); + if (row._descRow) { + text += " " + row._descRow.textContent.toLowerCase(); + } + if (row._expandRow) { + text += " " + row._expandRow.textContent.toLowerCase(); } row._searchText = text; } @@ -152,6 +160,12 @@ function applyFilters() { } if (row.hidden !== !show) row.hidden = !show; + if (row._descRow) { + const descHidden = !show || !descRowsVisible; + if (row._descRow.hidden !== descHidden) { + row._descRow.hidden = descHidden; + } + } if (show) { visibleCount++; @@ -262,7 +276,8 @@ function sortRows() { const frag = document.createDocumentFragment(); arr.forEach(function (row) { frag.appendChild(row); - frag.appendChild(row._expandRow); + if (row._descRow) frag.appendChild(row._descRow); + if (row._expandRow) frag.appendChild(row._expandRow); }); tbody.appendChild(frag); applyFilters(); @@ -291,7 +306,11 @@ if (tbody) { // Don't toggle if clicking a link or tag button if (e.target.closest("a") || e.target.closest(".tag")) return; - const row = e.target.closest("tr.row"); + let row = e.target.closest("tr.row"); + if (!row) { + const descRow = e.target.closest("tr.desc-row"); + if (descRow) row = descRow.previousElementSibling; + } if (!row) return; const isOpen = row.classList.contains("open"); @@ -328,7 +347,7 @@ tags.forEach(function (tag) { } applyFilters(); } else if (url) { - window.location.href = url; + window.location.href = url + "#library-index"; } }); }); diff --git a/website/static/style.css b/website/static/style.css index 53604de1..bbdac357 100644 --- a/website/static/style.css +++ b/website/static/style.css @@ -788,6 +788,11 @@ kbd { box-shadow: inset 3px 0 0 var(--accent); } +.row:has(+ .desc-row:not([hidden])) td { + border-bottom-color: transparent; + padding-bottom: 0.35rem; +} + .row.open td { background: linear-gradient(180deg, var(--row-open-start), var(--row-open-end)); border-bottom-color: transparent; @@ -919,14 +924,66 @@ th[data-sort].sort-asc::after { transform: rotate(90deg); } +.desc-row td { + padding-top: 0; + padding-bottom: 1rem; + border-bottom: 1px solid var(--line); + color: var(--ink-soft); + font-size: var(--text-sm); + line-height: 1.6; + text-wrap: pretty; + overflow-wrap: break-word; + transition: + background-color 180ms ease, + border-color 180ms ease; +} + +.row:not(.open) + .desc-row td { + border-bottom: 1px solid var(--line); +} + +.row + .desc-row { + cursor: pointer; +} + +.row:not(.open):hover td, +.row:not(.open):hover + .desc-row td { + background: var(--row-hover); +} + +.row.open + .desc-row td { + background: linear-gradient(180deg, var(--row-open-start), var(--row-open-end)); + border-bottom-color: transparent; +} + +.desc-text { + max-width: none; +} + +.desc-text a { + color: var(--accent-deep); +} + +.desc-text a:hover { + color: var(--accent); + text-decoration: underline; + text-decoration-color: var(--accent-underline); + text-underline-offset: 0.2em; +} + .expand-row { display: none; } +.row.open + .desc-row + .expand-row, .row.open + .expand-row { display: table-row; } +.desc-row:not([hidden]) + .expand-row .expand-desc { + display: none; +} + .expand-row td { padding-top: 0.1rem; padding-bottom: 1.15rem; @@ -993,7 +1050,7 @@ th[data-sort].sort-asc::after { background: var(--accent-soft); color: var(--accent-deep); padding: 0.14rem 0.48rem; - font-size: 0.6rem; + font-size: var(--text-xs); font-weight: 700; letter-spacing: 0.02em; cursor: pointer; @@ -1426,8 +1483,8 @@ th[data-sort].sort-asc::after { .footer-left { display: flex; - flex-direction: column; - gap: 0.3rem; + align-items: center; + gap: 0.6rem; } .footer-brand { @@ -1561,7 +1618,7 @@ th[data-sort].sort-asc::after { .tag { padding: 0.38rem 0.65rem; - font-size: 0.65rem; + font-size: var(--text-xs); } .table-wrap { diff --git a/website/templates/base.html b/website/templates/base.html index 22b56e98..72f246e1 100644 --- a/website/templates/base.html +++ b/website/templates/base.html @@ -53,6 +53,7 @@ gtag("js", new Date()); gtag("config", "G-0LMLYE0HER"); + {% block extra_head %}{% endblock %}
Skip to content @@ -69,7 +70,9 @@