navbar is now penguinui

This commit is contained in:
Priec
2026-06-18 16:06:51 +02:00
parent 36a5e7c5fc
commit 68381d558a
5 changed files with 87 additions and 48 deletions

File diff suppressed because one or more lines are too long

View File

@@ -106,7 +106,16 @@
<!-- content column --> <!-- content column -->
<div class="flex min-h-screen flex-col md:ml-60"> <div class="flex min-h-screen flex-col md:ml-60">
<header class="sticky top-0 z-20 flex h-16 items-center gap-4 border-b border-outline bg-surface/95 px-4 backdrop-blur dark:border-outline-dark dark:bg-surface-dark/95"> <header class="sticky top-0 z-20 flex h-16 items-center gap-4 border-b border-outline bg-surface/95 px-4 backdrop-blur dark:border-outline-dark dark:bg-surface-dark/95">
{{ ui::icon_button(aria_label=t(key='menu', lang=lang | default(value='sk')), attrs='@click="showSidebar = !showSidebar" :aria-expanded="showSidebar"', extra="md:hidden", icon='<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /></svg>') }} <!-- Penguin animated hamburger (bars ↔ X) in our ghost-square shell -->
<button type="button" @click="showSidebar = !showSidebar" :aria-expanded="showSidebar" aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}"
class="inline-flex size-9 shrink-0 items-center justify-center rounded-radius bg-transparent text-secondary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary active:opacity-100 active:outline-offset-0 md:hidden dark:text-secondary-dark dark:focus-visible:outline-secondary-dark">
<svg x-show="!showSidebar" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
<svg x-cloak x-show="showSidebar" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
<span class="text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong"> <span class="text-sm font-semibold text-on-surface-strong dark:text-on-surface-dark-strong">
{% block crumb %}{{ t(key="admin-title", lang=lang | default(value='sk')) }}{% endblock crumb %} {% block crumb %}{{ t(key="admin-title", lang=lang | default(value='sk')) }}{% endblock crumb %}

View File

