--- name: magento2-hyva-dev description: | This skill should be used when the user asks to "create a Hyvä theme", "set up a child theme", "build an Alpine.js component", "make this CSP-compliant", "add Tailwind CSS classes", "convert Luma to Hyvä", "migrate Knockout to Alpine", work on "Hyvä checkout" or "Hyvä React components", or handle "Tailwind configuration" or "CSP nonce registration". Provides expert Hyvä theme development for Magento 2. This is a SPECIALIZED skill for Hyvä-specific patterns. DEPENDENT on magento2-dev-core for PHP/backend patterns. compatibility: claude, codex, opencode, copilot depends: [magento2-dev-core] metadata: audience: frontend developers workflow: magento references: - https://github.com/hyva-themes/hyva-ai-tools - https://docs.hyva.io/ --- # Magento 2 Hyvä Developer Hyvä is a modern Magento 2 frontend framework with dramatically simplified JavaScript and CSS. This skill covers Hyvä-specific patterns. ## Related Skills **REQUIRED BACKGROUND:** Load `magento2-dev-core` first — it defines the PHP/backend patterns (DI, escaping, repositories) this skill assumes for any ViewModel or backend code behind a Hyvä template. Hyvä and Luma (`magento2-frontend-dev`) are mutually exclusive theme stacks — check the theme's `theme.xml` parent (`Hyva/default`/`Hyva/reset` vs `Magento/blank`) and `composer.json` for `hyva-themes/*` packages before assuming either applies. Pair with `govard-magento` for the container/CLI side. ## Detect the project's actual setup first Hyvä/Tailwind conventions vary a lot by project age — check before applying a pattern: - **Tailwind version**: v4 uses CSS-based config and `hyva.config.json` design tokens; v2/v3 use `tailwind.config.js`. Check `web/tailwind/package.json`. - **CSP build**: `Hyva/default-csp` vs the plain `Hyva/default`/`Hyva/reset` parent in `theme.xml`. Applying CSP-only nonce patterns to a non-CSP theme (or vice versa) wastes effort. - **Parent theme**: `Hyva/reset` (built from scratch) vs `Hyva/default` (full starter) changes how much markup/CSS already exists to extend rather than rewrite. ## Hyvä vs Luma Comparison | Aspect | Luma | Hyvä | |--------|------|------| | JavaScript | ~200 resources (RequireJS/Knockout) | 2 resources (Alpine.js) | | CSS | LESS-based | Tailwind CSS | | Bundle Size | 500KB+ | <50KB | | Core Web Vitals | Challenging | Optimized | | Learning Curve | Steep | Gentle | | Maintenance | Complex | Simple | ## Theme Structure ### Creating a Child Theme Always copy `web/` from the parent theme rather than creating it from scratch — it carries the Tailwind config and build tooling the theme needs: ```bash mkdir -p app/design/frontend///web cp -r vendor/hyva-themes/magento2-default-theme/web/* app/design/frontend///web/ # For a CSP theme, copy from magento2-default-theme-csp instead ``` Then add `registration.php`, `theme.xml` (parent: `Hyva/default`, `Hyva/reset`, or `Hyva/default-csp`), and `composer.json`, install Tailwind deps and build, then `bin/magento setup:upgrade && bin/magento cache:flush` to pick up the new theme. ``` app/design/frontend/Vendor/Theme/ ├── registration.php ├── theme.xml ├── composer.json ├── package.json ├── tailwind.config.js ├── package.json ├── web/ │ ├── tailwind/ │ │ ├── base/ # Preflight, resets │ │ ├── components/ # Reusable components │ │ │ ├── buttons.css │ │ │ ├── forms.css │ │ │ └── messages.css │ │ ├── utilities/ # Custom utilities │ │ └── theme/ # Page-specific │ └── js/ │ └── alpinejs/ # Alpine components ├── layout/ │ └── default.xml └── templates/ └── ... ``` ## CSP (Content Security Policy) Compliance ### Critical: PCI-DSS 4.0 (Required since April 2025) Payment pages MUST NOT use: - `unsafe-eval` CSP directive - `unsafe-inline` CSP directive ### CSP Nonce Registration **Every inline script MUST register with CSP:** ```php registerInlineScript() ?> ``` ### CSP-Compatible Alpine.js Patterns **WRONG (CSP violations):** ```html Ready ``` **CORRECT (CSP-compliant):** ```html Ready ``` ```javascript function initComponent() { return { count: 0, loading: true, name: '', increment() { this.count++; }, isNotLoading() { return !this.loading; }, updateName(event) { this.name = event.target.value; } } } window.addEventListener('alpine:init', () => { Alpine.data('initComponent', initComponent); }, {once: true}) ``` ### Registering Alpine Components ```php registerInlineScript() ?> ``` ## Alpine.js Component Structure ### Basic Component ```javascript // web/js/alpinejs/Example.js function initExample() { return { // Observable state isOpen: false, items: [], selectedId: null, // Computed (reactive) get hasItems() { return this.items.length > 0; }, // Methods toggle() { this.isOpen = !this.isOpen; }, select(id) { this.selectedId = id; }, // Lifecycle init() { // Called when component initializes this.loadData(); }, loadData() { fetch('/api/data') .then(res => res.json()) .then(data => this.items = data); } } } window.addEventListener('alpine:init', () => Alpine.data('initExample', initExample), {once: true}) ``` ### Template Usage ```html
registerInlineScript() ?> ``` ### Passing Data from PHP ```php
registerInlineScript() ?> ``` ## Hyvä Utilities Hyvä provides global utilities via the `hyva` object: ### Form Handling ```javascript // Get form key hyva.getFormKey() // Submit form via POST hyva.postForm({ action: '/checkout', data: { product_id: 123, qty: 1 } }) // Alternative with fetch async function submitForm(url, data) { const formKey = hyva.getFormKey(); const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Requested-With': 'XMLHttpRequest' }, body: JSON.stringify({ ...data, form_key: formKey }) }); return response.json(); } ``` ### Cookies ```javascript hyva.getCookie('customer_segment') hyva.setCookie('recent_viewed', productId, 30) ``` ### Formatting ```javascript hyva.formatPrice(price, showSign) hyva.str('Hello {0}', name) hyva.safeParseNumber(value) ``` ### DOM Manipulation ```javascript hyva.replaceDomElement('#target', 'New content') hyva.trapFocus(modalElement) ``` ### Events ```javascript // After Alpine initialization hyva.alpineInitialized(function() { console.log('Alpine ready'); }) ``` ## View Models Prefer view models (`Hyva\Theme\Model\ViewModelInterface` / Magento's `ArgumentInterface`) over blocks for passing data to templates — they keep PHP logic out of the theme directory (which should hold only templates, layout, `i18n`, and `web/` assets) and in a proper `app/code` module where Magento's DI can autoload the class. ```php // app/code/Vendor/Module/ViewModel/ProductInfo.php declare(strict_types=1); namespace Vendor\Module\ViewModel; use Hyva\Theme\Model\ViewModelInterface; use Magento\Framework\View\LayoutInterface; class ProductInfo implements ViewModelInterface { public function __construct( private readonly LayoutInterface $layout ) {} public function isInStock(): bool { $product = $this->layout->getBlock('product.info')->getProduct(); return $product && $product->isInStock(); } } ``` ```xml Vendor\Module\ViewModel\ProductInfo ``` ```php isInStock()): ?> ``` ## Tailwind CSS ### Tailwind v4 (CSS-based config) Newer Hyvä themes use Tailwind v4, which drops `tailwind.config.js` for a CSS-based config plus a `hyva.config.json` design-token file — check `web/tailwind/package.json` first, the two configs are not interchangeable. ```css /* web/tailwind/tailwind-source.css */ @import "tailwindcss"; @theme { --color-primary: oklch(46% 0.2 265); --spacing-xs: 0.5rem; } @layer components { .btn-primary { @apply bg-primary text-white px-4 py-2 rounded; } } ``` ```json // hyva.config.json { "tokens": { "src": "hyva.design.tokens.json", "format": "default", "cssSelector": "@theme" } } ``` Generate tokens/sources with `npx hyva-sources` / `npx hyva-tokens` rather than hand-rolling them. ### Directory Structure ``` web/tailwind/ ├── base/ │ └── _styles.pcss # Preflight, typography ├── components/ │ ├── _buttons.pcss │ ├── _forms.pcss │ └── _messages.pcss ├── utilities/ │ └── _custom-utilities.pcss ├── theme/ │ ├── _header.pcss │ └── _footer.pcss └── app.css # Main entry ``` ### Build Commands ```bash # Development with watch npm run watch # Production build npm run build # PurgeCSS config (auto-included) # Tailwind automatically removes unused classes ``` ### Common Classes ```html
``` ### Responsive Design ```html
``` ## Layout XML ### Hyvä-Specific Handles ```xml