diff --git a/.ddev/addon-metadata/phpmyadmin/manifest.yaml b/.ddev/addon-metadata/phpmyadmin/manifest.yaml new file mode 100644 index 00000000..78e7bac0 --- /dev/null +++ b/.ddev/addon-metadata/phpmyadmin/manifest.yaml @@ -0,0 +1,10 @@ +name: phpmyadmin +repository: ddev/ddev-phpmyadmin +version: v1.0.2 +install_date: "2026-04-04T16:48:16+02:00" +project_files: + - docker-compose.phpmyadmin.yaml + - docker-compose.phpmyadmin_norouter.yaml + - commands/host/phpmyadmin +global_files: [] +removal_actions: [] diff --git a/.ddev/commands/host/phpmyadmin b/.ddev/commands/host/phpmyadmin new file mode 100755 index 00000000..5f7d0edc --- /dev/null +++ b/.ddev/commands/host/phpmyadmin @@ -0,0 +1,19 @@ +#!/usr/bin/env bash + +## #ddev-generated: If you want to edit and own this file, remove this line. +## Description: Launch a browser with PhpMyAdmin +## Usage: phpmyadmin +## Example: "ddev phpmyadmin" + +DDEV_PHPMYADMIN_PORT=8036 +DDEV_PHPMYADMIN_HTTPS_PORT=8037 +if [ ${DDEV_PRIMARY_URL%://*} = "http" ] || [ -n "${GITPOD_WORKSPACE_ID:-}" ] || [ "${CODESPACES:-}" = "true" ]; then + # Gitpod: "gp preview" opens a blank page for PhpMyAdmin, use "xdg-open" instead + if [ "${OSTYPE:-}" = "linux-gnu" ] && [ -n "${GITPOD_WORKSPACE_ID:-}" ] && [ -z "${DDEV_DEBUG:-}" ]; then + xdg-open "$(DDEV_DEBUG=true ddev launch :$DDEV_PHPMYADMIN_PORT | grep "FULLURL" | awk '{print $2}')" + else + ddev launch :$DDEV_PHPMYADMIN_PORT + fi +else + ddev launch :$DDEV_PHPMYADMIN_HTTPS_PORT +fi diff --git a/.ddev/config.yaml b/.ddev/config.yaml index 5b94d56a..7d8495f6 100644 --- a/.ddev/config.yaml +++ b/.ddev/config.yaml @@ -3,7 +3,7 @@ type: cakephp docroot: webroot php_version: "8.4" webserver_type: nginx-fpm -xdebug_enabled: true +xdebug_enabled: false additional_hostnames: [] additional_fqdns: [] database: diff --git a/.ddev/docker-compose.phpmyadmin.yaml b/.ddev/docker-compose.phpmyadmin.yaml new file mode 100644 index 00000000..54f6140e --- /dev/null +++ b/.ddev/docker-compose.phpmyadmin.yaml @@ -0,0 +1,31 @@ +#ddev-generated +services: + phpmyadmin: + container_name: ddev-${DDEV_SITENAME}-phpmyadmin + image: ${PHPMYADMIN_DOCKER_IMAGE:-phpmyadmin:5} + working_dir: "/root" + restart: "no" + labels: + com.ddev.site-name: ${DDEV_SITENAME} + com.ddev.approot: ${DDEV_APPROOT} + volumes: + - ".:/mnt/ddev_config" + - "ddev-global-cache:/mnt/ddev-global-cache" + expose: + - "80" + environment: + - PMA_USER=root + - PMA_PASSWORD=root + - PMA_HOST=db + - PMA_PORT=3306 + - VIRTUAL_HOST=$DDEV_HOSTNAME + - UPLOAD_LIMIT=4000M + - HTTP_EXPOSE=8036:80 + - HTTPS_EXPOSE=8037:80 + healthcheck: + test: ["CMD-SHELL", "true"] + interval: 120s + timeout: 2s + retries: 1 + depends_on: + - db diff --git a/.ddev/docker-compose.phpmyadmin_norouter.yaml b/.ddev/docker-compose.phpmyadmin_norouter.yaml new file mode 100644 index 00000000..f369b695 --- /dev/null +++ b/.ddev/docker-compose.phpmyadmin_norouter.yaml @@ -0,0 +1,4 @@ +#ddev-generated +# If omit_containers[ddev-router] then this file will be replaced +# with another with a `ports` statement to directly expose port 80 to 8036 +services: {} diff --git a/composer.json b/composer.json index c85f2d20..1ee33754 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,8 @@ "config": { "allow-plugins": { "cakephp/plugin-installer": true, - "dealerdirect/phpcodesniffer-composer-installer": true + "dealerdirect/phpcodesniffer-composer-installer": true, + "php-http/discovery": true }, "platform-check": true, "sort-packages": true diff --git a/config/form-templates.php b/config/form-templates.php index d93d158c..1a855137 100644 --- a/config/form-templates.php +++ b/config/form-templates.php @@ -5,7 +5,7 @@ * Custom templates for pagination elements. */ return [ - 'button' => '', + 'button' => '', 'checkbox' => '', 'checkboxWrapper' => '
{{label}}
', 'error' => '
{{content}}
', diff --git a/package-lock.json b/package-lock.json index 6ffdc795..24d40a80 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5,14 +5,40 @@ "packages": { "": { "dependencies": { + "htmx.org": "^2.0.8", + "slim-select": "^3.4.3", "swiper": "^12.1.3" }, "devDependencies": { "@tailwindcss/vite": "^4.1.4", + "daisyui": "^5.5.19", "tailwindcss": "^4.0.0", "vite": "^8.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emnapi/wasi-threads": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", @@ -648,6 +674,16 @@ "tslib": "^2.4.0" } }, + "node_modules/daisyui": { + "version": "5.5.19", + "resolved": "https://registry.npmjs.org/daisyui/-/daisyui-5.5.19.tgz", + "integrity": "sha512-pbFAkl1VCEh/MPCeclKL61I/MqRIFFhNU7yiXoDDRapXN4/qNCoMxeCCswyxEEhqL5eiTTfwHvucFtOE71C9sA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/saadeghi/daisyui?sponsor=1" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -712,6 +748,12 @@ "dev": true, "license": "ISC" }, + "node_modules/htmx.org": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/htmx.org/-/htmx.org-2.0.8.tgz", + "integrity": "sha512-fm297iru0iWsNJlBrjvtN7V9zjaxd+69Oqjh4F/Vq9Wwi2kFisLcrLCiv5oBX0KLfOX/zG8AUo9ROMU5XUB44Q==", + "license": "0BSD" + }, "node_modules/jiti": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", @@ -1025,7 +1067,6 @@ "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1096,6 +1137,28 @@ "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" } }, + "node_modules/slim-select": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/slim-select/-/slim-select-3.4.3.tgz", + "integrity": "sha512-6jeOO7nVXNAdsAMAUIw2igcPG379yzGoa2mPBJ4tuoidenPz4AvFTnEewhVR4AnKBOSDf6L7trRJHuumJwJqOg==", + "license": "MIT", + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "vue": "^3.0.0" + }, + "peerDependenciesMeta": { + "react": { + "optional": true + }, + "react-dom": { + "optional": true + }, + "vue": { + "optional": true + } + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -1177,7 +1240,6 @@ "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "lightningcss": "^1.32.0", "picomatch": "^4.0.4", diff --git a/package.json b/package.json index f8b67110..318c7cd0 100644 --- a/package.json +++ b/package.json @@ -2,6 +2,7 @@ "type": "module", "devDependencies": { "@tailwindcss/vite": "^4.1.4", + "daisyui": "^5.5.19", "tailwindcss": "^4.0.0", "vite": "^8.0.0" }, @@ -10,6 +11,8 @@ "build": "vite build" }, "dependencies": { + "htmx.org": "^2.0.8", + "slim-select": "^3.4.3", "swiper": "^12.1.3" } } diff --git a/resources/css/style.css b/resources/css/style.css index 154a6ed4..88fccf2b 100644 --- a/resources/css/style.css +++ b/resources/css/style.css @@ -2,6 +2,48 @@ @import "./fonts.css"; @import "swiper/css"; @import "swiper/css/navigation"; +@import "swiper/css/pagination"; +@import "slim-select/styles"; +@plugin "daisyui" { + themes: cakephp --default; +} + +@plugin "daisyui/theme" { + name: "cakephp"; + default: true; + prefersdark: false; + color-scheme: light; + + --color-base-100: hsl(0 0% 100%); + --color-base-200: hsl(210 20% 98%); + --color-base-300: hsl(210 20% 94%); + --color-base-content: hsl(220 13% 18%); + + --color-primary: hsl(214 84% 46%); + --color-primary-content: hsl(0 0% 100%); + --color-secondary: hsl(262 80% 50%); + --color-secondary-content: hsl(0 0% 100%); + --color-accent: hsl(199 89% 48%); + --color-accent-content: hsl(0 0% 100%); + --color-neutral: hsl(220 13% 18%); + --color-neutral-content: hsl(0 0% 100%); + + --color-info: hsl(198 93% 60%); + --color-info-content: hsl(198 100% 12%); + --color-success: hsl(158 64% 52%); + --color-success-content: hsl(158 100% 10%); + --color-warning: hsl(43 96% 56%); + --color-warning-content: hsl(43 100% 11%); + --color-error: hsl(0 72% 46%); + --color-error-content: hsl(0 0% 100%); + + --radius-box: 1.5rem; + --radius-field: 1rem; + --radius-selector: 1rem; + --border: 1px; + --depth: 1; + --noise: 0; +} @theme { --font-raleway: "Raleway", sans-serif; @@ -40,9 +82,46 @@ --ss-primary-color: var(--color-blue-500); } +.htmx-indicator { + opacity: 1; + transition: opacity 0.2s ease; +} + +.htmx-request .htmx-indicator, +.htmx-request.htmx-indicator { + opacity: 1; +} + +.main-loading-overlay { + position: fixed; + inset: 0; + z-index: 80; + display: flex; + align-items: center; + justify-content: center; + background: hsl(210 40% 98% / 0.72); + backdrop-filter: blur(2px); + opacity: 0; + visibility: hidden; + pointer-events: none; + transition: opacity 0.2s ease; +} + +body.is-htmx-loading .main-loading-overlay, +.main-loading-overlay.htmx-request { + opacity: 1; + visibility: visible; + pointer-events: auto; +} + +.main-loading-indicator-card { + opacity: 1; + z-index: 90; +} + @layer components { .featured-packages-slider-shell { - @apply -mx-3 px-3 pb-4; + @apply pb-4; } .featured-packages-slider { @@ -53,21 +132,13 @@ @apply h-auto py-2; } - .featured-packages-slider-button { - @apply inline-flex h-11 w-11 items-center justify-center rounded-full border border-slate-200 bg-white text-lg text-slate-700 shadow-sm transition hover:border-cake-red hover:text-cake-red disabled:cursor-not-allowed disabled:opacity-40; - } - - .featured-packages-slider-button::after { - content: none; - } - .packages-section-divider { - @apply flex items-center gap-4 text-xs font-semibold uppercase tracking-[0.24em] text-slate-400; + @apply flex items-center gap-4 text-xs font-semibold uppercase tracking-[0.24em] opacity-40; } .packages-section-divider::before, .packages-section-divider::after { content: ""; - @apply h-px flex-1 bg-slate-200; + @apply h-px flex-1 bg-base-300; } } diff --git a/resources/js/app.js b/resources/js/app.js index 816e2bbb..6929cbeb 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -1,18 +1,25 @@ import Swiper from 'swiper' -import { Autoplay, Navigation } from 'swiper/modules' +import { Autoplay, Navigation, Pagination } from 'swiper/modules' +import htmx from 'htmx.org' +import SlimSelect from 'slim-select' -const initializeSelects = () => { - if (typeof window.SlimSelect !== 'function') { - return - } +window.htmx = htmx +const initializeSelects = (root = document) => { const selects = document.querySelectorAll('select') - selects.forEach((element) => { - const placeholder = element.getAttribute('data-placeholder') + selects.forEach(select => { + const initialized = select.getAttribute('data-slimselect-initialized'); + + if (initialized === 'true') { + return + } - new window.SlimSelect({ - select: element, + select.setAttribute('data-slimselect-initialized', 'true') + const placeholder = select.getAttribute('data-placeholder'); + + new SlimSelect({ + select: select, settings: { placeholderText: placeholder, }, @@ -20,15 +27,21 @@ const initializeSelects = () => { }) } -const initializeFeaturedPackagesSlider = () => { - const slider = document.querySelector('[data-featured-packages-slider]') +const initializeFeaturedPackagesSlider = (root = document) => { + const slider = root.querySelector('[data-featured-packages-slider]') if (!slider) { return } + if (slider.dataset.swiperInitialized === 'true') { + return + } + + const pagination = root.querySelector('[data-featured-packages-pagination]') + new Swiper(slider, { - modules: [Autoplay, Navigation], + modules: [Autoplay, Navigation, Pagination], loop: true, loopAdditionalSlides: 3, slidesPerView: 1, @@ -43,6 +56,10 @@ const initializeFeaturedPackagesSlider = () => { nextEl: '[data-featured-packages-next]', prevEl: '[data-featured-packages-prev]', }, + pagination: { + el: pagination, + clickable: true, + }, breakpoints: { 768: { slidesPerView: 2, @@ -52,7 +69,32 @@ const initializeFeaturedPackagesSlider = () => { }, }, }) + + slider.dataset.swiperInitialized = 'true' } -initializeSelects() -initializeFeaturedPackagesSlider() +document.addEventListener('DOMContentLoaded', () => { + initializeFeaturedPackagesSlider() + initializeSelects() +}) + +if (typeof window.htmx !== 'undefined') { + const reinitializeDynamicUi = () => { + initializeFeaturedPackagesSlider(document) + initializeSelects(document) + } + + document.body.addEventListener('htmx:afterSettle', reinitializeDynamicUi) + + // Track loading state + let isLoading = false + document.body.addEventListener('htmx:beforeRequest', () => { + isLoading = true + document.body.classList.add('is-htmx-loading') + }) + + document.body.addEventListener('htmx:afterRequest', () => { + isLoading = false + document.body.classList.remove('is-htmx-loading') + }) +} diff --git a/src/Application.php b/src/Application.php index 29251ba0..6675681b 100644 --- a/src/Application.php +++ b/src/Application.php @@ -24,6 +24,7 @@ use Cake\Http\Middleware\BodyParserMiddleware; use Cake\Http\Middleware\CsrfProtectionMiddleware; use Cake\Http\MiddlewareQueue; +use Cake\Http\ServerRequest; use Cake\ORM\Locator\TableLocator; use Cake\Routing\Middleware\AssetMiddleware; use Cake\Routing\Middleware\RoutingMiddleware; @@ -64,6 +65,18 @@ public function bootstrap(): void */ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue { + $csrf = new CsrfProtectionMiddleware([ + 'httponly' => true, + ]); + $csrf->skipCheckCallback(function (ServerRequest $request) { + if ( + $request->getParam('controller') === 'Packages' && + $request->getParam('action') === 'index' + ) { + return true; + } + }); + $middlewareQueue // Catch any exceptions in the lower layers, // and make an error page/response @@ -87,9 +100,7 @@ public function middleware(MiddlewareQueue $middlewareQueue): MiddlewareQueue // Cross Site Request Forgery (CSRF) Protection Middleware // https://book.cakephp.org/5/en/security/csrf.html#cross-site-request-forgery-csrf-middleware - ->add(new CsrfProtectionMiddleware([ - 'httponly' => true, - ])); + ->add($csrf); return $middlewareQueue; } diff --git a/src/Controller/PackagesController.php b/src/Controller/PackagesController.php index 13cde9a3..f3d4a847 100644 --- a/src/Controller/PackagesController.php +++ b/src/Controller/PackagesController.php @@ -60,7 +60,7 @@ public function index() $featuredPackages = array_values(array_filter( array_map( - static fn (string $packageName) => $featuredPackages[$packageName] ?? null, + static fn(string $packageName) => $featuredPackages[$packageName] ?? null, $featuredPackageNames, ), )); diff --git a/templates/Packages/index.php b/templates/Packages/index.php index 47157b00..6e1b9259 100644 --- a/templates/Packages/index.php +++ b/templates/Packages/index.php @@ -7,23 +7,88 @@ * @var iterable<\Tags\Model\Entity\Tag> $phpTags */ ?> -
+ + start('above_content'); ?> +
+
+
+

