perf: split CSS bundles for shop and admin pages

Create separate CSS bundles to reduce shop page load times:
- app-shop.css (45KB/7.8KB gzip): Shop pages only, no daisyUI
- app.css (139KB): Admin pages with daisyUI and theme editor

Key changes:
- Add app-shop.css with targeted @source paths for shop files only
- Move .preview-frame rules from theme-layer2-attributes.css to app.css
- Delete fonts.css (fonts now generated inline by CSSGenerator)
- Add inline all-fonts generation in theme editor for typography switching
- Configure separate Tailwind profiles and watchers for both bundles

Shop pages now load 54% less CSS by excluding:
- daisyUI components (admin only)
- .preview-frame theme switching rules (editor only)
- Admin-specific Tailwind utilities

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Jamey Greenwood 2026-01-25 11:36:20 +00:00
parent b1635c7313
commit 1b12dc3e7f
10 changed files with 254 additions and 455 deletions

58
assets/css/app-shop.css Normal file
View File

@ -0,0 +1,58 @@
/* Shop CSS - Tailwind without daisyUI
This is the CSS bundle for public shop pages (localhost:4001).
It excludes daisyUI which is only needed for admin pages.
See app.css for the full admin version. */
@import "tailwindcss" source(none);
@source "../css";
@source "../js";
/* Only scan shop-specific files, not admin pages */
@source "../../lib/simpleshop_theme_web/live/shop_live";
@source "../../lib/simpleshop_theme_web/components/shop_components.ex";
@source "../../lib/simpleshop_theme_web/components/page_templates";
@source "../../lib/simpleshop_theme_web/components/layouts/shop.html.heex";
@source "../../lib/simpleshop_theme_web/components/layouts/shop_root.html.heex";
/* Heroicons plugin */
@plugin "../vendor/heroicons";
/* NO daisyUI - shop pages use the custom .themed system instead */
/* Add variants based on LiveView classes */
@custom-variant phx-click-loading (.phx-click-loading&, .phx-click-loading &);
@custom-variant phx-submit-loading (.phx-submit-loading&, .phx-submit-loading &);
@custom-variant phx-change-loading (.phx-change-loading&, .phx-change-loading &);
/* Make LiveView wrapper divs transparent for layout */
[data-phx-session], [data-phx-teleported-src] { display: contents }
/* Theme CSS - Layer 1: Primitives (fixed CSS variables) */
@import "./theme-primitives.css";
/* Theme CSS - Layer 2: Shared styles only (.themed selectors)
Note: .preview-frame rules are still included but unused on shop pages.
This is acceptable as it's only ~5KB and splitting adds complexity. */
@import "./theme-layer2-attributes.css";
/* Theme CSS - Layer 3: Semantic aliases */
@import "./theme-semantic.css";
/* Cart drawer open state styles */
.cart-drawer.open {
right: 0 !important;
}
.cart-drawer-overlay.open {
opacity: 1 !important;
visibility: visible !important;
}
/* Product gallery thumbnail styles */
.pdp-thumbnail {
border: 2px solid var(--t-border-default);
transition: border-color 0.15s ease;
}
.pdp-thumbnail-active {
border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
}

View File

