Files
kompress_eshop/hardcoded-inventory.md
2026-06-18 10:51:07 +02:00

496 lines
23 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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
**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 45 items, manual `aria-current` routing
- **Cart icon + badge**: `base.html:96-109` — hand-rolled SVG cart icon + an Alpine `x-data` badge that reads `document.cookie` directly
- **Settings dropdown**: `base.html:110-162` — gear-icon trigger + language-switcher `<form>` + theme tristate (system/light/dark)
- **Mobile hamburger**: `base.html:164-172` — hamburger SVG button
- **Mobile menu panel**: `base.html:175-190` — dropdown `<ul>` with duplicated nav links
**Penguin navbar variants:** `default-navbar.html`, `with-call-to-action.html`, `with-search.html`, `with-user-profile.html`
---
## 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
**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-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
**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 }}"` with `object-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 with `has-[:checked]:border-primary` border highlight
- Radio input triggers `@change` to 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 `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
**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 `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
**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/writes `localStorage`
- `matchMedia('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-162` and `admin/base.html:117-166`) — 100% copy-paste
- **Theme toggle JS** (`base.html:13-30` and `admin/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-75` vs `hover:opacity-90`