# Handcoded UI Components — Penguin UI Replacement Index

> **Scope**: Every handcoded UI component.
> Each item maps to a [Penguin UI](https://github.com/SalarHoushvand/penguinui-components/tree/main) component that duplicates the same purpose with fewer lines and better accessibility.

## Vendoring convention

The full library is now vendored locally at repo-root
`penguinui-components/` (177 component `.html` files, moved there
2026-06-18). Read components from there — **NO** network/curl/WebFetch
needed anymore.

### HARD RULE — read-only, never edit

`penguinui-components/` is a read-only third-party library, **NOT our
code**. Never edit, never `{% include %}`, never adapt in place. It is
reference only; **copy markup OUT** of it and adapt at the use-site.

When a Penguin UI component can replace a handcoded one:

1. Find the component in the local `penguinui-components/` directory.
2. Copy its markup **out** into the appropriate `assets/views/` location,
   adapting it where used (strip docs-only demo triggers, fix obvious
   upstream bugs, wire data bindings, map Penguin classes to our design
   tokens). Note the deviations in a comment next to the adapted copy.
3. Keep the original `penguinui-components/` file **untouched** — it stays
   as a byte-for-byte reference snapshot.
4. Rebuild Tailwind (`make css`) so any new utility classes get compiled.
5. Mark the section below as ✅ **DONE**.

### Why it's at repo root + the build guard

Moved OUT of `assets/views/` to repo root because Tailwind v4 auto-detects
sources from the project root — so `assets/css/app.css` carries
`@source not "../../penguinui-components";` to explicitly exclude it
from the build. If the build ever balloons, check that exclusion is intact.

---

## 0. Toast — ✅ DONE

**Penguin UI: `toast-notification/stacking-toast-notification.html`**

- Exact upstream mirror at `penguinui-components/toast-notification/stacking-toast-notification.html` (reference only)
- Adapted/rendered copy lives inline in `assets/views/base.html` (demo triggers
  removed; the upstream dismiss-button `<svg>` quote bugs fixed)
- The global `toast('message')` JS helper now dispatches the component's
  `notify` event (`{ variant: 'success', message }`), so existing callsites
  (`shop/show.html`, `shop/_card.html`) keep working unchanged.

---

## 1. Navbar — ✅ DONE
**Penguin UI: `navbar/default-navbar.html`**

- Exact upstream mirror at `penguinui-components/navbar/default-navbar.html` (reference only)
- **Link treatment** adopted from Penguin (matches the already-ported sidebars): the
  desktop nav links lost the pill-hover (`hover:bg-surface-alt` + `px-3 py-1.5`) for
  Penguin's text-only `underline-offset-2 hover:text-primary focus-visible:underline
  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 |
|---|----------|------------|
| 1 | `assets/views/base.html` | Full site navbar (brand, links, cart badge, settings, mobile menu) |
| 2 | `assets/views/admin/base.html` | Admin top bar: animated hamburger + breadcrumb + settings |

---

## 2. Sidebar (Admin) — ✅ DONE
**Penguin UI: `sidebar/simple-sidebar.html`**

- Exact upstream mirror at `penguinui-components/sidebar/simple-sidebar.html` (reference only)
- Adapted at use-site in `assets/views/admin/base.html`: the nav links + bottom
  exit/logout now use Penguin's link treatment (`hover:bg-primary/5`,
  `underline-offset-2 focus-visible:underline focus:outline-hidden`) and the
  subtle active state (`bg-primary/10` + `text-on-surface-strong`) mapped onto
  our `data-nav`/`aria-current` so `markActiveNav()` still drives it.
- The fixed-rail translate-X show/hide mechanics + mobile overlay (#4) are layout
  scaffolding, kept as-is. Icons were intentionally not added (no verified icon
  set yet) — possible follow-up.

---

## 3. Sidebar (Category Accordion) — ✅ DONE
**Penguin UI: `sidebar/sidebar-with-collapsible-menus.html`**

- Exact upstream mirror at `penguinui-components/sidebar/sidebar-with-collapsible-menus.html` (reference only)
- Adapted at use-site in `assets/views/shop/_sidebar.html`: Penguin link treatment +
  active state + chevron-down rotation (`rotate-180`); child items now sit in a
  bordered/indented list instead of the old `padding-left:28px` + `↳`. Kept our
  htmx partial, data-driven `category_groups`, auto-expand `x-init`, and
  `data-nav`/`markActiveNav()` active routing.
- Deviations: group row keeps our link + chevron-toggle split (categories are
  navigable, not just expandable); uses `x-show`/`x-transition` instead of
  upstream's `x-collapse` (that Alpine plugin isn't bundled in our build).
