23 KiB
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
When a Penguin UI component can replace a handcoded one, we vendor its source and then use it (instead of hand-rolling):
- Copy the component's source byte-for-byte from the Penguin UI repo
into
assets/views/penguinui/, mirroring the upstream repo hierarchy (e.g.toast-notification/stacking-toast-notification.html). This directory is reserved exclusively for vendored Penguin UI components and is kept an exact, unmodified mirror of upstream — demo triggers, bugs and all. It's a reference, not the rendered markup. - Adapt it where it's actually used (strip docs-only demo triggers, fix obvious upstream bugs, wire data bindings). Note the deviations in a comment next to the adapted copy.
- Rebuild Tailwind (
make css) so any new utility classes get compiled. - Mark the section below as ✅ DONE.
0. Toast — ✅ DONE
Penguin UI: toast-notification/stacking-toast-notification.html
- Exact upstream mirror at
assets/views/penguinui/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'snotifyevent ({ variant: 'success', message }), so existing callsites (shop/show.html,shop/_card.html) keep working unchanged.
1. Navbar
Penguin UI: navbar/
| # | Location | What it is | Size |
|---|---|---|---|
| 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 |
| 2 | assets/views/admin/base.html:102-114 |
Admin top bar: hamburger toggle + breadcrumb text | ~13 lines |
Details for #1 (site navbar):
- Brand/logo:
base.html:74-77— plain<a>with text - Desktop nav links:
base.html:80-92—<ul>with 4–5 items, manualaria-currentrouting - Cart icon + badge:
base.html:96-109— hand-rolled SVG cart icon + an Alpinex-databadge that readsdocument.cookiedirectly - 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
2. Sidebar (Admin) — ✅ DONE
Penguin UI: sidebar/simple-sidebar.html
- Exact upstream mirror at
assets/views/penguinui/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 ourdata-nav/aria-currentsomarkActiveNav()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
assets/views/penguinui/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 oldpadding-left:28px+↳. Kept our htmx partial, data-drivencategory_groups, auto-expandx-init, anddata-nav/markActiveNav()active routing. - Deviations: group row keeps our link + chevron-toggle split (categories are
navigable, not just expandable); uses
x-show/x-transitioninstead of upstream'sx-collapse(that Alpine plugin isn't bundled in our build). - The
<aside>drawer + mobile overlay (#6) inbase.htmlare layout scaffolding, kept as-is.
4. Dropdown (Settings) — ✅ DONE
Penguin UI: dropdowns/dropdown-with-click.html
- Exact upstream mirror at
assets/views/penguinui/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 bothbase.htmlandadmin/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/$focusneed the Alpine Focus plugin we don't bundle); item hover usesbg-primary/5for consistency with the rest of the UI.
5. Country / Phone Combobox
Penguin UI: text-input/ + custom dropdown list
| # | 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-datawithprefix,prefixOpen,optsarray of{ v, l }(9 country codes) - Manual
filteredcomputed property - Inline chevron SVG that rotates via
:class="prefixOpen && 'rotate-180'" - Dropdown list with
<template x-for>and@clickselection
Details for #10:
- Same pattern as #9 but with translate-able country names (6 countries)
- Includes
+421prefix shortcut
6. Product Card — ✅ DONE
Penguin UI: card/ecommerce-product-card.html
- Exact upstream mirror at
assets/views/penguinui/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 + htmxhx-postadd-to-cart +toast(). Demo-only rating stars, hardcoded content andmax-w-sm(fights the shop grid) were dropped; whole card links to the product page; out-of-stock badge kept.
7. Product Image Gallery
Penguin UI: carousel/ (3 variants)
| # | Location | What it is | Size |
|---|---|---|---|
| 12 | assets/views/shop/show.html:8-26 |
Image gallery with main image + thumbnail strip, Alpine x-data="{ active: 0 }" |
~19 lines |
Details:
- Main image:
x-show="active === {{ loop.index0 }}"withobject-cover - Thumbnail buttons: border changes to indicate active state
- No transition/animation between images — just x-show toggling
8. Radio-Button Groups
Penguin UI: radio (part of form inputs)
| # | Location | What it is | Size |
|---|---|---|---|
| 13 | assets/views/shop/checkout.html:133-165 |
Carrier selection radio group (each option shows name + price) | ~33 lines |
| 14 | assets/views/shop/checkout.html:167-180 |
Payment method radio group (COD + bank transfer) | ~14 lines |
Details for #13:
{% for m in shipping_methods %}loop- Each
<label>is a styled card withhas-[:checked]:border-primaryborder highlight - Radio input triggers
@changeto update Alpine state (carrier, carrierPrice, requiresPoint) - Pickup-point sub-panel shown via
x-show="requiresPoint"
Details for #14:
- Two hardcoded radio options: COD and bank_transfer
x-model="paymentMethod"binding
9. Checkbox — ✅ DONE
Penguin UI: checkbox/default-checkbox.html
- Exact upstream mirror at
assets/views/penguinui/checkbox/default-checkbox.html(reference only) ui::checkbox(name, label, id, value="on", checked, attrs)macro inmacros/ui.html(full Penguin control: custom box + check-icon + label,has-checked:/peervariants).- Adopted: product/category "Published" + shipping "Enabled".
10. Text Input — ✅ DONE
Penguin UI: text-input/default-text-input.html
- Exact upstream mirror at
assets/views/penguinui/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
@changehandler 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(waspx-3) and bg isbg-surface-alt(wasbg-surface) — the real Penguin look.
11. Textarea — ✅ DONE
Penguin UI: text-area/default-textarea.html
- Exact upstream mirror at
assets/views/penguinui/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
assets/views/penguinui/select/default-select.html(reference only) - Adopted inline (3 sites: product category, category parent, order status) — Penguin
appearance-noneselect onbg-surface-altwrapped inrelativewith 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
assets/views/penguinui/file-input/default-file-input.html(reference only) ui::file_input(name, id, accept, attrs, extra)macro (verbatim Penguinfile:styling).- Adopted: product & category image upload.
14. Table
Penguin UI: table/ (7 variants)
| # | Location | What it is | Size |
|---|---|---|---|
| 34 | assets/views/admin/orders/index.html:11-36 |
Orders table: number, customer, status pill, total, "View" link | ~26 lines |
| 35 | assets/views/admin/orders/show.html:20-44 |
Order items table: product, quantity, line total + tfoot summary | ~25 lines |
| 36 | assets/views/admin/catalog/products.html:20-70 |
Products table: image+name+category, price, stock, status pill, edit/view/delete actions | ~51 lines |
| 37 | assets/views/admin/catalog/categories.html:20-59 |
Categories table: tree-indented name, product count, status pill, edit/delete | ~40 lines |
| 38 | assets/views/shop/_cart_body.html:6-59 |
Cart table: product link, price, quantity input, line total, remove button + tfoot total | ~54 lines |
Pattern: Every table uses the same class structure:
<table class="w-full text-left text-sm">
<thead class="border-b border-outline bg-surface-alt text-xs uppercase tracking-wide text-on-surface/70">
<tbody class="divide-y divide-outline">
<tr class="hover:bg-surface-alt">
This is copy-pasted 5 times.
15. Alert / Error Banner — ✅ DONE
Penguin UI: alert/default-alert.html
- Exact upstream mirror at
assets/views/penguinui/alert/default-alert.html(reference only) - Adapted into the
ui::alert_danger(message, extra="")macro inassets/views/macros/ui.html(compact one-line danger alert + danger icon). - Adopted at both sites:
admin/login.html(login error) andadmin/orders/show.html(ship error).
16. Badge / Status Pill — ✅ DONE
Penguin UI: badge/soft-color-badge.html
- Exact upstream mirror at
assets/views/penguinui/badge/soft-color-badge.html(reference only) - Adapted into the
ui::badge(label, variant)macro inassets/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
assets/views/penguinui/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")andui::icon_button(icon, variant="ghost-secondary", aria_label, attrs, …). The per-variant class strings are the verbatim Penguin variants (solidprimary|secondary|danger|success|warning|info,outline-*,ghost-*) — onlyinline-flex items-center justify-center gap-2is added so<a>/w-full/iconrender, and upstream'stext-onDanger/text-onSuccess… token typos are fixed to our realtext-on-*tokens.href→<a>else<button>;attrsis raw (htmx /:disabled/ name / value);iconis a raw<svg>rendered before the label (Penguin button-with-icon). - Sizes are NOT normalized:
sizedefaults to Penguin'spx-4 py-2 text-smbut each call site that differed keeps it (px-3 py-2form-header cancels & order back,px-5 py-2add-to-cart / cart-checkout / order-confirmed continue,px-6 py-2.5checkout place-order,px-3 py-1.5 text-xstable 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 (:disabledviaattrs), 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 (livex-initbadge) 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 toicon_button. - Table row-actions (
edit/view/delete/View/label) →ui::buttonoutline-secondary/outline-dangeratsize="px-3 py-1.5 text-xs"; cart "Remove" →ghost-danger; card add-to-cart →ui::buttonwith the carticon. - 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 callsui::must also{% import "macros/ui.html" as ui %}itself (seeshop/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
Penguin UI: toggle/ (2 variants)
| # | 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/writeslocalStoragematchMedia('prefers-color-scheme: dark')listener- All hand-written vanilla JS, duplicated twice (36 lines total)
19. Inline SVG Icons
Penguin UI: none (Penguin uses Heroicons-equivalent inline SVGs)
| # | Location | Icon | Occurrences |
|---|---|---|---|
| 55 | base.html:70-72,168-170 |
Hamburger (3-line menu) | 2 |
| 56 | base.html:104-105 |
Shopping cart | 1 |
| 57 | base.html:116-121 |
Gear/cog (settings) | 1 |
| 58 | base.html:220-221 |
Checkmark (toast success) | ✅ removed — now in vendored toast component |
| 59 | checkout.html:62-64,115-117 |
Chevron-down (dropdown arrow) | 2 |
| 60 | _sidebar.html:30-33 |
Chevron-right (accordion expand) | 1 |
| 61 | admin/base.html:106-108 |
Hamburger (admin sidebar toggle) | 1 |
| 62 | admin/base.html:121-125 |
Gear/cog (admin settings) | 1 |
All are raw inline <svg> with hardcoded <path d="..."> — no icon library, no partials.
20. Empty State
Penguin UI: no direct component, but table empty states exist in Penguin tables
| # | 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
Penguin UI: card/
| # | 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
Penguin UI: card/ (ecommerce-summary style)
| # | 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
Penguin UI: card/
| # | 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
Penguin UI: card/
| # | 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
Penguin UI: card/
| # | 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
Penguin UI: card/
| # | 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
Penguin UI: card/
| # | 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>.
Summary
| # | Component | Penguin UI Directory | Handcoded Instances | Total Lines |
|---|---|---|---|---|
| 1 | Navbar | navbar/ |
2 | ~143 |
| 2 | Sidebar (admin) | sidebar/ |
2 | ~46 |
| 3 | Sidebar (category accordion) | sidebar/ |
2 | ~62 |
| 4 | Dropdown (settings) | dropdown-menu/ |
2 duplicates | ~103 |
| 5 | Country/Phone combobox | text-input/ |
2 | ~51 |
| 6 | Product card | card/ |
1 | ~30 |
| 7 | Image gallery | carousel/ |
1 | ~19 |
| 8 | Radio groups | (form inputs) | 2 | ~47 |
| 9 | Checkbox | checkbox/ |
3 | ~15 |
| 10 | Text input | text-input/ |
8 | ~146 |
| 11 | Textarea | textarea/ |
3 | ~10 |
| 12 | Select | select/ |
3 | ~23 |
| 13 | File input | file-input/ |
2 | ~12 |
| 14 | Table | table/ |
5 | ~196 |
| 15 | Alert/Error | alert/ |
2 | ~9 |
| 16 | Badge/Pill | badge/ |
6 | ~17 |
| 17 | Button | buttons/ |
50+ occurrences | ~200+ |
| 18 | Toggle (theme) | toggle/ |
2 duplicates | ~36 |
| 19 | Inline SVG icons | N/A | 8 distinct icons | ~50 |
| 20 | Empty state | (table variants) | 5 | ~22 |
| 21 | Dashboard cards | card/ |
1 | ~16 |
| 22 | Checkout summary | card/ |
1 | ~29 |
| 23 | Login card | card/ |
1 | ~56 |
| 24 | Checkout fieldsets | card/ |
4 | ~142 |
| 25 | Order info panels | card/ |
3 | ~64 |
| 26 | Shipping settings row | card/ |
1 | ~21 |
| 27 | Form wrappers | card/ |
2 | ~150 |
Grand total: ~27 distinct handcoded UI component types across ~80 instances, representing approximately 1,600+ lines of handcoded HTML/Tailwind/Alpine that could be replaced by Penguin UI components.
Duplication hotspots:
- Settings dropdown (
base.html:110-162andadmin/base.html:117-166) — 100% copy-paste - Theme toggle JS (
base.html:13-30andadmin/base.html:13-30) — 100% copy-paste - Text input class string — same 80-character Tailwind string appears 15+ times
- Table class strings (thead, tbody, tr) — copy-pasted 5 times
- Button variants — inconsistent
hover:opacity-75vshover:opacity-90