+

+
+ +
+ +
+ end(); ?> + + +
+ +
-
+
-

-

+

+ + Paginator->counter('{{count}}') ?> +

Form->create(null, ['valueSources' => 'query', 'class' => 'flex flex-wrap items-center gap-4 rounded-3xl border border-slate-200 bg-white p-4 shadow-sm']); - echo $this->Form->control('search', ['label' => false, 'placeholder' => __('Search...')]); + // Create form that preserves existing query params via valueSources + echo $this->Form->create(null, [ + 'type' => 'get', + 'valueSources' => 'query', + 'class' => 'join join-horizontal flex-wrap gap-2' + ]); + // Preserve search query when applying filters + echo $this->Form->hidden('search'); echo $this->Form->control('cakephp_slugs', [ 'label' => false, 'options' => $cakephpTags, - 'empty' => __('No Filter'), 'multiple' => true, 'data-placeholder' => __('CakePHP Version'), + 'class' => 'select select-bordered join-item' ]); // echo $this->Form->control('php_slugs', [ // 'label' => false, @@ -32,71 +97,68 @@ // 'multiple' => true, // 'data-placeholder' => __('PHP Version'), // 'data-is-php-filter' => true, +// 'class' => 'select select-bordered join-item' // ]); - echo $this->Form->button('Search', ['type' => 'submit']); + echo $this->Form->button(__('Apply'), [ + 'type' => 'submit', + ]); echo $this->Form->end(); ?> -
-