@ -102,9 +102,6 @@
/* Make LiveView wrapper divs transparent for layout */ /* Make LiveView wrapper divs transparent for layout */
[data-phx-session], [data-phx-teleported-src] { display: contents } [data-phx-session], [data-phx-teleported-src] { display: contents }
/* Self-hosted fonts - all font-face declarations */
@import "./fonts.css";
/* Theme CSS - Layer 1: Primitives (fixed CSS variables) */ /* Theme CSS - Layer 1: Primitives (fixed CSS variables) */
@import "./theme-primitives.css"; @import "./theme-primitives.css";
@ -135,3 +132,167 @@
.pdp-thumbnail-active { .pdp-thumbnail-active {
border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l)); border-color: hsl(var(--t-accent-h) var(--t-accent-s) var(--t-accent-l));
} }
/* =============================================
THEME EDITOR ONLY: .preview-frame CSS variable switching
These rules enable live theme switching in the editor.
Shop pages get CSS variables inline from CSSGenerator.
============================================= */
.preview-frame {
/* Mood - Default (Neutral) */
--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;
/* Typography - Default (Clean) */
--t-font-heading: var(--p-font-manrope);
--t-font-body: var(--p-font-inter);
--t-heading-weight: 600;
--t-heading-tracking: -0.02em;
/* Shape - Default (Soft) */
--t-radius-sm: var(--p-radius-sm);
--t-radius-md: var(--p-radius-md);
--t-radius-lg: var(--p-radius-lg);
--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);
/* Density - Default (Balanced) */
--t-density: 1;
--space-xs: calc(var(--p-space-2) * var(--t-density));
--space-sm: calc(var(--p-space-3) * var(--t-density));
--space-md: calc(var(--p-space-4) * var(--t-density));
--space-lg: calc(var(--p-space-6) * var(--t-density));
--space-xl: calc(var(--p-space-8) * var(--t-density));
--space-2xl: calc(var(--p-space-12) * var(--t-density));
/* Mood Variants */
&[data-mood="warm"] {
--t-surface-base: #fdf8f3;
--t-surface-raised: #fffcf8;
--t-surface-sunken: #f5ebe0;
--t-text-primary: #1c1917;
--t-text-secondary: #57534e;
--t-text-tertiary: #a8a29e;
--t-border-default: #e7e0d8;
--t-border-subtle: #f0ebe4;
}
&[data-mood="cool"] {
--t-surface-base: #f4f7fb;
--t-surface-raised: #f8fafc;
--t-surface-sunken: #e8eff7;
--t-text-primary: #0f172a;
--t-text-secondary: #475569;
--t-text-tertiary: #94a3b8;
--t-border-default: #d4dce8;
--t-border-subtle: #e8eff5;
}
&[data-mood="dark"] {
--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;
}
/* Typography Variants */
&[data-typography="editorial"] {
--t-font-heading: var(--p-font-playfair);
--t-font-body: var(--p-font-raleway);
--t-heading-weight: 500;
--t-heading-tracking: -0.01em;
}
&[data-typography="modern"] {
--t-font-heading: var(--p-font-space);
--t-font-body: var(--p-font-inter);
--t-heading-weight: 500;
--t-heading-tracking: -0.03em;
}
&[data-typography="classic"] {
--t-font-heading: var(--p-font-cormorant);
--t-font-body: var(--p-font-source-serif);
--t-heading-weight: 500;
--t-heading-tracking: 0;
}
&[data-typography="friendly"] {
--t-font-heading: var(--p-font-fraunces);
--t-font-body: var(--p-font-work-sans);
--t-heading-weight: 600;
--t-heading-tracking: -0.01em;
}
&[data-typography="minimal"] {
--t-font-heading: var(--p-font-dm-sans);
--t-font-body: var(--p-font-source-serif);
--t-heading-weight: 500;
--t-heading-tracking: 0;
}
&[data-typography="impulse"] {
--t-font-heading: var(--p-font-raleway);
--t-font-body: var(--p-font-inter);
--t-heading-weight: 300;
--t-heading-tracking: 0.02em;
}
/* Shape Variants */
&[data-shape="sharp"] {
--t-radius-sm: 0;
--t-radius-md: 0;
--t-radius-lg: 0;
--t-radius-button: 0;
--t-radius-card: 0;
--t-radius-input: 0;
--t-radius-image: 0;
}
&[data-shape="round"] {
--t-radius-sm: var(--p-radius-md);
--t-radius-md: var(--p-radius-lg);
--t-radius-lg: var(--p-radius-xl);
--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);
}
&[data-shape="pill"] {
--t-radius-sm: var(--p-radius-full);
--t-radius-md: var(--p-radius-full);
--t-radius-lg: var(--p-radius-xl);
--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);
}
/* Density Variants */
&[data-density="spacious"] {
--t-density: 1.25;
}
&[data-density="compact"] {
--t-density: 0.85;
}
}

View File

