From 03f610feeda8dacfeac188f2756a858185a34f98 Mon Sep 17 00:00:00 2001 From: Richie Cahill Date: Mon, 29 Jun 2026 10:14:13 -0400 Subject: [PATCH] feat(ui): refactor HTML templates for consistent structure and improved styling --- python/ebook_search/api/static/style.css | 363 +++++++++++++++--- python/ebook_search/api/templates/admin.html | 98 +++-- python/ebook_search/api/templates/base.html | 71 ++++ .../api/templates/book_detail.html | 52 +-- python/ebook_search/api/templates/books.html | 50 +-- .../api/templates/partials/results.html | 8 +- python/ebook_search/api/templates/search.html | 40 +- python/ebook_search/api/web.py | 10 + 8 files changed, 499 insertions(+), 193 deletions(-) create mode 100644 python/ebook_search/api/templates/base.html diff --git a/python/ebook_search/api/static/style.css b/python/ebook_search/api/static/style.css index 5447825..0a74b4e 100644 --- a/python/ebook_search/api/static/style.css +++ b/python/ebook_search/api/static/style.css @@ -1,27 +1,210 @@ +:root { + --bg: #f4f5f7; + --surface: #ffffff; + --border: #e3e5ea; + --text: #1c1f24; + --muted: #6b7280; + --accent: #4f46e5; + --accent-soft: #eef0fe; + --danger: #b42318; + --warn-bg: #fff8eb; + --warn-border: #e0a92e; + --warn-text: #7a5008; + --radius: 12px; + --shadow: 0 1px 2px rgba(16, 24, 40, 0.04), 0 1px 3px rgba(16, 24, 40, 0.08); +} + +html.theme-dark { + --bg: #0f1117; + --surface: #1a1d25; + --border: #2b303b; + --text: #e6e8ec; + --muted: #9aa1ad; + --accent: #818cf8; + --accent-soft: #262b45; + --danger: #f97066; + --warn-bg: #2a2410; + --warn-border: #b9881f; + --warn-text: #e8c97a; + --shadow: 0 1px 2px rgba(0, 0, 0, 0.3), 0 1px 3px rgba(0, 0, 0, 0.4); + color-scheme: dark; +} + +* { + box-sizing: border-box; +} + body { margin: 0; - background: #f7f7f4; - color: #202124; + background: var(--bg); + color: var(--text); font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + line-height: 1.55; } main { - max-width: 960px; + max-width: 820px; margin: 0 auto; - padding: 24px; + padding: 32px 20px 64px; } -nav { +/* Header / nav */ +.site-header { + background: var(--surface); + border-bottom: 1px solid var(--border); + position: sticky; + top: 0; + z-index: 10; +} + +.site-nav { + max-width: 820px; + margin: 0 auto; + padding: 12px 20px; display: flex; - gap: 12px; align-items: center; - margin-bottom: 20px; + gap: 20px; } -nav form { +.brand { + font-weight: 700; + font-size: 1.05rem; + color: var(--text); + text-decoration: none; +} + +.nav-links { + display: flex; + gap: 6px; + margin-right: auto; +} + +.nav-links a { + padding: 6px 12px; + border-radius: 8px; + color: var(--muted); + text-decoration: none; + font-size: 0.94rem; + transition: background 0.15s, color 0.15s; +} + +.nav-links a:hover { + background: var(--accent-soft); + color: var(--accent); +} + +.dev-toggle { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 0.85rem; + color: var(--muted); + cursor: pointer; + user-select: none; +} + +.theme-toggle { + display: inline-flex; + align-items: center; + justify-content: center; + width: 34px; + height: 34px; + padding: 0; + font-size: 1rem; + line-height: 1; + color: var(--text); + background: var(--bg); + border: 1px solid var(--border); + border-radius: 8px; + cursor: pointer; +} + +.theme-toggle:hover { + border-color: var(--accent); + filter: none; +} + +h1 { + font-size: 1.6rem; + margin: 0 0 20px; +} + +h2 { + font-size: 1.15rem; + margin: 0 0 8px; +} + +/* Cards */ +.card { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 20px; +} + +/* Search form */ +form { margin: 0; } +label { + font-weight: 600; + font-size: 0.92rem; +} + +textarea { + display: block; + width: 100%; + margin: 8px 0 16px; + padding: 12px 14px; + font: inherit; + color: var(--text); + background: var(--surface); + border: 1px solid var(--border); + border-radius: 10px; + resize: vertical; + transition: border-color 0.15s, box-shadow 0.15s; +} + +textarea:focus { + outline: none; + border-color: var(--accent); + box-shadow: 0 0 0 3px var(--accent-soft); +} + +.form-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; +} + +button { + padding: 10px 20px; + font: inherit; + font-weight: 600; + color: #fff; + background: var(--accent); + border: none; + border-radius: 10px; + cursor: pointer; + transition: filter 0.15s; +} + +button:hover { + filter: brightness(1.08); +} + +.check { + display: inline-flex; + gap: 8px; + align-items: center; + font-weight: 500; + color: var(--muted); +} + .actions { display: flex; flex-wrap: wrap; @@ -29,121 +212,203 @@ nav form { margin-bottom: 24px; } -textarea { +/* Answer + results */ +#results { display: block; - width: 100%; - margin: 8px 0 12px; -} - -button { - padding: 8px 14px; -} - -.check { - display: inline-flex; - gap: 8px; - align-items: center; - margin-right: 12px; + margin-top: 28px; } .rank-label { - margin-top: 24px; - font-weight: 700; + font-size: 0.82rem; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + color: var(--muted); + margin-bottom: 16px; +} + +.answer { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 20px; + margin-bottom: 24px; +} + +.answer p:last-child { + margin-bottom: 0; } .results { - padding-left: 24px; + list-style: none; + padding: 0; + margin: 0; + display: grid; + gap: 16px; } -.meta, -.scores, -.status { - color: #626a73; +.results > li { + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 18px 20px; +} + +.results h2 { + font-size: 1.05rem; +} + +.results h2 a { + color: var(--text); + text-decoration: none; +} + +.results h2 a:hover { + color: var(--accent); +} + +.meta { + color: var(--muted); + font-size: 0.88rem; + margin: 0 0 10px; } .scores { display: flex; flex-wrap: wrap; gap: 8px; - margin: 12px 0; + margin: 14px 0 0; } .scores div { display: inline-flex; - gap: 4px; + gap: 6px; align-items: baseline; + padding: 3px 10px; + background: var(--bg); + border: 1px solid var(--border); + border-radius: 999px; + font-size: 0.78rem; } .scores dt { - font-weight: 700; + font-weight: 600; + color: var(--muted); } .scores dd { margin: 0; + font-variant-numeric: tabular-nums; } +/* Runtime — developer diagnostics, hidden unless dev mode is on */ .runtime { - margin-top: 16px; + display: none; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + box-shadow: var(--shadow); + padding: 18px 20px; + margin-bottom: 24px; +} + +html.dev .runtime { + display: block; } .timing-chart { display: grid; gap: 8px; padding: 0; + margin: 12px 0 0; list-style: none; } .timing-chart li { display: grid; grid-template-columns: minmax(150px, 1fr) minmax(160px, 2fr) auto auto; - gap: 8px; + gap: 10px; align-items: center; + font-size: 0.85rem; } .timing-bar { - height: 10px; + height: 8px; overflow: hidden; - background: #e5e5df; + background: var(--bg); + border-radius: 999px; } .timing-bar span { display: block; height: 100%; - background: #3767c8; + background: var(--accent); + border-radius: 999px; } .timing-value, .timing-remaining { - color: #626a73; + color: var(--muted); font-variant-numeric: tabular-nums; + text-align: right; } +/* Tables */ table { width: 100%; border-collapse: collapse; + background: var(--surface); + border: 1px solid var(--border); + border-radius: var(--radius); + overflow: hidden; } th, td { - padding: 8px; - border-bottom: 1px solid #d8d8d2; + padding: 10px 14px; + border-bottom: 1px solid var(--border); text-align: left; + font-size: 0.9rem; } th { - font-weight: 700; + font-weight: 600; + color: var(--muted); + background: var(--bg); } +tbody tr:last-child td { + border-bottom: none; +} + +dl dt { + font-weight: 600; + color: var(--muted); + font-size: 0.85rem; +} + +dl dd { + margin: 0 0 12px; +} + +/* States */ .error { - color: #9f1d20; - font-weight: 700; + color: var(--danger); + font-weight: 600; } .notice { - margin: 8px 0; - padding: 8px 12px; - border-left: 4px solid #c8881d; - background: #fcf3e2; - color: #6b4a06; - font-weight: 600; + margin: 12px 0; + padding: 10px 14px; + border-left: 3px solid var(--warn-border); + border-radius: 6px; + background: var(--warn-bg); + color: var(--warn-text); + font-weight: 500; +} + +.status { + color: var(--muted); } diff --git a/python/ebook_search/api/templates/admin.html b/python/ebook_search/api/templates/admin.html index 12e588e..b2656ee 100644 --- a/python/ebook_search/api/templates/admin.html +++ b/python/ebook_search/api/templates/admin.html @@ -1,57 +1,45 @@ - - - - - - EPUB Admin - - - - -
- -

Admin

-
-
-
- -
-
- -
-
- -
-
-
-

Embeddings

- - +{% extends "base.html" %} + +{% block title %}EPUB Admin{% endblock %} +{% block head %}{% endblock %} + +{% block content %} +

Admin

+
+
+
+ + +
+ + +
+ + +
+
+

Embeddings

+
+ + + + + + + + + + + {% for item in stats %} - - - - - + + + + + - - - {% for item in stats %} - - - - - - - - {% endfor %} - -
ModelDimensionsEmbeddedMissingTotal chunks
ModelDimensionsEmbeddedMissingTotal chunks{{ item.model_name }}{{ item.dimension }}{{ item.embedded_chunks }}{{ item.missing_chunks }}{{ item.total_chunks }}
{{ item.model_name }}{{ item.dimension }}{{ item.embedded_chunks }}{{ item.missing_chunks }}{{ item.total_chunks }}
-
-
- - + {% endfor %} + + + +{% endblock %} diff --git a/python/ebook_search/api/templates/base.html b/python/ebook_search/api/templates/base.html new file mode 100644 index 0000000..252b0cf --- /dev/null +++ b/python/ebook_search/api/templates/base.html @@ -0,0 +1,71 @@ + + + + + + {% block title %}EPUB Search{% endblock %} + {% block head %}{% endblock %} + + + + + +
+ {% block content %}{% endblock %} +
+ + + diff --git a/python/ebook_search/api/templates/book_detail.html b/python/ebook_search/api/templates/book_detail.html index 735aeaf..0da0fd6 100644 --- a/python/ebook_search/api/templates/book_detail.html +++ b/python/ebook_search/api/templates/book_detail.html @@ -1,32 +1,20 @@ - - - - - - {% if source %}{{ source.title }}{% else %}Book not found{% endif %} - - - -
- - {% if source %} -

{{ source.title }}

-

{{ source.author or "Unknown author" }}

-
-
File
-
{{ source.file_path }}
-
Chapters
-
{{ chapter_count }}
-
Chunks
-
{{ chunk_count }}
-
- {% else %} -

Book not found

- {% endif %} -
- - +{% extends "base.html" %} + +{% block title %}{% if source %}{{ source.title }}{% else %}Book not found{% endif %}{% endblock %} + +{% block content %} + {% if source %} +

{{ source.title }}

+

{{ source.author or "Unknown author" }}

+
+
File
+
{{ source.file_path }}
+
Chapters
+
{{ chapter_count }}
+
Chunks
+
{{ chunk_count }}
+
+ {% else %} +

Book not found

+ {% endif %} +{% endblock %} diff --git a/python/ebook_search/api/templates/books.html b/python/ebook_search/api/templates/books.html index c7bc487..5d4acca 100644 --- a/python/ebook_search/api/templates/books.html +++ b/python/ebook_search/api/templates/books.html @@ -1,31 +1,19 @@ - - - - - - EPUB Books - - - -
- -

Books

- {% if sources %} -
    - {% for source in sources %} -
  1. -

    {{ source.title }}

    -

    {{ source.author or "Unknown author" }}

    -
  2. - {% endfor %} -
- {% else %} -

No EPUBs indexed.

- {% endif %} -
- - +{% extends "base.html" %} + +{% block title %}EPUB Books{% endblock %} + +{% block content %} +

Books

+ {% if sources %} +
    + {% for source in sources %} +
  1. +

    {{ source.title }}

    +

    {{ source.author or "Unknown author" }}

    +
  2. + {% endfor %} +
+ {% else %} +

No EPUBs indexed.

+ {% endif %} +{% endblock %} diff --git a/python/ebook_search/api/templates/partials/results.html b/python/ebook_search/api/templates/partials/results.html index 72e933b..2e0b7a1 100644 --- a/python/ebook_search/api/templates/partials/results.html +++ b/python/ebook_search/api/templates/partials/results.html @@ -39,7 +39,13 @@
    {% for result in response.results %}
  1. -

    {{ result.source_title }}

    +

    + {% if result.source_id %} + {{ result.source_title }} + {% else %} + {{ result.source_title }} + {% endif %} +

    {% if result.source_author %}{{ result.source_author }}{% endif %} {% if result.chapter_title %} · {{ result.chapter_title }}{% endif %} diff --git a/python/ebook_search/api/templates/search.html b/python/ebook_search/api/templates/search.html index df566c5..9ae3a43 100644 --- a/python/ebook_search/api/templates/search.html +++ b/python/ebook_search/api/templates/search.html @@ -1,30 +1,20 @@ - - - - - - EPUB Search - - - - -

    - -

    EPUB Search

    -
    - - +{% extends "base.html" %} + +{% block title %}EPUB Search{% endblock %} +{% block head %}{% endblock %} + +{% block content %} +

    Search

    + + + +
    - -
    -
    - - + + +
    +{% endblock %} diff --git a/python/ebook_search/api/web.py b/python/ebook_search/api/web.py index 85f6128..cb67661 100644 --- a/python/ebook_search/api/web.py +++ b/python/ebook_search/api/web.py @@ -10,4 +10,14 @@ PACKAGE_DIR = Path(__file__).resolve().parent TEMPLATE_DIR = PACKAGE_DIR / "templates" STATIC_DIR = PACKAGE_DIR / "static" + +def static_version(filename: str) -> int: + """Return a cache-busting token for a static file based on its modification time.""" + try: + return int((STATIC_DIR / filename).stat().st_mtime) + except OSError: + return 0 + + templates = Jinja2Templates(directory=TEMPLATE_DIR) +templates.env.globals["static_version"] = static_version