@@ -73,19 +73,19 @@
{{ t(key="brand", lang=lang | default(value='sk')) }} {{ t(key="brand", lang=lang | default(value='sk')) }}
</a> </a>
<!-- desktop links --> <!-- desktop links — Penguin navbar link treatment via ui::nav_link -->
<ul class="ml-2 hidden items-center gap-1 md:flex"> <ul class="ml-2 hidden items-center gap-6 md:flex">
<li><a href="/" data-nav="/" class="rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary aria-[current=page]:text-primary aria-[current=page]:font-semibold dark:text-on-surface-dark dark:hover:bg-surface-dark-alt dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li> <li>{{ ui::nav_link(label=t(key="nav-home", lang=lang | default(value='sk')), href="/", data_nav="/") }}</li>
<li><a href="/shop" data-nav="/shop" class="rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary aria-[current=page]:text-primary aria-[current=page]:font-semibold dark:text-on-surface-dark dark:hover:bg-surface-dark-alt dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-shop", lang=lang | default(value='sk')) }}</a></li> <li>{{ ui::nav_link(label=t(key="nav-shop", lang=lang | default(value='sk')), href="/shop", data_nav="/shop") }}</li>
{% if logged_in_admin %} {% if logged_in_admin %}
<li><a href="/admin/dashboard" hx-boost="false" data-nav="/admin" class="rounded-radius px-3 py-1.5 text-sm font-medium text-warning transition hover:bg-surface-alt dark:hover:bg-surface-dark-alt">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li> <li>{{ ui::nav_link(label=t(key="admin-title", lang=lang | default(value='sk')), href="/admin/dashboard", data_nav="/admin", variant="warning", attrs='hx-boost="false"') }}</li>
<li> <li>
<form method="post" action="/admin/logout" hx-boost="false"> <form method="post" action="/admin/logout" hx-boost="false">
<button type="submit" class="rounded-radius px-3 py-1.5 text-sm font-medium text-danger transition hover:bg-surface-alt dark:hover:bg-surface-dark-alt">{{ t(key="logout", lang=lang | default(value='sk')) }}</button> <button type="submit" class="text-sm font-medium text-danger underline-offset-2 transition hover:opacity-75 focus:outline-hidden focus-visible:underline">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
</form> </form>
</li> </li>
{% else %} {% else %}
<li><a href="/admin/login" data-nav="/admin/login" class="rounded-radius px-3 py-1.5 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary aria-[current=page]:text-primary aria-[current=page]:font-semibold dark:text-on-surface-dark dark:hover:bg-surface-dark-alt dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-admin", lang=lang | default(value='sk')) }}</a></li> <li>{{ ui::nav_link(label=t(key="nav-admin", lang=lang | default(value='sk')), href="/admin/login", data_nav="/admin/login") }}</li>
{% endif %} {% endif %}
</ul> </ul>
@@ -109,24 +109,34 @@
{% include "partials/settings_dropdown.html" %} {% include "partials/settings_dropdown.html" %}
</div> </div>
<!-- mobile hamburger --> <!-- mobile hamburger — Penguin animated icon swap (bars ↔ X), kept in
{{ ui::icon_button(aria_label=t(key='menu', lang=lang | default(value='sk')), attrs='@click="mobile = !mobile" :aria-expanded="mobile"', extra="md:hidden", icon='<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6"><path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" /></svg>') }} our ghost-square icon-button shell for consistency with cart/gear -->
<button type="button" @click="mobile = !mobile" :aria-expanded="mobile" aria-label="{{ t(key='menu', lang=lang | default(value='sk')) }}"
class="inline-flex size-9 shrink-0 items-center justify-center rounded-radius bg-transparent text-secondary transition hover:opacity-75 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-secondary active:opacity-100 active:outline-offset-0 md:hidden dark:text-secondary-dark dark:focus-visible:outline-secondary-dark">
<svg x-show="!mobile" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M3.75 6.75h16.5M3.75 12h16.5m-16.5 5.25h16.5" />
</svg>
<svg x-cloak x-show="mobile" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="size-6" aria-hidden="true">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 18 18 6M6 6l12 12" />
</svg>
</button>
</div> </div>
<!-- mobile menu panel --> <!-- mobile menu panel — Penguin sidebar-style menu rows (hover:bg-primary/5,
underline focus), active state via data-nav + markActiveNav() -->
<ul x-show="mobile" x-cloak @click.outside="mobile = false" x-transition <ul x-show="mobile" x-cloak @click.outside="mobile = false" x-transition
class="absolute inset-x-0 top-full mx-4 mt-2 flex flex-col gap-1 rounded-radius border border-outline bg-surface p-2 shadow-lg md:hidden dark:border-outline-dark dark:bg-surface-dark-alt"> class="absolute inset-x-0 top-full mx-4 mt-2 flex flex-col gap-1 rounded-radius border border-outline bg-surface p-2 shadow-lg md:hidden dark:border-outline-dark dark:bg-surface-dark-alt">
<li><a href="/" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li> <li><a href="/" data-nav="/" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-home", lang=lang | default(value='sk')) }}</a></li>
<li><a href="/shop" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark">{{ t(key="nav-shop", lang=lang | default(value='sk')) }}</a></li> <li><a href="/shop" data-nav="/shop" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-shop", lang=lang | default(value='sk')) }}</a></li>
{% if logged_in_admin %} {% if logged_in_admin %}
<li><a href="/admin/dashboard" hx-boost="false" class="block rounded-radius px-3 py-2 text-sm font-medium text-warning hover:bg-surface-alt dark:hover:bg-surface-dark">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li> <li><a href="/admin/dashboard" hx-boost="false" data-nav="/admin" class="block rounded-radius px-3 py-2 text-sm font-medium text-warning underline-offset-2 transition hover:bg-primary/5 focus:outline-hidden focus-visible:underline">{{ t(key="admin-title", lang=lang | default(value='sk')) }}</a></li>
<li> <li>
<form method="post" action="/admin/logout" hx-boost="false"> <form method="post" action="/admin/logout" hx-boost="false">
<button type="submit" class="block w-full rounded-radius px-3 py-2 text-left text-sm font-medium text-danger hover:bg-surface-alt dark:hover:bg-surface-dark">{{ t(key="logout", lang=lang | default(value='sk')) }}</button> <button type="submit" class="block w-full rounded-radius px-3 py-2 text-left text-sm font-medium text-danger underline-offset-2 transition hover:bg-primary/5 focus:outline-hidden focus-visible:underline">{{ t(key="logout", lang=lang | default(value='sk')) }}</button>
</form> </form>
</li> </li>
{% else %} {% else %}
<li><a href="/admin/login" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface transition hover:bg-surface-alt hover:text-primary dark:text-on-surface-dark dark:hover:bg-surface-dark dark:hover:text-primary-dark">{{ t(key="nav-admin", lang=lang | default(value='sk')) }}</a></li> <li><a href="/admin/login" data-nav="/admin/login" class="block rounded-radius px-3 py-2 text-sm font-medium text-on-surface underline-offset-2 transition hover:bg-primary/5 hover:text-primary focus:outline-hidden focus-visible:underline aria-[current=page]:font-semibold aria-[current=page]:bg-primary/10 aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark">{{ t(key="nav-admin", lang=lang | default(value='sk')) }}</a></li>
{% endif %} {% endif %}
</ul> </ul>
</nav> </nav>

View File