@ -1,269 +0,0 @@
/* Self-hosted Google Fonts
* All fonts loaded locally for privacy and performance.
* Browsers only download fonts actually used on the page.
*/
/* Inter - Clean, Modern, Impulse presets (body) */
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/fonts/inter-v20-latin-300.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/inter-v20-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/inter-v20-latin-500.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/inter-v20-latin-600.woff2') format('woff2');
}
@font-face {
font-family: 'Inter';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/inter-v20-latin-700.woff2') format('woff2');
}
/* Manrope - Clean preset (heading) */
@font-face {
font-family: 'Manrope';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/manrope-v20-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Manrope';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/manrope-v20-latin-500.woff2') format('woff2');
}
@font-face {
font-family: 'Manrope';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/manrope-v20-latin-600.woff2') format('woff2');
}
@font-face {
font-family: 'Manrope';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/manrope-v20-latin-700.woff2') format('woff2');
}
/* Work Sans - Friendly preset (body) */
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/fonts/work-sans-v24-latin-300.woff2') format('woff2');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/work-sans-v24-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/work-sans-v24-latin-500.woff2') format('woff2');
}
@font-face {
font-family: 'Work Sans';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/work-sans-v24-latin-600.woff2') format('woff2');
}
/* DM Sans - Minimal preset (heading) */
@font-face {
font-family: 'DM Sans';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/dm-sans-v17-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'DM Sans';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/dm-sans-v17-latin-500.woff2') format('woff2');
}
@font-face {
font-family: 'DM Sans';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/dm-sans-v17-latin-600.woff2') format('woff2');
}
@font-face {
font-family: 'DM Sans';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/dm-sans-v17-latin-700.woff2') format('woff2');
}
/* Raleway - Editorial, Impulse presets (body/heading) */
@font-face {
font-family: 'Raleway';
font-style: normal;
font-weight: 300;
font-display: swap;
src: url('/fonts/raleway-v37-latin-300.woff2') format('woff2');
}
@font-face {
font-family: 'Raleway';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/raleway-v37-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Raleway';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/raleway-v37-latin-500.woff2') format('woff2');
}
/* Space Grotesk - Modern preset (heading) */
@font-face {
font-family: 'Space Grotesk';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/space-grotesk-v22-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Space Grotesk';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/space-grotesk-v22-latin-500.woff2') format('woff2');
}
@font-face {
font-family: 'Space Grotesk';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/space-grotesk-v22-latin-600.woff2') format('woff2');
}
/* Playfair Display - Editorial preset (heading) */
@font-face {
font-family: 'Playfair Display';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/playfair-display-v40-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Playfair Display';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/playfair-display-v40-latin-500.woff2') format('woff2');
}
@font-face {
font-family: 'Playfair Display';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/playfair-display-v40-latin-700.woff2') format('woff2');
}
/* Cormorant Garamond - Classic preset (heading) */
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/cormorant-garamond-v21-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/cormorant-garamond-v21-latin-500.woff2') format('woff2');
}
@font-face {
font-family: 'Cormorant Garamond';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/cormorant-garamond-v21-latin-600.woff2') format('woff2');
}
/* Source Serif 4 - Classic, Minimal presets (body) */
@font-face {
font-family: 'Source Serif 4';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/source-serif-4-v14-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Source Serif 4';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/source-serif-4-v14-latin-600.woff2') format('woff2');
}
/* Fraunces - Friendly preset (heading) */
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 400;
font-display: swap;
src: url('/fonts/fraunces-v38-latin-regular.woff2') format('woff2');
}
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('/fonts/fraunces-v38-latin-500.woff2') format('woff2');
}
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 600;
font-display: swap;
src: url('/fonts/fraunces-v38-latin-600.woff2') format('woff2');
}
@font-face {
font-family: 'Fraunces';
font-style: normal;
font-weight: 700;
font-display: swap;
src: url('/fonts/fraunces-v38-latin-700.woff2') format('woff2');
}

View File

