Five missing line-height values caused pixel shifts when Tailwind utilities (text-sm, text-lg, text-xs) were replaced with semantic classes that only set font-size. Also remove phantom padding-bottom on .admin-header (the old pb-4 utility was never defined). Fixes: .admin-header, .admin-header-subtitle, .admin-error, .admin-brand, .admin-text-secondary Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
416 lines
18 KiB
Markdown
416 lines
18 KiB
Markdown
# Plan: CSS migration — Tailwind + DaisyUI to modern hand-written CSS
|
|
|
|
Status: Complete (all phases 0-8). Admin CSS semantic refactor in progress — see PROGRESS.md.
|
|
|
|
## Overview
|
|
|
|
Replace Tailwind CSS v4 and DaisyUI with a modern, layered CSS system using `@layer`, native nesting, container queries, `oklch()`, `@property`, and other 2024+ CSS features. Zero framework dependencies. Smaller output. Full cascade control.
|
|
|
|
## Final state (post Phase 7)
|
|
|
|
| Bundle | Minified | Gzipped | Contents |
|
|
|--------|----------|---------|----------|
|
|
| `shop.css` | 54.1 KB | 9.8 KB | Hand-written component CSS + three-layer theme |
|
|
| `admin.css` | 90.8 KB | 17.8 KB | Utility CSS + themes + heroicon data URIs |
|
|
| `app.js` | 139.2 KB | 42.9 KB | LiveView + hooks |
|
|
|
|
Zero framework dependencies. No Tailwind, no DaisyUI, no PostCSS plugins.
|
|
|
|
Three esbuild profiles handle all asset builds. Three root layouts isolate CSS:
|
|
- `shop_root.html.heex` loads `shop.css`
|
|
- `root.html.heex` loads `admin.css` (auth pages)
|
|
- `admin_root.html.heex` loads both `admin.css` + `shop.css`
|
|
|
|
## Pre-migration state (for reference)
|
|
|
|
| Bundle | Compiled | Contents |
|
|
|--------|----------|----------|
|
|
| `app-shop.css` | **53 KB** | Tailwind v4 (no DaisyUI) + three-layer theme |
|
|
| `app.css` | **212 KB** | Tailwind v4 + DaisyUI + three-layer theme |
|
|
|
|
281 `style=` attributes across 10 files. All removed in Phases 2-4.
|
|
|
|
**Three-layer theme system** (1,130 lines) preserved throughout:
|
|
- Primitives (94 lines): `:root` tokens
|
|
- Attributes (717 lines): `.themed` + `data-*` selectors
|
|
- Semantic (319 lines): aliases, accessibility, component styles
|
|
|
|
**CSSGenerator** (Elixir): generates inline `<style>` from DB settings. Unchanged.
|
|
|
|
## Architecture: CSS layer order
|
|
|
|
```css
|
|
@layer reset, primitives, tokens, components, layout, utilities, overrides;
|
|
```
|
|
|
|
| Layer | Purpose | Source |
|
|
|-------|---------|--------|
|
|
| `reset` | Box-sizing, margin reset, img/list defaults | New ~40 lines |
|
|
| `primitives` | `:root` design tokens (spacing, radii, fonts, type scale) | Existing `theme-primitives.css` |
|
|
| `tokens` | Theme token switching via `data-*` attrs on `.themed` | Existing `theme-layer2-attributes.css` |
|
|
| `components` | All component styles (product cards, cart, gallery, etc.) | Existing semantic.css + extracted inline styles |
|
|
| `layout` | Layout primitives (stack, cluster, row, auto-grid, etc.) | New ~200 lines |
|
|
| `utilities` | Tiny utility set (sr-only, truncate, text-balance) | New ~60 lines |
|
|
| `overrides` | Theme editor `.preview-frame` rules | Existing from `app.css` |
|
|
|
|
`@layer` eliminates all `!important` hacks — later layers beat earlier ones.
|
|
|
|
## Layout primitives (replace Tailwind flex/grid utilities)
|
|
|
|
Seven primitives cover ~80% of Tailwind layout usage:
|
|
|
|
- **`.stack`** — vertical flex with gap (replaces `flex flex-col gap-*`)
|
|
- **`.cluster`** — horizontal flex-wrap with gap (replaces `flex flex-wrap gap-*`)
|
|
- **`.row`** — horizontal flex, no wrap (replaces `flex items-center gap-*`)
|
|
- **`.auto-grid`** — intrinsic responsive grid using `auto-fill` + `minmax()` (replaces `grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-4`)
|
|
- **`.container-page`** — centered max-width (replaces `max-w-7xl mx-auto px-*`)
|
|
- **`.with-sidebar`** — main + aside flex layout (replaces PDP two-column grid)
|
|
- **`.center`** — flex center alignment
|
|
|
|
Modifiers via custom properties: `--stack-gap`, `--grid-gap`, `--auto-grid-min`.
|
|
|
|
## Responsive strategy
|
|
|
|
**Intrinsic design over breakpoints:**
|
|
1. `auto-fill`/`auto-fit` grids eliminate most responsive classes
|
|
2. Container queries (`@container`) for component-level responsiveness
|
|
3. Two retained media queries for genuinely viewport-dependent layout:
|
|
- `@media (width >= 40em)` — mobile to tablet
|
|
- `@media (width >= 64em)` — tablet to desktop
|
|
4. `@media (hover: hover)` for desktop hover effects (already in use)
|
|
5. Fluid typography via `clamp()` (already in use)
|
|
|
|
## Admin component replacements (DaisyUI)
|
|
|
|
~300 lines of `admin-components.css` replacing DaisyUI's 15 used components:
|
|
|
|
| DaisyUI | Replacement |
|
|
|---------|-------------|
|
|
| `drawer` | CSS grid layout + checkbox toggle |
|
|
| `btn` variants | `.admin-btn` + modifiers |
|
|
| `modal` | Native `<dialog>` + `::backdrop` |
|
|
| `table` | Styled `<table>` with `.admin-table` |
|
|
| `input/select/textarea` | `.admin-input` with `:focus-visible` |
|
|
| `checkbox/toggle` | `appearance: none` + custom styling |
|
|
| `range` | `accent-color` on native `<input type=range>` |
|
|
| `alert/toast` | Left-border accent box + fixed-position container |
|
|
| `badge` | Inline pill with `border-radius: 9999px` |
|
|
|
|
## Build pipeline changes
|
|
|
|
**During migration** (Phases 1-5): Tailwind runs alongside new CSS for shop.
|
|
**After shop migration** (Phase 5): Remove `tailwind_shop` watcher.
|
|
**After full migration** (Phase 7): Remove `tailwind` dep entirely.
|
|
|
|
**Production CSS**: esbuild handles `@import` resolution + minification (already available). Lightning CSS added optionally in Phase 8 for vendor prefixing.
|
|
|
|
**Dev**: No CSS build step needed — Phoenix `live_reload` watches `.css` files directly. Modern CSS features (nesting, `@layer`, `@container`) work in all current browsers natively.
|
|
|
|
## Visual regression testing
|
|
|
|
**Tool**: Mix task wrapping Playwright for automated screenshots.
|
|
|
|
**Breakpoints**: 375px (mobile), 768px (tablet), 1024px (laptop), 1440px (desktop)
|
|
|
|
**Pages** (15 states):
|
|
1. Home (`/`)
|
|
2. Collection (`/collections/all`)
|
|
3. Collection with active filters
|
|
4. PDP single image (`/products/:id`)
|
|
5. PDP multi-image gallery
|
|
6. Cart with items (`/cart`)
|
|
7. Cart empty
|
|
8. Cart drawer open
|
|
9. Checkout success
|
|
10. About (`/about`)
|
|
11. Contact (`/contact`)
|
|
12. Delivery (`/delivery`)
|
|
13. Content page (privacy/terms)
|
|
14. Search modal open
|
|
15. 404 error page
|
|
|
|
= 60 screenshots per phase (15 pages x 4 breakpoints)
|
|
|
|
**Process per phase**:
|
|
1. Capture "before" golden screenshots
|
|
2. Complete phase work
|
|
3. Capture "after" screenshots
|
|
4. Visual diff (pixelmatch or manual comparison)
|
|
5. Fix any regressions
|
|
6. Run `mix test` (757 tests)
|
|
7. Commit
|
|
|
|
---
|
|
|
|
## Phase 0: Foundation and testing infrastructure
|
|
|
|
**Estimate**: 1 session (~1.5h) | **Shippable**: yes (no visual changes)
|
|
|
|
Tasks:
|
|
1. Create Playwright screenshot Mix task (`mix screenshots`)
|
|
2. Capture golden screenshots for all 15 pages at 4 breakpoints
|
|
3. Create `assets/css/shop/` directory structure:
|
|
- `reset.css`, `primitives.css`, `tokens.css`, `components.css`, `layout.css`, `utilities.css`, `overrides.css`
|
|
4. Create new entry file `assets/css/shop.css` with `@layer` declaration
|
|
5. Wire nothing yet — just file structure
|
|
|
|
Files created:
|
|
- `assets/css/shop.css` (entry, not wired)
|
|
- `assets/css/shop/reset.css` (skeleton)
|
|
- `assets/css/shop/layout.css` (skeleton)
|
|
- `assets/css/shop/components.css` (skeleton)
|
|
- `assets/css/shop/utilities.css` (skeleton)
|
|
- `assets/css/shop/overrides.css` (skeleton)
|
|
- `lib/mix/tasks/screenshots.ex`
|
|
|
|
Acceptance: golden screenshots captured, `mix test` passes, no visual changes.
|
|
|
|
---
|
|
|
|
## Phase 1: Layout primitives and reset
|
|
|
|
**Estimate**: 1 session (~1.5h) | **Shippable**: yes
|
|
|
|
Tasks:
|
|
1. Write CSS reset in `shop/reset.css` (~40 lines)
|
|
2. Write layout primitives in `shop/layout.css` (~200 lines)
|
|
3. Copy existing theme CSS into `@layer` wrappers in new structure
|
|
4. Add LiveView variants and `[data-phx-session] { display: contents }`
|
|
5. Wire new `shop.css` alongside existing `app-shop.css` (both loaded)
|
|
6. Visual regression — should be no changes (Tailwind still present)
|
|
|
|
Files modified:
|
|
- `assets/css/shop/*.css` (populate reset + layout)
|
|
- `assets/css/app-shop.css` (add import of new shop.css)
|
|
|
|
Acceptance: layout primitives available, no visual changes, all tests pass.
|
|
|
|
---
|
|
|
|
## Phase 2: Inline style extraction — product components
|
|
|
|
**Estimate**: 2 sessions (~3h) | **Shippable**: after each session
|
|
|
|
**2a** — Product cards, grid, badges, hero, categories (~1.5h):
|
|
- Extract 40+ inline styles from `product.ex` into `components.css`
|
|
- Classes: `.product-card-body`, `.product-card-title`, `.product-card-price`, `.hero-section`, `.hero-content`, `.category-card`, `.category-card-overlay`
|
|
- Visual regression: home page, collection page
|
|
|
|
**2b** — PDP, variant selector, gallery, accordion (~1.5h):
|
|
- Extract remaining ~40 inline styles from `product.ex`
|
|
- Classes: `.pdp-layout`, `.variant-selector`, `.color-swatch`, `.size-button`, `.quantity-selector`, `.add-to-cart-btn`, `.accordion-item`
|
|
- Visual regression: PDP page
|
|
|
|
Files modified:
|
|
- `lib/berrypod_web/components/shop_components/product.ex` (83 style= -> 0)
|
|
- `assets/css/shop/components.css`
|
|
|
|
Acceptance: `product.ex` has zero inline styles, visual regression clean.
|
|
|
|
---
|
|
|
|
## Phase 3: Inline style extraction — layout + cart
|
|
|
|
**Estimate**: 2 sessions (~3h) | **Shippable**: after each session
|
|
|
|
**3a** — Layout components (~1.5h):
|
|
- Extract 59 inline styles from `layout.ex`
|
|
- Classes: `.announcement-bar`, `.mobile-bottom-nav`, `.mobile-nav-item`, `.search-modal-overlay`, `.search-modal-panel`, `.search-result-item`, `.shop-footer-section`
|
|
- Visual regression: all pages (header/footer/nav are global)
|
|
|
|
**3b** — Cart components (~1.5h):
|
|
- Extract 51 inline styles from `cart.ex`
|
|
- Classes: `.cart-drawer-header`, `.cart-drawer-body`, `.cart-drawer-footer`, `.cart-item-details`, `.cart-remove-btn`, `.cart-empty-state`, `.checkout-btn`, `.order-summary`
|
|
- Visual regression: cart page, cart drawer
|
|
|
|
Files modified:
|
|
- `lib/berrypod_web/components/shop_components/layout.ex` (59 -> 0)
|
|
- `lib/berrypod_web/components/shop_components/cart.ex` (51 -> 0)
|
|
- `assets/css/shop/components.css`
|
|
|
|
Acceptance: both files zero inline styles, visual regression clean.
|
|
|
|
---
|
|
|
|
## Phase 4: Inline style extraction — content + page templates
|
|
|
|
**Estimate**: 1.5 sessions (~2.5h) | **Shippable**: yes
|
|
|
|
**4a** — Content components (~1.5h):
|
|
- Extract 57 inline styles from `content.ex`
|
|
- Classes: `.content-body`, `.contact-form`, `.info-card`, `.trust-badge`, `.review-card`, `.star-rating`, `.newsletter-card`, `.page-title`
|
|
|
|
**4b** — Page templates + remaining (~1h):
|
|
- Extract 29 inline styles from `.heex` page templates
|
|
- `checkout_success.html.heex` (23), `content.html.heex` (3), others (3)
|
|
- Also `base.ex` (2)
|
|
|
|
Files modified:
|
|
- `lib/berrypod_web/components/shop_components/content.ex` (57 -> 0)
|
|
- `lib/berrypod_web/components/shop_components/base.ex` (2 -> 0)
|
|
- `lib/berrypod_web/components/page_templates/*.html.heex` (29 -> 0)
|
|
- `assets/css/shop/components.css`
|
|
|
|
Acceptance: **zero inline styles remain** (0/281), full visual regression clean.
|
|
|
|
---
|
|
|
|
## Phase 5: Remove Tailwind from shop pages
|
|
|
|
**Estimate**: 2 sessions (~3h) | **Shippable**: yes
|
|
|
|
**5a** — Replace Tailwind utility classes in components (~1.5h):
|
|
- Replace layout utilities with `.stack`, `.cluster`, `.row`, `.auto-grid`, etc.
|
|
- Replace typography utilities with `.t-caption`, `.t-small`, `.t-heading-*`
|
|
- Replace colour utilities with theme CSS variables
|
|
- Replace responsive prefixes with container queries / media queries
|
|
|
|
**5b** — Remove Tailwind shop build (~1.5h):
|
|
- Replace remaining Tailwind classes in `.heex` page templates
|
|
- Remove `@import "tailwindcss"` from `app-shop.css`
|
|
- Remove `berrypod_shop` Tailwind profile from `config/config.exs`
|
|
- Remove `tailwind_shop` watcher from `config/dev.exs`
|
|
- Update `assets.build` and `assets.deploy` Mix aliases
|
|
- Full visual regression
|
|
|
|
Files modified:
|
|
- All shop component `.ex` files and page template `.heex` files
|
|
- `assets/css/app-shop.css` -> becomes `assets/css/shop.css` (pure CSS entry)
|
|
- `config/config.exs`, `config/dev.exs`, `mix.exs`
|
|
|
|
Acceptance: no Tailwind classes in shop code, Tailwind shop build removed, admin still on Tailwind + DaisyUI, all tests pass, visual regression clean.
|
|
|
|
---
|
|
|
|
## Phase 6: Replace DaisyUI (admin pages)
|
|
|
|
**Estimate**: 2 sessions (~3h) | **Shippable**: yes
|
|
|
|
**6a** — Admin layout and navigation (~1.5h):
|
|
- Create `assets/css/admin-components.css` (~300 lines)
|
|
- Admin drawer (CSS grid), navbar, sidebar menu, button styles
|
|
- Update `admin.html.heex`, `admin_root.html.heex`
|
|
|
|
**6b** — Admin components + auth pages (~1.5h):
|
|
- Admin modal (`<dialog>`), alert/toast, badge, divider, toggle, range
|
|
- Update `core_components.ex`
|
|
- Update admin LiveViews and auth pages
|
|
- Remove `@plugin "../vendor/daisyui"` from `app.css`
|
|
- Delete `assets/vendor/daisyui.js` and `assets/vendor/daisyui-theme.js`
|
|
|
|
Files modified:
|
|
- `assets/css/admin-components.css` (new)
|
|
- `assets/css/app.css` (remove DaisyUI)
|
|
- `lib/berrypod_web/components/core_components.ex`
|
|
- All admin LiveView files
|
|
- Auth LiveView files
|
|
|
|
Acceptance: zero DaisyUI classes, vendor files deleted, admin looks identical.
|
|
|
|
---
|
|
|
|
## Phase 7: Remove Tailwind entirely
|
|
|
|
**Estimate**: 1 session (~1.5h) | **Shippable**: yes
|
|
|
|
Tasks:
|
|
1. Replace remaining Tailwind utilities in admin/auth templates
|
|
2. Remove `@import "tailwindcss"` from `app.css`
|
|
3. Replace heroicons Tailwind plugin with plain CSS icon sizing
|
|
4. Remove `tailwind` dep from `mix.exs`
|
|
5. Remove all Tailwind config from `config/config.exs` and `config/dev.exs`
|
|
6. Update `assets.build` and `assets.deploy` aliases
|
|
7. Delete `assets/vendor/heroicons.js`
|
|
8. Full visual regression across shop + admin
|
|
9. All tests pass
|
|
|
|
Files modified:
|
|
- `mix.exs` (remove `:tailwind` dep)
|
|
- `config/config.exs` (remove tailwind config)
|
|
- `config/dev.exs` (remove tailwind watchers)
|
|
- `assets/css/app.css` (pure CSS entry)
|
|
- All remaining admin/auth template files with Tailwind classes
|
|
|
|
Acceptance: `tailwind` gone from deps, zero Tailwind classes anywhere, CSS builds correctly, all tests pass, visual regression clean.
|
|
|
|
---
|
|
|
|
## Phase 8: Optimisation and modern CSS enhancements
|
|
|
|
**Estimate**: 1-2 sessions (~2-3h) | **Shippable**: yes (each item independent)
|
|
|
|
1. **Lightning CSS** — production minification + vendor prefixing
|
|
2. **Remove all `!important`** — `@layer` handles cascade
|
|
3. **Container queries** — product grid, PDP layout respond to container width
|
|
4. **`content-visibility: auto`** — skip paint for offscreen sections
|
|
5. **CSS containment** — `contain: layout style paint` on product cards
|
|
6. **oklch colours** — upgrade CSSGenerator from HSL to oklch
|
|
- Better perceptual uniformity across hues
|
|
- `color-mix(in oklch, var(--accent) 80%, black)` replaces HSL lightness math
|
|
- Relative colour syntax: `oklch(from var(--accent) calc(l - 0.1) c h)`
|
|
7. **`@property`** — register typed custom properties for animated transitions
|
|
8. **Font optimisation** — `font-display: swap`, `size-adjust`, latin subsetting
|
|
9. **Critical CSS inlining** — inline above-fold CSS in `shop_root.html.heex`
|
|
10. **Performance audit** — measure file sizes, Lighthouse scores
|
|
|
|
---
|
|
|
|
## Post-migration efficiencies
|
|
|
|
### Tree-shaking and dead code
|
|
|
|
With Tailwind, tree-shaking is essential — it generates thousands of utility classes and purges unused ones. **Hand-written CSS doesn't need this** because you only write what you use. The CSS is "pre-shaken" by definition.
|
|
|
|
Efficiencies that do apply:
|
|
1. **Dead rule elimination**: Lightning CSS strips empty rules and unreachable selectors during minification
|
|
2. **`@layer` zero-cost empties**: Unused `@layer` blocks add zero bytes and zero specificity impact
|
|
3. **CSSGenerator already optimised**: Only generates CSS for the active theme variant (not all 4 moods, 7 typographies, etc.)
|
|
4. **Scope-based pruning**: All shop CSS scoped under `.themed` / `.shop-container` — unmatched selectors have no performance cost
|
|
|
|
### Actual sizes (post migration)
|
|
|
|
| Bundle | Before | After (minified) | After (gzipped) | Reduction |
|
|
|--------|--------|-------------------|-----------------|-----------|
|
|
| Shop CSS | 53 KB | 54.1 KB | 9.8 KB | similar size but zero framework |
|
|
| Admin CSS | 212 KB | 90.8 KB | 17.8 KB | ~57% minified |
|
|
| **Total** | **265 KB** | **144.9 KB** | **27.6 KB** | **~45% minified** |
|
|
|
|
Shop CSS grew slightly vs original Tailwind because all component styles are now explicit (Tailwind tree-shook aggressively). Admin CSS dropped substantially because DaisyUI's 212 KB base is gone. The gzipped sizes are what matters for users: 9.8 KB shop, 17.8 KB admin.
|
|
|
|
### Other gains
|
|
|
|
- **No build step in dev**: CSS changes are instant (no Tailwind compilation)
|
|
- **No framework version chasing**: Pure CSS doesn't break on updates
|
|
- **Better cacheability**: Semantic class names change less often than utilities
|
|
- **Composable cascade**: `@layer` means styles compose without fighting
|
|
- **Animatable tokens**: `@property` enables transitions on custom properties (smooth accent colour changes in theme editor)
|
|
- **`content-visibility`**: Browsers skip layout/paint for offscreen product grid items
|
|
|
|
---
|
|
|
|
## Risk mitigation
|
|
|
|
1. **Theme editor preview**: `.preview-frame` rules in `overrides` layer always win. Test theme editor after every phase.
|
|
2. **CSSGenerator**: Generates inline `<style>` setting CSS custom properties. Completely unaffected by the migration.
|
|
3. **JS hooks**: `SearchModal`, `CartDrawer`, `ProductImageScroll`, `Lightbox`, `CollectionFilters` all reference DOM classes/data-attrs. Verify after each phase that referenced class names match.
|
|
4. **Rollback**: Each phase is an atomic commit. Tailwind runs alongside new CSS during Phases 1-4, so old styles remain as a safety net.
|
|
|
|
---
|
|
|
|
## Summary
|
|
|
|
| Phase | What | Sessions | Hours |
|
|
|-------|------|----------|-------|
|
|
| 0 | Foundation + screenshot tooling | 1 | 1.5 |
|
|
| 1 | Layout primitives + reset | 1 | 1.5 |
|
|
| 2 | Extract product inline styles | 2 | 3 |
|
|
| 3 | Extract layout + cart inline styles | 2 | 3 |
|
|
| 4 | Extract content + template inline styles | 1.5 | 2.5 |
|
|
| 5 | Remove Tailwind from shop | 2 | 3 |
|
|
| 6 | Replace DaisyUI (admin) | 2 | 3 |
|
|
| 7 | Remove Tailwind entirely | 1 | 1.5 |
|
|
| 8 | Optimisation + modern enhancements | 1.5 | 2.5 |
|
|
| **Total** | | **14** | **~22h** |
|
|
|
|
Shop fully migrated after Phase 5. Admin after Phase 7. Phase 8 is polish. Each phase is independently shippable with visual regression verification.
|