fix(templates): align table position and structure across all list pages
Move list partials outside the section element on bags, gear, and brews pages so they become separate flex children of <main>, matching the pattern used by roasters, roasts, cafes, and cups. Add mt-6 to each partial's outer div for consistent spacing. Standardise brew and gear list partials to match the canonical structure: empty-state conditional with dashed amber border, <section> wrapper for the table, and data-star-scope attribute on the outer div. Update CLAUDE.md to document page template and list partial structure requirements, preventing future drift.
This commit is contained in:
parent
f4090ba329
commit
3ae3963c5b
7 changed files with 73 additions and 28 deletions
52
CLAUDE.md
52
CLAUDE.md
|
|
@ -424,16 +424,41 @@ The route handler in `application/routes/cafes.rs` accepts either `lat`/`lng` qu
|
|||
|
||||
## Table & List Patterns
|
||||
|
||||
### Template Structure
|
||||
### Page Template Structure
|
||||
|
||||
Each list page template (`templates/{entity}.html`) places the form and list as **separate siblings** so they become independent flex children of `<main>` (which uses `flex flex-col gap-6`):
|
||||
|
||||
```html
|
||||
<section data-signals:_show-form="false">
|
||||
<header>...</header>
|
||||
{% if is_authenticated %}
|
||||
<div data-show="$_showForm" style="display: none">
|
||||
<!-- Form -->
|
||||
</div>
|
||||
{% endif %}
|
||||
</section>
|
||||
|
||||
{% include "partials/{entity}_list.html" %} {% endblock %}
|
||||
```
|
||||
|
||||
The list include must be **outside** `</section>`, never inside it. Placing it inside removes the flex gap between the form section and the list, causing the table to sit higher on the page.
|
||||
|
||||
### List Partial Structure
|
||||
|
||||
List partials live in `templates/partials/` (e.g., `roaster_list.html`, `brew_list.html`). Each follows the same structure:
|
||||
|
||||
```
|
||||
```html
|
||||
{% import "partials/table.html" as table %}
|
||||
|
||||
<div id="{entity}-list">
|
||||
<div id="{entity}-list" class="mt-6" data-star-scope="{entity}">
|
||||
{% if items.is_empty() && !navigator.has_search() %}
|
||||
<!-- Empty state (no data, no search active) -->
|
||||
<div
|
||||
class="rounded-lg border border-dashed border-amber-300 bg-amber-100/40 px-4 py-6 text-sm text-stone-600"
|
||||
>
|
||||
<p class="text-center">
|
||||
No {entities} recorded yet. Use the form above to add your first {entity}.
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<section class="rounded-lg border border-amber-300 bg-amber-100/80 shadow-sm"
|
||||
{% if items.has_next() %}data-infinite-scroll data-next-url="..." data-target="#{entity}-list"{% endif %}
|
||||
|
|
@ -444,7 +469,7 @@ List partials live in `templates/partials/` (e.g., `roaster_list.html`, `brew_li
|
|||
<tbody>...</tbody>
|
||||
</table>
|
||||
{% if items.is_empty() %}
|
||||
<!-- "No results match your search" message -->
|
||||
<div class="p-8 text-center text-stone-500">No {entities} match your search.</div>
|
||||
{% endif %}
|
||||
{% call table::pagination_header(items, navigator, "#{entity}-list") %}
|
||||
{% if items.has_next() %}
|
||||
|
|
@ -455,10 +480,19 @@ List partials live in `templates/partials/` (e.g., `roaster_list.html`, `brew_li
|
|||
</div>
|
||||
```
|
||||
|
||||
Key points:
|
||||
- The outer `<div>` with `id="{entity}-list"` is the Datastar fragment target for replacements
|
||||
- Empty state only shows when there are no items **and** no active search query
|
||||
- When a search is active but returns no results, the table section renders with the search bar and a "no matches" message
|
||||
Required attributes and elements on every list partial:
|
||||
|
||||
| Element | Requirement |
|
||||
|---------|-------------|
|
||||
| Outer `<div>` | `id="{entity}-list"`, `class="mt-6"`, `data-star-scope="{entity}"` |
|
||||
| Empty state | Conditional on `items.is_empty() && !navigator.has_search()`, dashed amber border |
|
||||
| Table wrapper | `<section>` (not `<div>`) |
|
||||
| Search header | `{% call table::search_header(...) %}` |
|
||||
| No-results msg | Inside the `<section>`, shown when search is active but returns nothing |
|
||||
| Pagination | `{% call table::pagination_header(...) %}` |
|
||||
| Scroll sentinel | `class="infinite-scroll-sentinel h-4 md:hidden"` |
|
||||
|
||||
**Exception**: the bags partial uses a dual-section layout (open-bag cards + history table) and does not follow this pattern exactly.
|
||||
|
||||
### Shared Table Macros (`templates/partials/table.html`)
|
||||
|
||||
|
|
|
|||
|
|
@ -109,6 +109,6 @@
|
|||
</div>
|
||||
{% endif %} {% endif %}
|
||||
|
||||
<div class="mt-8">{% include "partials/bag_list.html" %}</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% include "partials/bag_list.html" %} {% endblock %}
|
||||
|
|
|
|||
|
|
@ -159,6 +159,6 @@
|
|||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-8">{% include "partials/brew_list.html" %}</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% include "partials/brew_list.html" %} {% endblock %}
|
||||
|
|
|
|||
|
|
@ -91,6 +91,6 @@
|
|||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="mt-8">{% include "partials/gear_list.html" %}</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
|
||||
{% include "partials/gear_list.html" %} {% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
{% import "partials/table.html" as table %}
|
||||
|
||||
<div id="bag-list" class="space-y-8" data-star-scope="bags">
|
||||
<div id="bag-list" class="mt-6 space-y-8" data-star-scope="bags">
|
||||
{% if !open_bags.is_empty() %}
|
||||
<section>
|
||||
<h2 class="text-2xl font-bold mb-4 text-stone-800">Open Bags</h2>
|
||||
|
|
|
|||
|
|
@ -1,7 +1,16 @@
|
|||
{% import "partials/table.html" as table %}
|
||||
|
||||
<div id="brew-list" class="space-y-4" data-star-scope="brews">
|
||||
<div class="rounded-lg border border-amber-300 bg-amber-100/80 shadow-sm"
|
||||
<div id="brew-list" class="mt-6" data-star-scope="brews">
|
||||
{% if brews.items.is_empty() && !navigator.has_search() %}
|
||||
<div
|
||||
class="rounded-lg border border-dashed border-amber-300 bg-amber-100/40 px-4 py-6 text-sm text-stone-600"
|
||||
>
|
||||
<p class="text-center">
|
||||
No brews logged yet. Use the form above to log your first brew.
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<section class="rounded-lg border border-amber-300 bg-amber-100/80 shadow-sm"
|
||||
{% if brews.has_next() %}data-infinite-scroll data-next-url="{{ navigator.fragment_page_href(brews.next_page().unwrap())|safe }}" data-target="#brew-list"{% endif %}
|
||||
>
|
||||
{% call table::search_header(navigator, "#brew-list") %}
|
||||
|
|
@ -118,7 +127,7 @@
|
|||
|
||||
{% if brews.items.is_empty() %}
|
||||
<div class="p-8 text-center text-stone-500">
|
||||
No brews logged yet. Start by logging your first cup!
|
||||
No brews match your search.
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
|
|
@ -127,5 +136,6 @@
|
|||
{% if brews.has_next() %}
|
||||
<div class="infinite-scroll-sentinel h-4 md:hidden" aria-hidden="true"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,15 +1,16 @@
|
|||
{% import "partials/table.html" as table %}
|
||||
|
||||
<div id="gear-list">
|
||||
<div id="gear-list" class="mt-6" data-star-scope="gear">
|
||||
{% if gear.items.is_empty() && !navigator.has_search() %}
|
||||
<div class="rounded-lg border border-stone-200 bg-stone-50 p-8 text-center">
|
||||
<p class="text-stone-500">No gear found.</p>
|
||||
{% if is_authenticated %}
|
||||
<p class="mt-2 text-sm text-stone-500">Add your first piece of brewing equipment above.</p>
|
||||
{% endif %}
|
||||
<div
|
||||
class="rounded-lg border border-dashed border-amber-300 bg-amber-100/40 px-4 py-6 text-sm text-stone-600"
|
||||
>
|
||||
<p class="text-center">
|
||||
No gear found. Use the form above to add your first piece of brewing equipment.
|
||||
</p>
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="rounded-lg border border-amber-300 bg-amber-100/80 shadow-sm"
|
||||
<section class="rounded-lg border border-amber-300 bg-amber-100/80 shadow-sm"
|
||||
{% if gear.has_next() %}data-infinite-scroll data-next-url="{{ navigator.fragment_page_href(gear.next_page().unwrap())|safe }}" data-target="#gear-list"{% endif %}
|
||||
>
|
||||
{% call table::search_header(navigator, "#gear-list") %}
|
||||
|
|
@ -71,6 +72,6 @@
|
|||
{% if gear.has_next() %}
|
||||
<div class="infinite-scroll-sentinel h-4 md:hidden" aria-hidden="true"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</section>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Reference in a new issue