8 changed files with 611 additions and 79 deletions
Generated
+62 -62
View File
@@ -515,11 +515,11 @@
},
{
"name": "boboko/core",
"version": "0.27.2",
"version": "0.27.4",
"source": {
"type": "git",
"url": "https://code.radical-elements.com/boboko/core.git",
"reference": "332bf92e4429cf5b787e106b9e8613d912dfcc4a"
"reference": "004f2382cb4260c80bbef1f941b318e9a6ba194d"
},
"require": {
"laravel/framework": "^12.0",
@@ -573,7 +573,7 @@
}
},
"description": "Core module — authentication and shared panel behaviour",
"time": "2026-09-29T11:53:08+00:00"
"time": "2026-09-29T18:30:41+00:00"
},
{
"name": "brick/math",
@@ -3111,16 +3111,16 @@
},
{
"name": "laravel/framework",
"version": "v12.69.2",
"version": "v12.69.3",
"source": {
"type": "git",
"url": "https://github.com/laravel/framework.git",
"reference": "17d034ef1e209b63a1d3a89c90ce5e89a4eb3fc5"
"reference": "58ea544a2a80dc03c168e13a5dc9a1d176a88717"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/laravel/framework/zipball/17d034ef1e209b63a1d3a89c90ce5e89a4eb3fc5",
"reference": "17d034ef1e209b63a1d3a89c90ce5e89a4eb3fc5",
"url": "https://api.github.com/repos/laravel/framework/zipball/58ea544a2a80dc03c168e13a5dc9a1d176a88717",
"reference": "58ea544a2a80dc03c168e13a5dc9a1d176a88717",
"shasum": ""
},
"require": {
@@ -3329,7 +3329,7 @@
"issues": "https://github.com/laravel/framework/issues",
"source": "https://github.com/laravel/framework"
},
"time": "2026-09-08T14:29:09+00:00"
"time": "2026-09-29T12:53:25+00:00"
},
{
"name": "laravel/prompts",
@@ -9243,16 +9243,16 @@
},
{
"name": "symfony/console",
"version": "v7.4.19",
"version": "v7.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "3a1973458b153f566a8d7499bc06ece323d60063"
"reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/3a1973458b153f566a8d7499bc06ece323d60063",
"reference": "3a1973458b153f566a8d7499bc06ece323d60063",
"url": "https://api.github.com/repos/symfony/console/zipball/d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d",
"reference": "d7bca2fe05241dc7732d0c8b6d551b879f5d0a7d",
"shasum": ""
},
"require": {
@@ -9317,7 +9317,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v7.4.19"
"source": "https://github.com/symfony/console/tree/v7.4.20"
},
"funding": [
{
@@ -9337,7 +9337,7 @@
"type": "tidelift"
}
],
"time": "2026-09-13T10:38:41+00:00"
"time": "2026-09-18T15:55:00+00:00"
},
{
"name": "symfony/css-selector",
@@ -9551,16 +9551,16 @@
},
{
"name": "symfony/error-handler",
"version": "v7.4.17",
"version": "v7.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/error-handler.git",
"reference": "8373921e231e190a88e2ad526951bbaa791576fa"
"reference": "82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/8373921e231e190a88e2ad526951bbaa791576fa",
"reference": "8373921e231e190a88e2ad526951bbaa791576fa",
"url": "https://api.github.com/repos/symfony/error-handler/zipball/82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0",
"reference": "82239fe00f7cdc9f17c4166fff4ebdc22bd26ec0",
"shasum": ""
},
"require": {
@@ -9609,7 +9609,7 @@
"description": "Provides tools to manage errors and ease debugging PHP code",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/error-handler/tree/v7.4.17"
"source": "https://github.com/symfony/error-handler/tree/v7.4.20"
},
"funding": [
{
@@ -9629,7 +9629,7 @@
"type": "tidelift"
}
],
"time": "2026-08-21T17:40:08+00:00"
"time": "2026-09-22T18:56:40+00:00"
},
{
"name": "symfony/event-dispatcher",
@@ -9799,16 +9799,16 @@
},
{
"name": "symfony/finder",
"version": "v7.4.19",
"version": "v7.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
"reference": "1b900bc6ae5ba60c5eee69c11ba44566190576fe"
"reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/finder/zipball/1b900bc6ae5ba60c5eee69c11ba44566190576fe",
"reference": "1b900bc6ae5ba60c5eee69c11ba44566190576fe",
"url": "https://api.github.com/repos/symfony/finder/zipball/2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e",
"reference": "2dd31e5208db7a0fda7bfc998ad727b79c2e4d1e",
"shasum": ""
},
"require": {
@@ -9843,7 +9843,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/finder/tree/v7.4.19"
"source": "https://github.com/symfony/finder/tree/v7.4.20"
},
"funding": [
{
@@ -9863,20 +9863,20 @@
"type": "tidelift"
}
],
"time": "2026-09-10T19:14:10+00:00"
"time": "2026-09-26T06:31:10+00:00"
},
{
"name": "symfony/html-sanitizer",
"version": "v8.1.7",
"version": "v8.1.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/html-sanitizer.git",
"reference": "3d79bdecd01f71bccc45ddfa1d33c024df347a1f"
"reference": "c67061a71d3f8b55f899faee3167422efa4db027"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/3d79bdecd01f71bccc45ddfa1d33c024df347a1f",
"reference": "3d79bdecd01f71bccc45ddfa1d33c024df347a1f",
"url": "https://api.github.com/repos/symfony/html-sanitizer/zipball/c67061a71d3f8b55f899faee3167422efa4db027",
"reference": "c67061a71d3f8b55f899faee3167422efa4db027",
"shasum": ""
},
"require": {
@@ -9915,7 +9915,7 @@
"sanitizer"
],
"support": {
"source": "https://github.com/symfony/html-sanitizer/tree/v8.1.7"
"source": "https://github.com/symfony/html-sanitizer/tree/v8.1.8"
},
"funding": [
{
@@ -9935,20 +9935,20 @@
"type": "tidelift"
}
],
"time": "2026-09-04T10:14:04+00:00"
"time": "2026-09-28T08:00:04+00:00"
},
{
"name": "symfony/http-foundation",
"version": "v7.4.19",
"version": "v7.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-foundation.git",
"reference": "f7b953be7801974a5a0685e1228bbc3b12b35936"
"reference": "9d0761c0da6bd5b801b401666834901a9a1ffc33"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/f7b953be7801974a5a0685e1228bbc3b12b35936",
"reference": "f7b953be7801974a5a0685e1228bbc3b12b35936",
"url": "https://api.github.com/repos/symfony/http-foundation/zipball/9d0761c0da6bd5b801b401666834901a9a1ffc33",
"reference": "9d0761c0da6bd5b801b401666834901a9a1ffc33",
"shasum": ""
},
"require": {
@@ -9997,7 +9997,7 @@
"description": "Defines an object-oriented layer for the HTTP specification",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-foundation/tree/v7.4.19"
"source": "https://github.com/symfony/http-foundation/tree/v7.4.20"
},
"funding": [
{
@@ -10017,20 +10017,20 @@
"type": "tidelift"
}
],
"time": "2026-09-14T17:41:17+00:00"
"time": "2026-09-24T07:12:44+00:00"
},
{
"name": "symfony/http-kernel",
"version": "v7.4.19",
"version": "v7.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/http-kernel.git",
"reference": "683507424cc84ca85ac1551adb609b6b98216667"
"reference": "392784b5d7b959b8d872542a2530c50c540a788f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/683507424cc84ca85ac1551adb609b6b98216667",
"reference": "683507424cc84ca85ac1551adb609b6b98216667",
"url": "https://api.github.com/repos/symfony/http-kernel/zipball/392784b5d7b959b8d872542a2530c50c540a788f",
"reference": "392784b5d7b959b8d872542a2530c50c540a788f",
"shasum": ""
},
"require": {
@@ -10116,7 +10116,7 @@
"description": "Provides a structured process for converting a Request into a Response",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/http-kernel/tree/v7.4.19"
"source": "https://github.com/symfony/http-kernel/tree/v7.4.20"
},
"funding": [
{
@@ -10136,7 +10136,7 @@
"type": "tidelift"
}
],
"time": "2026-09-15T07:09:35+00:00"
"time": "2026-09-29T14:05:16+00:00"
},
{
"name": "symfony/mailer",
@@ -11358,16 +11358,16 @@
},
{
"name": "symfony/routing",
"version": "v7.4.18",
"version": "v7.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/routing.git",
"reference": "ddd558991e98f693ae6bf5063cc1b0362c6bbec3"
"reference": "2bacb72be5eadeaf9874d5accb75bfb33d21c74b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/routing/zipball/ddd558991e98f693ae6bf5063cc1b0362c6bbec3",
"reference": "ddd558991e98f693ae6bf5063cc1b0362c6bbec3",
"url": "https://api.github.com/repos/symfony/routing/zipball/2bacb72be5eadeaf9874d5accb75bfb33d21c74b",
"reference": "2bacb72be5eadeaf9874d5accb75bfb33d21c74b",
"shasum": ""
},
"require": {
@@ -11419,7 +11419,7 @@
"url"
],
"support": {
"source": "https://github.com/symfony/routing/tree/v7.4.18"
"source": "https://github.com/symfony/routing/tree/v7.4.20"
},
"funding": [
{
@@ -11439,7 +11439,7 @@
"type": "tidelift"
}
],
"time": "2026-08-17T13:12:36+00:00"
"time": "2026-09-28T19:09:50+00:00"
},
{
"name": "symfony/service-contracts",
@@ -11795,16 +11795,16 @@
},
{
"name": "symfony/uid",
"version": "v7.4.17",
"version": "v7.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/uid.git",
"reference": "69d732355a139c6f8881337d28515aa01f12b8be"
"reference": "a96bbdb02a04ff3b5592cee6741b03edc78b56aa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/uid/zipball/69d732355a139c6f8881337d28515aa01f12b8be",
"reference": "69d732355a139c6f8881337d28515aa01f12b8be",
"url": "https://api.github.com/repos/symfony/uid/zipball/a96bbdb02a04ff3b5592cee6741b03edc78b56aa",
"reference": "a96bbdb02a04ff3b5592cee6741b03edc78b56aa",
"shasum": ""
},
"require": {
@@ -11849,7 +11849,7 @@
"uuid"
],
"support": {
"source": "https://github.com/symfony/uid/tree/v7.4.17"
"source": "https://github.com/symfony/uid/tree/v7.4.20"
},
"funding": [
{
@@ -11869,7 +11869,7 @@
"type": "tidelift"
}
],
"time": "2026-08-11T07:38:58+00:00"
"time": "2026-09-23T16:18:28+00:00"
},
{
"name": "symfony/var-dumper",
@@ -11960,16 +11960,16 @@
},
{
"name": "symfony/yaml",
"version": "v7.4.18",
"version": "v7.4.20",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "4cef939e55b8a21c5780418de0d98ab00d0736e1"
"reference": "0222bf4dfaa1e3ceb8fff579635d655f2950d5a7"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/4cef939e55b8a21c5780418de0d98ab00d0736e1",
"reference": "4cef939e55b8a21c5780418de0d98ab00d0736e1",
"url": "https://api.github.com/repos/symfony/yaml/zipball/0222bf4dfaa1e3ceb8fff579635d655f2950d5a7",
"reference": "0222bf4dfaa1e3ceb8fff579635d655f2950d5a7",
"shasum": ""
},
"require": {
@@ -12012,7 +12012,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v7.4.18"
"source": "https://github.com/symfony/yaml/tree/v7.4.20"
},
"funding": [
{
@@ -12032,7 +12032,7 @@
"type": "tidelift"
}
],
"time": "2026-08-30T00:47:26+00:00"
"time": "2026-09-26T06:31:10+00:00"
},
{
"name": "tbachert/spi",
+7
View File
@@ -133,6 +133,13 @@ services:
stoic:
image: ghcr.io/lexx27/stoic:latest
# Runs as root — the app container's entrypoint.sh chowns the whole
# storage/ tree to www-data on every boot (docker/entrypoint.sh),
# and Stoic's own image user isn't www-data or in its group, so it
# can't write thumbnails into storage/app/public/stoic/ otherwise
# (libvips reports this as "Failed to write VipsImage to file").
# Same fix already applied in docker-compose.dev.yml.
user: root
ports:
- "127.0.0.1:${STOIC_PORT:-2727}:2727"
environment:
+11
View File
@@ -25,6 +25,17 @@ server {
if ($request_method = POST) { set $skip_cache 1; }
if ($request_uri ~* "^/(boboko|up)") { set $skip_cache 1; }
if ($query_string) { set $skip_cache 1; }
# Any logged-in request must never be served a cached response —
# the cache key is URL-only ($scheme$request_method$host$request_uri,
# see fastcgi_cache_key below), with no per-session variation. Without
# this, the first cached hit on an authenticated GET page (e.g.
# /account) gets replayed verbatim to every other visitor of that same
# URL for up to fastcgi_cache_valid's 10m window — a real account/
# session data leak, not just a staleness bug. Laravel's session
# cookie name is env('SESSION_COOKIE', Str::slug(APP_NAME).'-session')
# (config/session.php) — APP_NAME varies per environment, so this
# matches the fixed '-session' suffix rather than a hardcoded name.
if ($http_cookie ~* "-session") { set $skip_cache 1; }
# Vite build assets — content-hashed filenames, safe to cache forever
location /build/ {
+2 -2
View File
@@ -20,8 +20,8 @@
}
},
"node_modules/@boboko/core": {
"version": "0.27.2",
"resolved": "git+https://code.radical-elements.com/boboko/core.git#332bf92e4429cf5b787e106b9e8613d912dfcc4a",
"version": "0.27.4",
"resolved": "git+https://code.radical-elements.com/boboko/core.git#004f2382cb4260c80bbef1f941b318e9a6ba194d",
"dependencies": {
"@hotwired/stimulus": "^3.2.2",
"leaflet": "^1.9.4"
+5 -5
View File
@@ -1,12 +1,12 @@
@import "tailwindcss";
@import "./fonts.css";
@import "./dropdown.css";
@import "./checkout.css";
/* Note for anyone theming the checkout module (resources/css/checkout.css,
.bbk-* classes): it's deliberately plain, unlayered CSS, not inside any
@layer — so override it with plain rules here too, not from inside
@layer components/utilities, which would lose to it. See checkout.css's
file-level comment for why. */
/* checkout.css themes the cart/checkout module (.bbk-* classes). Its base
styles (boboko-core's checkout.css) are plain, unlayered CSS, so theme
overrides must be plain rules too, never inside @layer components/utilities,
which would lose to them. */
@source '../../vendor/laravel/framework/src/Illuminate/Pagination/resources/views/*.blade.php';
@source '../../storage/framework/views/*.php';
@source '../**/*.blade.php';
+504
View File
@@ -0,0 +1,504 @@
/*
* 3dealer theme for the cart + checkout module (.bbk-* classes).
*
* Sits on top of boboko-core's generic checkout.css, which loads first (its
* own @vite entry). Rules here are deliberately plain and unlayered: the base
* file is unlayered too, and an unlayered rule beats any rule inside
* @layer components/utilities. Don't wrap this file in a layer.
*
* Everything maps onto an existing 3dealer pattern (x-ui.input, x-ui.button,
* x-ui.checkbox, x-ui.quantity, account-page headings). Square everywhere:
* no border-radius.
*/
/* ═══════════════════════════════════════════════════════════════════
TOKENS — the module's own variables, pointed at ours
═══════════════════════════════════════════════════════════════════ */
:root {
--bbk-font: var(--font-sans);
--bbk-color-text: var(--color-black);
--bbk-color-muted: var(--color-neutral-500);
--bbk-color-bg: var(--color-neutral-200);
--bbk-color-bg-muted: var(--color-neutral-300);
--bbk-color-border: var(--color-black);
--bbk-color-accent: var(--color-black);
--bbk-color-accent-text: var(--color-neutral-200);
--bbk-color-danger: var(--color-red-600);
--bbk-radius: 0;
--bbk-radius-sm: 0;
--bbk-shadow: none;
}
.bbk-cart,
.bbk-checkout-page,
.bbk-confirmation {
font-size: inherit;
line-height: inherit;
}
/* ═══════════════════════════════════════════════════════════════════
SHARED CONTROLS
═══════════════════════════════════════════════════════════════════ */
/* Primary CTA — x-ui.button "primary". The offset black block is a
box-shadow here rather than .btn-primary's ::before/::after, because the
module's buttons have no inner <span> to lift the label above a
positioned ::before. Same look, same easing. No `uppercase`: the module
markup can't carry the class strip-accents.js needs for Greek. */
.bbk-cart-checkout,
.bbk-checkout-continue {
@apply border-black bg-neutral-200 px-6 py-3.5 text-base font-bold italic text-black;
box-shadow: 10px 10px 0 var(--color-black);
transition: box-shadow 0.35s cubic-bezier(0.2, 0.78, 0.12, 0.86);
}
.bbk-checkout-continue {
@apply px-[46px] py-5 text-[19px];
}
.bbk-cart-checkout:hover,
.bbk-checkout-continue:hover {
opacity: 1;
}
.bbk-cart-checkout:not(:disabled):hover,
.bbk-checkout-continue:not(:disabled):hover {
box-shadow: 0 0 0 var(--color-black);
}
.bbk-cart-checkout:disabled,
.bbk-checkout-continue:disabled {
@apply opacity-50;
}
/* Secondary button — x-ui.button "secondary", size sm */
.bbk-cart-coupon-submit {
@apply border-black bg-transparent px-6 py-2 text-sm font-semibold transition-colors hover:bg-black hover:text-neutral-200;
}
/* Text inputs — x-ui.input: bottom border only, transparent */
.bbk-field-input,
.bbk-cart-coupon-input,
.bbk-checkout-box-now-locker-search {
@apply border-0 border-b border-black bg-transparent px-0 py-2 placeholder:text-neutral-400;
}
.bbk-field-input:focus,
.bbk-field-input:focus-visible,
.bbk-cart-coupon-input:focus-visible,
.bbk-checkout-box-now-locker-search:focus {
@apply outline-none;
}
.bbk-field-input:disabled {
@apply cursor-not-allowed bg-transparent text-black opacity-50;
}
.bbk-field-input--error {
@apply border-red-600;
}
/* x-ui.select — native arrow off, arrow-down icon in its place */
select.bbk-field-input {
@apply cursor-pointer appearance-none pr-10;
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' stroke='black'%3E%3Cpath fill='none' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' d='M12 5V19M12 19L6 13M12 19L18 13'/%3E%3C/svg%3E");
background-repeat: no-repeat;
background-position: right 1rem center;
background-size: 16px;
}
/* x-ui.textarea */
textarea.bbk-field-input {
@apply resize-none;
}
/* Read-only value (the store's single country) — x-ui.input read-only */
.bbk-field-static {
@apply border-0 border-b border-black bg-transparent px-0 py-2 text-black opacity-60;
}
/* x-ui.field */
.bbk-field {
@apply gap-2;
}
.bbk-field-row {
@apply gap-8;
}
.bbk-field-label {
@apply text-black;
font-size: inherit;
font-weight: inherit;
}
.bbk-field-error {
@apply text-sm;
}
/* x-ui.checkbox — square box, brand fill when ticked. Radios in the
shipping/payment cards use the same box (square design, no circles). */
.bbk-checkbox {
@apply gap-3;
font-size: inherit;
}
.bbk-checkbox--stacked {
@apply mt-0 text-sm;
}
.bbk-checkbox--stacked a,
.bbk-checkout-withdrawal a,
.bbk-checkout-login-prompt a,
.bbk-checkout-note a {
@apply underline hover:no-underline;
}
.bbk-checkbox input[type="checkbox"],
.bbk-checkout-shipping-option input[type="radio"],
.bbk-checkout-payment-option input[type="radio"] {
@apply relative size-4 shrink-0 cursor-pointer appearance-none border border-current bg-transparent;
}
.bbk-checkbox input[type="checkbox"]:checked::after,
.bbk-checkout-shipping-option input[type="radio"]:checked::after,
.bbk-checkout-payment-option input[type="radio"]:checked::after {
content: "";
position: absolute;
inset: 2px;
background: var(--color-brand);
}
.bbk-checkbox input[type="checkbox"]:focus-visible,
.bbk-checkout-shipping-option input[type="radio"]:focus-visible,
.bbk-checkout-payment-option input[type="radio"]:focus-visible {
@apply outline-2 outline-offset-2 outline-current;
}
/* ═══════════════════════════════════════════════════════════════════
CART DRAWER
═══════════════════════════════════════════════════════════════════ */
.bbk-cart-panel {
@apply border-l border-black;
}
.bbk-cart-panel-header {
@apply px-8 py-6;
}
.bbk-cart-heading {
@apply font-display text-h4 font-extrabold;
}
/* Icon buttons — same as the header's icons */
.bbk-cart-dismiss,
.bbk-cart-item-remove {
@apply text-black transition-opacity hover:bg-transparent hover:text-black hover:opacity-70;
}
.bbk-cart-panel-body {
@apply p-8;
}
.bbk-cart-error {
@apply px-8;
}
.bbk-cart-items {
@apply mb-8 gap-6;
}
.bbk-cart-item-media img,
.bbk-confirmation-line .bbk-cart-item-media img {
@apply border border-black;
}
.bbk-cart-item-title {
@apply font-bold;
}
.bbk-cart-item-variant,
.bbk-cart-item-unit,
.bbk-line-fields {
@apply text-sm;
}
.bbk-cart-item-total {
@apply font-display font-bold;
}
/* Quantity stepper — x-ui.quantity */
.bbk-cart-qty-btn {
@apply size-8 bg-transparent text-xl font-bold text-black transition-colors hover:bg-black hover:text-neutral-200;
}
.bbk-cart-qty-input {
@apply h-8 w-10 bg-transparent font-display font-bold;
}
.bbk-cart-qty-btn:focus-visible,
.bbk-cart-qty-input:focus-visible {
@apply outline-2 outline-offset-0 outline-black;
}
/* Summary */
.bbk-cart-summary {
@apply gap-3 pt-6;
}
.bbk-cart-coupon {
@apply mb-3;
}
.bbk-cart-coupon-form {
@apply items-end gap-4;
}
/* Applied coupon — x-ui.status box */
.bbk-cart-coupon-applied {
@apply border-black bg-brand px-6 py-3 text-black;
}
.bbk-cart-coupon-remove {
@apply text-sm text-black underline hover:text-black hover:no-underline;
}
.bbk-cart-summary-row--total {
@apply mt-3 pt-4 font-display text-xl font-extrabold;
}
.bbk-cart-summary .bbk-cart-checkout {
@apply mt-6;
}
.bbk-cart-empty {
@apply py-10;
}
/* ═══════════════════════════════════════════════════════════════════
CHECKOUT PAGE
═══════════════════════════════════════════════════════════════════ */
.bbk-checkout-page {
@apply max-w-6xl px-8 py-16 lg:px-16;
}
.bbk-checkout-heading {
@apply mb-12 font-display text-h3 font-black tracking-tight sm:text-h2;
}
.bbk-checkout {
@apply gap-16;
}
.bbk-checkout-main {
@apply gap-10;
}
.bbk-checkout-section {
@apply gap-8 pb-10;
}
.bbk-checkout-section-heading,
.bbk-checkout-summary-heading {
@apply font-display text-h4 font-extrabold;
}
.bbk-checkout-note,
.bbk-checkout-status,
.bbk-checkout-withdrawal,
.bbk-checkout-error {
@apply text-sm;
}
.bbk-checkout-login-prompt a {
@apply font-bold;
}
.bbk-invoice,
.bbk-checkout-shipping-fields {
@apply gap-8;
}
/* Shipping / payment choices — bordered cards that fill black when picked,
like x-ui.option-buttons' pressed state. */
.bbk-checkout-shipping-option,
.bbk-checkout-payment-option,
.bbk-checkout-shipping-confirmed {
@apply gap-4 px-6 py-4;
}
.bbk-checkout-shipping-option,
.bbk-checkout-payment-option {
@apply transition-colors;
}
.bbk-checkout-shipping-option:has(input:checked),
.bbk-checkout-payment-option:has(input:checked) {
@apply bg-black text-neutral-200;
}
.bbk-checkout-shipping-option-name,
.bbk-checkout-payment-option-name {
@apply font-bold;
}
.bbk-checkout-shipping-option-description {
@apply text-sm;
}
.bbk-checkout-shipping-option:has(input:checked) .bbk-checkout-shipping-option-description {
@apply text-neutral-400;
}
.bbk-checkout-shipping-option-price {
@apply font-display font-bold;
}
/* Box Now locker picker. Leaflet's own CSS arrives in a lazy chunk (after
this file), so its selectors are scoped under the picker to outrank it. */
.bbk-checkout-box-now-locker {
@apply mt-4 gap-4 p-6;
}
.bbk-checkout-box-now-locker-label {
@apply text-base font-bold;
}
.bbk-checkout-box-now-locker-map {
@apply border border-black;
}
.bbk-checkout-box-now-locker-chosen {
@apply text-sm font-bold;
}
.bbk-checkout-box-now-locker .leaflet-popup-content-wrapper,
.bbk-box-now-popup-image,
.bbk-box-now-popup-name::before {
border-radius: 0;
}
.bbk-checkout-box-now-locker .leaflet-popup-content-wrapper {
@apply border border-black bg-neutral-200 shadow-none;
}
.bbk-checkout-box-now-locker .leaflet-popup-tip {
@apply bg-neutral-200;
}
.bbk-box-now-popup-name::before {
@apply bg-brand;
}
.bbk-box-now-popup-note {
@apply bg-neutral-300;
}
.bbk-box-now-popup-select {
@apply border border-black bg-black text-sm text-neutral-200 transition-colors hover:bg-neutral-200 hover:text-black;
}
.bbk-box-now-popup-select:hover {
transform: none;
}
.bbk-box-now-popup-select--selected,
.bbk-box-now-popup-select--selected:hover {
@apply border-neutral-500 bg-neutral-500 text-neutral-200;
}
/* Order summary column. The header is sticky too (lg:h-26), so the
aside sticks below it rather than behind it. */
.bbk-checkout-aside {
@apply top-32;
}
.bbk-checkout-summary {
@apply bg-transparent p-8;
}
.bbk-checkout-summary-heading {
@apply mb-6;
}
/* Payment */
.bbk-payment-element {
@apply my-2;
}
/* Processing overlay — square spinner, no radius */
.bbk-spinner {
@apply size-8;
border-color: var(--color-neutral-300);
border-top-color: var(--color-black);
border-radius: 0;
}
/* ═══════════════════════════════════════════════════════════════════
CONFIRMATION PAGE — same shape as account/orders/show
═══════════════════════════════════════════════════════════════════ */
.bbk-confirmation {
@apply max-w-4xl px-8 py-16 lg:px-16;
}
.bbk-confirmation-heading {
@apply mb-12 font-display text-h3 font-black tracking-tight sm:text-h2;
}
.bbk-confirmation-meta {
@apply mb-6 gap-3 border-y border-black py-6;
}
.bbk-confirmation-meta-row {
font-size: inherit;
}
.bbk-confirmation-meta-row dt {
@apply text-sm;
}
.bbk-confirmation-meta-row dd {
@apply font-bold;
}
.bbk-confirmation .bbk-checkout-note + .bbk-checkout-note {
@apply mt-2;
}
.bbk-confirmation-body {
@apply my-12 gap-12;
}
.bbk-confirmation-lines {
@apply gap-6;
}
.bbk-confirmation-line-name {
@apply font-bold;
}
.bbk-confirmation-line-qty {
@apply font-normal;
}
.bbk-confirmation-line-total {
@apply font-display font-bold;
}
.bbk-confirmation-lines .bbk-cart-summary {
@apply mt-6;
}
.bbk-confirmation-addresses {
@apply gap-8;
}
.bbk-confirmation-address-heading {
@apply mb-3 font-display text-xl font-extrabold;
}
.bbk-address-lines {
@apply text-base text-black;
}
@@ -3,20 +3,25 @@ import { Controller } from "@hotwired/stimulus";
const SCROLL_STEP = 1;
export default class extends Controller {
static targets = ["main", "thumb", "track", "lightbox", "lightboxImage", "lightboxCounter"];
static targets = ["main", "thumb", "track", "arrow", "lightbox", "lightboxImage", "lightboxCounter"];
connect() {
this.offset = 0;
this.lightboxIndex = 0;
this.#syncHeight();
this.mainTarget.addEventListener("load", () => this.#syncHeight());
// Main image and thumbnails resize as their images load and on viewport
// changes; re-sync the track height and arrow visibility each time.
this._resizeObserver = new ResizeObserver(() => this.#syncHeight());
this._resizeObserver.observe(this.mainTarget);
this.thumbTargets.forEach((t) => this._resizeObserver.observe(t));
this._onKeydown = this.#handleKeydown.bind(this);
document.addEventListener("keydown", this._onKeydown);
}
disconnect() {
this._resizeObserver?.disconnect();
document.removeEventListener("keydown", this._onKeydown);
}
@@ -103,6 +108,14 @@ export default class extends Controller {
if (h > 0) {
this.trackTarget.style.maxHeight = h + "px";
}
this.#toggleArrows();
}
// Arrows stay in the layout (invisible, not hidden) so the thumbnail
// column keeps its alignment with the main image.
#toggleArrows() {
const overflowing = this.trackTarget.scrollHeight > this.trackTarget.clientHeight;
this.arrowTargets.forEach((a) => a.classList.toggle("invisible", !overflowing));
}
#handleKeydown(e) {
+5 -8
View File
@@ -37,7 +37,8 @@ class="relative flex gap-4 items-start"
<button
type="button"
data-action="click->product-gallery#scrollUp"
class="gallery-arrow w-full flex items-center justify-center py-2"
data-product-gallery-target="arrow"
class="gallery-arrow invisible w-full flex items-center justify-center py-2"
aria-label="Scroll thumbnails up"
><x-ui.icon name="arrow-up" :size="30" /></button>
@@ -67,7 +68,8 @@ class="block w-full shrink-0 border border-black overflow-hidden "
<button
type="button"
data-action="click->product-gallery#scrollDown"
class="gallery-arrow w-full flex items-center justify-center py-2"
data-product-gallery-target="arrow"
class="gallery-arrow invisible w-full flex items-center justify-center py-2"
aria-label="Scroll thumbnails down"
><x-ui.icon name="arrow-down" :size="30" /></button>
</div>
@@ -231,14 +233,9 @@ class="flex flex-col gap-6"
['id' => 'reviews', 'label' => __('storefront.product.reviews') . ' (' . $product['reviews']['count'] . ')'],
]">
<x-slot name="description">
<div class="leading-7 [&_p]:mt-4">
<div class="leading-7 [&_p]:mt-4 [&_ul]:flex [&_ul]:flex-col [&_ul]:gap-3 [&_ul]:text-neutral-600 [&_ul]:list-disc [&_ul]:list-outside [&_ul]:pl-5">
{!! $product['description'] !!}
</div>
<ul class="mt-8 flex flex-col gap-3 text-neutral-600 list-disc list-outside pl-5">
<li>Όλα τα προϊόντα εκτυπώνονται και προετοιμάζονται κατά παραγγελία. Ο χρόνος προετοιμασίας κυμαίνεται μεταξύ 2 και 7 εργάσιμων ημερών.</li>
<li>Όλα τα προϊόντα κατασκευάζονται με τρισδιάστατη εκτύπωση σε ειδικούς εκτυπωτές πλαστικού υλικού. Πιθανώς να έχουν εμφανείς γραμμές ένωσης, στρώσεις εκτύπωσης υλικού και μικρές ατέλειες. Είναι φυσιολογικό για το αποτέλεσμα αυτής της δημιουργικής διαδικασίας.</li>
<li>Όλα τα προϊόντα είναι σχεδιασμένα και κατασκευασμένα είτε για διακόσμηση είτε για χρήση σε λογικά, ρεαλιστικά πλαίσια. Υπερβολική ισχύς, υψηλότατες θερμοκρασίες και αποσυναρμολόγηση μπορεί να προκαλέσουν ζημιά στο προϊόν για την οποία δεν ευθύνεται το 3Dealer.</li>
</ul>
</x-slot>
<x-slot name="reviews">
@if(session('reviewSubmitted'))