Files
kompress_eshop/hardcoded-inventory.md
Priec 7af0a48e92
Some checks failed
CI / Check Style (push) Has been cancelled
CI / Run Clippy (push) Has been cancelled
CI / Run Tests (push) Has been cancelled
last penguin ui port
2026-06-18 16:42:51 +02:00

32 KiB
Raw Blame History

Handcoded UI Components — Penguin UI Replacement Index

Scope: Every handcoded UI component. Each item maps to a Penguin UI 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.

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-labels.

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.htmlshop/_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 (#1827) 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 (#1827) are already internally Penguin-adapted or have no applicable match — the migration is effectively complete.