fix: resolve compilation warnings and update tests to match implementation
- Remove unused generate_mood/1, generate_typography/1, generate_shape/1, generate_density/1 functions from CSSGenerator (now handled via CSS data attributes) - Prefix unused _opts parameters in Printify.Client - Remove unused created_products variable from MockupGenerator - Update CSSGeneratorTest to test actual generated CSS (accent colors, font size scale, layout width, etc.) - Update PresetsTest to match 8 presets (not 9) - Fix PreviewDataTest to accept local image paths - Update ThemeLiveTest to use correct selectors and match actual UI
This commit is contained in:
parent
974d91ce33
commit
d4dbd8998f
1
.env
Normal file
1
.env
Normal file
@ -0,0 +1 @@
|
||||
set PRINTIFY_API_TOKEN 'eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJhdWQiOiIzN2Q0YmQzMDM1ZmUxMWU5YTgwM2FiN2VlYjNjY2M5NyIsImp0aSI6Ijk3ZjY0YzEwZjIyNGRjOGZkYmM1NDgwN2VhNzI2YzY3N2NmYjYzYTZkNmZmMmYzNjExMmVkMzBmYTM1YjdmYTVmZGZiOWRlMDMzMTZiM2I0IiwiaWF0IjoxNzY4NDI4MTk2LjExNjM4OSwibmJmIjoxNzY4NDI4MTk2LjExNjM5MSwiZXhwIjoxNzk5OTY0MTk2LjEwOTY2Mywic3ViIjoiMjU4ODY0NDQiLCJzY29wZXMiOlsic2hvcHMubWFuYWdlIiwic2hvcHMucmVhZCIsImNhdGFsb2cucmVhZCIsIm9yZGVycy5yZWFkIiwib3JkZXJzLndyaXRlIiwicHJvZHVjdHMucmVhZCIsInByb2R1Y3RzLndyaXRlIiwid2ViaG9va3MucmVhZCIsIndlYmhvb2tzLndyaXRlIiwidXBsb2Fkcy5yZWFkIiwidXBsb2Fkcy53cml0ZSIsInByaW50X3Byb3ZpZGVycy5yZWFkIiwidXNlci5pbmZvIl19.CcBAOWMK2xGiTVPaq0B5JP_YBCJQMG9UiwY5hCfQRKBnc33CEyXm8eGJjt5ugy3ENH-0QCiFBtETvZ6f-hc2atVIsAwRPJ2-ey76wTZ1MlBM1LWTMj33jd2aFy3WguI0OMsJil2cEUBCmU47Hhv2Wgsf1Njp6vJEGAuwwfxVk6TArqMad0yHu6ZH4xyKD3F98MDcAk-lroDp8VmkBfGgYJZri7pHKcFZ7BvOGUZUnkO5PiFKZJyFTjIqYXwuCmbzDR1BSnz5LkSBdEe43tYPl_9-gleSWaADOnjQGJYJT0yZRk5oC9N_IgPjZakZcH_XVhvZ1yjRfWMkvmZQArtHrz7FPn1FwM7yDW82x3vHGmZ56l-AvMf85OUWh1pq_J5yvN7aOVkeNi1aDsWZTWkH6xqtWU4vhVjhE_nr5EoqmN5tIj4dy-v6XJ4XpDzOxxmZF01ty57KcmIY0W7uSd2CZGPGjnicWKKOBR7gKChRCBkSawLEEcDrxe5_RJQw9jHxid7Mfwp5MCApmbd_vfQd5NtdpAC5OA_sGAU_tfA9j4qV0PpFW_733lN--CjWjD0lVnGRnU-p2WH-t0qZpjd6UnvJT_C4p2q9vrpd8x29N7UJYfYCwTBLsrYouVu6OILYGV3uMs9oa4U7gay-EM1qBUww8mc_4xhQT3EvtanQrZY'
|
||||
@ -19,7 +19,7 @@ defmodule SimpleshopTheme.Printify.Client do
|
||||
@doc """
|
||||
Make a GET request to the Printify API.
|
||||
"""
|
||||
def get(path, opts \\ []) do
|
||||
def get(path, _opts \\ []) do
|
||||
url = @base_url <> path
|
||||
|
||||
case Req.get(url, headers: auth_headers(), receive_timeout: 30_000) do
|
||||
@ -37,7 +37,7 @@ defmodule SimpleshopTheme.Printify.Client do
|
||||
@doc """
|
||||
Make a POST request to the Printify API.
|
||||
"""
|
||||
def post(path, body, opts \\ []) do
|
||||
def post(path, body, _opts \\ []) do
|
||||
url = @base_url <> path
|
||||
|
||||
case Req.post(url, json: body, headers: auth_headers(), receive_timeout: 60_000) do
|
||||
@ -55,7 +55,7 @@ defmodule SimpleshopTheme.Printify.Client do
|
||||
@doc """
|
||||
Make a DELETE request to the Printify API.
|
||||
"""
|
||||
def delete(path, opts \\ []) do
|
||||
def delete(path, _opts \\ []) do
|
||||
url = @base_url <> path
|
||||
|
||||
case Req.delete(url, headers: auth_headers(), receive_timeout: 30_000) do
|
||||
|
||||
@ -361,9 +361,6 @@ defmodule SimpleshopTheme.Printify.MockupGenerator do
|
||||
IO.puts("Using shop ID: #{shop_id}")
|
||||
IO.puts("")
|
||||
|
||||
# Track created products for cleanup
|
||||
created_products = []
|
||||
|
||||
results =
|
||||
product_definitions()
|
||||
|> Enum.map(fn product_def ->
|
||||
|
||||
@ -49,188 +49,6 @@ defmodule SimpleshopTheme.Theme.CSSGenerator do
|
||||
"""
|
||||
end
|
||||
|
||||
# Mood variations (color schemes)
|
||||
defp generate_mood("neutral") do
|
||||
"""
|
||||
--t-surface-base: #ffffff;
|
||||
--t-surface-raised: #ffffff;
|
||||
--t-surface-sunken: #f5f5f5;
|
||||
--t-surface-overlay: rgba(255, 255, 255, 0.95);
|
||||
--t-text-primary: #171717;
|
||||
--t-text-secondary: #525252;
|
||||
--t-text-tertiary: #a3a3a3;
|
||||
--t-text-inverse: #ffffff;
|
||||
--t-border-default: #e5e5e5;
|
||||
--t-border-subtle: #f0f0f0;
|
||||
"""
|
||||
end
|
||||
|
||||
defp generate_mood("warm") do
|
||||
"""
|
||||
--t-surface-base: #fdf8f3;
|
||||
--t-surface-raised: #fffcf8;
|
||||
--t-surface-sunken: #f5ebe0;
|
||||
--t-surface-overlay: rgba(253, 248, 243, 0.95);
|
||||
--t-text-primary: #1c1917;
|
||||
--t-text-secondary: #57534e;
|
||||
--t-text-tertiary: #a8a29e;
|
||||
--t-text-inverse: #ffffff;
|
||||
--t-border-default: #e7e0d8;
|
||||
--t-border-subtle: #f0ebe4;
|
||||
"""
|
||||
end
|
||||
|
||||
defp generate_mood("cool") do
|
||||
"""
|
||||
--t-surface-base: #f4f7fb;
|
||||
--t-surface-raised: #f8fafc;
|
||||
--t-surface-sunken: #e8eff7;
|
||||
--t-surface-overlay: rgba(244, 247, 251, 0.95);
|
||||
--t-text-primary: #0f172a;
|
||||
--t-text-secondary: #475569;
|
||||
--t-text-tertiary: #94a3b8;
|
||||
--t-text-inverse: #ffffff;
|
||||
--t-border-default: #d4dce8;
|
||||
--t-border-subtle: #e8eff5;
|
||||
"""
|
||||
end
|
||||
|
||||
defp generate_mood("dark") do
|
||||
"""
|
||||
--t-surface-base: #0a0a0a;
|
||||
--t-surface-raised: #171717;
|
||||
--t-surface-sunken: #000000;
|
||||
--t-surface-overlay: rgba(23, 23, 23, 0.95);
|
||||
--t-text-primary: #fafafa;
|
||||
--t-text-secondary: #a3a3a3;
|
||||
--t-text-tertiary: #737373;
|
||||
--t-text-inverse: #171717;
|
||||
--t-border-default: #262626;
|
||||
--t-border-subtle: #1c1c1c;
|
||||
--p-shadow-strength: 0.25;
|
||||
"""
|
||||
end
|
||||
|
||||
# Typography styles
|
||||
defp generate_typography("clean") do
|
||||
"""
|
||||
--t-font-heading: var(--p-font-inter);
|
||||
--t-font-body: var(--p-font-inter);
|
||||
--t-heading-weight: 600;
|
||||
--t-heading-tracking: -0.025em;
|
||||
"""
|
||||
end
|
||||
|
||||
defp generate_typography("editorial") do
|
||||
"""
|
||||
--t-font-heading: var(--p-font-fraunces);
|
||||
--t-font-body: var(--p-font-source);
|
||||
--t-heading-weight: 600;
|
||||
--t-heading-tracking: -0.02em;
|
||||
"""
|
||||
end
|
||||
|
||||
defp generate_typography("modern") do
|
||||
"""
|
||||
--t-font-heading: var(--p-font-space);
|
||||
--t-font-body: var(--p-font-space);
|
||||
--t-heading-weight: 500;
|
||||
--t-heading-tracking: -0.03em;
|
||||
"""
|
||||
end
|
||||
|
||||
defp generate_typography("classic") do
|
||||
"""
|
||||
--t-font-heading: var(--p-font-baskerville);
|
||||
--t-font-body: var(--p-font-source);
|
||||
--t-heading-weight: 400;
|
||||
--t-heading-tracking: 0;
|
||||
"""
|
||||
end
|
||||
|
||||
defp generate_typography("friendly") do
|
||||
"""
|
||||
--t-font-heading: var(--p-font-nunito);
|
||||
--t-font-body: var(--p-font-nunito);
|
||||
--t-heading-weight: 700;
|
||||
--t-heading-tracking: -0.01em;
|
||||
"""
|
||||
end
|
||||
|
||||
defp generate_typography("minimal") do
|
||||
"""
|
||||
--t-font-heading: var(--p-font-outfit);
|
||||
--t-font-body: var(--p-font-outfit);
|
||||
--t-heading-weight: 300;
|
||||
--t-heading-tracking: 0;
|
||||
"""
|
||||
end
|
||||
|
||||
defp generate_typography("impulse") do
|
||||
"""
|
||||
--t-font-heading: var(--p-font-avenir);
|
||||
--t-font-body: var(--p-font-avenir);
|
||||
--t-heading-weight: 300;
|
||||
--t-heading-tracking: 0.02em;
|
||||
"""
|
||||
end
|
||||
|
||||
# Shape variations (border radius)
|
||||
defp generate_shape("sharp") do
|
||||
"""
|
||||
--t-radius-button: 0;
|
||||
--t-radius-card: 0;
|
||||
--t-radius-input: 0;
|
||||
--t-radius-image: 0;
|
||||
"""
|
||||
end
|
||||
|
||||
defp generate_shape("soft") do
|
||||
"""
|
||||
--t-radius-button: var(--p-radius-md);
|
||||
--t-radius-card: var(--p-radius-lg);
|
||||
--t-radius-input: var(--p-radius-md);
|
||||
--t-radius-image: var(--p-radius-md);
|
||||
"""
|
||||
end
|
||||
|
||||
defp generate_shape("round") do
|
||||
"""
|
||||
--t-radius-button: var(--p-radius-lg);
|
||||
--t-radius-card: var(--p-radius-xl);
|
||||
--t-radius-input: var(--p-radius-lg);
|
||||
--t-radius-image: var(--p-radius-lg);
|
||||
"""
|
||||
end
|
||||
|
||||
defp generate_shape("pill") do
|
||||
"""
|
||||
--t-radius-button: var(--p-radius-full);
|
||||
--t-radius-card: var(--p-radius-xl);
|
||||
--t-radius-input: var(--p-radius-full);
|
||||
--t-radius-image: var(--p-radius-lg);
|
||||
"""
|
||||
end
|
||||
|
||||
# Density variations (spacing multiplier)
|
||||
defp generate_density("spacious") do
|
||||
"""
|
||||
--t-density: 1.25;
|
||||
"""
|
||||
end
|
||||
|
||||
defp generate_density("balanced") do
|
||||
"""
|
||||
--t-density: 1;
|
||||
"""
|
||||
end
|
||||
|
||||
defp generate_density("compact") do
|
||||
"""
|
||||
--t-density: 0.85;
|
||||
"""
|
||||
end
|
||||
|
||||
# Font size variations
|
||||
# Using 18px as base for better accessibility (WCAG recommends 18px+)
|
||||
# Small: 18px, Medium: 19px, Large: 20px
|
||||
|
||||
375
simpleshop-typography.md
Normal file
375
simpleshop-typography.md
Normal file
@ -0,0 +1,375 @@
|
||||
# Professional typography systems for Tailwind e-commerce themes
|
||||
|
||||
A well-designed typography system using modular scales, vertical rhythm, and constrained customization can transform an e-commerce shop from generic to polished. The key insight: **typography accounts for 95% of web design**, yet most theme builders expose the wrong controls—offering granular font size pickers when users actually need curated presets with automatic scaling. The most successful approach combines a **Major Third (1.25) type scale**, **8px baseline grid**, and **2-3 carefully paired Google Fonts** with preset-based theme customization that prevents design chaos while enabling brand expression.
|
||||
|
||||
This report covers the complete technical foundation: mathematical type scales with CSS implementations, curated Google Font pairings across seven style categories, production-ready Tailwind configurations, DaisyUI integration patterns, and research-backed guidelines for building theme editors that balance freedom with quality outcomes.
|
||||
|
||||
---
|
||||
|
||||
## Type scales create mathematical harmony across all text sizes
|
||||
|
||||
Modular type scales use consistent mathematical ratios to generate font sizes, ensuring visual harmony without arbitrary decisions. Starting with a **16px base** (browser default), each size step multiplies by the chosen ratio.
|
||||
|
||||
| Ratio | Name | Best use case |
|
||||
|-------|------|--------------|
|
||||
| 1.125 | Major Second | Dense UIs, dashboards, documentation |
|
||||
| **1.25** | **Major Third** | **Recommended default for e-commerce** |
|
||||
| 1.333 | Perfect Fourth | Editorial, content-heavy sites |
|
||||
| 1.414 | Augmented Fourth | Landing pages, marketing |
|
||||
| 1.5 | Perfect Fifth | High-contrast headlines |
|
||||
| 1.618 | Golden Ratio | Luxury brands, creative projects |
|
||||
|
||||
The **Major Third (1.25)** strikes the ideal balance for e-commerce: sufficient hierarchy without oversized mobile headings. A full scale at this ratio: 12.8px → 16px → 20px → 25px → 31.25px → 39px → 48.8px.
|
||||
|
||||
**Implementation principle**: Use larger ratios (1.333–1.5) on desktop and smaller ratios (1.125–1.25) on mobile. Fluid typography with `clamp()` handles this automatically:
|
||||
|
||||
```css
|
||||
--fs-heading: clamp(1.5rem, 1rem + 2vw, 2.5rem);
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Vertical rhythm and baseline grids enforce consistent spacing
|
||||
|
||||
The **8-point grid system** (used by Material Design, iOS, and most design systems) creates visual consistency by constraining all spacing to multiples of 8px. Typography integrates with this grid through line-height alignment.
|
||||
|
||||
**Why 8px works**: Most screen sizes divide evenly by 8, scaling cleanly to @1x, @2x, and @3x resolutions. It reduces decision-making to a finite set of options: 8, 16, 24, 32, 40, 48px.
|
||||
|
||||
**Typography integration**: Base font of 16px with line-height 1.5 yields **24px line-height** (3 × 8), keeping text on the baseline grid. All margins and padding use 8px increments.
|
||||
|
||||
```css
|
||||
:root {
|
||||
--rhythm: 0.5rem; /* 8px */
|
||||
--line-height-body: 1.5rem; /* 24px = 3 × rhythm */
|
||||
}
|
||||
|
||||
body { font-size: 1rem; line-height: 1.5; }
|
||||
p { margin-bottom: 1rem; } /* 16px = 2 × rhythm */
|
||||
h2 { margin-bottom: 1.5rem; } /* 24px = 3 × rhythm */
|
||||
```
|
||||
|
||||
**Practical approach**: Accept line-height-based rhythm rather than true baseline alignment—CSS centers text vertically within line-height, making pixel-perfect baseline grids impractical without complex padding offsets.
|
||||
|
||||
---
|
||||
|
||||
## Line-height, measure, and letter-spacing have precise optimal values
|
||||
|
||||
These three parameters have research-backed optimal ranges that should rarely need customization in a theme editor.
|
||||
|
||||
**Line-height follows an inverse relationship with font size**—larger text needs tighter leading:
|
||||
|
||||
| Context | Line-height | Example at 16px base |
|
||||
|---------|-------------|---------------------|
|
||||
| Large headings (32px+) | 1.1–1.2 | 35–38px |
|
||||
| Subheadings (20–28px) | 1.2–1.3 | 24–36px |
|
||||
| **Body text** | **1.5–1.6** | **24–26px** |
|
||||
| Small text (<14px) | 1.4–1.5 | 20–21px |
|
||||
|
||||
WCAG accessibility requires **minimum 1.5 line-height** for body text. Always use unitless values (`line-height: 1.5`) so they scale proportionally.
|
||||
|
||||
**Measure (line length)** should stay within **45–75 characters** for optimal readability, with 66 being ideal. Implementation:
|
||||
|
||||
```css
|
||||
.prose { max-width: 65ch; }
|
||||
/* Or responsive: */
|
||||
.content { width: clamp(45ch, 90%, 75ch); }
|
||||
```
|
||||
|
||||
**Letter-spacing guidelines**:
|
||||
- **Body text**: Leave default (font is optimized)
|
||||
- **ALL CAPS**: Add 5–12% spacing (`letter-spacing: 0.08em`)
|
||||
- **Large headings (32px+)**: Tighten slightly (`letter-spacing: -0.02em`)
|
||||
- **Small text (<14px)**: Widen slightly (`letter-spacing: 0.02em`)
|
||||
|
||||
---
|
||||
|
||||
## Curated Google Font pairings for seven e-commerce styles
|
||||
|
||||
Each pairing includes heading and body fonts with specific weights to load. These combinations feel distinctive while maintaining professional readability.
|
||||
|
||||
### Minimal and modern (tech, sustainable goods, contemporary homeware)
|
||||
|
||||
**Manrope (500, 600) + Inter (400, 500)** — Geometric precision meets exceptional screen readability. Both feature clean lines without quirks.
|
||||
|
||||
**Plus Jakarta Sans (500, 600) + Literata (400, 400i)** — Newer, distinctive sans-serif with variable font support paired with a screen-optimized serif. Fresh but professional.
|
||||
|
||||
**DM Sans (500, 700) + Source Serif Pro (400, 400i)** — Low-contrast geometric sans contrasts elegantly with refined serif. Ideal for design tools, premium stationery.
|
||||
|
||||
### Warm and artisan (handmade goods, craft products, specialty food)
|
||||
|
||||
**Cormorant Garamond (500, 600) + Proza Libre (400, 400i)** — Lavish Garamond revival with strong calligraphic feel pairs with humanist sans optimized for screens. Perfect for artisan ceramics, specialty coffee.
|
||||
|
||||
**Calistoga (400) + IBM Plex Sans (400, 500)** — Rounded serifs with friendly sophistication. Excellent for bakeries, organic goods, candle shops.
|
||||
|
||||
**Josefin Sans Light (300, 400) + Crimson Text (400, 400i, 600)** — Thin letterforms create an airy feel; classic serif adds professional warmth. Suits makeup, jewelry, wellness.
|
||||
|
||||
### Bold and editorial (fashion, lifestyle brands)
|
||||
|
||||
**Playfair Display (500, 700) + Raleway Light (300, 400)** — High-contrast delicate hairlines of Playfair with thin Raleway strokes. Classic fashion magazine aesthetic.
|
||||
|
||||
**Instrument Serif + Instrument Sans (400, 500)** — Same-designer family creates cohesive editorial look with perfect built-in contrast. Art-forward brands, creative agencies.
|
||||
|
||||
**DM Serif Display (400) + Poppins (300, 400)** — Beautiful calligraphic strokes with thin modern geometric sans. Fashion accessories, beauty products.
|
||||
|
||||
### Playful and quirky (creative products, novelty gifts)
|
||||
|
||||
**Fraunces (variable, 400–900) + Work Sans (400, 500)** — Four axes including "wonk" create vintage-inspired whimsy while maintaining professionalism. Variable font with softness, weight, and optical size controls.
|
||||
|
||||
**Syne Bold (700) + Syne Regular (400)** — Footprint expands as weight increases for quirky but readable stand-out headlines. Music merchandise, creative agencies.
|
||||
|
||||
**Dynapuff (variable) + Nunito (400, 600)** — Bold, playful, round font with soft rounded sans. Children's products, candy shops.
|
||||
|
||||
### Luxury and elegant (high-end goods, premium products)
|
||||
|
||||
**Cinzel (400, 500) + Fauna One (400)** — High-contrast Roman inscription inspiration conveys timeless authority. Fine jewelry, luxury watches.
|
||||
|
||||
**Cormorant Garamond (400, 500) + Montserrat Light (300, 400)** — Elegant Garamond with clean urban sans creates sophisticated contrast. High-end cosmetics, luxury home decor.
|
||||
|
||||
**Prata (400) + Manrope Light (300, 400)** — Contrasting thick/thin strokes convey elegance; light Manrope maintains delicacy. Premium skincare, designer accessories.
|
||||
|
||||
### Vintage and retro (heritage brands, nostalgic products)
|
||||
|
||||
**Libre Franklin (500, 600) + Libre Baskerville (400, 400i)** — American Gothic + Transitional pairing used for 100+ years. Evokes established, traditional authenticity.
|
||||
|
||||
**Oswald (500, 600) + EB Garamond (400, 400i)** — Bold condensed sans (Franklin Gothic–inspired) with elegant Garamond. Vintage clothing, retro posters.
|
||||
|
||||
**Rokkitt (400, 700) + Raleway (400, 500)** — Geometric slab serif (Egyptian style) with elegant sans. Heritage stationery, letterpress products.
|
||||
|
||||
### Tech and futuristic (sci-fi prints, gadget accessories)
|
||||
|
||||
**Space Grotesk (500, 700) + IBM Plex Mono (400, 500)** — Distinctive geometric sans with monospace body creates coding/tech aesthetic.
|
||||
|
||||
---
|
||||
|
||||
## Variable fonts offer performance and flexibility advantages
|
||||
|
||||
Variable fonts contain multiple styles in a single file, reducing HTTP requests and enabling precise weight control (e.g., font-weight: 450).
|
||||
|
||||
| Font | Weight range | Other axes | Best for |
|
||||
|------|-------------|-----------|----------|
|
||||
| **Inter** | 100–900 | Optical size 14–32 | All-purpose workhorse |
|
||||
| **Roboto Flex** | 100–1000 | Width, grade, optical size | Maximum flexibility |
|
||||
| **Fraunces** | 100–900 | Soft, wonk, optical size | Playful brands |
|
||||
| **Outfit** | 100–900 | — | Modern headlines |
|
||||
| **DM Sans** | 100–1000 | Optical size 9–40 | Clean interfaces |
|
||||
|
||||
**Request syntax** for Google Fonts variable fonts:
|
||||
```html
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@200..900&display=swap">
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Font loading strategies that protect performance
|
||||
|
||||
**Recommended approach**: Use `font-display: swap` for headings (fastest text visibility) and `font-display: optional` for body text (prevents layout shifts if font fails to load).
|
||||
|
||||
**Self-hosting now outperforms Google Fonts CDN** due to Chrome and Safari's cache partitioning (since 2020)—cross-site caching no longer works. Self-hosting reported PageSpeed improvements of 66 → 94 on mobile scores.
|
||||
|
||||
**Performance checklist**:
|
||||
- Load only weights actually used (e.g., 400, 600 instead of full range)
|
||||
- Preconnect to font origins: `<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>`
|
||||
- Use WOFF2 format only (30% smaller than WOFF)
|
||||
- Limit to 2–3 font families maximum
|
||||
- Preload critical above-fold fonts:
|
||||
```html
|
||||
<link rel="preload" href="/fonts/Inter.woff2" as="font" type="font/woff2" crossorigin>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Tailwind configuration for production typography systems
|
||||
|
||||
Tailwind's fontSize config supports a tuple syntax for complete typographic definitions:
|
||||
|
||||
```javascript
|
||||
// tailwind.config.js
|
||||
module.exports = {
|
||||
theme: {
|
||||
fontSize: {
|
||||
'xs': ['0.75rem', { lineHeight: '1rem', letterSpacing: '0.025em' }],
|
||||
'sm': ['0.875rem', { lineHeight: '1.25rem' }],
|
||||
'base': ['1rem', { lineHeight: '1.5rem' }],
|
||||
'lg': ['1.125rem', { lineHeight: '1.75rem' }],
|
||||
'xl': ['1.25rem', { lineHeight: '1.75rem' }],
|
||||
'2xl': ['1.5rem', { lineHeight: '2rem', letterSpacing: '-0.01em' }],
|
||||
'3xl': ['1.875rem', { lineHeight: '2.25rem', letterSpacing: '-0.02em' }],
|
||||
'4xl': ['2.25rem', { lineHeight: '2.5rem', letterSpacing: '-0.02em' }],
|
||||
},
|
||||
spacing: {
|
||||
// 8px baseline grid
|
||||
'1': '0.5rem', // 8px
|
||||
'2': '1rem', // 16px
|
||||
'3': '1.5rem', // 24px
|
||||
'4': '2rem', // 32px
|
||||
'6': '3rem', // 48px
|
||||
},
|
||||
fontFamily: {
|
||||
sans: ['Inter', 'ui-sans-serif', 'system-ui'],
|
||||
display: ['Playfair Display', 'ui-serif', 'Georgia'],
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
**Fluid typography in Tailwind** uses arbitrary values or custom config:
|
||||
|
||||
```html
|
||||
<h1 class="text-[clamp(1.75rem,3vw,3rem)]">Fluid headline</h1>
|
||||
```
|
||||
|
||||
Or extend the config:
|
||||
```javascript
|
||||
fontSize: {
|
||||
'fluid-xl': 'clamp(1.5rem, 1rem + 2vw, 2.5rem)',
|
||||
'fluid-display': 'clamp(2.5rem, 1.5rem + 4vw, 5rem)',
|
||||
}
|
||||
```
|
||||
|
||||
**@tailwindcss/typography plugin** handles prose content with sensible defaults:
|
||||
|
||||
```html
|
||||
<article class="prose prose-lg dark:prose-invert prose-headings:font-display">
|
||||
{{ markdown }}
|
||||
</article>
|
||||
```
|
||||
|
||||
Override defaults in config:
|
||||
```javascript
|
||||
typography: {
|
||||
DEFAULT: {
|
||||
css: {
|
||||
maxWidth: '65ch',
|
||||
'h1, h2, h3': { fontFamily: 'var(--font-display)' },
|
||||
a: { color: '#3182ce', '&:hover': { color: '#2c5282' } },
|
||||
},
|
||||
},
|
||||
},
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## DaisyUI themes control colors while typography stays at Tailwind level
|
||||
|
||||
**Critical distinction**: DaisyUI themes only control colors, border-radius, and effects—not typography. Font families, sizes, and spacing are configured in Tailwind, applying consistently across all DaisyUI themes.
|
||||
|
||||
```css
|
||||
/* DaisyUI plugin configuration */
|
||||
@import "tailwindcss";
|
||||
@plugin "daisyui" {
|
||||
themes: light --default, dark --prefersdark;
|
||||
}
|
||||
|
||||
/* Typography at Tailwind level (applies to all themes) */
|
||||
@theme {
|
||||
--font-display: "Playfair Display", serif;
|
||||
--font-body: "Inter", sans-serif;
|
||||
}
|
||||
```
|
||||
|
||||
**Semantic color usage for text**: DaisyUI provides `base-content` for default text, `primary-content` for text on primary backgrounds, etc. Components auto-apply correct text colors (`btn-primary` uses `primary-content`).
|
||||
|
||||
**Overriding component typography**:
|
||||
```css
|
||||
@layer components {
|
||||
.btn {
|
||||
text-transform: none; /* Remove default uppercase */
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.01em;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Theme editors should offer presets, not granular controls
|
||||
|
||||
Research across Shopify, Squarespace, and Webflow reveals a clear pattern: the most successful theme systems use **constrained customization** that prevents design chaos.
|
||||
|
||||
### Parameters to make user-configurable
|
||||
|
||||
| Setting | Implementation | Why expose |
|
||||
|---------|---------------|-----------|
|
||||
| **Font pairing preset** | 5–10 curated combinations | Enables brand expression without typography expertise |
|
||||
| **Heading font family** | Dropdown of paired fonts | Primary brand identity signal |
|
||||
| **Body font family** | Dropdown of paired fonts | Readability preference |
|
||||
| **Font size scale** | Slider 90%–120% | Accessibility accommodation |
|
||||
| **Accent colors** | Color picker with presets | Brand customization |
|
||||
|
||||
### Parameters to lock down (prevent user adjustment)
|
||||
|
||||
| Setting | Locked value | Why restrict |
|
||||
|---------|-------------|-------------|
|
||||
| **Line-height ratios** | 1.5 body, 1.2 headings | Users break readability |
|
||||
| **Letter-spacing** | Preset per size | Technical, easy to ruin |
|
||||
| **Measure/line length** | 45–75ch | Readability science |
|
||||
| **Type scale ratio** | 1.25 | Mathematical harmony |
|
||||
| **Mobile scaling** | Automatic | Complex responsive logic |
|
||||
|
||||
### The Shopify model (maximum constraints)
|
||||
|
||||
Shopify exposes only **two font categories** (headings and body), a **single size slider** (100–150%), and no line-height or letter-spacing controls. Users select from a curated library of ~1,000 fonts. Result: hard to break, accessible to non-designers.
|
||||
|
||||
### The Squarespace model (balanced approach)
|
||||
|
||||
Squarespace offers **60+ Font Packs**—professionally curated heading/body pairs that guarantee aesthetic coherence. Users can also access individual controls (family, weight, size, line-height, letter-spacing) but the preset system handles most needs.
|
||||
|
||||
### Recommended implementation for e-commerce themes
|
||||
|
||||
1. **Offer 5–10 curated font pairing presets** covering different brand aesthetics
|
||||
2. **Allow 2 font category overrides** (heading + body) from a filtered quality list
|
||||
3. **Use range sliders with sensible limits** (size 90–120%, never expose line-height)
|
||||
4. **Provide real-time preview** before saving changes
|
||||
5. **Include accessibility validation** (contrast ratio warnings)
|
||||
6. **Group elements semantically** (all headings share one setting)
|
||||
|
||||
---
|
||||
|
||||
## E-commerce typography patterns that increase conversions
|
||||
|
||||
Research from Baymard Institute (200,000+ hours of UX testing) reveals specific typography patterns that improve shopping experiences.
|
||||
|
||||
### Product page hierarchy
|
||||
|
||||
- **Product title**: Largest text (20–28px), semi-bold to bold (600–700), first in visual hierarchy
|
||||
- **Price**: High prominence near Add to Cart, **4.5:1 minimum contrast**, consider showing price-per-unit (**74% of sites miss this**)
|
||||
- **CTA buttons**: Clean sans-serif, 14–16px, action verbs ("Add to Cart" outperforms "Submit")
|
||||
- **Body descriptions**: 16–18px, 1.5 line-height, structured with subheadings (**78% of sites don't use highlights format**)
|
||||
|
||||
### Trust signals through typography
|
||||
|
||||
Monotype/Neurons research found serif fonts increase **perceived quality by 13%** and **reliability by 9%**. For premium positioning:
|
||||
- Use serif fonts for headlines (Playfair Display, Cormorant Garamond)
|
||||
- Generous white space around text elements
|
||||
- Restrained design—avoid decorative excess
|
||||
- Limit to 2–3 fonts maximum (**4+ creates "cheap" perception**)
|
||||
|
||||
### Mobile e-commerce requirements
|
||||
|
||||
- **Touch targets**: 44×44px minimum for text links (WCAG AAA), 8px minimum spacing between targets
|
||||
- **Body text**: 16px minimum (prevents iOS input zoom)
|
||||
- **Avoid horizontal tabs on mobile**—27% of users overlook them entirely
|
||||
- Position price and CTA in natural thumb zone (bottom 2/3 of screen)
|
||||
|
||||
### Scanning patterns inform layout
|
||||
|
||||
**F-pattern** (product pages with descriptions): Users scan horizontally across top, then shorter horizontal line below, then vertically down left side. Place critical information (product name, price, CTAs) along these paths.
|
||||
|
||||
**Z-pattern** (landing pages, minimal text): Top left → top right → diagonal to bottom left → bottom right. Position logo top-left, navigation top-right, primary CTA bottom-right.
|
||||
|
||||
---
|
||||
|
||||
## Conclusion: A complete typography system for Tailwind e-commerce themes
|
||||
|
||||
The optimal system combines:
|
||||
|
||||
1. **Major Third (1.25) type scale** as the default, with automatic mobile scaling using `clamp()`
|
||||
2. **8px baseline grid** for all spacing, with line-heights aligned to 24px (1.5rem) for body text
|
||||
3. **2–3 Google Fonts** loaded as variable fonts with `font-display: swap`, self-hosted for performance
|
||||
4. **Tailwind configuration** with fontSize tuples including line-height and letter-spacing
|
||||
5. **DaisyUI for color theming** while typography remains at Tailwind level
|
||||
6. **Preset-based theme customization** offering curated font pairings rather than granular controls
|
||||
|
||||
The research consensus is clear: typography parameters like line-height (1.5 body, 1.2 headings), measure (65ch), and letter-spacing should be **locked to optimal values**. User customization should focus on font pairing presets and size scaling within safe ranges—enabling brand expression without the risk of breaking readability or professional appearance.
|
||||
|
||||
For print-on-demand shops specifically, the **Minimal/Modern** (Manrope + Inter) and **Playful/Quirky** (Fraunces + Work Sans) pairings offer the most versatility across product categories, while **Warm/Artisan** (Cormorant Garamond + Proza Libre) suits handmade goods and **Bold/Editorial** (Playfair Display + Raleway) elevates fashion-forward brands.
|
||||
276
simpleshop-ux-component-patterns.md
Normal file
276
simpleshop-ux-component-patterns.md
Normal file
@ -0,0 +1,276 @@
|
||||
# SimpleShop UX-Optimised Component Patterns
|
||||
|
||||
A research-backed guide to component design that avoids common anti-patterns and creates professional, ethical, high-converting e-commerce experiences.
|
||||
|
||||
---
|
||||
|
||||
## Spacing & Layout System
|
||||
|
||||
Building on the 8px grid from the typography guide:
|
||||
|
||||
**Spacing Tokens**:
|
||||
```css
|
||||
--space-1: 0.25rem; /* 4px - tight */
|
||||
--space-2: 0.5rem; /* 8px - compact */
|
||||
--space-3: 0.75rem; /* 12px */
|
||||
--space-4: 1rem; /* 16px - default */
|
||||
--space-5: 1.5rem; /* 24px */
|
||||
--space-6: 2rem; /* 32px */
|
||||
--space-8: 3rem; /* 48px - section gaps */
|
||||
--space-10: 4rem; /* 64px */
|
||||
--space-12: 6rem; /* 96px - major sections */
|
||||
--space-16: 8rem; /* 128px - hero spacing */
|
||||
```
|
||||
|
||||
**Tailwind Config**:
|
||||
```javascript
|
||||
module.exports = {
|
||||
theme: {
|
||||
spacing: {
|
||||
'0': '0',
|
||||
'1': '0.25rem',
|
||||
'2': '0.5rem',
|
||||
'3': '0.75rem',
|
||||
'4': '1rem',
|
||||
'5': '1.5rem',
|
||||
'6': '2rem',
|
||||
'8': '3rem',
|
||||
'10': '4rem',
|
||||
'12': '6rem',
|
||||
'16': '8rem',
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Section Spacing**:
|
||||
```html
|
||||
<!-- Page sections use consistent vertical spacing -->
|
||||
<section class="py-12 md:py-16">
|
||||
<div class="container mx-auto px-4">
|
||||
<!-- Content -->
|
||||
</div>
|
||||
</section>
|
||||
```
|
||||
|
||||
**Container Widths**:
|
||||
```css
|
||||
--container-prose: 65ch; /* Text content */
|
||||
--container-content: 80rem; /* 1280px - main content */
|
||||
--container-wide: 90rem; /* 1440px - full layouts */
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Component States
|
||||
|
||||
Every interactive component needs these states defined:
|
||||
|
||||
**Button States**:
|
||||
```
|
||||
Default → Base styling
|
||||
Hover → Subtle lift/colour shift (200ms ease)
|
||||
Active → Pressed state
|
||||
Focus → Visible ring (accessibility)
|
||||
Disabled → Greyed, no pointer
|
||||
Loading → Spinner, disabled
|
||||
```
|
||||
|
||||
**Form Input States**:
|
||||
```
|
||||
Default → Border, background
|
||||
Focus → Ring, border colour change
|
||||
Error → Red border, error message below
|
||||
Success → Green check (optional, for validation)
|
||||
Disabled → Greyed background
|
||||
```
|
||||
|
||||
**Card States**:
|
||||
```
|
||||
Default → Base styling
|
||||
Hover → Subtle lift (box-shadow increase)
|
||||
Focus → Visible ring (if interactive)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Accessibility Baseline
|
||||
|
||||
Every SimpleShop theme must meet WCAG 2.1 AA:
|
||||
|
||||
**Colour Contrast**:
|
||||
- Normal text: 4.5:1 minimum
|
||||
- Large text (18px+ or 14px bold): 3:1 minimum
|
||||
- UI components and graphics: 3:1 minimum
|
||||
|
||||
**Focus Indicators**:
|
||||
- Visible focus ring on all interactive elements
|
||||
- Never remove outline without replacement
|
||||
- Ring should have 3:1 contrast with adjacent colours
|
||||
|
||||
**Touch Targets**:
|
||||
- Minimum 44×44px for touch targets
|
||||
- 8px minimum spacing between targets
|
||||
|
||||
**Screen Reader**:
|
||||
- Semantic HTML (button for buttons, not div)
|
||||
- Alt text on all images
|
||||
- ARIA labels where needed
|
||||
- Skip links for keyboard navigation
|
||||
- Logical heading hierarchy (h1 → h2 → h3)
|
||||
|
||||
---
|
||||
|
||||
## Performance Patterns
|
||||
|
||||
**Image Loading**:
|
||||
```html
|
||||
<!-- Lazy load below-fold images -->
|
||||
<img
|
||||
src="product.jpg"
|
||||
alt="Product"
|
||||
loading="lazy"
|
||||
decoding="async"
|
||||
width="400"
|
||||
height="533"
|
||||
/>
|
||||
|
||||
<!-- Eager load above-fold images -->
|
||||
<img
|
||||
src="hero.jpg"
|
||||
alt="Hero"
|
||||
loading="eager"
|
||||
fetchpriority="high"
|
||||
/>
|
||||
```
|
||||
|
||||
**Font Loading**:
|
||||
```html
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link
|
||||
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap"
|
||||
rel="stylesheet"
|
||||
>
|
||||
```
|
||||
|
||||
**Critical CSS**:
|
||||
- Inline above-fold styles
|
||||
- Defer non-critical CSS
|
||||
- Remove unused styles
|
||||
|
||||
---
|
||||
|
||||
## Theme Presets
|
||||
|
||||
Combining typography pairings with component styling:
|
||||
|
||||
### Minimal Modern
|
||||
- **Fonts**: Manrope + Inter
|
||||
- **Cards**: No shadows, subtle borders
|
||||
- **Buttons**: Solid fills, sharp corners (border-radius: 0 or 2px)
|
||||
- **Spacing**: Generous, airy
|
||||
- **Colours**: Monochrome with single accent
|
||||
|
||||
### Warm Artisan
|
||||
- **Fonts**: Cormorant Garamond + Proza Libre
|
||||
- **Cards**: Subtle shadows, rounded corners
|
||||
- **Buttons**: Rounded, warm fills
|
||||
- **Spacing**: Comfortable, not sparse
|
||||
- **Colours**: Warm neutrals, earthy accents
|
||||
|
||||
### Bold Editorial
|
||||
- **Fonts**: Playfair Display + Raleway
|
||||
- **Cards**: Strong contrast, dramatic shadows
|
||||
- **Buttons**: High contrast, possibly outlined
|
||||
- **Spacing**: Dramatic, asymmetric
|
||||
- **Colours**: Black/white with bold accent
|
||||
|
||||
### Playful Quirky
|
||||
- **Fonts**: Fraunces + Work Sans
|
||||
- **Cards**: Rounded, possibly with borders
|
||||
- **Buttons**: Rounded/pill, playful hover states
|
||||
- **Spacing**: Varied, dynamic
|
||||
- **Colours**: Bright, fun palette
|
||||
|
||||
### Luxury Elegant
|
||||
- **Fonts**: Cinzel + Fauna One
|
||||
- **Cards**: Minimal, refined
|
||||
- **Buttons**: Thin borders, subtle
|
||||
- **Spacing**: Generous, refined
|
||||
- **Colours**: Muted, sophisticated
|
||||
|
||||
---
|
||||
|
||||
## Implementation Checklist
|
||||
|
||||
Before shipping any SimpleShop theme, verify:
|
||||
|
||||
**Layout**:
|
||||
- [ ] Mobile-first responsive design
|
||||
- [ ] No horizontal scroll on any viewport
|
||||
- [ ] Content readable at 320px minimum
|
||||
- [ ] Touch targets 44×44px minimum
|
||||
|
||||
**Navigation**:
|
||||
- [ ] Logo links to homepage
|
||||
- [ ] Cart always accessible
|
||||
- [ ] Current page indicated
|
||||
- [ ] Mobile menu works smoothly
|
||||
|
||||
**Product Cards**:
|
||||
- [ ] Consistent aspect ratios
|
||||
- [ ] Price clearly visible
|
||||
- [ ] Hover state provides value
|
||||
- [ ] Links are obvious
|
||||
|
||||
**Product Page**:
|
||||
- [ ] Accordion sections, not horizontal tabs
|
||||
- [ ] Images zoomable
|
||||
- [ ] Price prominent
|
||||
- [ ] Add to Cart sticky on mobile
|
||||
- [ ] Trust signals visible
|
||||
|
||||
**Checkout**:
|
||||
- [ ] Guest checkout prominent
|
||||
- [ ] Progress indicator clear
|
||||
- [ ] All costs shown upfront
|
||||
- [ ] Form fields properly labeled
|
||||
- [ ] Error messages inline
|
||||
- [ ] Trust signals present
|
||||
|
||||
**Ethical**:
|
||||
- [ ] No fake urgency/scarcity
|
||||
- [ ] No confirmshaming
|
||||
- [ ] No hidden costs
|
||||
- [ ] No exit-intent popups
|
||||
- [ ] Clear, neutral options
|
||||
- [ ] Honest product information
|
||||
|
||||
**Accessibility**:
|
||||
- [ ] Colour contrast passes WCAG AA
|
||||
- [ ] Focus rings visible
|
||||
- [ ] Semantic HTML
|
||||
- [ ] Alt text on images
|
||||
- [ ] Keyboard navigable
|
||||
|
||||
**Performance**:
|
||||
- [ ] Images lazy loaded (except hero)
|
||||
- [ ] Fonts preloaded
|
||||
- [ ] LCP under 2.5s
|
||||
- [ ] No layout shift (CLS < 0.1)
|
||||
|
||||
---
|
||||
|
||||
## Summary
|
||||
|
||||
SimpleShop's component system prioritises:
|
||||
|
||||
1. **Honesty over manipulation** — No dark patterns, ever
|
||||
2. **Clarity over cleverness** — Obvious UI beats novel UI
|
||||
3. **Research over assumptions** — Patterns backed by testing
|
||||
4. **Consistency over creativity** — Systems over one-offs
|
||||
5. **Accessibility over aesthetics** — Everyone can use it
|
||||
|
||||
These patterns, combined with the typography and spacing systems, create a foundation for themes that are not just beautiful but genuinely effective—converting browsers into buyers through trust and clarity rather than tricks and pressure.
|
||||
|
||||
The result: shops that sellers are proud of and customers enjoy using.
|
||||
@ -16,118 +16,6 @@ defmodule SimpleshopTheme.Theme.CSSGeneratorTest do
|
||||
assert css =~ "--t-accent-l:"
|
||||
end
|
||||
|
||||
test "generates correct mood variables for neutral" do
|
||||
settings = %ThemeSettings{mood: "neutral"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-surface-base: #ffffff"
|
||||
assert css =~ "--t-text-primary: #171717"
|
||||
assert css =~ "--t-border-default: #e5e5e5"
|
||||
end
|
||||
|
||||
test "generates correct mood variables for dark" do
|
||||
settings = %ThemeSettings{mood: "dark"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-surface-base: #0a0a0a"
|
||||
assert css =~ "--t-text-primary: #fafafa"
|
||||
assert css =~ "--t-border-default: #262626"
|
||||
assert css =~ "--p-shadow-strength: 0.25"
|
||||
end
|
||||
|
||||
test "generates correct mood variables for warm" do
|
||||
settings = %ThemeSettings{mood: "warm"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-surface-base: #fdf8f3"
|
||||
assert css =~ "--t-text-primary: #1c1917"
|
||||
end
|
||||
|
||||
test "generates correct mood variables for cool" do
|
||||
settings = %ThemeSettings{mood: "cool"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-surface-base: #f4f7fb"
|
||||
assert css =~ "--t-text-primary: #0f172a"
|
||||
end
|
||||
|
||||
test "generates correct typography for clean" do
|
||||
settings = %ThemeSettings{typography: "clean"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-font-heading: var(--p-font-inter)"
|
||||
assert css =~ "--t-font-body: var(--p-font-inter)"
|
||||
assert css =~ "--t-heading-weight: 600"
|
||||
end
|
||||
|
||||
test "generates correct typography for editorial" do
|
||||
settings = %ThemeSettings{typography: "editorial"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-font-heading: var(--p-font-fraunces)"
|
||||
assert css =~ "--t-font-body: var(--p-font-source)"
|
||||
end
|
||||
|
||||
test "generates correct typography for modern" do
|
||||
settings = %ThemeSettings{typography: "modern"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-font-heading: var(--p-font-space)"
|
||||
assert css =~ "--t-heading-weight: 500"
|
||||
end
|
||||
|
||||
test "generates correct shape for sharp" do
|
||||
settings = %ThemeSettings{shape: "sharp"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-radius-button: 0"
|
||||
assert css =~ "--t-radius-card: 0"
|
||||
end
|
||||
|
||||
test "generates correct shape for soft" do
|
||||
settings = %ThemeSettings{shape: "soft"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-radius-button: var(--p-radius-md)"
|
||||
assert css =~ "--t-radius-card: var(--p-radius-lg)"
|
||||
end
|
||||
|
||||
test "generates correct shape for round" do
|
||||
settings = %ThemeSettings{shape: "round"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-radius-button: var(--p-radius-lg)"
|
||||
assert css =~ "--t-radius-card: var(--p-radius-xl)"
|
||||
end
|
||||
|
||||
test "generates correct shape for pill" do
|
||||
settings = %ThemeSettings{shape: "pill"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-radius-button: var(--p-radius-full)"
|
||||
end
|
||||
|
||||
test "generates correct density for spacious" do
|
||||
settings = %ThemeSettings{density: "spacious"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-density: 1.25"
|
||||
end
|
||||
|
||||
test "generates correct density for balanced" do
|
||||
settings = %ThemeSettings{density: "balanced"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-density: 1"
|
||||
end
|
||||
|
||||
test "generates correct density for compact" do
|
||||
settings = %ThemeSettings{density: "compact"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-density: 0.85"
|
||||
end
|
||||
|
||||
test "converts hex colors to HSL" do
|
||||
settings = %ThemeSettings{accent_color: "#ff0000"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
@ -138,6 +26,26 @@ defmodule SimpleshopTheme.Theme.CSSGeneratorTest do
|
||||
assert css =~ "--t-accent-l: 50%"
|
||||
end
|
||||
|
||||
test "handles blue accent color" do
|
||||
settings = %ThemeSettings{accent_color: "#0000ff"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
# Blue should be H=240, S=100%, L=50%
|
||||
assert css =~ "--t-accent-h: 240"
|
||||
assert css =~ "--t-accent-s: 100%"
|
||||
assert css =~ "--t-accent-l: 50%"
|
||||
end
|
||||
|
||||
test "handles green accent color" do
|
||||
settings = %ThemeSettings{accent_color: "#00ff00"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
# Green should be H=120, S=100%, L=50%
|
||||
assert css =~ "--t-accent-h: 120"
|
||||
assert css =~ "--t-accent-s: 100%"
|
||||
assert css =~ "--t-accent-l: 50%"
|
||||
end
|
||||
|
||||
test "includes secondary colors" do
|
||||
settings = %ThemeSettings{
|
||||
secondary_accent_color: "#ea580c",
|
||||
@ -149,5 +57,103 @@ defmodule SimpleshopTheme.Theme.CSSGeneratorTest do
|
||||
assert css =~ "--t-secondary-accent: #ea580c"
|
||||
assert css =~ "--t-sale-color: #dc2626"
|
||||
end
|
||||
|
||||
test "generates font size scale for small" do
|
||||
settings = %ThemeSettings{font_size: "small"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-font-size-scale: 1.125"
|
||||
end
|
||||
|
||||
test "generates font size scale for medium" do
|
||||
settings = %ThemeSettings{font_size: "medium"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-font-size-scale: 1.1875"
|
||||
end
|
||||
|
||||
test "generates font size scale for large" do
|
||||
settings = %ThemeSettings{font_size: "large"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-font-size-scale: 1.25"
|
||||
end
|
||||
|
||||
test "generates heading weight override for regular" do
|
||||
settings = %ThemeSettings{heading_weight: "regular"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-heading-weight-override: 400"
|
||||
end
|
||||
|
||||
test "generates heading weight override for bold" do
|
||||
settings = %ThemeSettings{heading_weight: "bold"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-heading-weight-override: 700"
|
||||
end
|
||||
|
||||
test "generates layout width for contained" do
|
||||
settings = %ThemeSettings{layout_width: "contained"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-layout-max-width: 1100px"
|
||||
end
|
||||
|
||||
test "generates layout width for wide" do
|
||||
settings = %ThemeSettings{layout_width: "wide"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-layout-max-width: 1400px"
|
||||
end
|
||||
|
||||
test "generates layout width for full" do
|
||||
settings = %ThemeSettings{layout_width: "full"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-layout-max-width: 100%"
|
||||
end
|
||||
|
||||
test "generates button style for filled" do
|
||||
settings = %ThemeSettings{button_style: "filled"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-button-style: filled"
|
||||
end
|
||||
|
||||
test "generates button style for outline" do
|
||||
settings = %ThemeSettings{button_style: "outline"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-button-style: outline"
|
||||
end
|
||||
|
||||
test "generates product text alignment" do
|
||||
settings = %ThemeSettings{product_text_align: "center"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-product-text-align: center"
|
||||
end
|
||||
|
||||
test "generates image aspect ratio for square" do
|
||||
settings = %ThemeSettings{image_aspect_ratio: "square"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-image-aspect-ratio: 1 / 1"
|
||||
end
|
||||
|
||||
test "generates image aspect ratio for portrait" do
|
||||
settings = %ThemeSettings{image_aspect_ratio: "portrait"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-image-aspect-ratio: 3 / 4"
|
||||
end
|
||||
|
||||
test "generates image aspect ratio for landscape" do
|
||||
settings = %ThemeSettings{image_aspect_ratio: "landscape"}
|
||||
css = CSSGenerator.generate(settings)
|
||||
|
||||
assert css =~ "--t-image-aspect-ratio: 4 / 3"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -4,11 +4,11 @@ defmodule SimpleshopTheme.Theme.PresetsTest do
|
||||
alias SimpleshopTheme.Theme.Presets
|
||||
|
||||
describe "all/0" do
|
||||
test "returns all 9 presets" do
|
||||
test "returns all 8 presets" do
|
||||
presets = Presets.all()
|
||||
|
||||
assert is_map(presets)
|
||||
assert map_size(presets) == 9
|
||||
assert map_size(presets) == 8
|
||||
assert Map.has_key?(presets, :gallery)
|
||||
assert Map.has_key?(presets, :studio)
|
||||
assert Map.has_key?(presets, :boutique)
|
||||
@ -17,7 +17,6 @@ defmodule SimpleshopTheme.Theme.PresetsTest do
|
||||
assert Map.has_key?(presets, :minimal)
|
||||
assert Map.has_key?(presets, :night)
|
||||
assert Map.has_key?(presets, :classic)
|
||||
assert Map.has_key?(presets, :impulse)
|
||||
end
|
||||
end
|
||||
|
||||
@ -74,8 +73,8 @@ defmodule SimpleshopTheme.Theme.PresetsTest do
|
||||
test "returns minimal preset" do
|
||||
preset = Presets.get(:minimal)
|
||||
|
||||
assert preset.mood == "cool"
|
||||
assert preset.typography == "minimal"
|
||||
assert preset.mood == "neutral"
|
||||
assert preset.typography == "impulse"
|
||||
assert preset.shape == "sharp"
|
||||
assert preset.accent_color == "#171717"
|
||||
end
|
||||
@ -96,18 +95,6 @@ defmodule SimpleshopTheme.Theme.PresetsTest do
|
||||
assert preset.accent_color == "#166534"
|
||||
end
|
||||
|
||||
test "returns impulse preset with extended settings" do
|
||||
preset = Presets.get(:impulse)
|
||||
|
||||
assert preset.mood == "neutral"
|
||||
assert preset.typography == "impulse"
|
||||
assert preset.shape == "sharp"
|
||||
assert preset.accent_color == "#000000"
|
||||
assert preset.font_size == "medium"
|
||||
assert preset.heading_weight == "regular"
|
||||
assert preset.product_text_align == "center"
|
||||
end
|
||||
|
||||
test "returns nil for nonexistent preset" do
|
||||
assert Presets.get(:nonexistent) == nil
|
||||
end
|
||||
@ -118,7 +105,7 @@ defmodule SimpleshopTheme.Theme.PresetsTest do
|
||||
names = Presets.list_names()
|
||||
|
||||
assert is_list(names)
|
||||
assert length(names) == 9
|
||||
assert length(names) == 8
|
||||
assert :gallery in names
|
||||
assert :studio in names
|
||||
assert :boutique in names
|
||||
@ -127,7 +114,6 @@ defmodule SimpleshopTheme.Theme.PresetsTest do
|
||||
assert :minimal in names
|
||||
assert :night in names
|
||||
assert :classic in names
|
||||
assert :impulse in names
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -45,11 +45,14 @@ defmodule SimpleshopTheme.Theme.PreviewDataTest do
|
||||
|
||||
for product <- products do
|
||||
assert is_binary(product.image_url)
|
||||
assert String.starts_with?(product.image_url, "http")
|
||||
# Images can be either local paths (starting with /) or full URLs
|
||||
assert String.starts_with?(product.image_url, "/") or
|
||||
String.starts_with?(product.image_url, "http")
|
||||
|
||||
if product.hover_image_url do
|
||||
assert is_binary(product.hover_image_url)
|
||||
assert String.starts_with?(product.hover_image_url, "http")
|
||||
assert String.starts_with?(product.hover_image_url, "/") or
|
||||
String.starts_with?(product.hover_image_url, "http")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@ -29,12 +29,11 @@ defmodule SimpleshopThemeWeb.ThemeLiveTest do
|
||||
test "renders theme editor page", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/theme")
|
||||
|
||||
assert html =~ "Theme Editor"
|
||||
assert html =~ "Save Theme"
|
||||
assert html =~ "Presets"
|
||||
assert html =~ "Theme Studio"
|
||||
assert html =~ "preset"
|
||||
end
|
||||
|
||||
test "displays all 9 presets", %{conn: conn} do
|
||||
test "displays all 8 presets", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/theme")
|
||||
|
||||
assert html =~ "gallery"
|
||||
@ -45,7 +44,6 @@ defmodule SimpleshopThemeWeb.ThemeLiveTest do
|
||||
assert html =~ "minimal"
|
||||
assert html =~ "night"
|
||||
assert html =~ "classic"
|
||||
assert html =~ "impulse"
|
||||
end
|
||||
|
||||
test "displays current theme settings", %{conn: conn} do
|
||||
@ -61,10 +59,9 @@ defmodule SimpleshopThemeWeb.ThemeLiveTest do
|
||||
test "displays generated CSS in preview", %{conn: conn} do
|
||||
{:ok, _view, html} = live(conn, ~p"/admin/theme")
|
||||
|
||||
# CSS generator outputs accent colors and layout variables
|
||||
assert html =~ ".preview-frame, .shop-root"
|
||||
assert html =~ "--t-accent-h:"
|
||||
assert html =~ "--t-surface-base:"
|
||||
assert html =~ "--t-font-heading:"
|
||||
end
|
||||
|
||||
test "applies preset and updates preview", %{conn: conn} do
|
||||
@ -93,14 +90,18 @@ defmodule SimpleshopThemeWeb.ThemeLiveTest do
|
||||
assert html =~ "All Products"
|
||||
end
|
||||
|
||||
test "save theme button works", %{conn: conn} do
|
||||
test "theme settings are saved when applying a preset", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/theme")
|
||||
|
||||
# Apply a preset
|
||||
view
|
||||
|> element("button", "Save Theme")
|
||||
|> element("button", "gallery")
|
||||
|> render_click()
|
||||
|
||||
assert view |> element("button", "Save Theme") |> has_element?()
|
||||
# Verify settings were persisted
|
||||
theme_settings = Settings.get_theme_settings()
|
||||
assert theme_settings.mood == "warm"
|
||||
assert theme_settings.typography == "editorial"
|
||||
end
|
||||
|
||||
test "all preview page buttons are present", %{conn: conn} do
|
||||
@ -121,7 +122,7 @@ defmodule SimpleshopThemeWeb.ThemeLiveTest do
|
||||
# Click the "dark" mood button
|
||||
html =
|
||||
view
|
||||
|> element("button", "dark")
|
||||
|> element("button[phx-value-setting_value='dark']")
|
||||
|> render_click()
|
||||
|
||||
# Verify the setting was updated
|
||||
@ -135,7 +136,7 @@ defmodule SimpleshopThemeWeb.ThemeLiveTest do
|
||||
|
||||
# Click the "round" shape button
|
||||
view
|
||||
|> element("button", "round")
|
||||
|> element("button[phx-value-setting_value='round']")
|
||||
|> render_click()
|
||||
|
||||
# Verify the setting was updated
|
||||
@ -148,7 +149,7 @@ defmodule SimpleshopThemeWeb.ThemeLiveTest do
|
||||
|
||||
# Click the "compact" density button
|
||||
view
|
||||
|> element("button", "compact")
|
||||
|> element("button[phx-value-setting_value='compact']")
|
||||
|> render_click()
|
||||
|
||||
# Verify the setting was updated
|
||||
@ -159,9 +160,9 @@ defmodule SimpleshopThemeWeb.ThemeLiveTest do
|
||||
test "grid columns customization buttons work", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/theme")
|
||||
|
||||
# Click the "2" grid columns button
|
||||
# Click the "2 columns" grid columns button
|
||||
view
|
||||
|> element("button", "2")
|
||||
|> element("button", "2 columns")
|
||||
|> render_click()
|
||||
|
||||
# Verify the setting was updated
|
||||
@ -169,13 +170,13 @@ defmodule SimpleshopThemeWeb.ThemeLiveTest do
|
||||
assert theme_settings.grid_columns == "2"
|
||||
end
|
||||
|
||||
test "typography customization select works", %{conn: conn} do
|
||||
test "typography customization buttons work", %{conn: conn} do
|
||||
{:ok, view, _html} = live(conn, ~p"/admin/theme")
|
||||
|
||||
# Change typography via the form
|
||||
# Click the "modern" typography button
|
||||
view
|
||||
|> element("form[phx-value-field='typography']")
|
||||
|> render_change(%{"typography" => "modern"})
|
||||
|> element("button[phx-value-setting_value='modern']")
|
||||
|> render_click()
|
||||
|
||||
# Verify the setting was updated
|
||||
theme_settings = Settings.get_theme_settings()
|
||||
@ -187,7 +188,7 @@ defmodule SimpleshopThemeWeb.ThemeLiveTest do
|
||||
|
||||
# Click the "centered" header layout button
|
||||
view
|
||||
|> element("button", "centered")
|
||||
|> element("button[phx-value-setting_value='centered']")
|
||||
|> render_click()
|
||||
|
||||
# Verify the setting was updated
|
||||
@ -204,12 +205,12 @@ defmodule SimpleshopThemeWeb.ThemeLiveTest do
|
||||
# Change a setting
|
||||
new_html =
|
||||
view
|
||||
|> element("button", "dark")
|
||||
|> element("button[phx-value-setting_value='dark']")
|
||||
|> render_click()
|
||||
|
||||
# Verify CSS has changed (dark mode should have different surface colors)
|
||||
# Verify CSS has changed
|
||||
refute initial_css == new_html
|
||||
assert new_html =~ "--t-surface-base:"
|
||||
assert new_html =~ "--t-accent-h:"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
Loading…
Reference in New Issue
Block a user