@ -1,183 +1,12 @@
/* ======================================== /* ========================================
LAYER 2: THEME TOKENS (Attribute-based) LAYER 2: THEME TOKENS (Shared Styles)
======================================== ========================================
ARCHITECTURE: This file contains .themed styles used by both shop and theme editor.
- .themed is the shared class for all themed content (shop + preview) The .preview-frame CSS variable switching rules are in app.css (admin only).
- .preview-frame uses data-attribute selectors for CSS variable switching (editor only) Shop pages get CSS variables inline from CSSGenerator.
- Shop pages get CSS variable values inline from CSSGenerator
- All visual/behavioral styles use .themed
======================================== */ ======================================== */
/* =============================================
CSS VARIABLE SWITCHING (Editor-only)
These set CSS variables based on data attributes.
Shop pages get these values inline from CSSGenerator.
============================================= */
.preview-frame {
/* Mood - Default (Neutral) */
--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;
/* Typography - Default (Clean) */
--t-font-heading: var(--p-font-manrope);
--t-font-body: var(--p-font-inter);
--t-heading-weight: 600;
--t-heading-tracking: -0.02em;
/* Shape - Default (Soft) */
--t-radius-sm: var(--p-radius-sm);
--t-radius-md: var(--p-radius-md);
--t-radius-lg: var(--p-radius-lg);
--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);
/* Density - Default (Balanced) */
--t-density: 1;
--space-xs: calc(var(--p-space-2) * var(--t-density));
--space-sm: calc(var(--p-space-3) * var(--t-density));
--space-md: calc(var(--p-space-4) * var(--t-density));
--space-lg: calc(var(--p-space-6) * var(--t-density));
--space-xl: calc(var(--p-space-8) * var(--t-density));
--space-2xl: calc(var(--p-space-12) * var(--t-density));
/* Mood Variants */
&[data-mood="warm"] {
--t-surface-base: #fdf8f3;
--t-surface-raised: #fffcf8;
--t-surface-sunken: #f5ebe0;
--t-text-primary: #1c1917;
--t-text-secondary: #57534e;
--t-text-tertiary: #a8a29e;
--t-border-default: #e7e0d8;
--t-border-subtle: #f0ebe4;
}
&[data-mood="cool"] {
--t-surface-base: #f4f7fb;
--t-surface-raised: #f8fafc;
--t-surface-sunken: #e8eff7;
--t-text-primary: #0f172a;
--t-text-secondary: #475569;
--t-text-tertiary: #94a3b8;
--t-border-default: #d4dce8;
--t-border-subtle: #e8eff5;
}
&[data-mood="dark"] {
--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;
}
/* Typography Variants */
&[data-typography="editorial"] {
--t-font-heading: var(--p-font-playfair);
--t-font-body: var(--p-font-raleway);
--t-heading-weight: 500;
--t-heading-tracking: -0.01em;
}
&[data-typography="modern"] {
--t-font-heading: var(--p-font-space);
--t-font-body: var(--p-font-inter);
--t-heading-weight: 500;
--t-heading-tracking: -0.03em;
}
&[data-typography="classic"] {
--t-font-heading: var(--p-font-cormorant);
--t-font-body: var(--p-font-source-serif);
--t-heading-weight: 500;
--t-heading-tracking: 0;
}
&[data-typography="friendly"] {
--t-font-heading: var(--p-font-fraunces);
--t-font-body: var(--p-font-work-sans);
--t-heading-weight: 600;
--t-heading-tracking: -0.01em;
}
&[data-typography="minimal"] {
--t-font-heading: var(--p-font-dm-sans);
--t-font-body: var(--p-font-source-serif);
--t-heading-weight: 500;
--t-heading-tracking: 0;
}
&[data-typography="impulse"] {
--t-font-heading: var(--p-font-raleway);
--t-font-body: var(--p-font-inter);
--t-heading-weight: 300;
--t-heading-tracking: 0.02em;
}
/* Shape Variants */
&[data-shape="sharp"] {
--t-radius-sm: 0;
--t-radius-md: 0;
--t-radius-lg: 0;
--t-radius-button: 0;
--t-radius-card: 0;
--t-radius-input: 0;
--t-radius-image: 0;
}
&[data-shape="round"] {
--t-radius-sm: var(--p-radius-md);
--t-radius-md: var(--p-radius-lg);
--t-radius-lg: var(--p-radius-xl);
--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);
}
&[data-shape="pill"] {
--t-radius-sm: var(--p-radius-full);
--t-radius-md: var(--p-radius-full);
--t-radius-lg: var(--p-radius-xl);
--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);
}
/* Density Variants */
&[data-density="spacious"] {
--t-density: 1.25;
}
&[data-density="compact"] {
--t-density: 0.85;
}
}
/* =============================================
SHARED STYLES (Both Editor and Shop)
All visual/behavioral styles use .themed
============================================= */
.themed { .themed {
/* Font size scale */ /* Font size scale */
font-size: calc(16px * var(--t-font-size-scale, 1)); font-size: calc(16px * var(--t-font-size-scale, 1));

View File

@ -63,6 +63,13 @@ config :tailwind,
--output=priv/static/assets/css/app.css --output=priv/static/assets/css/app.css
), ),
cd: Path.expand("..", __DIR__) cd: Path.expand("..", __DIR__)
],
simpleshop_theme_shop: [
args: ~w(
--input=assets/css/app-shop.css
--output=priv/static/assets/css/app-shop.css
),
cd: Path.expand("..", __DIR__)
] ]
# Configures Elixir's Logger # Configures Elixir's Logger

View File

@ -23,7 +23,8 @@ config :simpleshop_theme, SimpleshopThemeWeb.Endpoint,
secret_key_base: "Jk04sYT/pzfZ0cywS+i0vCURPoQYgqAGa72uS8bv2gydLyusWFc08kJyEnQP4zgT", secret_key_base: "Jk04sYT/pzfZ0cywS+i0vCURPoQYgqAGa72uS8bv2gydLyusWFc08kJyEnQP4zgT",
watchers: [ watchers: [
esbuild: {Esbuild, :install_and_run, [:simpleshop_theme, ~w(--sourcemap=inline --watch)]}, esbuild: {Esbuild, :install_and_run, [:simpleshop_theme, ~w(--sourcemap=inline --watch)]},
tailwind: {Tailwind, :install_and_run, [:simpleshop_theme, ~w(--watch)]} tailwind: {Tailwind, :install_and_run, [:simpleshop_theme, ~w(--watch)]},
tailwind_shop: {Tailwind, :install_and_run, [:simpleshop_theme_shop, ~w(--watch)]}
] ]
# ## SSL Support # ## SSL Support

