21 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)
Penguin UI: sidebar/ (6 variants)
| # | Location | What it is | Size |
|---|---|---|---|
| 3 | assets/views/admin/base.html:56-98 |
Admin fixed sidebar: 5 nav links (aria-current), bottom section (exit + logout button) |
~43 lines |
| 4 | assets/views/admin/base.html:51-53 |
Dark overlay behind sidebar on mobile (x-show, x-transition.opacity) |
~3 lines |
Details for #3:
- 60-column fixed left rail with CSS translate-X show/hide on mobile
- Nav links: Dashboard, Products, Categories, Orders, Shipping
- Each link has
data-nav+ manualaria-currentlogic - Bottom section: "Exit to shop" link + logout
<form>with danger button - Inline chevron SVG for sidebar toggle (hamburger icon at
admin/base.html:106-108)
3. Sidebar (Category Accordion)
Penguin UI: sidebar/
| # | Location | What it is | Size |
|---|---|---|---|
| 5 | assets/views/shop/_sidebar.html:1-60 |
Persistent category sidebar/accordion with expandable groups, tree indentation, and active-state routing | ~60 lines |
| 6 | assets/views/base.html:195-196 |
Dark overlay behind category drawer on mobile | ~2 lines |
Details for #5:
- "All Products" link at top
- Expandable parent categories (chevron rotates via Alpine
x-data="{ open: false }") - Child categories indented with
padding-left: 28px+↳arrow - Every link has
data-navfor the client-sidemarkActiveNav()function - Empty-state fallback paragraph
- The
<aside>container inbase.html:202-206useshx-preserveandhx-getto load this partial
4. Dropdown (Settings)
Penguin UI: dropdown-menu/ (dropdown-with-click.html, dropdown-with-icons.html)
| # | Location | What it is | Size |
|---|---|---|---|
| 7 | assets/views/base.html:110-162 |
Language + theme settings dropdown (gear icon trigger) | ~53 lines |
| 8 | assets/views/admin/base.html:117-166 |
100% duplicate of the same dropdown | ~50 lines |
Details:
- Both dropdowns are identical (copy-paste), totaling ~103 lines of duplicated code
- Alpine
x-data="{ open: false }"+@click.outside - Language switcher: English / Slovenčina buttons in a
<form method="post" action="/lang"> - Theme tristate: system / light / dark with
setTheme()andcurrentTheme()from inline JS - Gear cog inline SVG icon
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
Penguin UI: checkbox/ (3 variants)
| # | Location | What it is | Size |
|---|---|---|---|
| 15 | assets/views/admin/catalog/product_form.html:85-89 |
"Published" checkbox | ~5 lines |
| 16 | assets/views/admin/catalog/category_form.html:67-71 |
"Published" checkbox | ~5 lines |
| 17 | assets/views/admin/shipping/index.html:25-29 |
"Enabled" checkbox | ~5 lines |
10. Text Input
Penguin UI: text-input/ (8 variants)
| # | Location | What it is | Size |
|---|---|---|---|
| 18 | assets/views/shop/checkout.html:37-44 |
Email + name text inputs | ~8 lines |
| 19 | assets/views/shop/checkout.html:84-99 |
Address, city, ZIP text inputs | ~16 lines |
| 20 | assets/views/admin/login.html:34-51 |
Email + password inputs (with focus ring styles) | ~18 lines |
| 21 | assets/views/admin/catalog/product_form.html:19-68 |
Name, price, currency, stock, SKU, slug inputs + textarea | ~50 lines |
| 22 | assets/views/admin/catalog/category_form.html:19-55 |
Name, slug, position inputs + textarea | ~37 lines |
| 23 | assets/views/shop/show.html:46-48 |
Quantity number input | ~3 lines |
| 24 | assets/views/shop/_cart_body.html:30-38 |
Quantity number input with @change confirmation dialog |
~9 lines |
| 25 | assets/views/admin/shipping/index.html:20-24 |
Price text input | ~5 lines |
Pattern: Every input is hand-styled with:
w-full rounded-radius border border-outline bg-surface px-3 py-2 text-sm text-on-surface
focus:outline-2 focus:outline-primary
dark:border-outline-dark dark:bg-surface-dark dark:text-on-surface-dark
This exact class string appears 15+ times across the codebase.
11. Textarea
Penguin UI: textarea/
| # | Location | What it is | Size |
|---|---|---|---|
| 26 | assets/views/shop/checkout.html:183-186 |
Order note textarea | ~4 lines |
| 27 | assets/views/admin/catalog/product_form.html:71-73 |
Product description textarea | ~3 lines |
| 28 | assets/views/admin/catalog/category_form.html:53-55 |
Category description textarea | ~3 lines |
12. Select/Dropdown (Native)
Penguin UI: select/ (7 variants)
| # | Location | What it is | Size |
|---|---|---|---|
| 29 | assets/views/admin/catalog/product_form.html:53-60 |
Category select | ~8 lines |
| 30 | assets/views/admin/catalog/category_form.html:40-49 |
Parent category select (tree indented with — ) |
~10 lines |
| 31 | assets/views/admin/orders/show.html:108-112 |
Order status select | ~5 lines |
13. File Input
Penguin UI: file-input/
| # | Location | What it is | Size |
|---|---|---|---|
| 32 | assets/views/admin/catalog/product_form.html:78-82 |
Product image upload | ~5 lines |
| 33 | assets/views/admin/catalog/category_form.html:58-64 |
Category image upload | ~7 lines |
Both use the same Tailwind file:mr-3 file:... prefix pattern for styling.
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
Penguin UI: alert/ (6 variants)
| # | Location | What it is | Size |
|---|---|---|---|
| 39 | assets/views/admin/login.html:25-30 |
Login error alert (role="alert", danger border) |
~6 lines |
| 40 | assets/views/admin/orders/show.html:13-15 |
Ship error alert (danger border) | ~3 lines |
Pattern: rounded-radius border border-danger/40 bg-danger/10 px-3 py-2 text-sm text-danger
16. Badge / Status Pill
Penguin UI: badge/ (7 variants)
| # | Location | What it is | Size |
|---|---|---|---|
| 41 | assets/views/base.html:107-108 |
Cart item-count badge (absolute-positioned, number) | ~2 lines |
| 42 | assets/views/admin/login.html:14-16 |
"Auth" badge on login card header | ~3 lines |
| 43 | assets/views/admin/orders/index.html:27 |
Order status inline pill | ~1 line |
| 44 | assets/views/admin/catalog/products.html:49-53 |
Published/Draft status pill | ~5 lines |
| 45 | assets/views/admin/catalog/categories.html:40-44 |
Published/Draft status pill | ~5 lines |
| 46 | assets/views/shop/_card.html:27 |
"Out of stock" badge (danger background) | ~1 line |
17. Buttons
Penguin UI: buttons/ (6 variants)
| # | Location | What it is | Style | Count |
|---|---|---|---|---|
| 47 | Across all templates | Primary button | bg-primary text-on-primary rounded-radius px-4 py-2 |
15+ |
| 48 | Across all templates | Outline/secondary button | border border-outline text-on-surface rounded-radius px-3 py-1.5 |
20+ |
| 49 | base.html:86-88, admin/base.html:92-95 |
Danger button (logout, delete) | text-danger with hover bg |
4 |
| 50 | base.html:84 |
Warning-colored button (admin link) | text-warning |
1 |
| 51 | base.html:112-113, admin/base.html:118-119, ... |
Icon-only ghost button (gear, hamburger, chevron) | size-9 inline-flex items-center justify-center |
10+ |
| 52 | _cart_body.html:46 |
Text/link button (Remove) | text-danger hover:underline |
1 |
Summary of button variants handcoded:
bg-primary(solid primary)border border-outline(outline)file:bg-primary(file input button)text-danger/text-warning(semantic)hover:opacity-75vshover:opacity-90— inconsistent hover effectstracking-wideon some, not others
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