-
- Paginator->sort('downloads') ?> - Paginator->sort('stars') ?> +
+

+
+ Paginator->setTemplates([ + 'sort' => '{{text}}', + 'sortAsc' => '{{text}}', + 'sortDesc' => '{{text}}', + ]); + ?> + Paginator->sort('downloads', 'Downloads') ?> + Paginator->sort('stars', 'Stars') ?>
- -
-
-
-

-
- -
- -
- - -
-

Paginator->counter(__('Page {{page}} of {{pages}}, showing {{current}} record(s) out of {{count}} total')) ?>

- -
    +
    + Paginator->setTemplates([ + 'number' => '{{text}}', + 'current' => '', + 'ellipsis' => '{{text}}', + 'first' => '{{text}}', + 'last' => '{{text}}', + 'prevActive' => '', + 'prevDisabled' => '{{text}}', + 'nextActive' => '', + 'nextDisabled' => '{{text}}', + ]); + ?> +
    Paginator->first('« ' . __('first')) ?> - Paginator->prev('‹ ' . __('previous')) ?> + Paginator->numbers() ?> - Paginator->next(__('next') . ' ›') ?> + Paginator->last(__('last') . ' »') ?> -
+
diff --git a/templates/element/Packages/package-tile.php b/templates/element/Packages/package-tile.php index 7c1b477b..baffdafa 100644 --- a/templates/element/Packages/package-tile.php +++ b/templates/element/Packages/package-tile.php @@ -7,93 +7,123 @@ $isFeatured = $isFeatured ?? false; $query = $this->getRequest()->getQueryParams(); ?> -
- +
-
-