View File

@ -13,7 +13,7 @@
) do %> ) do %>
<link rel="preload" href={preload.href} as="font" type="font/woff2" crossorigin /> <link rel="preload" href={preload.href} as="font" type="font/woff2" crossorigin />
<% end %> <% end %>
<link phx-track-static rel="stylesheet" href={~p"/assets/css/app.css"} /> <link phx-track-static rel="stylesheet" href={~p"/assets/css/app-shop.css"} />
<script defer phx-track-static src={~p"/assets/js/app.js"}> <script defer phx-track-static src={~p"/assets/js/app.js"}>
</script> </script>
<!-- Generated theme CSS with @font-face declarations --> <!-- Generated theme CSS with @font-face declarations -->

View File

@ -938,6 +938,11 @@
data-shadow={@theme_settings.card_shadow} data-shadow={@theme_settings.card_shadow}
data-button-style={@theme_settings.button_style}> data-button-style={@theme_settings.button_style}>
<style> <style>
/* All font faces for theme switching */
<%= Phoenix.HTML.raw(SimpleshopTheme.Theme.Fonts.generate_all_font_faces(
&SimpleshopThemeWeb.Endpoint.static_path/1
)) %>
/* Generated theme CSS */
<%= Phoenix.HTML.raw(@generated_css) %> <%= Phoenix.HTML.raw(@generated_css) %>
</style> </style>

View File

@ -86,9 +86,15 @@ defmodule SimpleshopTheme.MixProject do
"ecto.reset": ["ecto.drop", "ecto.setup"], "ecto.reset": ["ecto.drop", "ecto.setup"],
test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"], test: ["ecto.create --quiet", "ecto.migrate --quiet", "test"],
"assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"], "assets.setup": ["tailwind.install --if-missing", "esbuild.install --if-missing"],
"assets.build": ["compile", "tailwind simpleshop_theme", "esbuild simpleshop_theme"], "assets.build": [
"compile",
"tailwind simpleshop_theme",
"tailwind simpleshop_theme_shop",
"esbuild simpleshop_theme"
],
"assets.deploy": [ "assets.deploy": [
"tailwind simpleshop_theme --minify", "tailwind simpleshop_theme --minify",
"tailwind simpleshop_theme_shop --minify",
"esbuild simpleshop_theme --minify", "esbuild simpleshop_theme --minify",
"phx.digest" "phx.digest"
], ],

View File

@ -5,9 +5,9 @@ defmodule SimpleshopThemeWeb.ThemeCSSConsistencyTest do
Architecture: Architecture:
- Both shop pages and preview use .themed class for shared styles - Both shop pages and preview use .themed class for shared styles
- Theme editor uses .preview-frame[data-*] selectors for live switching - Theme editor uses .preview-frame[data-*] selectors for live switching (in app.css)
- Shop pages get theme values via inline CSS from CSSGenerator - Shop pages get theme values via inline CSS from CSSGenerator (app-shop.css)
- Component styles use .themed for shared styling - Component styles use .themed for shared styling (theme-layer2-attributes.css)
""" """
use SimpleshopThemeWeb.ConnCase, async: false use SimpleshopThemeWeb.ConnCase, async: false
@ -88,12 +88,13 @@ defmodule SimpleshopThemeWeb.ThemeCSSConsistencyTest do
end end
describe "CSS file structure" do describe "CSS file structure" do
test "theme-layer2-attributes.css has .preview-frame variant selectors for editor" do test "app.css has .preview-frame variant selectors for theme editor" do
css_path = Path.join([File.cwd!(), "assets", "css", "theme-layer2-attributes.css"]) css_path = Path.join([File.cwd!(), "assets", "css", "app.css"])
css_content = File.read!(css_path) css_content = File.read!(css_path)
# Variant selectors are editor-only (.preview-frame) using CSS nesting # Variant selectors are editor-only (.preview-frame) using CSS nesting
# The file uses &[data-*] syntax inside .preview-frame { } # The file uses &[data-*] syntax inside .preview-frame { }
# These rules are in app.css (admin only), not app-shop.css
assert css_content =~ ".preview-frame" assert css_content =~ ".preview-frame"
assert css_content =~ "&[data-mood=\"dark\"]" assert css_content =~ "&[data-mood=\"dark\"]"
assert css_content =~ "&[data-mood=\"warm\"]" assert css_content =~ "&[data-mood=\"warm\"]"