From 12a65fc8b7019a86c3225576c34540706f1863d8 Mon Sep 17 00:00:00 2001 From: Krystian Podemski Date: Tue, 28 Apr 2026 13:06:21 +0200 Subject: [PATCH] POC Decouple OPC from Hummingbird --- .../front/AbstractOpcJsonFrontController.php | 16 + controllers/front/addresseslist.php | 4 +- controllers/front/addressform.php | 2 +- controllers/front/carriers.php | 2 +- controllers/front/paymentmethods.php | 2 +- docs/CORE_PORTING_PLAYBOOK.md | 6 +- docs/RULES.md | 2 +- ps_onepagecheckout.php | 18 + src/Checkout/CheckoutOnePageStep.php | 2 +- src/Checkout/PaymentSelectionKeyBuilder.php | 2 +- src/Form/BackOfficeConfigurationForm.php | 4 +- .../Controller/AddressFormControllerTest.php | 5 + views/js/opc-cart-summary-state.js | 68 ++ views/package-lock.json | 749 ++++++++++++++++++ views/package.json | 5 + views/public/one-page-checkout.bundle.js | 0 views/public/one-page-checkout.css | 1 + views/public/opc-cart-summary-state.bundle.js | 2 + ...c-cart-summary-state.bundle.js.LICENSE.txt | 24 + .../opc-checkout-layout.bundle.js.LICENSE.txt | 24 + views/scss/one-page-checkout.scss | 55 ++ .../front/checkout/_partials/index.php | 29 + .../one-page-checkout/address-fields.tpl | 75 ++ .../one-page-checkout/address-list.tpl | 125 +++ .../one-page-checkout/address-modal.tpl | 113 +++ .../one-page-checkout/addresses-section.tpl | 136 ++++ .../_partials/one-page-checkout/carriers.tpl | 60 ++ .../one-page-checkout/contact-section.tpl | 50 ++ .../delete-address-modal.tpl | 49 ++ .../one-page-checkout/delivery-section.tpl | 75 ++ .../_partials/one-page-checkout/index.php | 29 + .../_partials/one-page-checkout/opc-error.tpl | 26 + .../one-page-checkout/opc-loader.tpl | 20 + .../one-page-checkout/order-options.tpl | 70 ++ .../one-page-checkout/payment-methods.tpl | 75 ++ .../one-page-checkout/payment-section.tpl | 53 ++ .../front/checkout/_partials/steps/index.php | 29 + .../_partials/steps/one-page-checkout.tpl | 74 ++ views/templates/front/checkout/index.php | 29 + views/templates/front/index.php | 29 + views/webpack.config.js | 25 + 41 files changed, 2149 insertions(+), 15 deletions(-) create mode 100644 views/js/opc-cart-summary-state.js create mode 100644 views/public/one-page-checkout.bundle.js create mode 100644 views/public/one-page-checkout.css create mode 100644 views/public/opc-cart-summary-state.bundle.js create mode 100644 views/public/opc-cart-summary-state.bundle.js.LICENSE.txt create mode 100644 views/public/opc-checkout-layout.bundle.js.LICENSE.txt create mode 100644 views/scss/one-page-checkout.scss create mode 100644 views/templates/front/checkout/_partials/index.php create mode 100644 views/templates/front/checkout/_partials/one-page-checkout/address-fields.tpl create mode 100644 views/templates/front/checkout/_partials/one-page-checkout/address-list.tpl create mode 100644 views/templates/front/checkout/_partials/one-page-checkout/address-modal.tpl create mode 100644 views/templates/front/checkout/_partials/one-page-checkout/addresses-section.tpl create mode 100644 views/templates/front/checkout/_partials/one-page-checkout/carriers.tpl create mode 100644 views/templates/front/checkout/_partials/one-page-checkout/contact-section.tpl create mode 100644 views/templates/front/checkout/_partials/one-page-checkout/delete-address-modal.tpl create mode 100644 views/templates/front/checkout/_partials/one-page-checkout/delivery-section.tpl create mode 100644 views/templates/front/checkout/_partials/one-page-checkout/index.php create mode 100644 views/templates/front/checkout/_partials/one-page-checkout/opc-error.tpl create mode 100644 views/templates/front/checkout/_partials/one-page-checkout/opc-loader.tpl create mode 100644 views/templates/front/checkout/_partials/one-page-checkout/order-options.tpl create mode 100644 views/templates/front/checkout/_partials/one-page-checkout/payment-methods.tpl create mode 100644 views/templates/front/checkout/_partials/one-page-checkout/payment-section.tpl create mode 100644 views/templates/front/checkout/_partials/steps/index.php create mode 100644 views/templates/front/checkout/_partials/steps/one-page-checkout.tpl create mode 100644 views/templates/front/checkout/index.php create mode 100644 views/templates/front/index.php diff --git a/controllers/front/AbstractOpcJsonFrontController.php b/controllers/front/AbstractOpcJsonFrontController.php index 058a733..083b4d0 100644 --- a/controllers/front/AbstractOpcJsonFrontController.php +++ b/controllers/front/AbstractOpcJsonFrontController.php @@ -46,6 +46,22 @@ protected function getTechnicalErrorResponseExtra(): array return []; } + /** + * Render a module-owned Smarty template by relative path under views/templates/front/. + * Goes through the `module:` Smarty resource so theme overrides under + * `themes//modules/ps_onepagecheckout/...` still take precedence. + * + * @param array $params + */ + protected function renderModuleTemplate(string $relativePath, array $params): string + { + $this->context->smarty->assign($params); + + return $this->context->smarty->fetch( + 'module:ps_onepagecheckout/views/templates/front/' . ltrim($relativePath, '/') . '.tpl' + ); + } + /** * @param array $response */ diff --git a/controllers/front/addresseslist.php b/controllers/front/addresseslist.php index 18abc5e..43e9e73 100644 --- a/controllers/front/addresseslist.php +++ b/controllers/front/addresseslist.php @@ -33,7 +33,7 @@ protected function handleOpcRequest(): array return [ 'success' => true, 'address_count' => (int) ($response['address_count'] ?? 0), - 'delivery_html' => $this->render( + 'delivery_html' => $this->renderModuleTemplate( 'checkout/_partials/one-page-checkout/address-list', [ 'customer' => $response['customer'] ?? [], @@ -41,7 +41,7 @@ protected function handleOpcRequest(): array 'selected_address' => (int) ($response['selected_delivery_address'] ?? 0), ] ), - 'billing_html' => $this->render( + 'billing_html' => $this->renderModuleTemplate( 'checkout/_partials/one-page-checkout/address-list', [ 'customer' => $response['customer'] ?? [], diff --git a/controllers/front/addressform.php b/controllers/front/addressform.php index 86f89c1..3b76e0f 100644 --- a/controllers/front/addressform.php +++ b/controllers/front/addressform.php @@ -28,7 +28,7 @@ protected function handleOpcRequest(): array $templateVariables = $handler->getTemplateVariables(Tools::getAllValues()); return [ - 'addresses_section' => $this->render( + 'addresses_section' => $this->renderModuleTemplate( 'checkout/_partials/one-page-checkout/addresses-section', $templateVariables ), diff --git a/controllers/front/carriers.php b/controllers/front/carriers.php index c5123fa..fa49744 100644 --- a/controllers/front/carriers.php +++ b/controllers/front/carriers.php @@ -23,7 +23,7 @@ protected function handleOpcRequest(): array $response = $handler->handle(Tools::getAllValues()); if (!empty($response['success'])) { - $response['carriers_html'] = $this->render( + $response['carriers_html'] = $this->renderModuleTemplate( 'checkout/_partials/one-page-checkout/carriers', [ 'delivery_options' => $response['delivery_options'] ?? [], diff --git a/controllers/front/paymentmethods.php b/controllers/front/paymentmethods.php index 4cb466e..80c9515 100644 --- a/controllers/front/paymentmethods.php +++ b/controllers/front/paymentmethods.php @@ -24,7 +24,7 @@ protected function handleOpcRequest(): array $response = $handler->handle(Tools::getAllValues()); if (!empty($response['success'])) { - $response['payment_html'] = $this->render( + $response['payment_html'] = $this->renderModuleTemplate( 'checkout/_partials/one-page-checkout/payment-methods', [ 'payment_options' => $response['payment_options'] ?? [], diff --git a/docs/CORE_PORTING_PLAYBOOK.md b/docs/CORE_PORTING_PLAYBOOK.md index deb5bb5..b6dd706 100644 --- a/docs/CORE_PORTING_PLAYBOOK.md +++ b/docs/CORE_PORTING_PLAYBOOK.md @@ -18,11 +18,7 @@ When a Core checkout event already exists, document separately: - listener compatibility already expected by the existing runtime, - emitter ownership once the module becomes responsible for triggering the event. -Put a change in Hummingbird when it is: -- template structure, -- section placeholders, -- visual states, -- style-only behavior. +Put a change in the active theme only when it is page chrome unrelated to OPC step content (the surrounding `checkout.tpl` wrapper, breadcrumb, notifications, cart-summary panel). All OPC-step templates, partials, styles, and runtime JS live in this module. Put a change in Core only when: - the module would otherwise need an override, diff --git a/docs/RULES.md b/docs/RULES.md index 2651ef8..274e0f5 100644 --- a/docs/RULES.md +++ b/docs/RULES.md @@ -46,7 +46,7 @@ Both entry points must render the same module-owned configuration flow (no redir 1. Triage every future checkout change using the playbook before coding. 2. Correct existing parity gaps in the module before porting new Core behavior. 3. Keep Core changes to the minimal no-override surface only. -4. Keep checkout business logic in the module and DOM/visual ownership in Hummingbird. +4. Keep all OPC step DOM, styles, and runtime JS inside the module. The theme owns only its own page chrome (checkout page wrapper, cart summary, page-level notifications). ## Test workflow diff --git a/ps_onepagecheckout.php b/ps_onepagecheckout.php index 6e9f8d3..69d5784 100644 --- a/ps_onepagecheckout.php +++ b/ps_onepagecheckout.php @@ -287,6 +287,7 @@ public function hookActionFrontControllerSetMedia(): void ]); $this->registerOpcJavascriptAssets(); + $this->registerOpcStylesheets(); } public function hookActionFrontControllerSetVariables(array $params): void @@ -346,6 +347,7 @@ protected function registerOpcJavascriptAssets(): void ['module-ps-onepagecheckout-select-carrier', 'views/public/opc-carrier-select.bundle.js', 154], ['module-ps-onepagecheckout-payment-methods', 'views/public/opc-payment-list.bundle.js', 155], ['module-ps-onepagecheckout-select-payment', 'views/public/opc-payment-select.bundle.js', 156], + ['module-ps-onepagecheckout-cart-summary-state', 'views/public/opc-cart-summary-state.bundle.js', 157], ] as [$id, $path, $priority]) { $this->context->controller->registerJavascript( $id, @@ -363,6 +365,22 @@ protected function addOpcJavascriptDefinition(array $javascriptDefinition): void Media::addJsDef($javascriptDefinition); } + protected function registerOpcStylesheets(): void + { + if (!isset($this->context->controller)) { + return; + } + + $this->context->controller->registerStylesheet( + 'module-ps-onepagecheckout', + 'modules/' . $this->name . '/views/public/one-page-checkout.css', + [ + 'media' => 'all', + 'priority' => 200, + ] + ); + } + protected function installInParent(): bool { return parent::install(); diff --git a/src/Checkout/CheckoutOnePageStep.php b/src/Checkout/CheckoutOnePageStep.php index 3bbe6ce..6862e60 100644 --- a/src/Checkout/CheckoutOnePageStep.php +++ b/src/Checkout/CheckoutOnePageStep.php @@ -33,7 +33,7 @@ class CheckoutOnePageStep extends \AbstractCheckoutStep { - protected $template = 'checkout/_partials/steps/one-page-checkout.tpl'; + protected $template = 'module:ps_onepagecheckout/views/templates/front/checkout/_partials/steps/one-page-checkout.tpl'; /** * @var OnePageCheckoutForm diff --git a/src/Checkout/PaymentSelectionKeyBuilder.php b/src/Checkout/PaymentSelectionKeyBuilder.php index 3e1ae5f..4de96ac 100644 --- a/src/Checkout/PaymentSelectionKeyBuilder.php +++ b/src/Checkout/PaymentSelectionKeyBuilder.php @@ -50,7 +50,7 @@ public function buildSelectionKey(array $option): string return sprintf( '%s:%s', $moduleName !== '' ? $moduleName : 'unknown', - substr(hash('sha256', json_encode($signature)), 0, 24) + substr(hash('sha256', (string) json_encode($signature, JSON_INVALID_UTF8_SUBSTITUTE)), 0, 24) ); } diff --git a/src/Form/BackOfficeConfigurationForm.php b/src/Form/BackOfficeConfigurationForm.php index b58d66b..6c2a2d4 100644 --- a/src/Form/BackOfficeConfigurationForm.php +++ b/src/Form/BackOfficeConfigurationForm.php @@ -151,11 +151,11 @@ private function buildTemplateVariables(): array 'confirm_modal_title' => $this->trans('Change checkout appearance', 'Modules.PsOnePageCheckout.Admin'), 'confirm_modal_compatibility_title' => $this->trans('Check theme compatibility', 'Modules.PsOnePageCheckout.Admin'), 'confirm_modal_description' => $this->trans('You\'re about to update the Checkout appearance of your store.', 'Modules.PsOnePageCheckout.Admin'), - 'confirm_modal_compatibility_description' => $this->trans('One-page checkout layout requires a Hummingbird-compatible theme.', 'Modules.PsOnePageCheckout.Admin'), + 'confirm_modal_compatibility_description' => $this->trans('One-page checkout layout requires a Bootstrap 5-compatible theme.', 'Modules.PsOnePageCheckout.Admin'), 'confirm_modal_checklist_title' => $this->trans('Before you proceed, please make sure that:', 'Modules.PsOnePageCheckout.Admin'), 'confirm_modal_checklist' => [ $this->trans('You are NOT using the "Classic" theme: This layout is incompatible with the legacy Classic theme and will break your storefront.', 'Modules.PsOnePageCheckout.Admin'), - $this->trans('Your theme is Hummingbird-based: Ensure your custom or third-party theme supports this new checkout architecture.', 'Modules.PsOnePageCheckout.Admin'), + $this->trans('Your theme is compatible with the one-page checkout DOM and Bootstrap 5: Custom or third-party themes must support this new checkout architecture.', 'Modules.PsOnePageCheckout.Admin'), $this->trans('Your checkout modules are compatible. Some older modules may not be compatible with a one-page layout.', 'Modules.PsOnePageCheckout.Admin'), ], 'cancel_button_label' => $this->trans('Cancel', 'Admin.Actions'), diff --git a/tests/php/Unit/Controller/AddressFormControllerTest.php b/tests/php/Unit/Controller/AddressFormControllerTest.php index 26199e9..24526bd 100644 --- a/tests/php/Unit/Controller/AddressFormControllerTest.php +++ b/tests/php/Unit/Controller/AddressFormControllerTest.php @@ -173,6 +173,11 @@ protected function render($template, array $parameters = []) { return sprintf('rendered:%s', (string) $template); } + + protected function renderModuleTemplate(string $relativePath, array $params): string + { + return sprintf('rendered:%s', $relativePath); + } } class EnabledPsOnepagecheckoutModuleForAddressForm extends \Ps_Onepagecheckout diff --git a/views/js/opc-cart-summary-state.js b/views/js/opc-cart-summary-state.js new file mode 100644 index 0000000..1a67042 --- /dev/null +++ b/views/js/opc-cart-summary-state.js @@ -0,0 +1,68 @@ +/** + * Copyright since 2007 PrestaShop SA and Contributors + * PrestaShop is an International Registered Trademark & Property of PrestaShop SA + * + * NOTICE OF LICENSE + * + * This source file is subject to the Open Software License (OSL 3.0) + * that is bundled with this package in the file LICENSE.md. + * It is also available through the world-wide-web at this URL: + * https://opensource.org/licenses/OSL-3.0 + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@prestashop.com so we can send you a copy immediately. + * + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade PrestaShop to newer + * versions in the future. If you wish to customize PrestaShop for your + * needs please refer to https://devdocs.prestashop.com/ for more information. + * + * @author PrestaShop SA and Contributors + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ +import OPC_EVENTS from './events'; + +(function psOpcCartSummaryStateRuntime() { +const prestashop = window.prestashop || null; + +if (!prestashop || typeof prestashop.on !== 'function') { + return; +} + +// Bootstrap accordion items inside the cart summary collapse on every DOM +// replacement. Capture which ones were open before the update and reopen them +// after — no-op when the theme doesn't use Bootstrap accordions. +let openCollapseIds = []; + +prestashop.on(OPC_EVENTS.opcCartSummaryBeforeUpdate, ({selector}) => { + if (typeof selector !== 'string' || selector === '') { + return; + } + + openCollapseIds = Array.from(document.querySelectorAll(`${selector} .accordion-collapse.show`)) + .map((el) => el.id) + .filter(Boolean); +}); + +prestashop.on(OPC_EVENTS.opcCartSummaryUpdated, () => { + openCollapseIds.forEach((id) => { + const el = document.getElementById(id); + + if (!el) { + return; + } + + el.classList.add('show'); + + const btn = document.querySelector(`[data-bs-target="#${id}"]`); + + if (btn) { + btn.classList.remove('collapsed'); + btn.setAttribute('aria-expanded', 'true'); + } + }); + openCollapseIds = []; +}); +})(); diff --git a/views/package-lock.json b/views/package-lock.json index 4f81055..d923692 100644 --- a/views/package-lock.json +++ b/views/package-lock.json @@ -11,6 +11,11 @@ "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "babel-loader": "^9.2.1", + "bootstrap": "^5.3.8", + "css-loader": "^7.1.4", + "mini-css-extract-plugin": "^2.10.2", + "sass": "^1.99.0", + "sass-loader": "^16.0.7", "terser-webpack-plugin": "^5.3.10", "webpack": "^5.98.0", "webpack-cli": "^5.1.4" @@ -1505,6 +1510,328 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@parcel/watcher": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.6.tgz", + "integrity": "sha512-tmmZ3lQxAe/k/+rNnXQRawJ4NjxO2hqiOLTHvWchtGZULp4RyFeh6aU4XdOYBFe2KE1oShQTv4AblOs2iOrNnQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.3", + "is-glob": "^4.0.3", + "node-addon-api": "^7.0.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.6", + "@parcel/watcher-darwin-arm64": "2.5.6", + "@parcel/watcher-darwin-x64": "2.5.6", + "@parcel/watcher-freebsd-x64": "2.5.6", + "@parcel/watcher-linux-arm-glibc": "2.5.6", + "@parcel/watcher-linux-arm-musl": "2.5.6", + "@parcel/watcher-linux-arm64-glibc": "2.5.6", + "@parcel/watcher-linux-arm64-musl": "2.5.6", + "@parcel/watcher-linux-x64-glibc": "2.5.6", + "@parcel/watcher-linux-x64-musl": "2.5.6", + "@parcel/watcher-win32-arm64": "2.5.6", + "@parcel/watcher-win32-ia32": "2.5.6", + "@parcel/watcher-win32-x64": "2.5.6" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.6.tgz", + "integrity": "sha512-YQxSS34tPF/6ZG7r/Ih9xy+kP/WwediEUsqmtf0cuCV5TPPKw/PQHRhueUo6JdeFJaqV3pyjm0GdYjZotbRt/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.6.tgz", + "integrity": "sha512-Z2ZdrnwyXvvvdtRHLmM4knydIdU9adO3D4n/0cVipF3rRiwP+3/sfzpAwA/qKFL6i1ModaabkU7IbpeMBgiVEA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.6.tgz", + "integrity": "sha512-HgvOf3W9dhithcwOWX9uDZyn1lW9R+7tPZ4sug+NGrGIo4Rk1hAXLEbcH1TQSqxts0NYXXlOWqVpvS1SFS4fRg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.6.tgz", + "integrity": "sha512-vJVi8yd/qzJxEKHkeemh7w3YAn6RJCtYlE4HPMoVnCpIXEzSrxErBW5SJBgKLbXU3WdIpkjBTeUNtyBVn8TRng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.6.tgz", + "integrity": "sha512-9JiYfB6h6BgV50CCfasfLf/uvOcJskMSwcdH1PHH9rvS1IrNy8zad6IUVPVUfmXr+u+Km9IxcfMLzgdOudz9EQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.6.tgz", + "integrity": "sha512-Ve3gUCG57nuUUSyjBq/MAM0CzArtuIOxsBdQ+ftz6ho8n7s1i9E1Nmk/xmP323r2YL0SONs1EuwqBp2u1k5fxg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.6.tgz", + "integrity": "sha512-f2g/DT3NhGPdBmMWYoxixqYr3v/UXcmLOYy16Bx0TM20Tchduwr4EaCbmxh1321TABqPGDpS8D/ggOTaljijOA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.6.tgz", + "integrity": "sha512-qb6naMDGlbCwdhLj6hgoVKJl2odL34z2sqkC7Z6kzir8b5W65WYDpLB6R06KabvZdgoHI/zxke4b3zR0wAbDTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.6.tgz", + "integrity": "sha512-kbT5wvNQlx7NaGjzPFu8nVIW1rWqV780O7ZtkjuWaPUgpv2NMFpjYERVi0UYj1msZNyCzGlaCWEtzc+exjMGbQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.6.tgz", + "integrity": "sha512-1JRFeC+h7RdXwldHzTsmdtYR/Ku8SylLgTU/reMuqdVD7CtLwf0VR1FqeprZ0eHQkO0vqsbvFLXUmYm/uNKJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.6.tgz", + "integrity": "sha512-3ukyebjc6eGlw9yRt678DxVF7rjXatWiHvTXqphZLvo7aC5NdEgFufVwjFfY51ijYEWpXbqF5jtrK275z52D4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.6.tgz", + "integrity": "sha512-k35yLp1ZMwwee3Ez/pxBi5cf4AoBKYXj00CZ80jUz5h8prpiaQsiRPKQMxoLstNuqe2vR4RNPEAEcjEFzhEz/g==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.6", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.6.tgz", + "integrity": "sha512-hbQlYcCq5dlAX9Qx+kFb0FHue6vbjlf0FrNzSKdYK2APUf7tGfGxQCk2ihEREmbR6ZMc0MVAD5RIX/41gpUzTw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@popperjs/core": { + "version": "2.11.8", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz", + "integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/popperjs" + } + }, "node_modules/@types/eslint": { "version": "9.6.1", "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", @@ -1885,6 +2212,26 @@ "node": ">=6.0.0" } }, + "node_modules/bootstrap": { + "version": "5.3.8", + "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.8.tgz", + "integrity": "sha512-HP1SZDqaLDPwsNiqRqi5NcP0SSXciX2s9E+RyqJIIqGo+vJeN5AJVM98CXmW/Wux0nQ5L7jeWUdplCEf0Ee+tg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/twbs" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/bootstrap" + } + ], + "license": "MIT", + "peerDependencies": { + "@popperjs/core": "^2.11.8" + } + }, "node_modules/browserslist": { "version": "4.28.1", "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", @@ -1944,6 +2291,22 @@ } ] }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/chrome-trace-event": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", @@ -2018,6 +2381,68 @@ "node": ">= 8" } }, + "node_modules/css-loader": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", + "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.40", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.6.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/debug": { "version": "4.4.3", "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", @@ -2035,6 +2460,17 @@ } } }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.307", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", @@ -2265,6 +2701,26 @@ "node": ">= 0.4" } }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "dev": true, + "license": "MIT" + }, "node_modules/import-local": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", @@ -2381,6 +2837,31 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-plain-object": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", @@ -2543,18 +3024,66 @@ "node": ">= 0.6" } }, + "node_modules/mini-css-extract-plugin": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.10.2.tgz", + "integrity": "sha512-AOSS0IdEB95ayVkxn5oGzNQwqAi2J0Jb/kKm43t7H73s8+f5873g0yuj0PNvK4dO75mu5DHg4nlgp4k6Kga8eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", "dev": true }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/neo-async": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, "node_modules/node-releases": { "version": "2.0.36", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", @@ -2630,6 +3159,20 @@ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", "dev": true }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, "node_modules/pkg-dir": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", @@ -2645,6 +3188,133 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/postcss": { + "version": "8.5.12", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.12.tgz", + "integrity": "sha512-W62t/Se6rA0Az3DfCL0AqJwXuKwBeYg6nOaIgzP+xZ7N5BFCI7DYi1qs6ygUYT6rvfi6t9k65UMLJC+PHZpDAA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/rechoir": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", @@ -2760,6 +3430,68 @@ "node": ">=8" } }, + "node_modules/sass": { + "version": "1.99.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.99.0.tgz", + "integrity": "sha512-kgW13M54DUB7IsIRM5LvJkNlpH+WhMpooUcaWGFARkF1Tc82v9mIWkCbCYf+MBvpIUBSeSOTilpZjEPr2VYE6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "16.0.7", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.7.tgz", + "integrity": "sha512-w6q+fRHourZ+e+xA1kcsF27iGM6jdB8teexYCfdUw0sYgcDNeZESnDNT9sUmmPm3ooziwUJXGwZJSTF3kOdBfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, "node_modules/schema-utils": { "version": "4.3.3", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", @@ -2830,6 +3562,16 @@ "node": ">=0.10.0" } }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/source-map-support": { "version": "0.5.21", "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", @@ -3007,6 +3749,13 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/watchpack": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", diff --git a/views/package.json b/views/package.json index 7921771..141f3d5 100644 --- a/views/package.json +++ b/views/package.json @@ -10,6 +10,11 @@ "@babel/core": "^7.26.0", "@babel/preset-env": "^7.26.0", "babel-loader": "^9.2.1", + "bootstrap": "^5.3.8", + "css-loader": "^7.1.4", + "mini-css-extract-plugin": "^2.10.2", + "sass": "^1.99.0", + "sass-loader": "^16.0.7", "terser-webpack-plugin": "^5.3.10", "webpack": "^5.98.0", "webpack-cli": "^5.1.4" diff --git a/views/public/one-page-checkout.bundle.js b/views/public/one-page-checkout.bundle.js new file mode 100644 index 0000000..e69de29 diff --git a/views/public/one-page-checkout.css b/views/public/one-page-checkout.css new file mode 100644 index 0000000..a581d4f --- /dev/null +++ b/views/public/one-page-checkout.css @@ -0,0 +1 @@ +@media(min-width: 992px){.one-page-checkout{padding-right:2em}}.one-page-checkout__section{padding-top:1em;padding-bottom:1em}.one-page-checkout__section+.one-page-checkout__section,.js-opc-addresses-section+.one-page-checkout__section{padding-top:2em;border-top:1px solid var(--bs-border-color)}.one-page-checkout__links{display:flex;flex-wrap:wrap-reverse;gap:.5rem;justify-content:space-between;margin-bottom:1.5rem}.one-page-checkout__field{margin-bottom:1rem}.one-page-checkout__footer{padding-top:2em}.one-page-checkout__footer .form-check{margin-bottom:1rem}.one-page-checkout .opc-address-item .form-check-input,.one-page-checkout .opc-address-item .form-check-label{cursor:pointer} diff --git a/views/public/opc-cart-summary-state.bundle.js b/views/public/opc-cart-summary-state.bundle.js new file mode 100644 index 0000000..5ebb300 --- /dev/null +++ b/views/public/opc-cart-summary-state.bundle.js @@ -0,0 +1,2 @@ +/*! For license information please see opc-cart-summary-state.bundle.js.LICENSE.txt */ +(()=>{"use strict";const e={opcCarrierSelected:"opcCarrierSelected",opcCarriersUpdated:"opcCarriersUpdated",opcCarriersFailed:"opcCarriersFailed",opcCarriersLoading:"opcCarriersLoading",opcPaymentMethodsLoading:"opcPaymentMethodsLoading",opcPaymentMethodsUpdated:"opcPaymentMethodsUpdated",opcPaymentMethodsFailed:"opcPaymentMethodsFailed",opcPaymentMethodSelected:"opcPaymentMethodSelected",opcGuestInitSuccess:"opcGuestInitSuccess",opcFinalSubmitStarted:"opcFinalSubmitStarted",opcFormValidated:"opcFormValidated",opcBillingSectionToggled:"opcBillingSectionToggled",opcSubmitFailed:"opcSubmitFailed",opcDeliveryAddressUpdated:"opcDeliveryAddressUpdated",opcBillingAddressUpdated:"opcBillingAddressUpdated",opcDeliveryAddressSelected:"opcDeliveryAddressSelected",opcBillingAddressSelected:"opcBillingAddressSelected",opcCartSummaryBeforeUpdate:"opcCartSummaryBeforeUpdate",opcCartSummaryUpdated:"opcCartSummaryUpdated",updatedOpcAddressForm:"updatedOpcAddressForm",opcCarriersRetry:"opcCarriersRetry",opcPaymentMethodsRetry:"opcPaymentMethodsRetry"};!function(){var t=window.prestashop||null;if(t&&"function"==typeof t.on){var o=[];t.on(e.opcCartSummaryBeforeUpdate,function(e){var t=e.selector;"string"==typeof t&&""!==t&&(o=Array.from(document.querySelectorAll("".concat(t," .accordion-collapse.show"))).map(function(e){return e.id}).filter(Boolean))}),t.on(e.opcCartSummaryUpdated,function(){o.forEach(function(e){var t=document.getElementById(e);if(t){t.classList.add("show");var o=document.querySelector('[data-bs-target="#'.concat(e,'"]'));o&&(o.classList.remove("collapsed"),o.setAttribute("aria-expanded","true"))}}),o=[]})}}()})(); \ No newline at end of file diff --git a/views/public/opc-cart-summary-state.bundle.js.LICENSE.txt b/views/public/opc-cart-summary-state.bundle.js.LICENSE.txt new file mode 100644 index 0000000..5ff9a04 --- /dev/null +++ b/views/public/opc-cart-summary-state.bundle.js.LICENSE.txt @@ -0,0 +1,24 @@ +/** + * Copyright since 2007 PrestaShop SA and Contributors + * PrestaShop is an International Registered Trademark & Property of PrestaShop SA + * + * NOTICE OF LICENSE + * + * This source file is subject to the Open Software License (OSL 3.0) + * that is bundled with this package in the file LICENSE.md. + * It is also available through the world-wide-web at this URL: + * https://opensource.org/licenses/OSL-3.0 + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@prestashop.com so we can send you a copy immediately. + * + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade PrestaShop to newer + * versions in the future. If you wish to customize PrestaShop for your + * needs please refer to https://devdocs.prestashop.com/ for more information. + * + * @author PrestaShop SA and Contributors + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ diff --git a/views/public/opc-checkout-layout.bundle.js.LICENSE.txt b/views/public/opc-checkout-layout.bundle.js.LICENSE.txt new file mode 100644 index 0000000..5ff9a04 --- /dev/null +++ b/views/public/opc-checkout-layout.bundle.js.LICENSE.txt @@ -0,0 +1,24 @@ +/** + * Copyright since 2007 PrestaShop SA and Contributors + * PrestaShop is an International Registered Trademark & Property of PrestaShop SA + * + * NOTICE OF LICENSE + * + * This source file is subject to the Open Software License (OSL 3.0) + * that is bundled with this package in the file LICENSE.md. + * It is also available through the world-wide-web at this URL: + * https://opensource.org/licenses/OSL-3.0 + * If you did not receive a copy of the license and are unable to + * obtain it through the world-wide-web, please send an email + * to license@prestashop.com so we can send you a copy immediately. + * + * DISCLAIMER + * + * Do not edit or add to this file if you wish to upgrade PrestaShop to newer + * versions in the future. If you wish to customize PrestaShop for your + * needs please refer to https://devdocs.prestashop.com/ for more information. + * + * @author PrestaShop SA and Contributors + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/OSL-3.0 Open Software License (OSL 3.0) + */ diff --git a/views/scss/one-page-checkout.scss b/views/scss/one-page-checkout.scss new file mode 100644 index 0000000..66ac857 --- /dev/null +++ b/views/scss/one-page-checkout.scss @@ -0,0 +1,55 @@ +/** + * For the full copyright and license information, please view the + * docs/licenses/LICENSE.txt file that was distributed with this source code. + */ + +// Pull only Bootstrap's declaration-only files so we can use mixins like +// `media-breakpoint-up` without emitting any Bootstrap CSS from this stylesheet. +@import "~bootstrap/scss/functions"; +@import "~bootstrap/scss/variables"; +@import "~bootstrap/scss/maps"; +@import "~bootstrap/scss/mixins"; + +$component-name: one-page-checkout; + +.#{$component-name} { + @include media-breakpoint-up(lg) { + padding-right: 2em; + } + + &__section { + padding-top: 1em; + padding-bottom: 1em; + + & + &, + .js-opc-addresses-section + & { + padding-top: 2em; + border-top: 1px solid var(--bs-border-color); + } + } + + &__links { + display: flex; + flex-wrap: wrap-reverse; + gap: 0.5rem; + justify-content: space-between; + margin-bottom: 1.5rem; + } + + &__field { + margin-bottom: 1rem; + } + + &__footer { + padding-top: 2em; + + .form-check { + margin-bottom: 1rem; + } + } + + .opc-address-item .form-check-input, + .opc-address-item .form-check-label { + cursor: pointer; + } +} diff --git a/views/templates/front/checkout/_partials/index.php b/views/templates/front/checkout/_partials/index.php new file mode 100644 index 0000000..a7be738 --- /dev/null +++ b/views/templates/front/checkout/_partials/index.php @@ -0,0 +1,29 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/views/templates/front/checkout/_partials/one-page-checkout/address-fields.tpl b/views/templates/front/checkout/_partials/one-page-checkout/address-fields.tpl new file mode 100644 index 0000000..c0acf6e --- /dev/null +++ b/views/templates/front/checkout/_partials/one-page-checkout/address-fields.tpl @@ -0,0 +1,75 @@ +{** + * For the full copyright and license information, please view the + * LICENSE.md file that was distributed with this source code. + *} + +{** + * One Page Checkout - Address fields partial + *} + +{assign var="_prefix_len" value=$prefix|strlen} +{assign var="_key_firstname" value="{$prefix}firstname"} +{assign var="_key_lastname" value="{$prefix}lastname"} +{assign var="_key_city" value="{$prefix}city"} +{assign var="_key_postcode" value="{$prefix}postcode"} +{assign var="_key_id_state" value="{$prefix}id_state"} + +{assign var="_has_name_row" value=isset($formFields[$_key_firstname]) && isset($formFields[$_key_lastname])} +{assign var="_has_city_row" value=isset($formFields[$_key_city]) && isset($formFields[$_key_postcode])} +{assign var="_has_state" value=isset($formFields[$_key_id_state])} + +{foreach from=$formFields item="field"} + {if $prefix && strpos($field.name, $prefix) !== 0}{continue}{/if} + {if !$prefix && strpos($field.name, 'invoice_') === 0}{continue}{/if} + + {if $prefix} + {assign var="_base" value=$field.name|substr:$_prefix_len} + {else} + {assign var="_base" value=$field.name} + {/if} + + {if $_base === 'alias'} + {form_field field=$field} + + {elseif $_base === 'firstname' && $_has_name_row} + {include file='_partials/form-fields-row.tpl' + fields=[$formFields[$_key_firstname], $formFields[$_key_lastname]] + } + + {elseif $_base === 'lastname' && $_has_name_row} + {elseif $_base === 'city' && $_has_city_row} + {if $_has_state} + {include file='_partials/form-fields-row.tpl' + fields=[$formFields[$_key_city], $formFields[$_key_id_state], $formFields[$_key_postcode]] + } + {else} + {include file='_partials/form-fields-row.tpl' + fields=[$formFields[$_key_city], $formFields[$_key_postcode]] + } + {/if} + + {elseif $_base === 'postcode' && $_has_city_row} + {elseif $_base === 'id_state' && $_has_city_row} + {elseif $_base === 'id_country'} +
+ + + {include file='_partials/form-errors.tpl' errors=$field.errors|default:[]} +
+ + {else} + {form_field field=$field} + {/if} +{/foreach} diff --git a/views/templates/front/checkout/_partials/one-page-checkout/address-list.tpl b/views/templates/front/checkout/_partials/one-page-checkout/address-list.tpl new file mode 100644 index 0000000..5d32dc5 --- /dev/null +++ b/views/templates/front/checkout/_partials/one-page-checkout/address-list.tpl @@ -0,0 +1,125 @@ +{** + * For the full copyright and license information, please view the + * LICENSE.md file that was distributed with this source code. + *} + +{** + * One Page Checkout - Address List + *} + +
+ {assign var="_selected_address" value=$selected_address|intval} + {assign var="_address_count" value=$customer.addresses|count} + {foreach from=$customer.addresses item="address"} + {assign var="_address_id" value=$address.id|default:$address.id_address|intval} + {assign var="is_address_selected" value=$_address_id === $_selected_address || ($_selected_address === 0 && $_address_count === 1 && $address@first)} +
+
+ +
+ +
+ +

+ {l s='Address details:' d='Shop.Theme.Actions'} + {$address.firstname} {$address.lastname}
+ {$address.address1}{if $address.address2} {$address.address2}{/if}
+ {$address.postcode} {$address.city} +

+
+ +
+ +
+
+ {/foreach} + + {if $customer.addresses|count > 0} +
+
+ +
+
+ +
+
+ {/if} +
diff --git a/views/templates/front/checkout/_partials/one-page-checkout/address-modal.tpl b/views/templates/front/checkout/_partials/one-page-checkout/address-modal.tpl new file mode 100644 index 0000000..f1ed7cb --- /dev/null +++ b/views/templates/front/checkout/_partials/one-page-checkout/address-modal.tpl @@ -0,0 +1,113 @@ +{** + * For the full copyright and license information, please view the + * LICENSE.md file that was distributed with this source code. + *} + + diff --git a/views/templates/front/checkout/_partials/one-page-checkout/addresses-section.tpl b/views/templates/front/checkout/_partials/one-page-checkout/addresses-section.tpl new file mode 100644 index 0000000..0731bf0 --- /dev/null +++ b/views/templates/front/checkout/_partials/one-page-checkout/addresses-section.tpl @@ -0,0 +1,136 @@ +{** + * For the full copyright and license information, please view the + * LICENSE.md file that was distributed with this source code. + *} + +{* Delivery Address Modal *} +{include file='module:ps_onepagecheckout/views/templates/front/checkout/_partials/one-page-checkout/address-modal.tpl' +modal_id='modal-delivery' +formFields=$deliveryFields +title_new={l s='New delivery address' d='Shop.Theme.Checkout'} +title_edit={l s='Edit delivery address' d='Shop.Theme.Checkout'} +address_type='delivery' +prefix='' +} + +{* Billing Address Modal *} +{include file='module:ps_onepagecheckout/views/templates/front/checkout/_partials/one-page-checkout/address-modal.tpl' +modal_id='modal-invoice' +formFields=$invoiceFields +title_new={l s='New billing address' d='Shop.Theme.Checkout'} +title_edit={l s='Edit billing address' d='Shop.Theme.Checkout'} +address_type='invoice' +prefix='invoice_' +} + +{include file='module:ps_onepagecheckout/views/templates/front/checkout/_partials/one-page-checkout/delete-address-modal.tpl'} + +{assign var="_addresses_count" value=$customer.addresses|count} +{assign var="_delivery_selected_address" value=$deliveryFields.id_address_delivery.value|default:($cart.id_address_delivery|default:0)} +{assign var="_billing_selected_address" value=$invoiceMetaFields.id_address_invoice.value|default:($cart.id_address_invoice|default:0)} + + + + + + + + + +
+

+ {if $is_virtual_cart} + {l s='Billing address' d='Shop.Theme.Checkout'} + {else} + {l s='Delivery address' d='Shop.Theme.Checkout'} + {/if} +

+ +
+
+
+ {if $_addresses_count > 0} + {include file='module:ps_onepagecheckout/views/templates/front/checkout/_partials/one-page-checkout/address-list.tpl' + prefix='' + selected_address=$_delivery_selected_address + } + {/if} +
+ +
+ {include file='module:ps_onepagecheckout/views/templates/front/checkout/_partials/one-page-checkout/address-fields.tpl' + formFields=$deliveryFields + prefix='' + } +
+
+ + {if !$is_virtual_cart} +
+ + +
+ {/if} +
+
+ +{if !$is_virtual_cart} + +{/if} + +{capture name="address_selector_bottom"}{hook h='displayAddressSelectorBottom'}{/capture} +{if $smarty.capture.address_selector_bottom} + {block name='address_selector_bottom'} +
+ {$smarty.capture.address_selector_bottom nofilter} +
+ {/block} +{/if} diff --git a/views/templates/front/checkout/_partials/one-page-checkout/carriers.tpl b/views/templates/front/checkout/_partials/one-page-checkout/carriers.tpl new file mode 100644 index 0000000..3855dfa --- /dev/null +++ b/views/templates/front/checkout/_partials/one-page-checkout/carriers.tpl @@ -0,0 +1,60 @@ +{** + * For the full copyright and license information, please view the + * LICENSE.md file that was distributed with this source code. + *} + +{** + * OPC — Carrier list partial (AJAX refresh) + * Variables: $delivery_options, $delivery_option + *} + +{if $delivery_options|count} +
+ {foreach from=$delivery_options item=carrier key=carrier_id name=delivery_options} +
+ + +
+ {capture name='extra'}{$carrier.extraContent nofilter}{/capture} + {if !empty($smarty.capture.extra)} +
+ {$smarty.capture.extra nofilter} +
+ {/if} +
+
+ {/foreach} +
+{else} +

+ {l s='Unfortunately, there are no carriers available for your delivery address.' d='Shop.Theme.Checkout'} +

+{/if} diff --git a/views/templates/front/checkout/_partials/one-page-checkout/contact-section.tpl b/views/templates/front/checkout/_partials/one-page-checkout/contact-section.tpl new file mode 100644 index 0000000..87cba07 --- /dev/null +++ b/views/templates/front/checkout/_partials/one-page-checkout/contact-section.tpl @@ -0,0 +1,50 @@ +{** + * For the full copyright and license information, please view the + * LICENSE.md file that was distributed with this source code. + *} + +{hook h='displayPersonalInformationTop' customer=$customer} + +{$isGuestCheckoutEnabled = $configuration.is_guest_checkout_enabled} + +{if !$customer.is_logged} +
+

{l s='Contact information' d='Shop.Theme.Checkout'}

+ + + + {if $isGuestCheckoutEnabled} + {if isset($contactFields['email'])} +
+ + + {include file='_partials/form-errors.tpl' errors=$contactFields['email']['errors']|default:[]} +
+ {/if} + + {if isset($contactFields['optin'])} +
+ {form_field field=$contactFields['optin']} +
+ {/if} + + {foreach from=$additionalCustomerFields item="field"} +
+ {form_field field=$field} +
+ {/foreach} + {/if} +
+{else} +
+

{l s='Contact information' d='Shop.Theme.Checkout'}

+ {include file='checkout/_partials/connected-account-info.tpl'} +
+{/if} diff --git a/views/templates/front/checkout/_partials/one-page-checkout/delete-address-modal.tpl b/views/templates/front/checkout/_partials/one-page-checkout/delete-address-modal.tpl new file mode 100644 index 0000000..625fcc3 --- /dev/null +++ b/views/templates/front/checkout/_partials/one-page-checkout/delete-address-modal.tpl @@ -0,0 +1,49 @@ +{** + * For the full copyright and license information, please view the + * LICENSE.md file that was distributed with this source code. + *} + + diff --git a/views/templates/front/checkout/_partials/one-page-checkout/delivery-section.tpl b/views/templates/front/checkout/_partials/one-page-checkout/delivery-section.tpl new file mode 100644 index 0000000..ab5f371 --- /dev/null +++ b/views/templates/front/checkout/_partials/one-page-checkout/delivery-section.tpl @@ -0,0 +1,75 @@ +{** + * For the full copyright and license information, please view the + * LICENSE.md file that was distributed with this source code. + *} + +{** + * OPC — Delivery method section + * Included by one-page-checkout.tpl. + * Also renderable standalone for AJAX refresh. + * + * Variables: + * $delivery_options - available carriers (may be empty on first load) + * $delivery_option - currently selected carrier key + * $id_address_delivery - current cart delivery address ID + * $hookDisplayBeforeCarrier + * $hookDisplayAfterCarrier + *} + +
+

{l s='Delivery method' d='Shop.Theme.Checkout'}

+ +
+ {if isset($hookDisplayBeforeCarrier)} + {$hookDisplayBeforeCarrier nofilter} + {else} + {hook h='displayBeforeCarrier'} + {/if} +
+ +
+ {if $delivery_options|count} + {include file='module:ps_onepagecheckout/views/templates/front/checkout/_partials/one-page-checkout/carriers.tpl' + delivery_options=$delivery_options + delivery_option=$delivery_option + } + {else} +
+ {l s='You will see the available delivery methods once you\'ve entered your delivery address.' d='Shop.Theme.Checkout'} +
+ {/if} +
+ +
+ {if isset($hookDisplayAfterCarrier)} + {$hookDisplayAfterCarrier nofilter} + {else} + {hook h='displayAfterCarrier'} + {/if} +
+ +
+ + {include file='module:ps_onepagecheckout/views/templates/front/checkout/_partials/one-page-checkout/order-options.tpl' + delivery_message=$delivery_message|default:'' + recyclablePackAllowed=$recyclablePackAllowed|default:false + recyclable=$recyclable|default:false + gift=$gift|default:[] + } + + + + +
diff --git a/views/templates/front/checkout/_partials/one-page-checkout/index.php b/views/templates/front/checkout/_partials/one-page-checkout/index.php new file mode 100644 index 0000000..a7be738 --- /dev/null +++ b/views/templates/front/checkout/_partials/one-page-checkout/index.php @@ -0,0 +1,29 @@ + + * @copyright Since 2007 PrestaShop SA and Contributors + * @license https://opensource.org/licenses/AFL-3.0 Academic Free License version 3.0 + */ +header('Expires: Mon, 26 Jul 1997 05:00:00 GMT'); +header('Last-Modified: ' . gmdate('D, d M Y H:i:s') . ' GMT'); + +header('Cache-Control: no-store, no-cache, must-revalidate'); +header('Cache-Control: post-check=0, pre-check=0', false); +header('Pragma: no-cache'); + +header('Location: ../'); +exit; diff --git a/views/templates/front/checkout/_partials/one-page-checkout/opc-error.tpl b/views/templates/front/checkout/_partials/one-page-checkout/opc-error.tpl new file mode 100644 index 0000000..165af56 --- /dev/null +++ b/views/templates/front/checkout/_partials/one-page-checkout/opc-error.tpl @@ -0,0 +1,26 @@ +{** + * For the full copyright and license information, please view the + * LICENSE.md file that was distributed with this source code. + *} + +{** + * OPC — Error state + * Intended to be embedded as a