- package) ?> -

- - + +
+ - -
+
+ +

+ package) ?> +

-
-

+

+

description ?: __('No description available.')) ?>

cake_php_tags): ?> -
-

+

+

-
- cake_php_tag_groups as $majorVersion => $tags): ?> -
- - .x - - - slug, - ]))); - unset($tagQuery['page']); - ?> - - label)) ?> - - - 6): ?> - - ... - - -
+ cake_php_tag_groups; + krsort($tagGroups); + $existingSlugs = (array)($query['cakephp_slugs'] ?? []); + $packageId = preg_replace('/[^a-z0-9]/i', '-', strtolower($package->package)); + $dialogId = 'compat-' . $packageId; + ?> +
+ $tags): ?> + +
+ + + + +
- -
-
-
+
+
+
+
+
- -
+
+
Number->format($package->downloads) ?> - +
-
-
+
+
-
-
+
+
latest_stable_version ?: __('Unknown')) ?> - +
-
-
+
+
-
-
+
+
Number->format($package->stars) ?> - +
-
+
diff --git a/templates/layout/default.php b/templates/layout/default.php index 82b5165e..f8239778 100644 --- a/templates/layout/default.php +++ b/templates/layout/default.php @@ -15,9 +15,14 @@ */ $cakeDescription = 'CakePHP Plugins'; +$request = $this->getRequest(); +$isPackagesIndex = $request->getParam('controller') === 'Packages' && $request->getParam('action') === 'index'; +$searchValue = (string)$request->getQuery('search', ''); +$cakephpSlugs = (array)$request->getQuery('cakephp_slugs', []); +$phpSlugs = (array)$request->getQuery('php_slugs', []); ?> - + Html->charset() ?> @@ -29,28 +34,103 @@ Html->css(['cake']) ?> - - Html->script('app.js', ['type' => 'module']) ?> fetch('meta') ?> fetch('css') ?> fetch('script') ?> - - +
Flash->render() ?> fetch('content') ?> diff --git a/vite.config.js b/vite.config.js index d8139cb6..762af45e 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,11 +1,23 @@ import { defineConfig } from 'vite' import tailwindcss from '@tailwindcss/vite' +import { fileURLToPath } from 'node:url' export default defineConfig({ root: '.', plugins: [ tailwindcss(), ], + server: { + watch: { + ignored: ['**/webroot/**', '**/vendor/**', '**/tmp/**', '**/logs/**'], + }, + }, + resolve: { + conditions: ['module', 'browser', 'development', 'import'], + alias: { + 'slim-select/styles': fileURLToPath(new URL('./node_modules/slim-select/dist/slimselect.css', import.meta.url)) + } + }, build: { outDir: 'webroot', emptyOutDir: false, @@ -14,6 +26,13 @@ export default defineConfig({ cake: 'resources/css/style.css', app: 'resources/js/app.js', }, + onLog(level, log, handler) { + // Suppress EVAL warning from htmx (known limitation, safe in this context) + if (log.code === 'EVAL' && log.id?.includes('htmx')) { + return + } + handler(level, log) + }, output: { entryFileNames: (chunkInfo) => chunkInfo.name === 'app' ? 'js/app.js' : 'js/[name].js', assetFileNames: (assetInfo) => {