- The `<aside>` drawer + mobile overlay (#6) in `base.html` are layout
  scaffolding, kept as-is.

---

## 4. Dropdown (Settings) — ✅ DONE
**Penguin UI: `dropdowns/dropdown-with-click.html`**

- Exact upstream mirror at `penguinui-components/dropdowns/dropdown-with-click.html` (reference only)
- **De-duplicated**: the ~103-line copy-paste is now one shared partial
  `assets/views/partials/settings_dropdown.html`, included by both `base.html`
  and `admin/base.html` (each host keeps its own positioning wrapper
  `<div x-data="{ open:false }" class="relative [ml-auto]">`).
- Adopts Penguin's dropdown menu container + item treatment. Deviations: kept our
  gear icon-only trigger and core-Alpine open/@click.outside toggle (upstream's
  `x-trap`/`$focus` need the Alpine Focus plugin we don't bundle); item hover
  uses `bg-primary/5` for consistency with the rest of the UI.

---

## 5. Country / Phone Combobox — ⛔ WON'T PORT (conscious deviation)
**Penguin UI: `combobox/phone-number-input-with-country-code-dropdown.html`**

> **Decision 2026-06-18:** keep our lightweight hand-rolled comboboxes; do NOT
> port the Penguin one. The Penguin combobox depends on the **Alpine Focus
> plugin** (`x-trap`, `$focus.wrap().next()`) which this build does not bundle
> (same reason the settings dropdown & category accordion deviate), ships a
> 240-country `allOptions` list + `flagcdn.com` remote flag images, and a search
> field — far heavier than our deliberate 9-prefix / 6-country editable inputs.
> Our versions already use the Penguin design tokens (`bg-surface`, `border-outline`,
> `focus:outline-primary`) and emoji flags, so they look on-brand. Net: porting
> would add a JS dependency and external image loads for negative UX value.
> Revisit only if we adopt the Alpine Focus plugin project-wide.

| # | Location | What it is | Size |
|---|----------|------------|------|
| 9 | `assets/views/shop/checkout.html:49-74` | Phone prefix combobox (`+421`, `+420`, …, `+33`) | ~25 lines |
| 10 | `assets/views/shop/checkout.html:102-127` | Country combobox (SK, CZ, AT, DE, PL, HU) | ~26 lines |

**Details for #9:**
- Alpine `x-data` with `prefix`, `prefixOpen`, `opts` array of `{ v, l }` (9 country codes)
- Manual `filtered` computed property
- Inline chevron SVG that rotates via `:class="prefixOpen && 'rotate-180'"`
- Dropdown list with `<template x-for>` and `@click` selection

**Details for #10:**
- Same pattern as #9 but with translate-able country names (6 countries)
- Includes `+421` prefix shortcut

---

## 6. Product Card — ✅ DONE
**Penguin UI: `card/ecommerce-product-card.html`**

- Exact upstream mirror at `penguinui-components/card/ecommerce-product-card.html` (reference only)
- Adapted/rendered copy is `assets/views/shop/_card.html`: `<article>` shell + Penguin
  image/title/price layout and the cart-icon add-to-cart button, wired to our product
  data + i18n + htmx `hx-post` add-to-cart + `toast()`. Demo-only rating stars,
  hardcoded content and `max-w-sm` (fights the shop grid) were dropped; whole card
  links to the product page; out-of-stock badge kept.

---

## 7. Product Image Gallery — ✅ DONE
**Penguin UI: `carousel/default-carousel.html`**

- Exact upstream mirror at `penguinui-components/carousel/default-carousel.html` (reference only)
- Adapted at use-site in `assets/views/shop/show.html`: added Penguin's overlay
  prev/next arrow buttons (`bg-surface/40` rounded, verbatim chevron SVGs) and
  `x-transition.opacity.duration.300ms` fade between images. Added `prev()`/`next()`
  with wraparound to the gallery `x-data`; arrows + transitions only render when
  `images | length > 1`.
- Deviations: kept our **product thumbnail strip** (more useful than carousel
  dot indicators for a product page) and our **0-based `active`** index (Penguin
  uses 1-based `currentSlideIndex`); main images switched to `absolute inset-0`
  so the fade cross-dissolves inside the `aspect-square` frame. New i18n keys
  `gallery-prev`/`gallery-next` (sk + en) for the arrow `aria-label`s.

---

## 8. Radio-Button Groups — ✅ DONE
**Penguin UI: `radio/radio-with-container.html`**

- Exact upstream mirror at `penguinui-components/radio/radio-with-container.html` (reference only)
- New `ui::radio(name, value, id, checked, attrs, extra)` macro in `macros/ui.html`
  emits **only** the Penguin custom radio-dot `<input>` (verbatim `appearance-none`
  + `before:` dot + `checked:bg-primary`). Callers keep their own card-style
  `<label>` wrapper — we kept our `has-[:checked]:border-primary` card highlight,
  which is richer than Penguin's plain `bg-surface-alt` container.
- Adopted at `shop/checkout.html`: both **payment** radios (`ui::radio` with
  `attrs='required x-model="paymentMethod"'`). The **carrier** radio (in the
  `{% for m in shipping_methods %}` loop) keeps the same Penguin dot class **inline**
  because its `@change="carrier='{{ m.code }}'; …"` mixes nested single+double
  quotes that can't pass through a Tera macro arg (same convention as the cart
  qty input). Native `text-primary` radios are gone.

---

## 9. Checkbox — ✅ DONE
**Penguin UI: `checkbox/default-checkbox.html`**

- Exact upstream mirror at `penguinui-components/checkbox/default-checkbox.html` (reference only)
- `ui::checkbox(name, label, id, value="on", checked, attrs)` macro in `macros/ui.html`
  (full Penguin control: custom box + check-icon + label, `has-checked:`/`peer` variants).
- Adopted: product/category "Published" + shipping "Enabled".

## 10. Text Input — ✅ DONE
**Penguin UI: `text-input/default-text-input.html`**

- Exact upstream mirror at `penguinui-components/text-input/default-text-input.html` (reference only)
- `ui::input(name, type, id, value, placeholder, required, autocomplete, attrs, extra, width="w-full")`
  macro — **verbatim** Penguin classes (`bg-surface-alt`, `focus-visible:outline-*`).
  Adopted at every text/email/number/password input: login (2), checkout (email,
  name, phone, address, city, zip), product form (6), category form (3), product
  detail quantity, shipping price (`width="w-28"`).
- The cart-body quantity input keeps its complex `@change` handler **inline** with
  the same Penguin classes (mixed single/double quotes can't pass through a macro arg).
- Note: padding is Penguin's `px-2 py-2` (was `px-3`) and bg is `bg-surface-alt` (was
  `bg-surface`) — the real Penguin look.

## 11. Textarea — ✅ DONE
**Penguin UI: `text-area/default-textarea.html`**

- Exact upstream mirror at `penguinui-components/text-area/default-textarea.html` (reference only)
- `ui::textarea(name, id, value, rows, placeholder, required, attrs, extra)` macro.
- Adopted: checkout note, product & category description.

## 12. Select/Dropdown (Native) — ✅ DONE
**Penguin UI: `select/default-select.html`**

- Exact upstream mirror at `penguinui-components/select/default-select.html` (reference only)
- Adopted inline (3 sites: product category, category parent, order status) — Penguin
  `appearance-none` select on `bg-surface-alt` wrapped in `relative` with the chevron
  SVG. Inline rather than a macro because the `<option>` set is caller-specific.

## 13. File Input — ✅ DONE
**Penguin UI: `file-input/default-file-input.html`**

- Exact upstream mirror at `penguinui-components/file-input/default-file-input.html` (reference only)
- `ui::file_input(name, id, accept, attrs, extra)` macro (verbatim Penguin `file:` styling).
- Adopted: product & category image upload.

---

## 14. Table — ✅ DONE
**Penguin UI: `table/default-table.html`**

- Exact upstream mirror at `penguinui-components/table/default-table.html` (reference only)
- The same wrapper/thead/tbody/row/tfoot class structure was copy-pasted across all
  5 tables (orders index, order detail, products, categories, cart body). Centralized
  into **class-string macros** in `macros/ui.html`: `ui::table_wrap_cls()`,
  `ui::table_cls()`, `ui::thead_cls()`, `ui::tbody_cls()`, `ui::row_cls()` (hover),
  `ui::tfoot_cls()`, plus an element macro `ui::th(label, align="")` for header cells.
- **Why class-string macros, not full row macros:** Tera has no slot/`{% raw %}{% call %}{% endraw %}`
  mechanism, and the cells are heterogeneous (product image+name, htmx quantity input
  with inline Alpine `@change`, badges, action-button forms), so rows stay inline. The
  macros centralize only the drift-prone chrome styling — `class="{{ ui::thead_cls() }}"`.
- **Penguin improvement adopted:** the wrapper now carries `w-full overflow-x-auto`
  (from `default-table.html`) so wide tables scroll horizontally on mobile instead of
  overflowing. Our `text-xs uppercase` thead + `px-4 py-3` cells were kept (deliberate,
  richer than Penguin's `text-sm`/`p-4`).
- Interactive lists (orders/products/categories) use `ui::row_cls()` for the hover
  highlight; non-interactive rows (order items, cart) omit it. `tfoot` (order detail +
  cart totals) uses `ui::tfoot_cls()`.

| # | Location | What it is |
|---|----------|------------|
| 34 | `assets/views/admin/orders/index.html` | Orders table |
| 35 | `assets/views/admin/orders/show.html` | Order items table + tfoot summary |
| 36 | `assets/views/admin/catalog/products.html` | Products table (image+name, status pill, actions) |
| 37 | `assets/views/admin/catalog/categories.html` | Categories table (tree-indented, actions) |
| 38 | `assets/views/shop/_cart_body.html` | Cart table (htmx qty input, remove) + tfoot total |

---

## 15. Alert / Error Banner — ✅ DONE
**Penguin UI: `alert/default-alert.html`**

- Exact upstream mirror at `penguinui-components/alert/default-alert.html` (reference only)
- Adapted into the `ui::alert_danger(message, extra="")` macro in
  `assets/views/macros/ui.html` (compact one-line danger alert + danger icon).
- Adopted at both sites: `admin/login.html` (login error) and
  `admin/orders/show.html` (ship error).

---

## 16. Badge / Status Pill — ✅ DONE
**Penguin UI: `badge/soft-color-badge.html`**

- Exact upstream mirror at `penguinui-components/badge/soft-color-badge.html` (reference only)
- Adapted into the `ui::badge(label, variant)` macro in `assets/views/macros/ui.html`
  (variants: success | danger | warning | info | primary | neutral).
- Adopted at the status-pill sites: "Auth" badge (`admin/login.html`), order status
  (`orders/index.html`, neutral), Published/Draft pills (`products.html` +
  `categories.html`, success/neutral).
- Intentionally left inline (not soft-color pills): the cart item-count **notification**
  badge in `base.html` (count bubble, a different Penguin badge type) and the
  block-style "out of stock" notice in `_card.html`.

---

## 17. Buttons — ✅ DONE
**Penguin UI: `buttons/default-button.html`, `outline-button.html`, `ghost-button.html`, `button-with-icon.html`**

- Exact upstream mirrors at `penguinui-components/buttons/*.html` (reference only).
- Macros in `assets/views/macros/ui.html`:
  `ui::button(label, variant="primary", type, href, attrs, extra, icon, size="px-4 py-2 text-sm")`
  and `ui::icon_button(icon, variant="ghost-secondary", aria_label, attrs, …)`.
  The per-variant class strings are the **verbatim** Penguin variants (solid
  `primary|secondary|danger|success|warning|info`, `outline-*`, `ghost-*`) — only
  `inline-flex items-center justify-center gap-2` is added so `<a>`/`w-full`/`icon`
  render, and upstream's `text-onDanger`/`text-onSuccess`… token typos are fixed to
  our real `text-on-*` tokens. `href` → `<a>` else `<button>`; `attrs` is raw
  (htmx / `:disabled` / name / value); `icon` is a raw `<svg>` rendered before the
  label (Penguin button-with-icon).
- **Sizes are NOT normalized**: `size` defaults to Penguin's `px-4 py-2 text-sm`
  but each call site that differed keeps it (`px-3 py-2` form-header cancels &
  order back, `px-5 py-2` add-to-cart / cart-checkout / order-confirmed continue,
  `px-6 py-2.5` checkout place-order, `px-3 py-1.5 text-xs` table actions).
- Adopted across every standard filled/outline/submit button: login, product &
  category forms (save / cancel = `outline-secondary`), products/categories "new" +
  empty-state CTAs, orders detail (back/ship/status), shipping save, cart
  (continue/checkout/empty), checkout place-order (`:disabled` via `attrs`),
  product detail add-to-cart, order-confirmed continue.
- Icon-only buttons now use `ui::icon_button(icon, variant="ghost-secondary",
  aria_label, attrs, …)` — Penguin ghost treatment, square. Converted: settings
  gear, both hamburgers (site + admin), admin sidebar toggle, mobile category
  toggle. The cart link (live `x-init` badge) and the category-accordion chevron
  keep the same Penguin ghost classes **inline** only because their markup mixes
  single+double quotes that can't be passed through a Tera macro arg — visually
  identical to `icon_button`.
- Table row-actions (`edit`/`view`/`delete`/`View`/`label`) → `ui::button`
  `outline-secondary` / `outline-danger` at `size="px-3 py-1.5 text-xs"`; cart
  "Remove" → `ghost-danger`; card add-to-cart → `ui::button` with the cart `icon`.
- Still genuinely not this component (tracked elsewhere): toast dismiss/Reply
  buttons (part of the vendored toast mirror, already Penguin), settings dropdown
  menu items (Penguin dropdown items), gallery thumbnail buttons (carousel),
  sidebar logout/exit (Penguin sidebar link treatment), and navbar nav-menu
  links/logout (belong to §1 Navbar). The file-input button is §13.

> Gotcha for future macro use: Tera renders `{% include %}` in the **includer's**
> macro scope, so a template that includes a partial which calls `ui::` must also
> `{% import "macros/ui.html" as ui %}` itself (see `shop/cart.html` →
> `shop/_cart_body.html`). In an `{% extends %}` child the import must sit
> directly after `{% extends %}` with no comment/content before it.

---

## 18. Toggle / Switch — LOW PRIORITY (de-duplication, not replacement)
**Penguin UI: `toggle/` (3 variants)**

> **Priority: LOW** | ~36 lines,100% duplicated between `base.html` and `admin/base.html`.
> This is JavaScript theme-switching logic (`applyTheme`, `setTheme`, `matchMedia`),
> not a CSS toggle component. Penguin's `toggle/default-toggle.html` is a visual
> on/off switch — not applicable here.
> **Action:** de-duplicate the JS into a shared partial rather than porting.

| # | Location | What it is | Size |
|---|----------|------------|------|
| 53 | `assets/views/base.html:13-30` | Theme toggle (dark/light/system) — inline `<script>` JavaScript | ~18 lines |
| 54 | `assets/views/admin/base.html:13-30` | **Exact duplicate** of the theme toggle JS | ~18 lines |

**Details:**
- `applyTheme()`, `setTheme()`, `currentTheme()` — reads/writes `localStorage`
- `matchMedia('prefers-color-scheme: dark')` listener
- All hand-written vanilla JS, duplicated twice (36 lines total)

---

## 19. Inline SVG Icons — MOSTLY DONE
**Penguin UI: none (Penguin uses Heroicons-equivalent inline SVGs)**

> **Priority: LOW.** Penguin ships no icon library, so this is dedup, not a port.
> The repeated hamburger / close / cart SVGs are now centralized in the
> `ui::icon(name, size, extra, attrs)` macro (`macros/ui.html`); call sites use
> `{{ ui::icon(name="cart") }}` etc. The chevron dropdown arrows stay inline by
> design — they carry nested-quote Alpine `:class` / `x-bind:class` bindings,
> which Tera macro args can't pass cleanly (see the attrs note atop `ui.html`).

| # | Location | Icon | Status |
|---|----------|------|--------|
| 55 | `base.html` (categories + mobile toggle), `admin/base.html` (sidebar toggle) | Hamburger (3-line menu) | ✅ `ui::icon(name="hamburger")` |
| 56 | `base.html` (mobile toggle), `admin/base.html` (sidebar toggle) | Close (X) | ✅ `ui::icon(name="close")` |
| 57 | `base.html` (cart link) | Shopping cart | ✅ `ui::icon(name="cart")` |
| 58 | ~~`base.html:220-221`~~ | Checkmark (toast success) | ✅ removed — now in vendored toast component |
| 59 | `checkout.html:61,110` | Chevron-down (dropdown arrow) | inline — nested-quote `:class` binding |
| 60 | `_sidebar.html:35` | Chevron-down (accordion expand, rotates) | inline — nested-quote `x-bind:class` binding |
| 61 | `settings_dropdown.html` | Gear/cog (settings) | inline in `ui::icon_button` call (shared partial; single use) |

Remaining inline `<svg>` are the rotating chevrons (kept inline on purpose, above).

---

## 20. Empty State — LOW PRIORITY
**Penguin UI: no dedicated empty-state component**

> **Priority: LOW** | ~22 lines across5 sites.
> These are simple `<div>` messages, often with a CTA button already using
> `ui::button`. Nothing to port — already consistent with project styling.

| # | Location | What it is | Size |
|---|----------|------------|------|
| 63 | `assets/views/admin/orders/index.html:38-39` | "No orders" message | ~2 lines |
| 64 | `assets/views/admin/catalog/products.html:72-78` | "No products" with CTA button | ~7 lines |
| 65 | `assets/views/admin/catalog/categories.html:61-67` | "No categories" with CTA button | ~7 lines |
| 66 | `assets/views/shop/_cart_body.html:67-70` | "Cart empty" with CTA button | ~4 lines |
| 67 | `assets/views/shop/_sidebar.html:58-59` | "No categories" message | ~2 lines |

---

## 21. Dashboard Navigation Cards — LOW PRIORITY
**Penguin UI: `card/default-card.html` or `card/card-with-button.html`**

> **Priority: LOW** | ~16 lines.
> Already uses card styling (`rounded-radius border border-outline hover:border-primary`).
> Penguin's `default-card.html` adds a structured header/body layout — adopt if
> cards ever grow beyond a title+description link.

| # | Location | What it is | Size |
|---|----------|------------|------|
| 68 | `assets/views/admin/index.html:12-27` | 3 dashboard link cards (Products, Categories, Orders) | ~16 lines |

**Details:**
- Each card is an `<a>` styled with border, hover effect, and nested title+description
- Same hover pattern: `hover:border-primary`

---

## 22. Checkout Order Summary — LOW PRIORITY
**Penguin UI: `card/`**

> **Priority: LOW** | ~29 lines.
> Already uses card-like styling with `tabular-nums`. All internal buttons use
> `ui::button`. The only handcoded part is the outer `<div>` wrapper and line-item
> layout. Penguin doesn't have an ecommerce-specific summary component.

| # | Location | What it is | Size |
|---|----------|------------|------|
| 69 | `assets/views/shop/checkout.html:190-218` | Cart summary aside: item list, subtotal, shipping, total, place-order button | ~29 lines |

**Details:**
- Item list with name × quantity + line total
- Subtotal + shipping + total with `tabular-nums`
- Dynamic shipping price from Alpine `carrierPrice`
- Disabled submit button when `!canSubmit`

---

## 23. Login Card — LOW PRIORITY
**Penguin UI: `card/default-card.html`**

> **Priority: LOW** | ~56 lines.
> Already fully uses Penguin macros inside: `ui::input`, `ui::button`,
> `ui::badge`, `ui::alert_danger`. Only the outer card wrapper (border,
> bg-surface-alt, shadow-sm) is handcoded. Adopting `default-card.html`
> would add visual polish but little functional gain.

| # | Location | What it is | Size |
|---|----------|------------|------|
| 70 | `assets/views/admin/login.html:6-61` | Full login form: header with auth badge, email + password inputs, error alert, submit button | ~56 lines |

---

## 24. Checkout Fieldset Cards — LOW PRIORITY
**Penguin UI: `card/`**

> **Priority: LOW** | ~142 lines across4 fieldsets.
> Already uses card styling (`rounded-radius border border-outline bg-surface p-6`)
> and all internal form controls use Penguin macros (`ui::input`, `ui::textarea`,
> `ui::button`). Only the `<fieldset>` + `<legend>` wrapping is handcoded.
> Low value in replacing — fieldset semantics are correct here.

| # | Location | What it is | Size |
|---|----------|------------|------|
| 71 | `assets/views/shop/checkout.html:34-79` | Contact info fieldset (email, name, phone+prefix) | ~46 lines |
| 72 | `assets/views/shop/checkout.html:82-130` | Shipping address fieldset (address, city, zip, country) | ~49 lines |
| 73 | `assets/views/shop/checkout.html:133-165` | Carrier selection fieldset | ~33 lines |
| 74 | `assets/views/shop/checkout.html:167-180` | Payment method fieldset | ~14 lines |

Each fieldset uses `<fieldset>` + `<legend>` with the same `rounded-radius border border-outline bg-surface p-6` styling.

---

## 25. Order Detail Info Panel — LOW PRIORITY
**Penguin UI: `card/`**

> **Priority: LOW** | ~64 lines across3 panels.
> Already card-styled (`rounded-radius border border-outline bg-surface p-5`)
> with Penguin macros inside. Deviating from this simple structure would
> make the dense info layout harder to scan.

| # | Location | What it is | Size |
|---|----------|------------|------|
| 75 | `assets/views/admin/orders/show.html:49-77` | Customer + shipping + payment info panel | ~29 lines |
| 76 | `assets/views/admin/orders/show.html:79-103` | Fulfillment panel (tracking, label link, ship button) | ~25 lines |
| 77 | `assets/views/admin/orders/show.html:106-115` | Status update form panel | ~10 lines |

---

## 26. Shipping Method Settings Row — LOW PRIORITY
**Penguin UI: `card/`**

> **Priority: LOW** | ~21 lines.
> Already fully uses Penguin macros: `ui::input`, `ui::checkbox`, `ui::button`.
> The card wrapper is the same pattern as other admin panels. Nothing to port.

| # | Location | What it is | Size |
|---|----------|------------|------|
| 78 | `assets/views/admin/shipping/index.html:14-34` | Per-carrier settings: name label, price input, enabled checkbox, save button | ~21 lines |

---

## 27. Product/Category Form Wrapper — LOW PRIORITY
**Penguin UI: `card/`**

> **Priority: LOW** | ~150 lines across2 forms.
> Already fully uses Penguin macros: `ui::input`, `ui::textarea`, `ui::select`,
> `ui::file_input`, `ui::checkbox`, `ui::button`. The `<form>` card wrapper is
> the same border/bg pattern. Penguin doesn't have a form-specific layout component.

| # | Location | What it is | Size |
|---|----------|------------|------|
| 79 | `assets/views/admin/catalog/product_form.html:15-99` | Full product edit/create form with all fields | ~84 lines |
| 80 | `assets/views/admin/catalog/category_form.html:15-81` | Full category edit/create form with all fields | ~66 lines |

Both are wrapped in a single card-style `<form>`.

---

## Porting Roadmap (priority order)

### Phase 1 — HIGH (direct Penguin matches, clear win)

| # | Component | Penguin match | Est. effort | Lines saved |
|---|-----------|---------------|-------------|-------------|
| ~~5~~ | ~~Country/Phone Combobox~~ | ⛔ WON'T PORT — needs Alpine Focus plugin; our lightweight version kept | — | — |
| ~~7~~ | ~~Image Gallery~~ | ✅ DONE — `carousel/default-carousel.html` | Small | ~19 |
| ~~8~~ | ~~Radio Groups~~ | ✅ DONE — `radio/radio-with-container.html` | Small | ~47 |
| ~~14~~ | ~~Table~~ | ✅ DONE — `table/default-table.html` (class-string macros) | Medium | ~196 |

### Phase 2 — MEDIUM (good match, more integration risk)

| # | Component | Penguin match | Est. effort | Lines saved |
|---|-----------|---------------|-------------|-------------|
| ~~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)

| # | Component | Action |
|---|-----------|--------|
| 18 | Toggle/Switch | De-duplicate JS into shared partial (not a Penguin port) |
| 19 | Inline SVG Icons | Optional: extract `ui::icon(name)` macro |
| 20 | Empty State | Already fine — nothing to port |
| 21 | Dashboard Cards | Adopt `card/default-card.html` if cards grow |
| 22 | Checkout Summary | Already fine — nothing to port |
| 23 | Login Card | Already fine — only outer wrapper is handcoded |
| 24 | Checkout Fieldsets | Already fine — only `<fieldset>` wrapper is handcoded |
| 25 | Order Info Panels | Already fine — only card wrappers are handcoded |
| 26 | Shipping Settings Row | Already fully uses Penguin macros |
| 27 | Form Wrappers | Already fully uses Penguin macros |

### Already DONE (17 of 27)

| # | Component |
|---|-----------|
| 0 | Toast |
| 1 | Navbar |
| 2 | Sidebar (Admin) |
| 3 | Sidebar (Category Accordion) |
| 4 | Dropdown (Settings) |
| 6 | Product Card |
| 7 | Image Gallery |
| 8 | Radio Groups |
| 9 | Checkbox |
| 10 | Text Input |
| 11 | Textarea |
| 12 | Select/Dropdown |
| 13 | File Input |
| 14 | Table |
| 15 | Alert / Error Banner |
| 16 | Badge / Status Pill |
| 17 | Buttons |

**No real ports remain.** #5 Combobox is a conscious WON'T-PORT (Alpine Focus
plugin dependency). All Phase-3 items (#18–27) are already internally
Penguin-adapted or have no applicable component — leave as-is.

---

## Summary

| # | Component | Penguin UI Directory | Status | Lines |
|---|-----------|---------------------|--------|-------|
| 0 | Toast | `toast-notification/` | ✅ DONE | — |
| 1 | Navbar | `navbar/` | ✅ DONE | ~143 |
| 2 | Sidebar (admin) | `sidebar/` | ✅ DONE | ~46 |
| 3 | Sidebar (category accordion) | `sidebar/` | ✅ DONE | ~62 |
| 4 | Dropdown (settings) | `dropdowns/` | ✅ DONE | ~103 |
| 5 | Country/Phone combobox | `combobox/` | ⛔ WON'T PORT | ~51 |
| 6 | Product card | `card/` | ✅ DONE | ~30 |
| 7 | Image gallery | `carousel/` | ✅ DONE | ~19 |
| 8 | Radio groups | `radio/` | ✅ DONE | ~47 |
| 9 | Checkbox | `checkbox/` | ✅ DONE | ~15 |
| 10 | Text input | `text-input/` | ✅ DONE | ~146 |
| 11 | Textarea | `text-area/` | ✅ DONE | ~10 |
| 12 | Select | `select/` | ✅ DONE | ~23 |
| 13 | File input | `file-input/` | ✅ DONE | ~12 |
| 14 | Table | `table/` | ✅ DONE | ~196 |
| 15 | Alert/Error | `alert/` | ✅ DONE | ~9 |
| 16 | Badge/Pill | `badge/` | ✅ DONE | ~17 |
| 17 | Button | `buttons/` | ✅ DONE | ~200+ |
| 18 | Toggle (theme) | `toggle/` | LOW (dedup) | ~36 |
| 19 | Inline SVG icons | N/A | LOW | ~50 |
| 20 | Empty state | N/A | LOW | ~22 |
| 21 | Dashboard cards | `card/` | LOW | ~16 |
| 22 | Checkout summary | `card/` | LOW | ~29 |
| 23 | Login card | `card/` | LOW | ~56 |
| 24 | Checkout fieldsets | `card/` | LOW | ~142 |
| 25 | Order info panels | `card/` | LOW | ~64 |
| 26 | Shipping settings row | `card/` | LOW | ~21 |
| 27 | Form wrappers | `card/` | LOW | ~150 |

**Status: 17 of 27 components fully ported to Penguin UI. No real ports remain.
#5 Combobox is a conscious WON'T-PORT (Alpine Focus plugin dependency). The
remaining Phase-3 items (#18–27) are already internally Penguin-adapted or have
no applicable match — the migration is effectively complete.**