@@ -176,3 +176,16 @@ border-t border-outline dark:border-outline-dark
{% macro th(label, align="") -%} {% macro th(label, align="") -%}
<th class="px-4 py-3 font-semibold{% if align %} {{ align }}{% endif %}">{{ label }}</th> <th class="px-4 py-3 font-semibold{% if align %} {{ align }}{% endif %}">{{ label }}</th>
{%- endmacro th %} {%- endmacro th %}
{# Top-nav link. Penguin navbar/default-navbar.html link treatment: text-only,
underline on focus, hover:text-primary, active (aria-current=page, set by
markActiveNav() via data-nav) = font-semibold + primary. Matches the ported
sidebars. variant ∈ default | warning (admin) | danger (logout-style links).
Logout itself stays an inline <form><button> (not an <a>, so not this macro). #}
{% macro nav_link(label, href, data_nav="", variant="default", attrs="") -%}
{%- if variant == "warning" -%}{% set c = "text-warning hover:opacity-75 dark:text-warning" -%}
{%- elif variant == "danger" -%}{% set c = "text-danger hover:opacity-75 dark:text-danger" -%}
{%- else -%}{% set c = "text-on-surface hover:text-primary aria-[current=page]:font-semibold aria-[current=page]:text-primary dark:text-on-surface-dark dark:hover:text-primary-dark dark:aria-[current=page]:text-primary-dark" -%}
{%- endif -%}
<a href="{{ href }}"{% if data_nav %} data-nav="{{ data_nav }}"{% endif %} class="text-sm font-medium underline-offset-2 transition focus:outline-hidden focus-visible:underline {{ c }}" {{ attrs | safe }}>{{ label }}</a>
{%- endmacro nav_link %}

View File

@@ -50,29 +50,35 @@ from the build. If the build ever balloons, check that exclusion is intact.
--- ---
## 1. Navbar — PENDING ## 1. Navbar — ✅ DONE
**Penguin UI: `navbar/`** **Penguin UI: `navbar/default-navbar.html`**
> **Priority: MEDIUM** | ~143 lines across2 sites. - Exact upstream mirror at `penguinui-components/navbar/default-navbar.html` (reference only)
> Penguin match: `navbar/default-navbar.html` — provides mobile-responsive structure, - **Link treatment** adopted from Penguin (matches the already-ported sidebars): the
> hamburger animation, and link treatment that would replace our hand-rolled `<nav>`. desktop nav links lost the pill-hover (`hover:bg-surface-alt` + `px-3 py-1.5`) for
> **Watch out:** tight integration needed for cart badge, settings dropdown partial, Penguin's text-only `underline-offset-2 hover:text-primary focus-visible:underline
> language switcher, and Alpine mobile toggle — don't lose any of those. focus:outline-hidden`, active (`aria-current=page`, set by `markActiveNav()` via
`data-nav`) = `font-semibold` + primary. Centralized into a `ui::nav_link(label,
href, data_nav, variant, attrs)` macro in `macros/ui.html` (variant ∈ default |
warning admin | danger). Logout stays an inline `<form><button>` (not an `<a>`).
- **Hamburger animation** adopted: both the site mobile-menu button and the admin
sidebar toggle now swap bars ↔ X (`x-show="!open"`/`x-show="open"`, Penguin X-path
`M6 18 18 6M6 6l12 12`), kept inside our ghost-square icon-button shell for
consistency with the cart/gear buttons.
- **Mobile menu panel**: kept our compact dropdown (better for this app's dense top
bar than Penguin's full-screen `fixed inset-x-0 top-0 pt-20` overlay, which would
cover the cart/settings/category-toggle). Items now use the sidebar menu-row
treatment (`hover:bg-primary/5`, underline focus) + `data-nav` so they show the
active state too.
- **Preserved intact** (the integration risks flagged here): cart icon + live
cookie-read badge, the `partials/settings_dropdown.html` include (language switcher
+ theme tristate), the mobile category-drawer toggle, and all Alpine toggles
(`mobile`, `cats`, `showSidebar`).
| # | Location | What it is | Size | | # | Location | What it is |
|---|----------|------------|------| |---|----------|------------|
| 1 | `assets/views/base.html:63-191` | Full site navbar: brand, desktop nav links, cart icon+badge, settings dropdown, mobile hamburger → mobile panel | ~130 lines | | 1 | `assets/views/base.html` | Full site navbar (brand, links, cart badge, settings, mobile menu) |
| 2 | `assets/views/admin/base.html:102-114` | Admin top bar: hamburger toggle + breadcrumb text | ~13 lines | | 2 | `assets/views/admin/base.html` | Admin top bar: animated hamburger + breadcrumb + settings |
**Details for #1 (site navbar):**
- **Brand/logo**: `base.html:74-77` — plain `<a>` with text
- **Desktop nav links**: `base.html:80-92``<ul>` with 45 items, manual `aria-current` routing
- **Cart icon + badge**: `base.html:96-109` — hand-rolled SVG cart icon + an Alpine `x-data` badge that reads `document.cookie` directly
- **Settings dropdown**: `base.html:110-162` — gear-icon trigger + language-switcher `<form>` + theme tristate (system/light/dark)
- **Mobile hamburger**: `base.html:164-172` — hamburger SVG button
- **Mobile menu panel**: `base.html:175-190` — dropdown `<ul>` with duplicated nav links
**Penguin navbar variants:** `default-navbar.html`, `with-call-to-action.html`, `with-search.html`, `with-user-profile.html`
--- ---
@@ -546,7 +552,7 @@ Both are wrapped in a single card-style `<form>`.
| # | Component | Penguin match | Est. effort | Lines saved | | # | Component | Penguin match | Est. effort | Lines saved |
|---|-----------|---------------|-------------|-------------| |---|-----------|---------------|-------------|-------------|
| 1 | Navbar | `navbar/default-navbar.html` | Large | ~143 | | ~~1~~ | ~~Navbar~~ | ✅ DONE — `navbar/default-navbar.html` (link treatment + animated hamburger; `ui::nav_link`) | Large | ~143 |
### Phase 3 — LOW (mostly already Penguin, or no good match) ### Phase 3 — LOW (mostly already Penguin, or no good match)
@@ -563,11 +569,12 @@ Both are wrapped in a single card-style `<form>`.
| 26 | Shipping Settings Row | Already fully uses Penguin macros | | 26 | Shipping Settings Row | Already fully uses Penguin macros |
| 27 | Form Wrappers | Already fully uses Penguin macros | | 27 | Form Wrappers | Already fully uses Penguin macros |
### Already DONE (16 of 27) ### Already DONE (17 of 27)
| # | Component | | # | Component |
|---|-----------| |---|-----------|
| 0 | Toast | | 0 | Toast |
| 1 | Navbar |
| 2 | Sidebar (Admin) | | 2 | Sidebar (Admin) |
| 3 | Sidebar (Category Accordion) | | 3 | Sidebar (Category Accordion) |
| 4 | Dropdown (Settings) | | 4 | Dropdown (Settings) |
@@ -584,9 +591,9 @@ Both are wrapped in a single card-style `<form>`.
| 16 | Badge / Status Pill | | 16 | Badge / Status Pill |
| 17 | Buttons | | 17 | Buttons |
**Remaining real port: just #1 Navbar (~143 lines, Large).** #5 Combobox is a **No real ports remain.** #5 Combobox is a conscious WON'T-PORT (Alpine Focus
conscious WON'T-PORT (Alpine Focus plugin dependency). The Phase-3 items are plugin dependency). All Phase-3 items (#1827) are already internally
already internally Penguin-adapted or have no applicable component. Penguin-adapted or have no applicable component — leave as-is.
--- ---
@@ -595,7 +602,7 @@ already internally Penguin-adapted or have no applicable component.
| # | Component | Penguin UI Directory | Status | Lines | | # | Component | Penguin UI Directory | Status | Lines |
|---|-----------|---------------------|--------|-------| |---|-----------|---------------------|--------|-------|
| 0 | Toast | `toast-notification/` | ✅ DONE | — | | 0 | Toast | `toast-notification/` | ✅ DONE | — |
| 1 | Navbar | `navbar/` | PENDING (MED) | ~143 | | 1 | Navbar | `navbar/` | ✅ DONE | ~143 |
| 2 | Sidebar (admin) | `sidebar/` | ✅ DONE | ~46 | | 2 | Sidebar (admin) | `sidebar/` | ✅ DONE | ~46 |
| 3 | Sidebar (category accordion) | `sidebar/` | ✅ DONE | ~62 | | 3 | Sidebar (category accordion) | `sidebar/` | ✅ DONE | ~62 |
| 4 | Dropdown (settings) | `dropdowns/` | ✅ DONE | ~103 | | 4 | Dropdown (settings) | `dropdowns/` | ✅ DONE | ~103 |
@@ -623,7 +630,7 @@ already internally Penguin-adapted or have no applicable component.
| 26 | Shipping settings row | `card/` | LOW | ~21 | | 26 | Shipping settings row | `card/` | LOW | ~21 |
| 27 | Form wrappers | `card/` | LOW | ~150 | | 27 | Form wrappers | `card/` | LOW | ~150 |
**Status: 16 of 27 components fully ported to Penguin UI. Only 1 real port **Status: 17 of 27 components fully ported to Penguin UI. No real ports remain.
remains — #1 Navbar (MED). #5 Combobox is a conscious WON'T-PORT (Alpine Focus #5 Combobox is a conscious WON'T-PORT (Alpine Focus plugin dependency). The
plugin dependency). The remaining Phase-3 items are already internally remaining Phase-3 items (#1827) are already internally Penguin-adapted or have
Penguin-adapted or have no applicable match.** no applicable match — the migration is effectively complete.**