hyva-alpine-component by hyva-themes/hyva-ai-tools
npx skills add https://github.com/hyva-themes/hyva-ai-tools --skill hyva-alpine-component本技能为在 Hyvä 主题中编写兼容 CSP 的 Alpine.js 组件提供指导。Alpine CSP 是一个专门的 Alpine.js 构建版本,它无需 unsafe-eval CSP 指令即可运行,这对于支付相关页面符合 PCI-DSS 4.0 标准是必需的(自 2025 年 4 月 1 日起强制要求)。
关键原则: 兼容 CSP 的代码在标准 Alpine 和 Alpine CSP 构建中都能运行。所有 Alpine 代码都应使用 CSP 模式编写,以确保未来兼容性。
| 功能 | 标准 Alpine | Alpine CSP |
|---|---|---|
| 属性读取 | x-show="open" | 相同 |
| 取反 | x-show="!open" | 方法:x-show="isNotOpen" |
| 状态变更 |
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
@click="open = false"方法:@click="close" |
| 方法参数 | @click="setTab('info')" | 数据集:@click="setTab" data-tab="info" |
x-model | 可用 | 不支持 - 使用 :value + @input |
| 范围迭代 | x-for="i in 10" | 不支持 |
Hyvä 中的每个 Alpine 组件都遵循此结构:
<div x-data="initComponentName">
<!-- 模板内容 -->
</div>
<script>
function initComponentName() {
return {
// 属性
propertyName: initialValue,
// 生命周期
init() {
// 组件初始化时调用
},
// 用于状态访问的方法
isPropertyTrue() {
return this.propertyName === true;
},
// 用于状态变更的方法
setPropertyValue() {
this.propertyName = this.$event.target.value;
}
}
}
window.addEventListener('alpine:init', () => Alpine.data('initComponentName', initComponentName), {once: true})
</script>
<?php $hyvaCsp->registerInlineScript() ?>
关键要求:
alpine:init 事件监听器内使用 Alpine.data() 注册构造函数{once: true} 防止重复注册<script> 块后调用 $hyvaCsp->registerInlineScript()$escaper->escapeJs()$escaper->escapeHtmlAttr()(而非 escapeJs)function initMyComponent() {
return {
open: false
}
}
window.addEventListener('alpine:init', () => Alpine.data('initMyComponent', initMyComponent), {once: true})
为何使用具名全局函数? 构造函数被声明为全局作用域中的具名函数(而非内联在 Alpine.data() 回调中),以便它们可以在其他模板中被代理和扩展。这是 Hyvä 主题的一个可扩展性特性——其他模块或子主题可以在这些函数注册到 Alpine 之前包装或覆盖它们。
当组合对象时(例如与 hyva.modal),在构造函数内使用展开语法:
function initMyModal() {
return {
...hyva.modal.call(this),
...hyva.formValidation(this.$el),
customProperty: '',
customMethod() {
// 自定义逻辑
}
};
}
使用 .call(this) 将 Alpine 上下文传递给组合函数。
return {
item: {
is_visible: true,
title: 'Product'
}
}
<span x-show="item.is_visible" x-text="item.title"></span>
CSP 不允许内联转换。请创建方法:
错误(不兼容 CSP):
<span x-show="!item.deleted"></span>
<span x-text="item.title || item.value"></span>
正确:
<span x-show="isItemNotDeleted"></span>
<span x-text="itemLabel"></span>
return {
item: { deleted: false, title: '', value: '' },
isItemNotDeleted() {
return !this.item.deleted;
},
itemLabel() {
return this.item.title || this.item.value;
}
}
对于简单的布尔取反,使用括号表示法:
return {
deleted: false,
['!deleted']() {
return !this.deleted;
}
}
<template x-if="!deleted">
<div>The item is present</div>
</template>
错误(不兼容 CSP):
<button @click="open = !open">Toggle</button>
正确:
<button @click="toggle">Toggle</button>
return {
open: false,
toggle() {
this.open = !this.open;
}
}
错误(不兼容 CSP):
<button @click="selectItem(123)">Select</button>
正确:
<button @click="selectItem" data-item-id="<?= $escaper->escapeHtmlAttr($itemId) ?>">Select</button>
return {
selected: null,
selectItem() {
this.selected = this.$el.dataset.itemId;
}
}
重要: 对数据属性使用 escapeHtmlAttr,而非 escapeJs。
方法可以访问 Alpine 的特殊属性:
return {
onInput() {
// 访问事件
const value = this.$event.target.value;
this.inputValue = value;
},
getItemUrl() {
// 访问 x-for 循环变量
return `${BASE_URL}/product/id/${this.item.id}`;
}
}
x-model 在 Alpine CSP 中不可用。请改用双向绑定模式。
<input type="text"
:value="username"
@input="setUsername">
return {
username: '',
setUsername() {
this.username = this.$event.target.value;
}
}
对数值使用 hyva.safeParseNumber():
return {
quantity: 1,
setQuantity() {
this.quantity = hyva.safeParseNumber(this.$event.target.value);
}
}
<textarea @input="setComment" x-text="comment"></textarea>
return {
comment: '',
setComment() {
this.comment = this.$event.target.value;
}
}
<input type="checkbox"
:checked="isSubscribed"
@change="toggleSubscribed">
return {
isSubscribed: false,
toggleSubscribed() {
this.isSubscribed = this.$event.target.checked;
}
}
<template x-for="option in options" :key="option.id">
<input type="checkbox"
:value="option.id"
:checked="isOptionSelected"
@change="toggleOption"
:data-option-id="option.id">
</template>
return {
selectedOptions: [],
isOptionSelected() {
return this.selectedOptions.includes(this.option.id);
},
toggleOption() {
const optionId = this.$el.dataset.optionId;
const index = this.selectedOptions.indexOf(optionId);
if (index === -1) {
this.selectedOptions.push(optionId);
} else {
this.selectedOptions.splice(index, 1);
}
}
}
<select @change="setCountry">
<template x-for="country in countries" :key="country.code">
<option :value="country.code"
:selected="isCountrySelected"
x-text="country.name"></option>
</template>
</select>
return {
selectedCountry: '',
isCountrySelected() {
return this.selectedCountry === this.country.code;
},
setCountry() {
this.selectedCountry = this.$event.target.value;
}
}
<template x-for="(product, index) in products" :key="index">
<div x-text="product.name"></div>
</template>
循环变量(product、index)可以在方法中访问:
<template x-for="(product, index) in products" :key="index">
<span :class="getItemClasses" @click="goToProduct" x-text="product.name"></span>
</template>
return {
products: [],
getItemClasses() {
return {
'font-bold': this.index === 0,
'text-gray-500': this.product.disabled
};
},
goToProduct() {
window.location.href = `${BASE_URL}/product/${this.product.url_key}`;
}
}
值提供者可以是一个方法(调用时不带括号):
<template x-for="(item, index) in getFilteredItems" :key="index">
<div x-text="item.name"></div>
</template>
return {
items: [],
filter: '',
getFilteredItems() {
return this.items.filter(item => item.name.includes(this.filter));
}
}
注意: 范围迭代(x-for="i in 10")在 Alpine CSP 中不受支持。
全局 hyva 对象提供以下实用工具:
hyva.getFormKey() - 获取/生成用于 POST 请求的表单密钥hyva.getUenc() - 对当前 URL 进行 Base64 编码以用于重定向hyva.postForm({action, data, skipUenc}) - 以编程方式提交 POST 表单hyva.getCookie(name) - 获取 Cookie 值(尊重同意设置)hyva.setCookie(name, value, days, skipSetDomain) - 设置 Cookiehyva.setSessionCookie(name, value, skipSetDomain) - 设置会话 Cookiehyva.formatPrice(value, showSign, options) - 格式化货币hyva.str(template, ...args) - 使用 %1、%2 占位符进行字符串插值hyva.strf(template, ...args) - 基于零的字符串插值(%0、%1)hyva.safeParseNumber(rawValue) - 安全解析数字(用于替代 x-model.number)hyva.replaceDomElement(selector, content) - 用 HTML 内容替换 DOM 元素hyva.trapFocus(rootElement) - 将焦点限制在元素内(用于模态框)hyva.releaseFocus(rootElement) - 释放焦点限制hyva.getBrowserStorage() - 安全地获取 localStorage/sessionStorage对于切换组件,使用 hyva.createBooleanObject:
function initToggle() {
return {
...hyva.createBooleanObject('open', false),
// 额外的方法
};
}
这会生成:open()、notOpen()、toggleOpen()、setOpenTrue()、setOpenFalse()
hyva.alpineInitialized(fn) // Alpine 初始化后运行回调
<div x-data="initMyComponent"
@private-content-loaded.window="onPrivateContentLoaded"
@update-gallery.window="onGalleryUpdate">
return {
onPrivateContentLoaded() {
const data = this.$event.detail.data;
// 处理客户数据
},
onGalleryUpdate() {
const images = this.$event.detail;
this.images = images;
}
}
return {
updateQuantity() {
this.qty = newValue;
this.$dispatch('update-qty-' + this.productId, this.qty);
}
}
private-content-loaded - 客户部分数据已加载reload-customer-section-data - 请求刷新客户数据update-gallery - 产品图库图片已更改reset-gallery - 将图库重置为初始状态对于多个窗口/文档事件监听器,使用 x-bind 模式:
<div x-data="initGallery" x-bind="eventListeners">
return {
eventListeners: {
['@keydown.window.escape']() {
if (!this.fullscreen) return;
this.closeFullScreen();
},
['@update-gallery.window'](event) {
this.receiveImages(event.detail);
},
['@keyup.arrow-right.window']() {
if (!this.fullscreen) return;
this.nextItem();
}
}
}
从方法中返回类对象:
<div :class="containerClasses">
return {
fullscreen: false,
containerClasses() {
return {
'w-full h-full fixed top-0 left-0 bg-white z-50': this.fullscreen,
'relative': !this.fullscreen
};
}
}
<div x-data="initProductList"
data-products="<?= $escaper->escapeHtmlAttr(json_encode($products)) ?>"
data-config="<?= $escaper->escapeHtmlAttr(json_encode($config)) ?>">
return {
products: [],
config: {},
init() {
this.products = JSON.parse(this.$root.dataset.products || '[]');
this.config = JSON.parse(this.$root.dataset.config || '{}');
}
}
function initComponent() {
return {
productId: '<?= (int) $product->getId() ?>',
productName: '<?= $escaper->escapeJs($product->getName()) ?>',
config: <?= /* @noEscape */ json_encode($config) ?>
}
}
<?php
declare(strict_types=1);
use Hyva\Theme\ViewModel\HyvaCsp;
use Magento\Framework\Escaper;
/** @var Escaper $escaper */
/** @var HyvaCsp $hyvaCsp */
$productId = (int) $product->getId();
$minQty = 1;
$maxQty = 100;
$defaultQty = 1;
?>
<div x-data="initQtySelector">
<label for="qty-<?= $productId ?>" class="sr-only">
<?= $escaper->escapeHtml(__('Quantity')) ?>
</label>
<div class="flex items-center">
<button type="button"
class="btn"
@click="decrement"
:disabled="isMinQty"
:class="decrementClasses">
-
</button>
<input type="number"
id="qty-<?= $productId ?>"
name="qty"
:value="qty"
@input="onInput"
min="<?= $minQty ?>"
max="<?= $maxQty ?>"
class="form-input w-16 text-center">
<button type="button"
class="btn"
@click="increment"
:disabled="isMaxQty"
:class="incrementClasses">
+
</button>
</div>
</div>
<script>
function initQtySelector() {
return {
qty: <?= (int) $defaultQty ?>,
minQty: <?= (int) $minQty ?>,
maxQty: <?= (int) $maxQty ?>,
productId: '<?= $productId ?>',
onInput() {
let value = hyva.safeParseNumber(this.$event.target.value);
if (value < this.minQty) value = this.minQty;
if (value > this.maxQty) value = this.maxQty;
this.qty = value;
this.$dispatch('update-qty-' + this.productId, this.qty);
},
increment() {
if (this.qty < this.maxQty) {
this.qty++;
this.$dispatch('update-qty-' + this.productId, this.qty);
}
},
decrement() {
if (this.qty > this.minQty) {
this.qty--;
this.$dispatch('update-qty-' + this.productId, this.qty);
}
},
isMinQty() {
return this.qty <= this.minQty;
},
isMaxQty() {
return this.qty >= this.maxQty;
},
decrementClasses() {
return { 'opacity-50 cursor-not-allowed': this.isMinQty() };
},
incrementClasses() {
return { 'opacity-50 cursor-not-allowed': this.isMaxQty() };
}
}
}
window.addEventListener('alpine:init', () => Alpine.data('initQtySelector', initQtySelector), {once: true})
</script>
<?php $hyvaCsp->registerInlineScript() ?>
vendor/hyva-themes/magento2-default-theme-csp/vendor/hyva-themes/magento2-theme-module/src/view/frontend/templates/page/js/hyva.phtml每周安装数
160
代码仓库
GitHub 星标数
59
首次出现
2026年1月27日
安全审计
安装于
github-copilot156
opencode153
codex152
gemini-cli148
amp146
kimi-cli146
This skill provides guidance for writing CSP-compatible Alpine.js components in Hyvä themes. Alpine CSP is a specialized Alpine.js build that operates without the unsafe-eval CSP directive, which is required for PCI-DSS 4.0 compliance on payment-related pages (mandatory from April 1, 2025).
Key principle: CSP-compatible code functions in both standard and Alpine CSP builds. Write all Alpine code using CSP patterns for future-proofing.
| Capability | Standard Alpine | Alpine CSP |
|---|---|---|
| Property reads | x-show="open" | Same |
| Negation | x-show="!open" | Method: x-show="isNotOpen" |
| Mutations | @click="open = false" | Method: @click="close" |
| Method args | @click="setTab('info')" | Dataset: @click="setTab" data-tab="info" |
x-model | Available | Not supported - use :value + @input |
| Range iteration | x-for="i in 10" | Not supported |
Every Alpine component in Hyvä follows this structure:
<div x-data="initComponentName">
<!-- Template content -->
</div>
<script>
function initComponentName() {
return {
// Properties
propertyName: initialValue,
// Lifecycle
init() {
// Called when component initializes
},
// Methods for state access
isPropertyTrue() {
return this.propertyName === true;
},
// Methods for mutations
setPropertyValue() {
this.propertyName = this.$event.target.value;
}
}
}
window.addEventListener('alpine:init', () => Alpine.data('initComponentName', initComponentName), {once: true})
</script>
<?php $hyvaCsp->registerInlineScript() ?>
Critical requirements:
Alpine.data() inside alpine:init event listener{once: true} to prevent duplicate registrations$hyvaCsp->registerInlineScript() after every <script> block$escaper->escapeJs() for PHP values in JavaScript strings$escaper->escapeHtmlAttr() for data attributes (not escapeJs)function initMyComponent() {
return {
open: false
}
}
window.addEventListener('alpine:init', () => Alpine.data('initMyComponent', initMyComponent), {once: true})
Why named global functions? Constructor functions are declared as named functions in global scope (not inlined in the Alpine.data() callback) so they can be proxied and extended in other templates. This is an extensibility feature of Hyvä Themes - other modules or child themes can wrap or override these functions before they are registered with Alpine.
When combining objects (e.g., with hyva.modal), use spread syntax inside the constructor:
function initMyModal() {
return {
...hyva.modal.call(this),
...hyva.formValidation(this.$el),
customProperty: '',
customMethod() {
// Custom logic
}
};
}
Use .call(this) to pass Alpine context to composed functions.
return {
item: {
is_visible: true,
title: 'Product'
}
}
<span x-show="item.is_visible" x-text="item.title"></span>
CSP does not allow inline transformations. Create methods instead:
Wrong (CSP incompatible):
<span x-show="!item.deleted"></span>
<span x-text="item.title || item.value"></span>
Correct:
<span x-show="isItemNotDeleted"></span>
<span x-text="itemLabel"></span>
return {
item: { deleted: false, title: '', value: '' },
isItemNotDeleted() {
return !this.item.deleted;
},
itemLabel() {
return this.item.title || this.item.value;
}
}
For simple boolean negation, use bracket notation:
return {
deleted: false,
['!deleted']() {
return !this.deleted;
}
}
<template x-if="!deleted">
<div>The item is present</div>
</template>
Wrong (CSP incompatible):
<button @click="open = !open">Toggle</button>
Correct:
<button @click="toggle">Toggle</button>
return {
open: false,
toggle() {
this.open = !this.open;
}
}
Wrong (CSP incompatible):
<button @click="selectItem(123)">Select</button>
Correct:
<button @click="selectItem" data-item-id="<?= $escaper->escapeHtmlAttr($itemId) ?>">Select</button>
return {
selected: null,
selectItem() {
this.selected = this.$el.dataset.itemId;
}
}
Important: Use escapeHtmlAttr for data attributes, not escapeJs.
Methods can access Alpine's special properties:
return {
onInput() {
// Access event
const value = this.$event.target.value;
this.inputValue = value;
},
getItemUrl() {
// Access x-for loop variable
return `${BASE_URL}/product/id/${this.item.id}`;
}
}
x-model is not available in Alpine CSP. Use two-way binding patterns instead.
<input type="text"
:value="username"
@input="setUsername">
return {
username: '',
setUsername() {
this.username = this.$event.target.value;
}
}
Use hyva.safeParseNumber() for numeric values:
return {
quantity: 1,
setQuantity() {
this.quantity = hyva.safeParseNumber(this.$event.target.value);
}
}
<textarea @input="setComment" x-text="comment"></textarea>
return {
comment: '',
setComment() {
this.comment = this.$event.target.value;
}
}
<input type="checkbox"
:checked="isSubscribed"
@change="toggleSubscribed">
return {
isSubscribed: false,
toggleSubscribed() {
this.isSubscribed = this.$event.target.checked;
}
}
<template x-for="option in options" :key="option.id">
<input type="checkbox"
:value="option.id"
:checked="isOptionSelected"
@change="toggleOption"
:data-option-id="option.id">
</template>
return {
selectedOptions: [],
isOptionSelected() {
return this.selectedOptions.includes(this.option.id);
},
toggleOption() {
const optionId = this.$el.dataset.optionId;
const index = this.selectedOptions.indexOf(optionId);
if (index === -1) {
this.selectedOptions.push(optionId);
} else {
this.selectedOptions.splice(index, 1);
}
}
}
<select @change="setCountry">
<template x-for="country in countries" :key="country.code">
<option :value="country.code"
:selected="isCountrySelected"
x-text="country.name"></option>
</template>
</select>
return {
selectedCountry: '',
isCountrySelected() {
return this.selectedCountry === this.country.code;
},
setCountry() {
this.selectedCountry = this.$event.target.value;
}
}
<template x-for="(product, index) in products" :key="index">
<div x-text="product.name"></div>
</template>
Loop variables (product, index) are accessible in methods:
<template x-for="(product, index) in products" :key="index">
<span :class="getItemClasses" @click="goToProduct" x-text="product.name"></span>
</template>
return {
products: [],
getItemClasses() {
return {
'font-bold': this.index === 0,
'text-gray-500': this.product.disabled
};
},
goToProduct() {
window.location.href = `${BASE_URL}/product/${this.product.url_key}`;
}
}
The value provider can be a method (called without parentheses):
<template x-for="(item, index) in getFilteredItems" :key="index">
<div x-text="item.name"></div>
</template>
return {
items: [],
filter: '',
getFilteredItems() {
return this.items.filter(item => item.name.includes(this.filter));
}
}
Note: Range iteration (x-for="i in 10") is not supported in Alpine CSP.
The global hyva object provides these utilities:
hyva.getFormKey() - Get/generate form key for POST requestshyva.getUenc() - Base64 encode current URL for redirectshyva.postForm({action, data, skipUenc}) - Submit a POST form programmaticallyhyva.getCookie(name) - Get cookie value (respects consent)hyva.setCookie(name, value, days, skipSetDomain) - Set cookiehyva.setSessionCookie(name, value, skipSetDomain) - Set session cookiehyva.formatPrice(value, showSign, options) - Format currencyhyva.str(template, ...args) - String interpolation with %1, %2 placeholdershyva.strf(template, ...args) - Zero-based string interpolation (%0, %1)hyva.safeParseNumber(rawValue) - Parse number safely (for x-model.number replacement)hyva.replaceDomElement(selector, content) - Replace DOM element with HTML contenthyva.trapFocus(rootElement) - Trap focus within element (for modals)hyva.releaseFocus(rootElement) - Release focus traphyva.getBrowserStorage() - Get localStorage/sessionStorage safelyFor toggle components, use hyva.createBooleanObject:
function initToggle() {
return {
...hyva.createBooleanObject('open', false),
// Additional methods
};
}
This generates: open(), notOpen(), toggleOpen(), setOpenTrue(), setOpenFalse()
hyva.alpineInitialized(fn) // Run callback after Alpine initializes
<div x-data="initMyComponent"
@private-content-loaded.window="onPrivateContentLoaded"
@update-gallery.window="onGalleryUpdate">
return {
onPrivateContentLoaded() {
const data = this.$event.detail.data;
// Handle customer data
},
onGalleryUpdate() {
const images = this.$event.detail;
this.images = images;
}
}
return {
updateQuantity() {
this.qty = newValue;
this.$dispatch('update-qty-' + this.productId, this.qty);
}
}
private-content-loaded - Customer section data loadedreload-customer-section-data - Request customer data refreshupdate-gallery - Product gallery images changedreset-gallery - Reset gallery to initial stateFor multiple window/document event listeners, use the x-bind pattern:
<div x-data="initGallery" x-bind="eventListeners">
return {
eventListeners: {
['@keydown.window.escape']() {
if (!this.fullscreen) return;
this.closeFullScreen();
},
['@update-gallery.window'](event) {
this.receiveImages(event.detail);
},
['@keyup.arrow-right.window']() {
if (!this.fullscreen) return;
this.nextItem();
}
}
}
Return class objects from methods:
<div :class="containerClasses">
return {
fullscreen: false,
containerClasses() {
return {
'w-full h-full fixed top-0 left-0 bg-white z-50': this.fullscreen,
'relative': !this.fullscreen
};
}
}
<div x-data="initProductList"
data-products="<?= $escaper->escapeHtmlAttr(json_encode($products)) ?>"
data-config="<?= $escaper->escapeHtmlAttr(json_encode($config)) ?>">
return {
products: [],
config: {},
init() {
this.products = JSON.parse(this.$root.dataset.products || '[]');
this.config = JSON.parse(this.$root.dataset.config || '{}');
}
}
function initComponent() {
return {
productId: '<?= (int) $product->getId() ?>',
productName: '<?= $escaper->escapeJs($product->getName()) ?>',
config: <?= /* @noEscape */ json_encode($config) ?>
}
}
<?php
declare(strict_types=1);
use Hyva\Theme\ViewModel\HyvaCsp;
use Magento\Framework\Escaper;
/** @var Escaper $escaper */
/** @var HyvaCsp $hyvaCsp */
$productId = (int) $product->getId();
$minQty = 1;
$maxQty = 100;
$defaultQty = 1;
?>
<div x-data="initQtySelector">
<label for="qty-<?= $productId ?>" class="sr-only">
<?= $escaper->escapeHtml(__('Quantity')) ?>
</label>
<div class="flex items-center">
<button type="button"
class="btn"
@click="decrement"
:disabled="isMinQty"
:class="decrementClasses">
-
</button>
<input type="number"
id="qty-<?= $productId ?>"
name="qty"
:value="qty"
@input="onInput"
min="<?= $minQty ?>"
max="<?= $maxQty ?>"
class="form-input w-16 text-center">
<button type="button"
class="btn"
@click="increment"
:disabled="isMaxQty"
:class="incrementClasses">
+
</button>
</div>
</div>
<script>
function initQtySelector() {
return {
qty: <?= (int) $defaultQty ?>,
minQty: <?= (int) $minQty ?>,
maxQty: <?= (int) $maxQty ?>,
productId: '<?= $productId ?>',
onInput() {
let value = hyva.safeParseNumber(this.$event.target.value);
if (value < this.minQty) value = this.minQty;
if (value > this.maxQty) value = this.maxQty;
this.qty = value;
this.$dispatch('update-qty-' + this.productId, this.qty);
},
increment() {
if (this.qty < this.maxQty) {
this.qty++;
this.$dispatch('update-qty-' + this.productId, this.qty);
}
},
decrement() {
if (this.qty > this.minQty) {
this.qty--;
this.$dispatch('update-qty-' + this.productId, this.qty);
}
},
isMinQty() {
return this.qty <= this.minQty;
},
isMaxQty() {
return this.qty >= this.maxQty;
},
decrementClasses() {
return { 'opacity-50 cursor-not-allowed': this.isMinQty() };
},
incrementClasses() {
return { 'opacity-50 cursor-not-allowed': this.isMaxQty() };
}
}
}
window.addEventListener('alpine:init', () => Alpine.data('initQtySelector', initQtySelector), {once: true})
</script>
<?php $hyvaCsp->registerInlineScript() ?>
vendor/hyva-themes/magento2-default-theme-csp/vendor/hyva-themes/magento2-theme-module/src/view/frontend/templates/page/js/hyva.phtmlWeekly Installs
160
Repository
GitHub Stars
59
First Seen
Jan 27, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
github-copilot156
opencode153
codex152
gemini-cli148
amp146
kimi-cli146
Lark Mail CLI 使用指南:邮件管理、安全规则与自动化工作流
37,000 周安装